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