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