• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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