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