Home | History | Annotate | Download | only in location
      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 
    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      */
    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      */
    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      */
    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      */
    234     public List<Preference> getInjectedSettings(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<Preference>();
    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(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      */
    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      */
    268     private Preference addServiceSetting(List<Preference> prefs, InjectedSetting info) {
    269         PackageManager pm = mContext.getPackageManager();
    270         Drawable appIcon = pm.getDrawable(info.packageName, info.iconId, null);
    271         Drawable icon = pm.getUserBadgedIcon(appIcon, info.mUserHandle);
    272         CharSequence badgedAppLabel = pm.getUserBadgedLabel(info.title, info.mUserHandle);
    273         if (info.title.contentEquals(badgedAppLabel)) {
    274             // If badged label is not different from original then no need for it as
    275             // a separate content description.
    276             badgedAppLabel = null;
    277         }
    278         Preference pref = new DimmableIconPreference(mContext, badgedAppLabel);
    279         pref.setTitle(info.title);
    280         pref.setSummary(null);
    281         pref.setIcon(icon);
    282         pref.setOnPreferenceClickListener(new ServiceSettingClickedListener(info));
    283 
    284         prefs.add(pref);
    285         return pref;
    286     }
    287 
    288     private class ServiceSettingClickedListener
    289             implements Preference.OnPreferenceClickListener {
    290         private InjectedSetting mInfo;
    291 
    292         public ServiceSettingClickedListener(InjectedSetting info) {
    293             mInfo = info;
    294         }
    295 
    296         @Override
    297         public boolean onPreferenceClick(Preference preference) {
    298             // Activity to start if they click on the preference. Must start in new task to ensure
    299             // that "android.settings.LOCATION_SOURCE_SETTINGS" brings user back to
    300             // Settings > Location.
    301             Intent settingIntent = new Intent();
    302             settingIntent.setClassName(mInfo.packageName, mInfo.settingsActivity);
    303             // Sometimes the user may navigate back to "Settings" and launch another different
    304             // injected setting after one injected setting has been launched.
    305             //
    306             // FLAG_ACTIVITY_CLEAR_TOP allows multiple Activities to stack on each other. When
    307             // "back" button is clicked, the user will navigate through all the injected settings
    308             // launched before. Such behavior could be quite confusing sometimes.
    309             //
    310             // In order to avoid such confusion, we use FLAG_ACTIVITY_CLEAR_TASK, which always clear
    311             // up all existing injected settings and make sure that "back" button always brings the
    312             // user back to "Settings" directly.
    313             settingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
    314             mContext.startActivityAsUser(settingIntent, mInfo.mUserHandle);
    315             return true;
    316         }
    317     }
    318 
    319     /**
    320      * Loads the setting status values one at a time. Each load starts a subclass of {@link
    321      * SettingInjectorService}, so to reduce memory pressure we don't want to load too many at
    322      * once.
    323      */
    324     private final class StatusLoadingHandler extends Handler {
    325 
    326         /**
    327          * Settings whose status values need to be loaded. A set is used to prevent redundant loads.
    328          */
    329         private Set<Setting> mSettingsToLoad = new HashSet<Setting>();
    330 
    331         /**
    332          * Settings that are being loaded now and haven't timed out. In practice this should have
    333          * zero or one elements.
    334          */
    335         private Set<Setting> mSettingsBeingLoaded = new HashSet<Setting>();
    336 
    337         /**
    338          * Settings that are being loaded but have timed out. If only one setting has timed out, we
    339          * will go ahead and start loading the next setting so that one slow load won't delay the
    340          * load of the other settings.
    341          */
    342         private Set<Setting> mTimedOutSettings = new HashSet<Setting>();
    343 
    344         private boolean mReloadRequested;
    345 
    346         @Override
    347         public void handleMessage(Message msg) {
    348             if (Log.isLoggable(TAG, Log.DEBUG)) {
    349                 Log.d(TAG, "handleMessage start: " + msg + ", " + this);
    350             }
    351 
    352             // Update state in response to message
    353             switch (msg.what) {
    354                 case WHAT_RELOAD:
    355                     mReloadRequested = true;
    356                     break;
    357                 case WHAT_RECEIVED_STATUS:
    358                     final Setting receivedSetting = (Setting) msg.obj;
    359                     receivedSetting.maybeLogElapsedTime();
    360                     mSettingsBeingLoaded.remove(receivedSetting);
    361                     mTimedOutSettings.remove(receivedSetting);
    362                     removeMessages(WHAT_TIMEOUT, receivedSetting);
    363                     break;
    364                 case WHAT_TIMEOUT:
    365                     final Setting timedOutSetting = (Setting) msg.obj;
    366                     mSettingsBeingLoaded.remove(timedOutSetting);
    367                     mTimedOutSettings.add(timedOutSetting);
    368                     if (Log.isLoggable(TAG, Log.WARN)) {
    369                         Log.w(TAG, "Timed out after " + timedOutSetting.getElapsedTime()
    370                                 + " millis trying to get status for: " + timedOutSetting);
    371                     }
    372                     break;
    373                 default:
    374                     Log.wtf(TAG, "Unexpected what: " + msg);
    375             }
    376 
    377             // Decide whether to load additional settings based on the new state. Start by seeing
    378             // if we have headroom to load another setting.
    379             if (mSettingsBeingLoaded.size() > 0 || mTimedOutSettings.size() > 1) {
    380                 // Don't load any more settings until one of the pending settings has completed.
    381                 // To reduce memory pressure, we want to be loading at most one setting (plus at
    382                 // most one timed-out setting) at a time. This means we'll be responsible for
    383                 // bringing in at most two services.
    384                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    385                     Log.v(TAG, "too many services already live for " + msg + ", " + this);
    386                 }
    387                 return;
    388             }
    389 
    390             if (mReloadRequested && mSettingsToLoad.isEmpty() && mSettingsBeingLoaded.isEmpty()
    391                     && mTimedOutSettings.isEmpty()) {
    392                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    393                     Log.v(TAG, "reloading because idle and reload requesteed " + msg + ", " + this);
    394                 }
    395                 // Reload requested, so must reload all settings
    396                 mSettingsToLoad.addAll(mSettings);
    397                 mReloadRequested = false;
    398             }
    399 
    400             // Remove the next setting to load from the queue, if any
    401             Iterator<Setting> iter = mSettingsToLoad.iterator();
    402             if (!iter.hasNext()) {
    403                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    404                     Log.v(TAG, "nothing left to do for " + msg + ", " + this);
    405                 }
    406                 return;
    407             }
    408             Setting setting = iter.next();
    409             iter.remove();
    410 
    411             // Request the status value
    412             setting.startService();
    413             mSettingsBeingLoaded.add(setting);
    414 
    415             // Ensure that if receiving the status value takes too long, we start loading the
    416             // next value anyway
    417             Message timeoutMsg = obtainMessage(WHAT_TIMEOUT, setting);
    418             sendMessageDelayed(timeoutMsg, INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS);
    419 
    420             if (Log.isLoggable(TAG, Log.DEBUG)) {
    421                 Log.d(TAG, "handleMessage end " + msg + ", " + this
    422                         + ", started loading " + setting);
    423             }
    424         }
    425 
    426         @Override
    427         public String toString() {
    428             return "StatusLoadingHandler{" +
    429                     "mSettingsToLoad=" + mSettingsToLoad +
    430                     ", mSettingsBeingLoaded=" + mSettingsBeingLoaded +
    431                     ", mTimedOutSettings=" + mTimedOutSettings +
    432                     ", mReloadRequested=" + mReloadRequested +
    433                     '}';
    434         }
    435     }
    436 
    437     /**
    438      * Represents an injected setting and the corresponding preference.
    439      */
    440     private final class Setting {
    441 
    442         public final InjectedSetting setting;
    443         public final Preference preference;
    444         public long startMillis;
    445 
    446         private Setting(InjectedSetting setting, Preference preference) {
    447             this.setting = setting;
    448             this.preference = preference;
    449         }
    450 
    451         @Override
    452         public String toString() {
    453             return "Setting{" +
    454                     "setting=" + setting +
    455                     ", preference=" + preference +
    456                     '}';
    457         }
    458 
    459         /**
    460          * Returns true if they both have the same {@link #setting} value. Ignores mutable
    461          * {@link #preference} and {@link #startMillis} so that it's safe to use in sets.
    462          */
    463         @Override
    464         public boolean equals(Object o) {
    465             return this == o || o instanceof Setting && setting.equals(((Setting) o).setting);
    466         }
    467 
    468         @Override
    469         public int hashCode() {
    470             return setting.hashCode();
    471         }
    472 
    473         /**
    474          * Starts the service to fetch for the current status for the setting, and updates the
    475          * preference when the service replies.
    476          */
    477         public void startService() {
    478             final ActivityManager am = (ActivityManager)
    479                     mContext.getSystemService(Context.ACTIVITY_SERVICE);
    480             if (!am.isUserRunning(setting.mUserHandle.getIdentifier())) {
    481                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    482                     Log.v(TAG, "Cannot start service as user "
    483                             + setting.mUserHandle.getIdentifier() + " is not running");
    484                 }
    485                 return;
    486             }
    487             Handler handler = new Handler() {
    488                 @Override
    489                 public void handleMessage(Message msg) {
    490                     Bundle bundle = msg.getData();
    491                     boolean enabled = bundle.getBoolean(SettingInjectorService.ENABLED_KEY, true);
    492                     if (Log.isLoggable(TAG, Log.DEBUG)) {
    493                         Log.d(TAG, setting + ": received " + msg + ", bundle: " + bundle);
    494                     }
    495                     preference.setSummary(null);
    496                     preference.setEnabled(enabled);
    497                     mHandler.sendMessage(
    498                             mHandler.obtainMessage(WHAT_RECEIVED_STATUS, Setting.this));
    499                 }
    500             };
    501             Messenger messenger = new Messenger(handler);
    502 
    503             Intent intent = setting.getServiceIntent();
    504             intent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger);
    505 
    506             if (Log.isLoggable(TAG, Log.DEBUG)) {
    507                 Log.d(TAG, setting + ": sending update intent: " + intent
    508                         + ", handler: " + handler);
    509                 startMillis = SystemClock.elapsedRealtime();
    510             } else {
    511                 startMillis = 0;
    512             }
    513 
    514             // Start the service, making sure that this is attributed to the user associated with
    515             // the setting rather than the system user.
    516             mContext.startServiceAsUser(intent, setting.mUserHandle);
    517         }
    518 
    519         public long getElapsedTime() {
    520             long end = SystemClock.elapsedRealtime();
    521             return end - startMillis;
    522         }
    523 
    524         public void maybeLogElapsedTime() {
    525             if (Log.isLoggable(TAG, Log.DEBUG) && startMillis != 0) {
    526                 long elapsed = getElapsedTime();
    527                 Log.d(TAG, this + " update took " + elapsed + " millis");
    528             }
    529         }
    530     }
    531 }
    532