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.preference.Preference;
     36 import android.util.AttributeSet;
     37 import android.util.Log;
     38 import android.util.Xml;
     39 import com.android.settings.R;
     40 import org.xmlpull.v1.XmlPullParser;
     41 import org.xmlpull.v1.XmlPullParserException;
     42 
     43 import java.io.IOException;
     44 import java.util.ArrayList;
     45 import java.util.HashSet;
     46 import java.util.Iterator;
     47 import java.util.List;
     48 import java.util.Set;
     49 
     50 /**
     51  * Adds the preferences specified by the {@link InjectedSetting} objects to a preference group.
     52  *
     53  * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}. We do not use that
     54  * class directly because it is not a good match for our use case: we do not need the caching, and
     55  * so do not want the additional resource hit at app install/upgrade time; and we would have to
     56  * suppress the tie-breaking between multiple services reporting settings with the same name.
     57  * Code-sharing would require extracting {@link
     58  * android.content.pm.RegisteredServicesCache#parseServiceAttributes(android.content.res.Resources,
     59  * String, android.util.AttributeSet)} into an interface, which didn't seem worth it.
     60  */
     61 class SettingsInjector {
     62     static final String TAG = "SettingsInjector";
     63 
     64     /**
     65      * If reading the status of a setting takes longer than this, we go ahead and start reading
     66      * the next setting.
     67      */
     68     private static final long INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS = 1000;
     69 
     70     /**
     71      * {@link Message#what} value for starting to load status values
     72      * in case we aren't already in the process of loading them.
     73      */
     74     private static final int WHAT_RELOAD = 1;
     75 
     76     /**
     77      * {@link Message#what} value sent after receiving a status message.
     78      */
     79     private static final int WHAT_RECEIVED_STATUS = 2;
     80 
     81     /**
     82      * {@link Message#what} value sent after the timeout waiting for a status message.
     83      */
     84     private static final int WHAT_TIMEOUT = 3;
     85 
     86     private final Context mContext;
     87 
     88     /**
     89      * The settings that were injected
     90      */
     91     private final Set<Setting> mSettings;
     92 
     93     private final Handler mHandler;
     94 
     95     public SettingsInjector(Context context) {
     96         mContext = context;
     97         mSettings = new HashSet<Setting>();
     98         mHandler = new StatusLoadingHandler();
     99     }
    100 
    101     /**
    102      * Returns a list with one {@link InjectedSetting} object for each {@link android.app.Service}
    103      * that responds to {@link SettingInjectorService#ACTION_SERVICE_INTENT} and provides the
    104      * expected setting metadata.
    105      *
    106      * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}.
    107      *
    108      * TODO: unit test
    109      */
    110     private List<InjectedSetting> getSettings() {
    111         PackageManager pm = mContext.getPackageManager();
    112         Intent intent = new Intent(SettingInjectorService.ACTION_SERVICE_INTENT);
    113 
    114         List<ResolveInfo> resolveInfos =
    115                 pm.queryIntentServices(intent, PackageManager.GET_META_DATA);
    116         if (Log.isLoggable(TAG, Log.DEBUG)) {
    117             Log.d(TAG, "Found services: " + resolveInfos);
    118         }
    119         List<InjectedSetting> settings = new ArrayList<InjectedSetting>(resolveInfos.size());
    120         for (ResolveInfo resolveInfo : resolveInfos) {
    121             try {
    122                 InjectedSetting setting = parseServiceInfo(resolveInfo, pm);
    123                 if (setting == null) {
    124                     Log.w(TAG, "Unable to load service info " + resolveInfo);
    125                 } else {
    126                     settings.add(setting);
    127                 }
    128             } catch (XmlPullParserException e) {
    129                 Log.w(TAG, "Unable to load service info " + resolveInfo, e);
    130             } catch (IOException e) {
    131                 Log.w(TAG, "Unable to load service info " + resolveInfo, e);
    132             }
    133         }
    134         if (Log.isLoggable(TAG, Log.DEBUG)) {
    135             Log.d(TAG, "Loaded settings: " + settings);
    136         }
    137 
    138         return settings;
    139     }
    140 
    141     /**
    142      * Returns the settings parsed from the attributes of the
    143      * {@link SettingInjectorService#META_DATA_NAME} tag, or null.
    144      *
    145      * Duplicates some code from {@link android.content.pm.RegisteredServicesCache}.
    146      */
    147     private static InjectedSetting parseServiceInfo(ResolveInfo service, PackageManager pm)
    148             throws XmlPullParserException, IOException {
    149 
    150         ServiceInfo si = service.serviceInfo;
    151         ApplicationInfo ai = si.applicationInfo;
    152 
    153         if ((ai.flags & ApplicationInfo.FLAG_SYSTEM) == 0) {
    154             if (Log.isLoggable(TAG, Log.WARN)) {
    155                 Log.w(TAG, "Ignoring attempt to inject setting from app not in system image: "
    156                         + service);
    157                 return null;
    158             }
    159         }
    160 
    161         XmlResourceParser parser = null;
    162         try {
    163             parser = si.loadXmlMetaData(pm, SettingInjectorService.META_DATA_NAME);
    164             if (parser == null) {
    165                 throw new XmlPullParserException("No " + SettingInjectorService.META_DATA_NAME
    166                         + " meta-data for " + service + ": " + si);
    167             }
    168 
    169             AttributeSet attrs = Xml.asAttributeSet(parser);
    170 
    171             int type;
    172             while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
    173                     && type != XmlPullParser.START_TAG) {
    174             }
    175 
    176             String nodeName = parser.getName();
    177             if (!SettingInjectorService.ATTRIBUTES_NAME.equals(nodeName)) {
    178                 throw new XmlPullParserException("Meta-data does not start with "
    179                         + SettingInjectorService.ATTRIBUTES_NAME + " tag");
    180             }
    181 
    182             Resources res = pm.getResourcesForApplication(ai);
    183             return parseAttributes(si.packageName, si.name, res, attrs);
    184         } catch (PackageManager.NameNotFoundException e) {
    185             throw new XmlPullParserException(
    186                     "Unable to load resources for package " + si.packageName);
    187         } finally {
    188             if (parser != null) {
    189                 parser.close();
    190             }
    191         }
    192     }
    193 
    194     /**
    195      * Returns an immutable representation of the static attributes for the setting, or null.
    196      */
    197     private static InjectedSetting parseAttributes(
    198             String packageName, String className, Resources res, AttributeSet attrs) {
    199 
    200         TypedArray sa = res.obtainAttributes(attrs, android.R.styleable.SettingInjectorService);
    201         try {
    202             // Note that to help guard against malicious string injection, we do not allow dynamic
    203             // specification of the label (setting title)
    204             final String title = sa.getString(android.R.styleable.SettingInjectorService_title);
    205             final int iconId =
    206                     sa.getResourceId(android.R.styleable.SettingInjectorService_icon, 0);
    207             final String settingsActivity =
    208                     sa.getString(android.R.styleable.SettingInjectorService_settingsActivity);
    209             if (Log.isLoggable(TAG, Log.DEBUG)) {
    210                 Log.d(TAG, "parsed title: " + title + ", iconId: " + iconId
    211                         + ", settingsActivity: " + settingsActivity);
    212             }
    213             return InjectedSetting.newInstance(packageName, className,
    214                     title, iconId, settingsActivity);
    215         } finally {
    216             sa.recycle();
    217         }
    218     }
    219 
    220     /**
    221      * Gets a list of preferences that other apps have injected.
    222      */
    223     public List<Preference> getInjectedSettings() {
    224         Iterable<InjectedSetting> settings = getSettings();
    225         ArrayList<Preference> prefs = new ArrayList<Preference>();
    226         for (InjectedSetting setting : settings) {
    227             Preference pref = addServiceSetting(prefs, setting);
    228             mSettings.add(new Setting(setting, pref));
    229         }
    230 
    231         reloadStatusMessages();
    232 
    233         return prefs;
    234     }
    235 
    236     /**
    237      * Reloads the status messages for all the preference items.
    238      */
    239     public void reloadStatusMessages() {
    240         if (Log.isLoggable(TAG, Log.DEBUG)) {
    241             Log.d(TAG, "reloadingStatusMessages: " + mSettings);
    242         }
    243         mHandler.sendMessage(mHandler.obtainMessage(WHAT_RELOAD));
    244     }
    245 
    246     /**
    247      * Adds an injected setting to the root with status "Loading...".
    248      */
    249     private Preference addServiceSetting(List<Preference> prefs, InjectedSetting info) {
    250         Preference pref = new Preference(mContext);
    251         pref.setTitle(info.title);
    252         pref.setSummary(R.string.location_loading_injected_setting);
    253         PackageManager pm = mContext.getPackageManager();
    254         Drawable icon = pm.getDrawable(info.packageName, info.iconId, null);
    255         pref.setIcon(icon);
    256 
    257         Intent settingIntent = new Intent();
    258         settingIntent.setClassName(info.packageName, info.settingsActivity);
    259         pref.setIntent(settingIntent);
    260 
    261         prefs.add(pref);
    262         return pref;
    263     }
    264 
    265     /**
    266      * Loads the setting status values one at a time. Each load starts a subclass of {@link
    267      * SettingInjectorService}, so to reduce memory pressure we don't want to load too many at
    268      * once.
    269      */
    270     private final class StatusLoadingHandler extends Handler {
    271 
    272         /**
    273          * Settings whose status values need to be loaded. A set is used to prevent redundant loads.
    274          */
    275         private Set<Setting> mSettingsToLoad = new HashSet<Setting>();
    276 
    277         /**
    278          * Settings that are being loaded now and haven't timed out. In practice this should have
    279          * zero or one elements.
    280          */
    281         private Set<Setting> mSettingsBeingLoaded = new HashSet<Setting>();
    282 
    283         /**
    284          * Settings that are being loaded but have timed out. If only one setting has timed out, we
    285          * will go ahead and start loading the next setting so that one slow load won't delay the
    286          * load of the other settings.
    287          */
    288         private Set<Setting> mTimedOutSettings = new HashSet<Setting>();
    289 
    290         private boolean mReloadRequested;
    291 
    292         @Override
    293         public void handleMessage(Message msg) {
    294             if (Log.isLoggable(TAG, Log.DEBUG)) {
    295                 Log.d(TAG, "handleMessage start: " + msg + ", " + this);
    296             }
    297 
    298             // Update state in response to message
    299             switch (msg.what) {
    300                 case WHAT_RELOAD:
    301                     mReloadRequested = true;
    302                     break;
    303                 case WHAT_RECEIVED_STATUS:
    304                     final Setting receivedSetting = (Setting) msg.obj;
    305                     receivedSetting.maybeLogElapsedTime();
    306                     mSettingsBeingLoaded.remove(receivedSetting);
    307                     mTimedOutSettings.remove(receivedSetting);
    308                     removeMessages(WHAT_TIMEOUT, receivedSetting);
    309                     break;
    310                 case WHAT_TIMEOUT:
    311                     final Setting timedOutSetting = (Setting) msg.obj;
    312                     mSettingsBeingLoaded.remove(timedOutSetting);
    313                     mTimedOutSettings.add(timedOutSetting);
    314                     if (Log.isLoggable(TAG, Log.WARN)) {
    315                         Log.w(TAG, "Timed out after " + timedOutSetting.getElapsedTime()
    316                                 + " millis trying to get status for: " + timedOutSetting);
    317                     }
    318                     break;
    319                 default:
    320                     Log.wtf(TAG, "Unexpected what: " + msg);
    321             }
    322 
    323             // Decide whether to load additional settings based on the new state. Start by seeing
    324             // if we have headroom to load another setting.
    325             if (mSettingsBeingLoaded.size() > 0 || mTimedOutSettings.size() > 1) {
    326                 // Don't load any more settings until one of the pending settings has completed.
    327                 // To reduce memory pressure, we want to be loading at most one setting (plus at
    328                 // most one timed-out setting) at a time. This means we'll be responsible for
    329                 // bringing in at most two services.
    330                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    331                     Log.v(TAG, "too many services already live for " + msg + ", " + this);
    332                 }
    333                 return;
    334             }
    335 
    336             if (mReloadRequested && mSettingsToLoad.isEmpty() && mSettingsBeingLoaded.isEmpty()
    337                     && mTimedOutSettings.isEmpty()) {
    338                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    339                     Log.v(TAG, "reloading because idle and reload requesteed " + msg + ", " + this);
    340                 }
    341                 // Reload requested, so must reload all settings
    342                 mSettingsToLoad.addAll(mSettings);
    343                 mReloadRequested = false;
    344             }
    345 
    346             // Remove the next setting to load from the queue, if any
    347             Iterator<Setting> iter = mSettingsToLoad.iterator();
    348             if (!iter.hasNext()) {
    349                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    350                     Log.v(TAG, "nothing left to do for " + msg + ", " + this);
    351                 }
    352                 return;
    353             }
    354             Setting setting = iter.next();
    355             iter.remove();
    356 
    357             // Request the status value
    358             setting.startService();
    359             mSettingsBeingLoaded.add(setting);
    360 
    361             // Ensure that if receiving the status value takes too long, we start loading the
    362             // next value anyway
    363             Message timeoutMsg = obtainMessage(WHAT_TIMEOUT, setting);
    364             sendMessageDelayed(timeoutMsg, INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS);
    365 
    366             if (Log.isLoggable(TAG, Log.DEBUG)) {
    367                 Log.d(TAG, "handleMessage end " + msg + ", " + this
    368                         + ", started loading " + setting);
    369             }
    370         }
    371 
    372         @Override
    373         public String toString() {
    374             return "StatusLoadingHandler{" +
    375                     "mSettingsToLoad=" + mSettingsToLoad +
    376                     ", mSettingsBeingLoaded=" + mSettingsBeingLoaded +
    377                     ", mTimedOutSettings=" + mTimedOutSettings +
    378                     ", mReloadRequested=" + mReloadRequested +
    379                     '}';
    380         }
    381     }
    382 
    383     /**
    384      * Represents an injected setting and the corresponding preference.
    385      */
    386     private final class Setting {
    387 
    388         public final InjectedSetting setting;
    389         public final Preference preference;
    390         public long startMillis;
    391 
    392         private Setting(InjectedSetting setting, Preference preference) {
    393             this.setting = setting;
    394             this.preference = preference;
    395         }
    396 
    397         @Override
    398         public String toString() {
    399             return "Setting{" +
    400                     "setting=" + setting +
    401                     ", preference=" + preference +
    402                     '}';
    403         }
    404 
    405         /**
    406          * Returns true if they both have the same {@link #setting} value. Ignores mutable
    407          * {@link #preference} and {@link #startMillis} so that it's safe to use in sets.
    408          */
    409         @Override
    410         public boolean equals(Object o) {
    411             return this == o || o instanceof Setting && setting.equals(((Setting) o).setting);
    412         }
    413 
    414         @Override
    415         public int hashCode() {
    416             return setting.hashCode();
    417         }
    418 
    419         /**
    420          * Starts the service to fetch for the current status for the setting, and updates the
    421          * preference when the service replies.
    422          */
    423         public void startService() {
    424             Handler handler = new Handler() {
    425                 @Override
    426                 public void handleMessage(Message msg) {
    427                     Bundle bundle = msg.getData();
    428                     String summary = bundle.getString(SettingInjectorService.SUMMARY_KEY);
    429                     boolean enabled = bundle.getBoolean(SettingInjectorService.ENABLED_KEY, true);
    430                     if (Log.isLoggable(TAG, Log.DEBUG)) {
    431                         Log.d(TAG, setting + ": received " + msg + ", bundle: " + bundle);
    432                     }
    433                     preference.setSummary(summary);
    434                     preference.setEnabled(enabled);
    435                     mHandler.sendMessage(
    436                             mHandler.obtainMessage(WHAT_RECEIVED_STATUS, Setting.this));
    437                 }
    438             };
    439             Messenger messenger = new Messenger(handler);
    440 
    441             Intent intent = setting.getServiceIntent();
    442             intent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger);
    443 
    444             if (Log.isLoggable(TAG, Log.DEBUG)) {
    445                 Log.d(TAG, setting + ": sending update intent: " + intent
    446                         + ", handler: " + handler);
    447                 startMillis = SystemClock.elapsedRealtime();
    448             } else {
    449                 startMillis = 0;
    450             }
    451 
    452             // Start the service, making sure that this is attributed to the current user rather
    453             // than the system user.
    454             mContext.startServiceAsUser(intent, android.os.Process.myUserHandle());
    455         }
    456 
    457         public long getElapsedTime() {
    458             long end = SystemClock.elapsedRealtime();
    459             return end - startMillis;
    460         }
    461 
    462         public void maybeLogElapsedTime() {
    463             if (Log.isLoggable(TAG, Log.DEBUG) && startMillis != 0) {
    464                 long elapsed = getElapsedTime();
    465                 Log.d(TAG, this + " update took " + elapsed + " millis");
    466             }
    467         }
    468     }
    469 }
    470