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