1 /*
2  * Copyright (C) 2014 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.search;
18 
19 import android.accessibilityservice.AccessibilityService;
20 import android.accessibilityservice.AccessibilityServiceInfo;
21 import android.app.Activity;
22 import android.app.LoaderManager;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.Loader;
27 import android.content.pm.PackageManager;
28 import android.content.pm.ResolveInfo;
29 import android.content.pm.ServiceInfo;
30 import android.database.ContentObserver;
31 import android.hardware.input.InputManager;
32 import android.net.Uri;
33 import android.os.Bundle;
34 import android.os.UserHandle;
35 import android.os.UserManager;
36 import android.print.PrintManager;
37 import android.print.PrintServicesLoader;
38 import android.printservice.PrintServiceInfo;
39 import android.provider.Settings;
40 import android.provider.UserDictionary;
41 import android.support.annotation.Nullable;
42 import android.support.annotation.VisibleForTesting;
43 import android.util.Log;
44 import android.view.accessibility.AccessibilityManager;
45 import android.view.inputmethod.InputMethod;
46 import android.view.inputmethod.InputMethodInfo;
47 import android.view.inputmethod.InputMethodManager;
48 
49 import com.android.internal.content.PackageMonitor;
50 import com.android.settings.accessibility.AccessibilitySettings;
51 import com.android.settings.inputmethod.AvailableVirtualKeyboardFragment;
52 import com.android.settings.inputmethod.PhysicalKeyboardFragment;
53 import com.android.settings.inputmethod.VirtualKeyboardFragment;
54 import com.android.settings.language.LanguageAndInputSettings;
55 import com.android.settings.overlay.FeatureFactory;
56 import com.android.settings.print.PrintSettingsFragment;
57 import com.android.settings.search2.DatabaseIndexingManager;
58 
59 import java.util.ArrayList;
60 import java.util.List;
61 
62 public final class DynamicIndexableContentMonitor implements
63         LoaderManager.LoaderCallbacks<List<PrintServiceInfo>> {
64     // Shorten the class name because log TAG can be at most 23 chars.
65     private static final String TAG = "DynamicContentMonitor";
66 
67     private static final long DELAY_PROCESS_PACKAGE_CHANGE = 2000;
68     // A PackageMonitor shared among Settings activities.
69     private static final PackageChangeMonitor PACKAGE_CHANGE_MONITOR = new PackageChangeMonitor();
70 
71     // Null if not initialized.
72     @Nullable private DatabaseIndexingManager mIndexManager;
73     private Context mContext;
74     private boolean mHasFeaturePrinting;
75 
76     @VisibleForTesting
getAccessibilityServiceIntent(String packageName)77     static Intent getAccessibilityServiceIntent(String packageName) {
78         final Intent intent = new Intent(AccessibilityService.SERVICE_INTERFACE);
79         intent.setPackage(packageName);
80         return intent;
81     }
82 
83     @VisibleForTesting
getIMEServiceIntent(String packageName)84     static Intent getIMEServiceIntent(String packageName) {
85         final Intent intent = new Intent(InputMethod.SERVICE_INTERFACE);
86         intent.setPackage(packageName);
87         return intent;
88     }
89 
90     @VisibleForTesting
resetForTesting()91     static void resetForTesting() {
92         InputDevicesMonitor.getInstance().resetForTesting();
93         AccessibilityServicesMonitor.getInstance().resetForTesting();
94         InputMethodServicesMonitor.getInstance().resetForTesting();
95     }
96 
97     /**
98      * This instance holds a set of content monitor singleton objects.
99      *
100      * This object is created every time a sub-settings that extends {@code SettingsActivity}
101      * is created.
102      */
DynamicIndexableContentMonitor()103     public DynamicIndexableContentMonitor() {}
104 
105     /**
106      * Creates and initializes a set of content monitor singleton objects if not yet exist.
107      * Also starts loading the list of print services.
108      * <code>mIndex</code> has non-null value after successfully initialized.
109      *
110      * @param activity used to get {@link LoaderManager}.
111      * @param loaderId id for loading print services.
112      */
register(Activity activity, int loaderId)113     public void register(Activity activity, int loaderId) {
114         final boolean isUserUnlocked = activity
115                 .getSystemService(UserManager.class)
116                 .isUserUnlocked();
117         register(activity, loaderId, FeatureFactory.getFactory(activity)
118                         .getSearchFeatureProvider().getIndexingManager(activity), isUserUnlocked);
119     }
120 
121     /**
122      * For testing to inject {@link DatabaseIndexingManager} object.
123      * Also because currently Robolectric doesn't support API 24, we can not test code that calls
124      * {@link UserManager#isUserUnlocked()}.
125      */
126     @VisibleForTesting
register(Activity activity, int loaderId, DatabaseIndexingManager indexManager, boolean isUserUnlocked)127     void register(Activity activity, int loaderId, DatabaseIndexingManager indexManager,
128                   boolean isUserUnlocked) {
129         if (!isUserUnlocked) {
130             Log.w(TAG, "Skipping content monitoring because user is locked");
131             return;
132         }
133         final Context context = activity.getApplicationContext();
134         mContext = context;
135         mIndexManager = indexManager;
136 
137         PACKAGE_CHANGE_MONITOR.registerMonitor(context);
138         mHasFeaturePrinting = context.getPackageManager()
139                 .hasSystemFeature(PackageManager.FEATURE_PRINTING);
140         if (mHasFeaturePrinting) {
141             activity.getLoaderManager().initLoader(loaderId, null /* args */, this /* callbacks */);
142         }
143 
144         // Watch for input device changes.
145         InputDevicesMonitor.getInstance().initialize(context, mIndexManager);
146 
147         // Start tracking packages.
148         AccessibilityServicesMonitor.getInstance().initialize(context, mIndexManager);
149         InputMethodServicesMonitor.getInstance().initialize(context, mIndexManager);
150     }
151 
152     /**
153      * Aborts loading the list of print services.
154      * Note that a set of content monitor singletons keep alive while Settings app is running.
155      *
156      * @param activity user to get {@link LoaderManager}.
157      * @param loaderId id for loading print services.
158      */
unregister(Activity activity, int loaderId)159     public void unregister(Activity activity, int loaderId) {
160         if (mIndexManager == null) return;
161 
162         PACKAGE_CHANGE_MONITOR.unregisterMonitor();
163         if (mHasFeaturePrinting) {
164             activity.getLoaderManager().destroyLoader(loaderId);
165         }
166     }
167 
168     @Override
onCreateLoader(int id, Bundle args)169     public Loader<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
170         return new PrintServicesLoader(
171                 (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE), mContext,
172                 PrintManager.ALL_SERVICES);
173     }
174 
175     @Override
onLoadFinished(Loader<List<PrintServiceInfo>> loader, List<PrintServiceInfo> services)176     public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
177             List<PrintServiceInfo> services) {
178         mIndexManager.updateFromClassNameResource(PrintSettingsFragment.class.getName(),
179                 true /* includeInSearchResults */);
180     }
181 
182     @Override
onLoaderReset(Loader<List<PrintServiceInfo>> loader)183     public void onLoaderReset(Loader<List<PrintServiceInfo>> loader) {
184         // nothing to do
185     }
186 
187     // A singleton that monitors input devices changes and updates indexes of physical keyboards.
188     private static class InputDevicesMonitor implements InputManager.InputDeviceListener {
189 
190         // Null if not initialized.
191         @Nullable private DatabaseIndexingManager mIndexManager;
192         private InputManager mInputManager;
193 
InputDevicesMonitor()194         private InputDevicesMonitor() {}
195 
196         private static class SingletonHolder {
197             private static final InputDevicesMonitor INSTANCE = new InputDevicesMonitor();
198         }
199 
getInstance()200         static InputDevicesMonitor getInstance() {
201             return SingletonHolder.INSTANCE;
202         }
203 
204         @VisibleForTesting
resetForTesting()205         synchronized void resetForTesting() {
206             if (mIndexManager != null) {
207                 mInputManager.unregisterInputDeviceListener(this /* listener */);
208             }
209             mIndexManager = null;
210         }
211 
initialize(Context context, DatabaseIndexingManager indexManager)212         synchronized void initialize(Context context, DatabaseIndexingManager indexManager) {
213             if (mIndexManager != null) return;
214             mIndexManager = indexManager;
215             mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
216             buildIndex();
217 
218             // Watch for input device changes.
219             mInputManager.registerInputDeviceListener(this /* listener */, null /* handler */);
220         }
221 
buildIndex()222         private void buildIndex() {
223             mIndexManager.updateFromClassNameResource(PhysicalKeyboardFragment.class.getName(),
224                     true /* includeInSearchResults */);
225         }
226 
227         @Override
onInputDeviceAdded(int deviceId)228         public void onInputDeviceAdded(int deviceId) {
229             buildIndex();
230         }
231 
232         @Override
onInputDeviceRemoved(int deviceId)233         public void onInputDeviceRemoved(int deviceId) {
234             buildIndex();
235         }
236 
237         @Override
onInputDeviceChanged(int deviceId)238         public void onInputDeviceChanged(int deviceId) {
239             buildIndex();
240         }
241     }
242 
243     // A singleton that monitors package installing, uninstalling, enabling, and disabling.
244     // Then updates indexes of accessibility services and input methods.
245     private static class PackageChangeMonitor extends PackageMonitor {
246         private static final String TAG = PackageChangeMonitor.class.getSimpleName();
247 
248         // Null if not initialized. Guarded by {@link #mLock}.
249         @Nullable private PackageManager mPackageManager;
250         private final Object mLock = new Object();
251 
registerMonitor(Context context)252         public void registerMonitor(Context context) {
253             synchronized (mLock) {
254                 if (mPackageManager != null) {
255                     return;
256                 }
257                 mPackageManager = context.getPackageManager();
258 
259                 // Start tracking packages. Use background thread for monitoring. Note that no need
260                 // to unregister this monitor. This should be alive while Settings app is running.
261                 super.register(context, null /* thread */, UserHandle.CURRENT, false);
262             }
263         }
264 
unregisterMonitor()265         public void unregisterMonitor() {
266             synchronized (mLock) {
267                 if (mPackageManager == null) {
268                     return;
269                 }
270                 super.unregister();
271                 mPackageManager = null;
272             }
273         }
274 
275         // Covers installed, appeared external storage with the package, upgraded.
276         @Override
onPackageAppeared(String packageName, int reason)277         public void onPackageAppeared(String packageName, int reason) {
278             postPackageAvailable(packageName);
279         }
280 
281         // Covers uninstalled, removed external storage with the package.
282         @Override
onPackageDisappeared(String packageName, int reason)283         public void onPackageDisappeared(String packageName, int reason) {
284             postPackageUnavailable(packageName);
285         }
286 
287         // Covers enabled, disabled.
288         @Override
onPackageModified(String packageName)289         public void onPackageModified(String packageName) {
290             try {
291                 final int state = mPackageManager.getApplicationEnabledSetting(packageName);
292                 if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
293                         || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
294                     postPackageAvailable(packageName);
295                 } else {
296                     postPackageUnavailable(packageName);
297                 }
298             } catch (IllegalArgumentException e) {
299                 Log.e(TAG, "Package does not exist: " + packageName, e);
300             }
301         }
302 
postPackageAvailable(final String packageName)303         private void postPackageAvailable(final String packageName) {
304             getRegisteredHandler().postDelayed(() -> {
305                 AccessibilityServicesMonitor.getInstance().onPackageAvailable(packageName);
306                 InputMethodServicesMonitor.getInstance().onPackageAvailable(packageName);
307             }, DELAY_PROCESS_PACKAGE_CHANGE);
308         }
309 
postPackageUnavailable(final String packageName)310         private void postPackageUnavailable(final String packageName) {
311             getRegisteredHandler().postDelayed(() -> {
312                 AccessibilityServicesMonitor.getInstance().onPackageUnavailable(packageName);
313                 InputMethodServicesMonitor.getInstance().onPackageUnavailable(packageName);
314             }, DELAY_PROCESS_PACKAGE_CHANGE);
315         }
316     }
317 
318     // A singleton that holds list of available accessibility services and updates search index.
319     private static class AccessibilityServicesMonitor {
320 
321         // Null if not initialized.
322         @Nullable private DatabaseIndexingManager mIndexManager;
323         private PackageManager mPackageManager;
324         private final List<String> mAccessibilityServices = new ArrayList<>();
325 
AccessibilityServicesMonitor()326         private AccessibilityServicesMonitor() {}
327 
328         private static class SingletonHolder {
329             private static final AccessibilityServicesMonitor INSTANCE =
330                     new AccessibilityServicesMonitor();
331         }
332 
getInstance()333         static AccessibilityServicesMonitor getInstance() {
334             return SingletonHolder.INSTANCE;
335         }
336 
337         @VisibleForTesting
resetForTesting()338         synchronized void resetForTesting() {
339             mIndexManager = null;
340         }
341 
initialize(Context context, DatabaseIndexingManager index)342         synchronized void initialize(Context context, DatabaseIndexingManager index) {
343             if (mIndexManager != null) return;
344             mIndexManager = index;
345             mPackageManager = context.getPackageManager();
346             mAccessibilityServices.clear();
347             buildIndex();
348 
349             // Cache accessibility service packages to know when they go away.
350             AccessibilityManager accessibilityManager = (AccessibilityManager) context
351                     .getSystemService(Context.ACCESSIBILITY_SERVICE);
352             for (final AccessibilityServiceInfo accessibilityService
353                     : accessibilityManager.getInstalledAccessibilityServiceList()) {
354                 ResolveInfo resolveInfo = accessibilityService.getResolveInfo();
355                 if (resolveInfo != null && resolveInfo.serviceInfo != null) {
356                     mAccessibilityServices.add(resolveInfo.serviceInfo.packageName);
357                 }
358             }
359         }
360 
buildIndex()361         private void buildIndex() {
362             mIndexManager.updateFromClassNameResource(AccessibilitySettings.class.getName(),
363                     true /* includeInSearchResults */);
364         }
365 
onPackageAvailable(String packageName)366         synchronized void onPackageAvailable(String packageName) {
367             if (mIndexManager == null) return;
368             if (mAccessibilityServices.contains(packageName)) return;
369 
370             final Intent intent = getAccessibilityServiceIntent(packageName);
371             final List<ResolveInfo> services = mPackageManager
372                     .queryIntentServices(intent, 0 /* flags */);
373             if (services == null || services.isEmpty()) return;
374             mAccessibilityServices.add(packageName);
375             buildIndex();
376         }
377 
onPackageUnavailable(String packageName)378         synchronized void onPackageUnavailable(String packageName) {
379             if (mIndexManager == null) return;
380             if (!mAccessibilityServices.remove(packageName)) return;
381             buildIndex();
382         }
383     }
384 
385     // A singleton that holds list of available input methods and updates search index.
386     // Also it monitors user dictionary changes and updates search index.
387     private static class InputMethodServicesMonitor extends ContentObserver {
388 
389         private static final Uri ENABLED_INPUT_METHODS_CONTENT_URI =
390                 Settings.Secure.getUriFor(Settings.Secure.ENABLED_INPUT_METHODS);
391 
392         // Null if not initialized.
393         @Nullable private DatabaseIndexingManager mIndexManager;
394         private PackageManager mPackageManager;
395         private ContentResolver mContentResolver;
396         private final List<String> mInputMethodServices = new ArrayList<>();
397 
InputMethodServicesMonitor()398         private InputMethodServicesMonitor() {
399             // No need for handler because {@link #onChange(boolean,Uri)} is short and quick.
400             super(null /* handler */);
401         }
402 
403         private static class SingletonHolder {
404             private static final InputMethodServicesMonitor INSTANCE =
405                     new InputMethodServicesMonitor();
406         }
407 
getInstance()408         static InputMethodServicesMonitor getInstance() {
409             return SingletonHolder.INSTANCE;
410         }
411 
412         @VisibleForTesting
resetForTesting()413         synchronized void resetForTesting() {
414             if (mIndexManager != null) {
415                 mContentResolver.unregisterContentObserver(this /* observer */);
416             }
417             mIndexManager = null;
418         }
419 
initialize(Context context, DatabaseIndexingManager indexManager)420         synchronized void initialize(Context context, DatabaseIndexingManager indexManager) {
421             final boolean hasFeatureIme = context.getPackageManager()
422                     .hasSystemFeature(PackageManager.FEATURE_INPUT_METHODS);
423             if (!hasFeatureIme) return;
424 
425             if (mIndexManager != null) return;
426             mIndexManager = indexManager;
427             mPackageManager = context.getPackageManager();
428             mContentResolver = context.getContentResolver();
429             mInputMethodServices.clear();
430             // Build index of {@link UserDictionary}.
431             buildIndex(LanguageAndInputSettings.class);
432             // Build index of IMEs.
433             buildIndex(VirtualKeyboardFragment.class);
434             buildIndex(AvailableVirtualKeyboardFragment.class);
435 
436             // Cache IME service packages to know when they go away.
437             final InputMethodManager inputMethodManager = (InputMethodManager) context
438                     .getSystemService(Context.INPUT_METHOD_SERVICE);
439             for (final InputMethodInfo inputMethod : inputMethodManager.getInputMethodList()) {
440                 ServiceInfo serviceInfo = inputMethod.getServiceInfo();
441                 if (serviceInfo != null) {
442                     mInputMethodServices.add(serviceInfo.packageName);
443                 }
444             }
445 
446             // TODO: Implements by JobScheduler with TriggerContentUri parameters.
447             // Watch for related content URIs.
448             mContentResolver.registerContentObserver(UserDictionary.Words.CONTENT_URI,
449                     true /* notifyForDescendants */, this /* observer */);
450             // Watch for changing enabled IMEs.
451             mContentResolver.registerContentObserver(ENABLED_INPUT_METHODS_CONTENT_URI,
452                     false /* notifyForDescendants */, this /* observer */);
453         }
454 
buildIndex(Class<?> indexClass)455         private void buildIndex(Class<?> indexClass) {
456             mIndexManager.updateFromClassNameResource(indexClass.getName(),
457                     true /* includeInSearchResults */);
458         }
459 
onPackageAvailable(String packageName)460         synchronized void onPackageAvailable(String packageName) {
461             if (mIndexManager == null) return;
462             if (mInputMethodServices.contains(packageName)) return;
463 
464             final Intent intent = getIMEServiceIntent(packageName);
465             final List<ResolveInfo> services = mPackageManager
466                     .queryIntentServices(intent, 0 /* flags */);
467             if (services == null || services.isEmpty()) return;
468             mInputMethodServices.add(packageName);
469             buildIndex(VirtualKeyboardFragment.class);
470             buildIndex(AvailableVirtualKeyboardFragment.class);
471         }
472 
onPackageUnavailable(String packageName)473         synchronized void onPackageUnavailable(String packageName) {
474             if (mIndexManager == null) return;
475             if (!mInputMethodServices.remove(packageName)) return;
476             buildIndex(VirtualKeyboardFragment.class);
477             buildIndex(AvailableVirtualKeyboardFragment.class);
478         }
479 
480         @Override
onChange(boolean selfChange, Uri uri)481         public void onChange(boolean selfChange, Uri uri) {
482             if (ENABLED_INPUT_METHODS_CONTENT_URI.equals(uri)) {
483                 buildIndex(VirtualKeyboardFragment.class);
484                 buildIndex(AvailableVirtualKeyboardFragment.class);
485             } else if (UserDictionary.Words.CONTENT_URI.equals(uri)) {
486                 buildIndex(LanguageAndInputSettings.class);
487             }
488         }
489     }
490 }
491