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