1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.settingslib.location; 18 19 import android.app.ActivityManager; 20 import android.content.Context; 21 import android.content.Intent; 22 import android.content.pm.ApplicationInfo; 23 import android.content.pm.PackageManager; 24 import android.content.pm.ResolveInfo; 25 import android.content.pm.ServiceInfo; 26 import android.content.res.Resources; 27 import android.content.res.TypedArray; 28 import android.content.res.XmlResourceParser; 29 import android.location.SettingInjectorService; 30 import android.os.Bundle; 31 import android.os.Handler; 32 import android.os.Looper; 33 import android.os.Message; 34 import android.os.Messenger; 35 import android.os.SystemClock; 36 import android.os.UserHandle; 37 import android.os.UserManager; 38 import android.text.TextUtils; 39 import android.util.ArrayMap; 40 import android.util.ArraySet; 41 import android.util.AttributeSet; 42 import android.util.Log; 43 import android.util.Xml; 44 45 import androidx.preference.Preference; 46 47 import com.android.settingslib.R; 48 49 import org.xmlpull.v1.XmlPullParser; 50 import org.xmlpull.v1.XmlPullParserException; 51 52 import java.io.IOException; 53 import java.lang.ref.WeakReference; 54 import java.util.ArrayDeque; 55 import java.util.ArrayList; 56 import java.util.Deque; 57 import java.util.HashSet; 58 import java.util.List; 59 import java.util.Map; 60 import java.util.Set; 61 62 /** 63 * Adds the preferences specified by the {@link InjectedSetting} objects to a preference group. 64 * 65 * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. We do not use that 66 * class directly because it is not a good match for our use case: we do not need the caching, and 67 * so do not want the additional resource hit at app install/upgrade time; and we would have to 68 * suppress the tie-breaking between multiple services reporting settings with the same name. 69 * Code-sharing would require extracting {@link 70 * android.content.pm.RegisteredServicesCache#parseServiceAttributes(android.content.res.Resources, 71 * String, android.util.AttributeSet)} into an interface, which didn't seem worth it. 72 */ 73 public class SettingsInjector { 74 static final String TAG = "SettingsInjector"; 75 76 /** 77 * If reading the status of a setting takes longer than this, we go ahead and start reading 78 * the next setting. 79 */ 80 private static final long INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS = 1000; 81 82 /** 83 * {@link Message#what} value for starting to load status values 84 * in case we aren't already in the process of loading them. 85 */ 86 private static final int WHAT_RELOAD = 1; 87 88 /** 89 * {@link Message#what} value sent after receiving a status message. 90 */ 91 private static final int WHAT_RECEIVED_STATUS = 2; 92 93 /** 94 * {@link Message#what} value sent after the timeout waiting for a status message. 95 */ 96 private static final int WHAT_TIMEOUT = 3; 97 98 private final Context mContext; 99 100 /** 101 * The settings that were injected 102 */ 103 protected final Set<Setting> mSettings; 104 105 private final Handler mHandler; 106 SettingsInjector(Context context)107 public SettingsInjector(Context context) { 108 mContext = context; 109 mSettings = new HashSet<Setting>(); 110 mHandler = new StatusLoadingHandler(mSettings); 111 } 112 113 /** 114 * Returns a list for a profile with one {@link InjectedSetting} object for each 115 * {@link android.app.Service} that responds to 116 * {@link SettingInjectorService#ACTION_SERVICE_INTENT} and provides the expected setting 117 * metadata. 118 * 119 * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. 120 * 121 * TODO: unit test 122 */ getSettings(final UserHandle userHandle)123 protected List<InjectedSetting> getSettings(final UserHandle userHandle) { 124 PackageManager pm = mContext.getPackageManager(); 125 Intent intent = new Intent(SettingInjectorService.ACTION_SERVICE_INTENT); 126 127 final int profileId = userHandle.getIdentifier(); 128 List<ResolveInfo> resolveInfos = 129 pm.queryIntentServicesAsUser(intent, PackageManager.GET_META_DATA, profileId); 130 if (Log.isLoggable(TAG, Log.DEBUG)) { 131 Log.d(TAG, "Found services for profile id " + profileId + ": " + resolveInfos); 132 } 133 134 final PackageManager userPackageManager = mContext.createContextAsUser( 135 userHandle, /* flags */ 0).getPackageManager(); 136 List<InjectedSetting> settings = new ArrayList<InjectedSetting>(resolveInfos.size()); 137 for (ResolveInfo resolveInfo : resolveInfos) { 138 try { 139 InjectedSetting setting = parseServiceInfo(resolveInfo, userHandle, 140 userPackageManager); 141 if (setting == null) { 142 Log.w(TAG, "Unable to load service info " + resolveInfo); 143 } else { 144 settings.add(setting); 145 } 146 } catch (XmlPullParserException e) { 147 Log.w(TAG, "Unable to load service info " + resolveInfo, e); 148 } catch (IOException e) { 149 Log.w(TAG, "Unable to load service info " + resolveInfo, e); 150 } 151 } 152 if (Log.isLoggable(TAG, Log.DEBUG)) { 153 Log.d(TAG, "Loaded settings for profile id " + profileId + ": " + settings); 154 } 155 156 return settings; 157 } 158 159 /** 160 * Adds the InjectedSetting information to a Preference object 161 */ populatePreference(Preference preference, InjectedSetting setting)162 private void populatePreference(Preference preference, InjectedSetting setting) { 163 preference.setTitle(setting.title); 164 preference.setSummary(R.string.loading_injected_setting_summary); 165 preference.setOnPreferenceClickListener(new ServiceSettingClickedListener(setting)); 166 } 167 168 /** 169 * Gets a list of preferences that other apps have injected. 170 * 171 * @param profileId Identifier of the user/profile to obtain the injected settings for or 172 * UserHandle.USER_CURRENT for all profiles associated with current user. 173 */ getInjectedSettings(Context prefContext, final int profileId)174 public Map<Integer, List<Preference>> getInjectedSettings(Context prefContext, 175 final int profileId) { 176 final UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE); 177 final List<UserHandle> profiles = um.getUserProfiles(); 178 final ArrayMap<Integer, List<Preference>> result = new ArrayMap<>(); 179 mSettings.clear(); 180 for (UserHandle userHandle : profiles) { 181 if (profileId == UserHandle.USER_CURRENT || profileId == userHandle.getIdentifier()) { 182 final List<Preference> prefs = new ArrayList<>(); 183 Iterable<InjectedSetting> settings = getSettings(userHandle); 184 for (InjectedSetting setting : settings) { 185 Preference preference = createPreference(prefContext, setting); 186 populatePreference(preference, setting); 187 prefs.add(preference); 188 mSettings.add(new Setting(setting, preference)); 189 } 190 if (!prefs.isEmpty()) { 191 result.put(userHandle.getIdentifier(), prefs); 192 } 193 } 194 } 195 196 reloadStatusMessages(); 197 return result; 198 } 199 200 /** 201 * Creates an injected Preference 202 * 203 * @return the created Preference 204 */ createPreference(Context prefContext, InjectedSetting setting)205 protected Preference createPreference(Context prefContext, InjectedSetting setting) { 206 return new Preference(prefContext); 207 } 208 209 /** 210 * Gives descendants a chance to log Preference click event 211 */ logPreferenceClick(Intent intent)212 protected void logPreferenceClick(Intent intent) { 213 } 214 215 /** 216 * Returns the settings parsed from the attributes of the 217 * {@link SettingInjectorService#META_DATA_NAME} tag, or null. 218 * 219 * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. 220 */ parseServiceInfo(ResolveInfo service, UserHandle userHandle, PackageManager pm)221 private static InjectedSetting parseServiceInfo(ResolveInfo service, UserHandle userHandle, 222 PackageManager pm) throws XmlPullParserException, IOException { 223 224 ServiceInfo si = service.serviceInfo; 225 ApplicationInfo ai = si.applicationInfo; 226 227 if ((ai.flags & ApplicationInfo.FLAG_SYSTEM) == 0) { 228 if (Log.isLoggable(TAG, Log.WARN)) { 229 Log.w(TAG, "Ignoring attempt to inject setting from app not in system image: " 230 + service); 231 return null; 232 } 233 } 234 235 XmlResourceParser parser = null; 236 try { 237 parser = si.loadXmlMetaData(pm, SettingInjectorService.META_DATA_NAME); 238 if (parser == null) { 239 throw new XmlPullParserException("No " + SettingInjectorService.META_DATA_NAME 240 + " meta-data for " + service + ": " + si); 241 } 242 243 AttributeSet attrs = Xml.asAttributeSet(parser); 244 245 int type; 246 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT 247 && type != XmlPullParser.START_TAG) { 248 } 249 250 String nodeName = parser.getName(); 251 if (!SettingInjectorService.ATTRIBUTES_NAME.equals(nodeName)) { 252 throw new XmlPullParserException("Meta-data does not start with " 253 + SettingInjectorService.ATTRIBUTES_NAME + " tag"); 254 } 255 256 Resources res = pm.getResourcesForApplication(si.packageName); 257 return parseAttributes(si.packageName, si.name, userHandle, res, attrs); 258 } catch (PackageManager.NameNotFoundException e) { 259 throw new XmlPullParserException( 260 "Unable to load resources for package " + si.packageName); 261 } finally { 262 if (parser != null) { 263 parser.close(); 264 } 265 } 266 } 267 268 /** 269 * Returns an immutable representation of the static attributes for the setting, or null. 270 */ parseAttributes(String packageName, String className, UserHandle userHandle, Resources res, AttributeSet attrs)271 private static InjectedSetting parseAttributes(String packageName, String className, 272 UserHandle userHandle, Resources res, AttributeSet attrs) { 273 274 TypedArray sa = res.obtainAttributes(attrs, android.R.styleable.SettingInjectorService); 275 try { 276 // Note that to help guard against malicious string injection, we do not allow dynamic 277 // specification of the label (setting title) 278 final String title = sa.getString(android.R.styleable.SettingInjectorService_title); 279 final int iconId = 280 sa.getResourceId(android.R.styleable.SettingInjectorService_icon, 0); 281 final String settingsActivity = 282 sa.getString(android.R.styleable.SettingInjectorService_settingsActivity); 283 final String userRestriction = sa.getString( 284 android.R.styleable.SettingInjectorService_userRestriction); 285 if (Log.isLoggable(TAG, Log.DEBUG)) { 286 Log.d(TAG, "parsed title: " + title + ", iconId: " + iconId 287 + ", settingsActivity: " + settingsActivity); 288 } 289 return new InjectedSetting.Builder() 290 .setPackageName(packageName) 291 .setClassName(className) 292 .setTitle(title) 293 .setIconId(iconId) 294 .setUserHandle(userHandle) 295 .setSettingsActivity(settingsActivity) 296 .setUserRestriction(userRestriction) 297 .build(); 298 } finally { 299 sa.recycle(); 300 } 301 } 302 303 /** 304 * Reloads the status messages for all the preference items. 305 */ reloadStatusMessages()306 public void reloadStatusMessages() { 307 if (Log.isLoggable(TAG, Log.DEBUG)) { 308 Log.d(TAG, "reloadingStatusMessages: " + mSettings); 309 } 310 mHandler.sendMessage(mHandler.obtainMessage(WHAT_RELOAD)); 311 } 312 313 protected class ServiceSettingClickedListener 314 implements Preference.OnPreferenceClickListener { 315 private InjectedSetting mInfo; 316 ServiceSettingClickedListener(InjectedSetting info)317 public ServiceSettingClickedListener(InjectedSetting info) { 318 mInfo = info; 319 } 320 321 @Override onPreferenceClick(Preference preference)322 public boolean onPreferenceClick(Preference preference) { 323 // Activity to start if they click on the preference. 324 Intent settingIntent = new Intent(); 325 settingIntent.setClassName(mInfo.packageName, mInfo.settingsActivity); 326 // No flags set to ensure the activity is launched within the same settings task. 327 logPreferenceClick(settingIntent); 328 mContext.startActivityAsUser(settingIntent, mInfo.mUserHandle); 329 return true; 330 } 331 } 332 333 /** 334 * Loads the setting status values one at a time. Each load starts a subclass of {@link 335 * SettingInjectorService}, so to reduce memory pressure we don't want to load too many at 336 * once. 337 */ 338 private static final class StatusLoadingHandler extends Handler { 339 /** 340 * References all the injected settings. 341 */ 342 WeakReference<Set<Setting>> mAllSettings; 343 344 /** 345 * Settings whose status values need to be loaded. A set is used to prevent redundant loads. 346 */ 347 private Deque<Setting> mSettingsToLoad = new ArrayDeque<Setting>(); 348 349 /** 350 * Settings that are being loaded now and haven't timed out. In practice this should have 351 * zero or one elements. 352 */ 353 private Set<Setting> mSettingsBeingLoaded = new ArraySet<Setting>(); 354 StatusLoadingHandler(Set<Setting> allSettings)355 public StatusLoadingHandler(Set<Setting> allSettings) { 356 super(Looper.getMainLooper()); 357 mAllSettings = new WeakReference<>(allSettings); 358 } 359 360 @Override handleMessage(Message msg)361 public void handleMessage(Message msg) { 362 if (Log.isLoggable(TAG, Log.DEBUG)) { 363 Log.d(TAG, "handleMessage start: " + msg + ", " + this); 364 } 365 366 // Update state in response to message 367 switch (msg.what) { 368 case WHAT_RELOAD: { 369 final Set<Setting> allSettings = mAllSettings.get(); 370 if (allSettings != null) { 371 // Reload requested, so must reload all settings 372 mSettingsToLoad.clear(); 373 mSettingsToLoad.addAll(allSettings); 374 } 375 break; 376 } 377 case WHAT_RECEIVED_STATUS: 378 final Setting receivedSetting = (Setting) msg.obj; 379 receivedSetting.maybeLogElapsedTime(); 380 mSettingsBeingLoaded.remove(receivedSetting); 381 removeMessages(WHAT_TIMEOUT, receivedSetting); 382 break; 383 case WHAT_TIMEOUT: 384 final Setting timedOutSetting = (Setting) msg.obj; 385 mSettingsBeingLoaded.remove(timedOutSetting); 386 if (Log.isLoggable(TAG, Log.WARN)) { 387 Log.w(TAG, "Timed out after " + timedOutSetting.getElapsedTime() 388 + " millis trying to get status for: " + timedOutSetting); 389 } 390 break; 391 default: 392 Log.wtf(TAG, "Unexpected what: " + msg); 393 } 394 395 // Decide whether to load additional settings based on the new state. Start by seeing 396 // if we have headroom to load another setting. 397 if (mSettingsBeingLoaded.size() > 0) { 398 // Don't load any more settings until one of the pending settings has completed. 399 // To reduce memory pressure, we want to be loading at most one setting. 400 if (Log.isLoggable(TAG, Log.VERBOSE)) { 401 Log.v(TAG, "too many services already live for " + msg + ", " + this); 402 } 403 return; 404 } 405 406 if (mSettingsToLoad.isEmpty()) { 407 if (Log.isLoggable(TAG, Log.VERBOSE)) { 408 Log.v(TAG, "nothing left to do for " + msg + ", " + this); 409 } 410 return; 411 } 412 Setting setting = mSettingsToLoad.removeFirst(); 413 414 // Request the status value 415 setting.startService(); 416 mSettingsBeingLoaded.add(setting); 417 418 // Ensure that if receiving the status value takes too long, we start loading the 419 // next value anyway 420 Message timeoutMsg = obtainMessage(WHAT_TIMEOUT, setting); 421 sendMessageDelayed(timeoutMsg, INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS); 422 423 if (Log.isLoggable(TAG, Log.DEBUG)) { 424 Log.d(TAG, "handleMessage end " + msg + ", " + this 425 + ", started loading " + setting); 426 } 427 } 428 429 @Override toString()430 public String toString() { 431 return "StatusLoadingHandler{" + 432 "mSettingsToLoad=" + mSettingsToLoad + 433 ", mSettingsBeingLoaded=" + mSettingsBeingLoaded + 434 '}'; 435 } 436 } 437 438 private static class MessengerHandler extends Handler { 439 private WeakReference<Setting> mSettingRef; 440 private Handler mHandler; 441 MessengerHandler(Setting setting, Handler handler)442 public MessengerHandler(Setting setting, Handler handler) { 443 mSettingRef = new WeakReference(setting); 444 mHandler = handler; 445 } 446 447 @Override handleMessage(Message msg)448 public void handleMessage(Message msg) { 449 final Setting setting = mSettingRef.get(); 450 if (setting == null) { 451 return; 452 } 453 Bundle bundle = msg.getData(); 454 if (Log.isLoggable(TAG, Log.DEBUG)) { 455 Log.d(TAG, setting + ": received " + msg + ", bundle: " + bundle); 456 } 457 boolean enabled = bundle.getBoolean(SettingInjectorService.ENABLED_KEY, true); 458 String summary = bundle.getString(SettingInjectorService.SUMMARY_KEY); 459 final Preference preference = setting.preference; 460 if (TextUtils.isEmpty(summary)) { 461 // Set a placeholder summary when received empty summary from injected service. 462 // This is necessary to avoid preference height change. 463 preference.setSummary(R.string.summary_placeholder); 464 } else { 465 preference.setSummary(summary); 466 } 467 preference.setEnabled(enabled); 468 mHandler.sendMessage(mHandler.obtainMessage(WHAT_RECEIVED_STATUS, setting)); 469 } 470 } 471 472 /** 473 * Represents an injected setting and the corresponding preference. 474 */ 475 protected final class Setting { 476 public final InjectedSetting setting; 477 public final Preference preference; 478 public long startMillis; 479 480 Setting(InjectedSetting setting, Preference preference)481 public Setting(InjectedSetting setting, Preference preference) { 482 this.setting = setting; 483 this.preference = preference; 484 } 485 486 @Override toString()487 public String toString() { 488 return "Setting{" + 489 "setting=" + setting + 490 ", preference=" + preference + 491 '}'; 492 } 493 494 /** 495 * Starts the service to fetch for the current status for the setting, and updates the 496 * preference when the service replies. 497 */ startService()498 public void startService() { 499 final ActivityManager am = (ActivityManager) 500 mContext.getSystemService(Context.ACTIVITY_SERVICE); 501 if (!am.isUserRunning(setting.mUserHandle.getIdentifier())) { 502 if (Log.isLoggable(TAG, Log.VERBOSE)) { 503 Log.v(TAG, "Cannot start service as user " 504 + setting.mUserHandle.getIdentifier() + " is not running"); 505 } 506 return; 507 } 508 Handler handler = new MessengerHandler(this, mHandler); 509 Messenger messenger = new Messenger(handler); 510 511 Intent intent = setting.getServiceIntent(); 512 intent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger); 513 514 if (Log.isLoggable(TAG, Log.DEBUG)) { 515 Log.d(TAG, setting + ": sending update intent: " + intent 516 + ", handler: " + handler); 517 startMillis = SystemClock.elapsedRealtime(); 518 } else { 519 startMillis = 0; 520 } 521 522 // Start the service, making sure that this is attributed to the user associated with 523 // the setting rather than the system user. 524 mContext.startServiceAsUser(intent, setting.mUserHandle); 525 } 526 getElapsedTime()527 public long getElapsedTime() { 528 long end = SystemClock.elapsedRealtime(); 529 return end - startMillis; 530 } 531 maybeLogElapsedTime()532 public void maybeLogElapsedTime() { 533 if (Log.isLoggable(TAG, Log.DEBUG) && startMillis != 0) { 534 long elapsed = getElapsedTime(); 535 Log.d(TAG, this + " update took " + elapsed + " millis"); 536 } 537 } 538 } 539 } 540