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.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;
     41 
     42 import com.android.settings.R;
     43 
     44 import org.xmlpull.v1.XmlPullParser;
     45 import org.xmlpull.v1.XmlPullParserException;
     46 
     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;
     52 import java.util.Set;
     53 
     54 /**
     55  * Adds the preferences specified by the {@link InjectedSetting} objects to a preference group.
     56  *
     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.
     64  */
     65 class SettingsInjector {
     66     static final String TAG = "SettingsInjector";
     67 
     68     /**
     69      * If reading the status of a setting takes longer than this, we go ahead and start reading
     70      * the next setting.
     71      */
     72     private static final long INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS = 1000;
     73 
     74     /**
     75      * {@link Message#what} value for starting to load status values
     76      * in case we aren't already in the process of loading them.
     77      */
     78     private static final int WHAT_RELOAD = 1;
     79 
     80     /**
     81      * {@link Message#what} value sent after receiving a status message.
     82      */
     83     private static final int WHAT_RECEIVED_STATUS = 2;
     84 
     85     /**
     86      * {@link Message#what} value sent after the timeout waiting for a status message.
     87      */
     88     private static final int WHAT_TIMEOUT = 3;
     89 
     90     private final Context mContext;
     91 
     92     /**
     93      * The settings that were injected
     94      */
     95     private final Set<Setting> mSettings;
     96 
     97     private final Handler mHandler;
     98 
     99     public SettingsInjector(Context context) {
    100         mContext = context;
    101         mSettings = new HashSet<Setting>();
    102         mHandler = new StatusLoadingHandler();
    103     }
    104 
    105     /**
    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
    109      * metadata.
    110      *
    111      * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}.
    112      *
    113      * TODO: unit test
    114      */
    115     private List<InjectedSetting> getSettings(final UserHandle userHandle) {
    116         PackageManager pm = mContext.getPackageManager();
    117         Intent intent = new Intent(SettingInjectorService.ACTION_SERVICE_INTENT);
    118 
    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);
    124         }
    125         List<InjectedSetting> settings = new ArrayList<InjectedSetting>(resolveInfos.size());
    126         for (ResolveInfo resolveInfo : resolveInfos) {
    127             try {
    128                 InjectedSetting setting = parseServiceInfo(resolveInfo, userHandle, pm);
    129                 if (setting == null) {
    130                     Log.w(TAG, "Unable to load service info " + resolveInfo);
    131                 } else {
    132                     settings.add(setting);
    133                 }
    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);
    138             }
    139         }
    140         if (Log.isLoggable(TAG, Log.DEBUG)) {
    141             Log.d(TAG, "Loaded settings for profile id " + profileId + ": " + settings);
    142         }
    143 
    144         return settings;
    145     }
    146 
    147     /**
    148      * Returns the settings parsed from the attributes of the
    149      * {@link SettingInjectorService#META_DATA_NAME} tag, or null.
    150      *
    151      * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}.
    152      */
    153     private static InjectedSetting parseServiceInfo(ResolveInfo service, UserHandle userHandle,
    154             PackageManager pm) throws XmlPullParserException, IOException {
    155 
    156         ServiceInfo si = service.serviceInfo;
    157         ApplicationInfo ai = si.applicationInfo;
    158 
    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: "
    162                         + service);
    163                 return null;
    164             }
    165         }
    166 
    167         XmlResourceParser parser = null;
    168         try {
    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);
    173             }
    174 
    175             AttributeSet attrs = Xml.asAttributeSet(parser);
    176 
    177             int type;
    178             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
    179                     && type != XmlPullParser.START_TAG) {
    180             }
    181 
    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");
    186             }
    187 
    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);
    194         } finally {
    195             if (parser != null) {
    196                 parser.close();
    197             }
    198         }
    199     }
    200 
    201     /**
    202      * Returns an immutable representation of the static attributes for the setting, or null.
    203      */
    204     private static InjectedSetting parseAttributes(String packageName, String className,
    205             UserHandle userHandle, Resources res, AttributeSet attrs) {
    206 
    207         TypedArray sa = res.obtainAttributes(attrs, android.R.styleable.SettingInjectorService);
    208         try {
    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);
    212             final int iconId =
    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);
    219             }
    220             return InjectedSetting.newInstance(packageName, className,
    221                     title, iconId, userHandle, settingsActivity);
    222         } finally {
    223             sa.recycle();
    224         }
    225     }
    226 
    227     /**
    228      * Gets a list of preferences that other apps have injected.
    229      *
    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.
    232      */
    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));
    245                 }
    246             }
    247         }
    248 
    249         reloadStatusMessages();
    250 
    251         return prefs;
    252     }
    253 
    254     /**
    255      * Reloads the status messages for all the preference items.
    256      */
    257     public void reloadStatusMessages() {
    258         if (Log.isLoggable(TAG, Log.DEBUG)) {
    259             Log.d(TAG, "reloadingStatusMessages: " + mSettings);
    260         }
    261         mHandler.sendMessage(mHandler.obtainMessage(WHAT_RELOAD));
    262     }
    263 
    264     /**
    265      * Adds an injected setting to the root.
    266      */
    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;
    276         }
    277         Preference pref = new DimmableIconPreference(mContext, badgedAppLabel);
    278         pref.setTitle(info.title);
    279         pref.setSummary(null);
    280         pref.setIcon(icon);
    281         pref.setOnPreferenceClickListener(new ServiceSettingClickedListener(info));
    282 
    283         prefs.add(pref);
    284         return pref;
    285     }
    286 
    287     private class ServiceSettingClickedListener
    288             implements Preference.OnPreferenceClickListener {
    289         private InjectedSetting mInfo;
    290 
    291         public ServiceSettingClickedListener(InjectedSetting info) {
    292             mInfo = info;
    293         }
    294 
    295         @Override
    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);
    304             return true;
    305         }
    306     }
    307 
    308     /**
    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
    311      * once.
    312      */
    313     private final class StatusLoadingHandler extends Handler {
    314 
    315         /**
    316          * Settings whose status values need to be loaded. A set is used to prevent redundant loads.
    317          */
    318         private Set<Setting> mSettingsToLoad = new HashSet<Setting>();
    319 
    320         /**
    321          * Settings that are being loaded now and haven't timed out. In practice this should have
    322          * zero or one elements.
    323          */
    324         private Set<Setting> mSettingsBeingLoaded = new HashSet<Setting>();
    325 
    326         /**
    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.
    330          */
    331         private Set<Setting> mTimedOutSettings = new HashSet<Setting>();
    332 
    333         private boolean mReloadRequested;
    334 
    335         @Override
    336         public void handleMessage(Message msg) {
    337             if (Log.isLoggable(TAG, Log.DEBUG)) {
    338                 Log.d(TAG, "handleMessage start: " + msg + ", " + this);
    339             }
    340 
    341             // Update state in response to message
    342             switch (msg.what) {
    343                 case WHAT_RELOAD:
    344                     mReloadRequested = true;
    345                     break;
    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);
    352                     break;
    353                 case WHAT_TIMEOUT:
    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);
    360                     }
    361                     break;
    362                 default:
    363                     Log.wtf(TAG, "Unexpected what: " + msg);
    364             }
    365 
    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);
    375                 }
    376                 return;
    377             }
    378 
    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);
    383                 }
    384                 // Reload requested, so must reload all settings
    385                 mSettingsToLoad.addAll(mSettings);
    386                 mReloadRequested = false;
    387             }
    388 
    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);
    394                 }
    395                 return;
    396             }
    397             Setting setting = iter.next();
    398             iter.remove();
    399 
    400             // Request the status value
    401             setting.startService();
    402             mSettingsBeingLoaded.add(setting);
    403 
    404             // Ensure that if receiving the status value takes too long, we start loading the
    405             // next value anyway
    406             Message timeoutMsg = obtainMessage(WHAT_TIMEOUT, setting);
    407             sendMessageDelayed(timeoutMsg, INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS);
    408 
    409             if (Log.isLoggable(TAG, Log.DEBUG)) {
    410                 Log.d(TAG, "handleMessage end " + msg + ", " + this
    411                         + ", started loading " + setting);
    412             }
    413         }
    414 
    415         @Override
    416         public String toString() {
    417             return "StatusLoadingHandler{" +
    418                     "mSettingsToLoad=" + mSettingsToLoad +
    419                     ", mSettingsBeingLoaded=" + mSettingsBeingLoaded +
    420                     ", mTimedOutSettings=" + mTimedOutSettings +
    421                     ", mReloadRequested=" + mReloadRequested +
    422                     '}';
    423         }
    424     }
    425 
    426     /**
    427      * Represents an injected setting and the corresponding preference.
    428      */
    429     private final class Setting {
    430 
    431         public final InjectedSetting setting;
    432         public final Preference preference;
    433         public long startMillis;
    434 
    435         private Setting(InjectedSetting setting, Preference preference) {
    436             this.setting = setting;
    437             this.preference = preference;
    438         }
    439 
    440         @Override
    441         public String toString() {
    442             return "Setting{" +
    443                     "setting=" + setting +
    444                     ", preference=" + preference +
    445                     '}';
    446         }
    447 
    448         /**
    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.
    451          */
    452         @Override
    453         public boolean equals(Object o) {
    454             return this == o || o instanceof Setting && setting.equals(((Setting) o).setting);
    455         }
    456 
    457         @Override
    458         public int hashCode() {
    459             return setting.hashCode();
    460         }
    461 
    462         /**
    463          * Starts the service to fetch for the current status for the setting, and updates the
    464          * preference when the service replies.
    465          */
    466         public void startService() {
    467             Handler handler = new Handler() {
    468                 @Override
    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);
    474                     }
    475                     preference.setSummary(null);
    476                     preference.setEnabled(enabled);
    477                     mHandler.sendMessage(
    478                             mHandler.obtainMessage(WHAT_RECEIVED_STATUS, Setting.this));
    479                 }
    480             };
    481             Messenger messenger = new Messenger(handler);
    482 
    483             Intent intent = setting.getServiceIntent();
    484             intent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger);
    485 
    486             if (Log.isLoggable(TAG, Log.DEBUG)) {
    487                 Log.d(TAG, setting + ": sending update intent: " + intent
    488                         + ", handler: " + handler);
    489                 startMillis = SystemClock.elapsedRealtime();
    490             } else {
    491                 startMillis = 0;
    492             }
    493 
    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);
    497         }
    498 
    499         public long getElapsedTime() {
    500             long end = SystemClock.elapsedRealtime();
    501             return end - startMillis;
    502         }
    503 
    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");
    508             }
    509         }
    510     }
    511 }
    512