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