• 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.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