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