2 * Copyright (C) 2013 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings.location;
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;
42 import com.android.settings.DimmableIconPreference;
44 import org.xmlpull.v1.XmlPullParser;
45 import org.xmlpull.v1.XmlPullParserException;
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;
55 * Adds the preferences specified by the {@link InjectedSetting} objects to a preference group.
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.
65 class SettingsInjector {
66 static final String TAG = "SettingsInjector";
69 * If reading the status of a setting takes longer than this, we go ahead and start reading
72 private static final long INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS = 1000;
75 * {@link Message#what} value for starting to load status values
76 * in case we aren't already in the process of loading them.
78 private static final int WHAT_RELOAD = 1;
81 * {@link Message#what} value sent after receiving a status message.
83 private static final int WHAT_RECEIVED_STATUS = 2;
86 * {@link Message#what} value sent after the timeout waiting for a status message.
88 private static final int WHAT_TIMEOUT = 3;
90 private final Context mContext;
93 * The settings that were injected
95 private final Set<Setting> mSettings;
97 private final Handler mHandler;
99 public SettingsInjector(Context context) {
101 mSettings = new HashSet<Setting>();
102 mHandler = new StatusLoadingHandler();
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
111 * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}.
115 private List<InjectedSetting> getSettings(final UserHandle userHandle) {
116 PackageManager pm = mContext.getPackageManager();
117 Intent intent = new Intent(SettingInjectorService.ACTION_SERVICE_INTENT);
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);
125 List<InjectedSetting> settings = new ArrayList<InjectedSetting>(resolveInfos.size());
126 for (ResolveInfo resolveInfo : resolveInfos) {
128 InjectedSetting setting = parseServiceInfo(resolveInfo, userHandle, pm);
129 if (setting == null) {
130 Log.w(TAG, "Unable to load service info " + resolveInfo);
132 settings.add(setting);
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);
140 if (Log.isLoggable(TAG, Log.DEBUG)) {
141 Log.d(TAG, "Loaded settings for profile id " + profileId + ": " + settings);
148 * Returns the settings parsed from the attributes of the
149 * {@link SettingInjectorService#META_DATA_NAME} tag, or null.
151 * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}.
153 protected InjectedSetting parseServiceInfo(ResolveInfo service, UserHandle userHandle,
154 PackageManager pm) throws XmlPullParserException, IOException {
156 ServiceInfo si = service.serviceInfo;
157 ApplicationInfo ai = si.applicationInfo;
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: "
167 XmlResourceParser parser = null;
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);
175 AttributeSet attrs = Xml.asAttributeSet(parser);
178 while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
179 && type != XmlPullParser.START_TAG) {
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");
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);
195 if (parser != null) {
202 * Returns an immutable representation of the static attributes for the setting, or null.
204 private static InjectedSetting parseAttributes(String packageName, String className,
205 UserHandle userHandle, Resources res, AttributeSet attrs) {
207 TypedArray sa = res.obtainAttributes(attrs, android.R.styleable.SettingInjectorService);
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);
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);
220 return InjectedSetting.newInstance(packageName, className,
221 title, iconId, userHandle, settingsActivity);
228 * Gets a list of preferences that other apps have injected.
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.
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));
249 reloadStatusMessages();
255 * Reloads the status messages for all the preference items.
257 public void reloadStatusMessages() {
258 if (Log.isLoggable(TAG, Log.DEBUG)) {
259 Log.d(TAG, "reloadingStatusMessages: " + mSettings);
261 mHandler.sendMessage(mHandler.obtainMessage(WHAT_RELOAD));
265 * Adds an injected setting to the root.
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;
277 Preference pref = new DimmableIconPreference(mContext, badgedAppLabel);
278 pref.setTitle(info.title);
279 pref.setSummary(null);
281 pref.setOnPreferenceClickListener(new ServiceSettingClickedListener(info));
287 private class ServiceSettingClickedListener
288 implements Preference.OnPreferenceClickListener {
289 private InjectedSetting mInfo;
291 public ServiceSettingClickedListener(InjectedSetting info) {
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);
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
313 private final class StatusLoadingHandler extends Handler {
316 * Settings whose status values need to be loaded. A set is used to prevent redundant loads.
318 private Set<Setting> mSettingsToLoad = new HashSet<Setting>();
321 * Settings that are being loaded now and haven't timed out. In practice this should have
322 * zero or one elements.
324 private Set<Setting> mSettingsBeingLoaded = new HashSet<Setting>();
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.
331 private Set<Setting> mTimedOutSettings = new HashSet<Setting>();
333 private boolean mReloadRequested;
336 public void handleMessage(Message msg) {
337 if (Log.isLoggable(TAG, Log.DEBUG)) {
338 Log.d(TAG, "handleMessage start: " + msg + ", " + this);
341 // Update state in response to message
344 mReloadRequested = true;
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);
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);
363 Log.wtf(TAG, "Unexpected what: " + msg);
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);
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);
384 // Reload requested, so must reload all settings
385 mSettingsToLoad.addAll(mSettings);
386 mReloadRequested = false;
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);
397 Setting setting = iter.next();
400 // Request the status value
401 setting.startService();
402 mSettingsBeingLoaded.add(setting);
404 // Ensure that if receiving the status value takes too long, we start loading the
406 Message timeoutMsg = obtainMessage(WHAT_TIMEOUT, setting);
407 sendMessageDelayed(timeoutMsg, INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS);
409 if (Log.isLoggable(TAG, Log.DEBUG)) {
410 Log.d(TAG, "handleMessage end " + msg + ", " + this
411 + ", started loading " + setting);
416 public String toString() {
417 return "StatusLoadingHandler{" +
418 "mSettingsToLoad=" + mSettingsToLoad +
419 ", mSettingsBeingLoaded=" + mSettingsBeingLoaded +
420 ", mTimedOutSettings=" + mTimedOutSettings +
421 ", mReloadRequested=" + mReloadRequested +
427 * Represents an injected setting and the corresponding preference.
429 private final class Setting {
431 public final InjectedSetting setting;
432 public final Preference preference;
433 public long startMillis;
435 private Setting(InjectedSetting setting, Preference preference) {
436 this.setting = setting;
437 this.preference = preference;
441 public String toString() {
443 "setting=" + setting +
444 ", preference=" + preference +
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.
453 public boolean equals(Object o) {
454 return this == o || o instanceof Setting && setting.equals(((Setting) o).setting);
458 public int hashCode() {
459 return setting.hashCode();
463 * Starts the service to fetch for the current status for the setting, and updates the
464 * preference when the service replies.
466 public void startService() {
467 Handler handler = new Handler() {
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);
475 preference.setSummary(null);
476 preference.setEnabled(enabled);
477 mHandler.sendMessage(
478 mHandler.obtainMessage(WHAT_RECEIVED_STATUS, Setting.this));
481 Messenger messenger = new Messenger(handler);
483 Intent intent = setting.getServiceIntent();
484 intent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger);
486 if (Log.isLoggable(TAG, Log.DEBUG)) {
487 Log.d(TAG, setting + ": sending update intent: " + intent
488 + ", handler: " + handler);
489 startMillis = SystemClock.elapsedRealtime();
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);
499 public long getElapsedTime() {
500 long end = SystemClock.elapsedRealtime();
501 return end - startMillis;
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");