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.
    248      */
    249     private Preference addServiceSetting(List<Preference> prefs, InjectedSetting info) {
    250         Preference pref = new DimmableIconPreference(mContext);
    251         pref.setTitle(info.title);
    252         pref.setSummary(null);
    253         PackageManager pm = mContext.getPackageManager();
    254         Drawable icon = pm.getDrawable(info.packageName, info.iconId, null);
    255         pref.setIcon(icon);
    256 
    257         // Activity to start if they click on the preference. Must start in new task to ensure
    258         // that "android.settings.LOCATION_SOURCE_SETTINGS" brings user back to Settings > Location.
    259         Intent settingIntent = new Intent();
    260         settingIntent.setClassName(info.packageName, info.settingsActivity);
    261         settingIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
    262         pref.setIntent(settingIntent);
    263 
    264         prefs.add(pref);
    265         return pref;
    266     }
    267 
    268     /**
    269      * Loads the setting status values one at a time. Each load starts a subclass of {@link
    270      * SettingInjectorService}, so to reduce memory pressure we don't want to load too many at
    271      * once.
    272      */
    273     private final class StatusLoadingHandler extends Handler {
    274 
    275         /**
    276          * Settings whose status values need to be loaded. A set is used to prevent redundant loads.
    277          */
    278         private Set<Setting> mSettingsToLoad = new HashSet<Setting>();
    279 
    280         /**
    281          * Settings that are being loaded now and haven't timed out. In practice this should have
    282          * zero or one elements.
    283          */
    284         private Set<Setting> mSettingsBeingLoaded = new HashSet<Setting>();
    285 
    286         /**
    287          * Settings that are being loaded but have timed out. If only one setting has timed out, we
    288          * will go ahead and start loading the next setting so that one slow load won't delay the
    289          * load of the other settings.
    290          */
    291         private Set<Setting> mTimedOutSettings = new HashSet<Setting>();
    292 
    293         private boolean mReloadRequested;
    294 
    295         @Override
    296         public void handleMessage(Message msg) {
    297             if (Log.isLoggable(TAG, Log.DEBUG)) {
    298                 Log.d(TAG, "handleMessage start: " + msg + ", " + this);
    299             }
    300 
    301             // Update state in response to message
    302             switch (msg.what) {
    303                 case WHAT_RELOAD:
    304                     mReloadRequested = true;
    305                     break;
    306                 case WHAT_RECEIVED_STATUS:
    307                     final Setting receivedSetting = (Setting) msg.obj;
    308                     receivedSetting.maybeLogElapsedTime();
    309                     mSettingsBeingLoaded.remove(receivedSetting);
    310                     mTimedOutSettings.remove(receivedSetting);
    311                     removeMessages(WHAT_TIMEOUT, receivedSetting);
    312                     break;
    313                 case WHAT_TIMEOUT:
    314                     final Setting timedOutSetting = (Setting) msg.obj;
    315                     mSettingsBeingLoaded.remove(timedOutSetting);
    316                     mTimedOutSettings.add(timedOutSetting);
    317                     if (Log.isLoggable(TAG, Log.WARN)) {
    318                         Log.w(TAG, "Timed out after " + timedOutSetting.getElapsedTime()
    319                                 + " millis trying to get status for: " + timedOutSetting);
    320                     }
    321                     break;
    322                 default:
    323                     Log.wtf(TAG, "Unexpected what: " + msg);
    324             }
    325 
    326             // Decide whether to load additional settings based on the new state. Start by seeing
    327             // if we have headroom to load another setting.
    328             if (mSettingsBeingLoaded.size() > 0 || mTimedOutSettings.size() > 1) {
    329                 // Don't load any more settings until one of the pending settings has completed.
    330                 // To reduce memory pressure, we want to be loading at most one setting (plus at
    331                 // most one timed-out setting) at a time. This means we'll be responsible for
    332                 // bringing in at most two services.
    333                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    334                     Log.v(TAG, "too many services already live for " + msg + ", " + this);
    335                 }
    336                 return;
    337             }
    338 
    339             if (mReloadRequested && mSettingsToLoad.isEmpty() && mSettingsBeingLoaded.isEmpty()
    340                     && mTimedOutSettings.isEmpty()) {
    341                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    342                     Log.v(TAG, "reloading because idle and reload requesteed " + msg + ", " + this);
    343                 }
    344                 // Reload requested, so must reload all settings
    345                 mSettingsToLoad.addAll(mSettings);
    346                 mReloadRequested = false;
    347             }
    348 
    349             // Remove the next setting to load from the queue, if any
    350             Iterator<Setting> iter = mSettingsToLoad.iterator();
    351             if (!iter.hasNext()) {
    352                 if (Log.isLoggable(TAG, Log.VERBOSE)) {
    353                     Log.v(TAG, "nothing left to do for " + msg + ", " + this);
    354                 }
    355                 return;
    356             }
    357             Setting setting = iter.next();
    358             iter.remove();
    359 
    360             // Request the status value
    361             setting.startService();
    362             mSettingsBeingLoaded.add(setting);
    363 
    364             // Ensure that if receiving the status value takes too long, we start loading the
    365             // next value anyway
    366             Message timeoutMsg = obtainMessage(WHAT_TIMEOUT, setting);
    367             sendMessageDelayed(timeoutMsg, INJECTED_STATUS_UPDATE_TIMEOUT_MILLIS);
    368 
    369             if (Log.isLoggable(TAG, Log.DEBUG)) {
    370                 Log.d(TAG, "handleMessage end " + msg + ", " + this
    371                         + ", started loading " + setting);
    372             }
    373         }
    374 
    375         @Override
    376         public String toString() {
    377             return "StatusLoadingHandler{" +
    378                     "mSettingsToLoad=" + mSettingsToLoad +
    379                     ", mSettingsBeingLoaded=" + mSettingsBeingLoaded +
    380                     ", mTimedOutSettings=" + mTimedOutSettings +
    381                     ", mReloadRequested=" + mReloadRequested +
    382                     '}';
    383         }
    384     }
    385 
    386     /**
    387      * Represents an injected setting and the corresponding preference.
    388      */
    389     private final class Setting {
    390 
    391         public final InjectedSetting setting;
    392         public final Preference preference;
    393         public long startMillis;
    394 
    395         private Setting(InjectedSetting setting, Preference preference) {
    396             this.setting = setting;
    397             this.preference = preference;
    398         }
    399 
    400         @Override
    401         public String toString() {
    402             return "Setting{" +
    403                     "setting=" + setting +
    404                     ", preference=" + preference +
    405                     '}';
    406         }
    407 
    408         /**
    409          * Returns true if they both have the same {@link #setting} value. Ignores mutable
    410          * {@link #preference} and {@link #startMillis} so that it's safe to use in sets.
    411          */
    412         @Override
    413         public boolean equals(Object o) {
    414             return this == o || o instanceof Setting && setting.equals(((Setting) o).setting);
    415         }
    416 
    417         @Override
    418         public int hashCode() {
    419             return setting.hashCode();
    420         }
    421 
    422         /**
    423          * Starts the service to fetch for the current status for the setting, and updates the
    424          * preference when the service replies.
    425          */
    426         public void startService() {
    427             Handler handler = new Handler() {
    428                 @Override
    429                 public void handleMessage(Message msg) {
    430                     Bundle bundle = msg.getData();
    431                     boolean enabled = bundle.getBoolean(SettingInjectorService.ENABLED_KEY, true);
    432                     if (Log.isLoggable(TAG, Log.DEBUG)) {
    433                         Log.d(TAG, setting + ": received " + msg + ", bundle: " + bundle);
    434                     }
    435                     preference.setSummary(null);
    436                     preference.setEnabled(enabled);
    437                     mHandler.sendMessage(
    438                             mHandler.obtainMessage(WHAT_RECEIVED_STATUS, Setting.this));
    439                 }
    440             };
    441             Messenger messenger = new Messenger(handler);
    442 
    443             Intent intent = setting.getServiceIntent();
    444             intent.putExtra(SettingInjectorService.MESSENGER_KEY, messenger);
    445 
    446             if (Log.isLoggable(TAG, Log.DEBUG)) {
    447                 Log.d(TAG, setting + ": sending update intent: " + intent
    448                         + ", handler: " + handler);
    449                 startMillis = SystemClock.elapsedRealtime();
    450             } else {
    451                 startMillis = 0;
    452             }
    453 
    454             // Start the service, making sure that this is attributed to the current user rather
    455             // than the system user.
    456             mContext.startServiceAsUser(intent, android.os.Process.myUserHandle());
    457         }
    458 
    459         public long getElapsedTime() {
    460             long end = SystemClock.elapsedRealtime();
    461             return end - startMillis;
    462         }
    463 
    464         public void maybeLogElapsedTime() {
    465             if (Log.isLoggable(TAG, Log.DEBUG) && startMillis != 0) {
    466                 long elapsed = getElapsedTime();
    467                 Log.d(TAG, this + " update took " + elapsed + " millis");
    468             }
    469         }
    470     }
    471 }
    472