OSDN Git Service

Merge tag 'android-6.0.1_r74' into HEAD
[android-x86/packages-apps-Settings.git] / src / com / android / settings / location / SettingsInjector.java
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.DimmableIconPreference;
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     protected 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 }