/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.settings.search; import android.accessibilityservice.AccessibilityService; import android.app.Activity; import android.app.LoaderManager; import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.Loader; import android.content.pm.PackageManager; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.database.ContentObserver; import android.hardware.input.InputManager; import android.net.Uri; import android.os.Bundle; import android.os.UserHandle; import android.os.UserManager; import android.print.PrintManager; import android.print.PrintServicesLoader; import android.printservice.PrintServiceInfo; import android.provider.Settings; import android.provider.UserDictionary; import android.support.annotation.Nullable; import android.support.annotation.VisibleForTesting; import android.util.Log; import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodManager; import com.android.internal.content.PackageMonitor; import com.android.settings.inputmethod.AvailableVirtualKeyboardFragment; import com.android.settings.inputmethod.PhysicalKeyboardFragment; import com.android.settings.inputmethod.VirtualKeyboardFragment; import com.android.settings.language.LanguageAndInputSettings; import com.android.settings.overlay.FeatureFactory; import com.android.settings.print.PrintSettingsFragment; import java.util.ArrayList; import java.util.List; public final class DynamicIndexableContentMonitor implements LoaderManager.LoaderCallbacks> { // Shorten the class name because log TAG can be at most 23 chars. private static final String TAG = "DynamicContentMonitor"; private static final long DELAY_PROCESS_PACKAGE_CHANGE = 2000; // A PackageMonitor shared among Settings activities. private static final PackageChangeMonitor PACKAGE_CHANGE_MONITOR = new PackageChangeMonitor(); // Null if not initialized. @Nullable private DatabaseIndexingManager mIndexManager; private Context mContext; private boolean mHasFeaturePrinting; @VisibleForTesting static Intent getAccessibilityServiceIntent(String packageName) { final Intent intent = new Intent(AccessibilityService.SERVICE_INTERFACE); intent.setPackage(packageName); return intent; } @VisibleForTesting static Intent getIMEServiceIntent(String packageName) { final Intent intent = new Intent(InputMethod.SERVICE_INTERFACE); intent.setPackage(packageName); return intent; } @VisibleForTesting static void resetForTesting() { InputDevicesMonitor.getInstance().resetForTesting(); InputMethodServicesMonitor.getInstance().resetForTesting(); } /** * This instance holds a set of content monitor singleton objects. * * This object is created every time a sub-settings that extends {@code SettingsActivity} * is created. */ public DynamicIndexableContentMonitor() {} /** * Creates and initializes a set of content monitor singleton objects if not yet exist. * Also starts loading the list of print services. * mIndex has non-null value after successfully initialized. * * @param activity used to get {@link LoaderManager}. * @param loaderId id for loading print services. */ public void register(Activity activity, int loaderId) { final boolean isUserUnlocked = activity .getSystemService(UserManager.class) .isUserUnlocked(); register(activity, loaderId, FeatureFactory.getFactory(activity) .getSearchFeatureProvider().getIndexingManager(activity), isUserUnlocked); } /** * For testing to inject {@link DatabaseIndexingManager} object. * Also because currently Robolectric doesn't support API 24, we can not test code that calls * {@link UserManager#isUserUnlocked()}. */ @VisibleForTesting void register(Activity activity, int loaderId, DatabaseIndexingManager indexManager, boolean isUserUnlocked) { if (!isUserUnlocked) { Log.w(TAG, "Skipping content monitoring because user is locked"); return; } final Context context = activity.getApplicationContext(); mContext = context; mIndexManager = indexManager; PACKAGE_CHANGE_MONITOR.registerMonitor(context); mHasFeaturePrinting = context.getPackageManager() .hasSystemFeature(PackageManager.FEATURE_PRINTING); if (mHasFeaturePrinting) { activity.getLoaderManager().initLoader(loaderId, null /* args */, this /* callbacks */); } // Watch for input device changes. InputDevicesMonitor.getInstance().initialize(context, mIndexManager); // Start tracking packages. InputMethodServicesMonitor.getInstance().initialize(context, mIndexManager); } /** * Aborts loading the list of print services. * Note that a set of content monitor singletons keep alive while Settings app is running. * * @param activity user to get {@link LoaderManager}. * @param loaderId id for loading print services. */ public void unregister(Activity activity, int loaderId) { if (mIndexManager == null) return; PACKAGE_CHANGE_MONITOR.unregisterMonitor(); if (mHasFeaturePrinting) { activity.getLoaderManager().destroyLoader(loaderId); } } @Override public Loader> onCreateLoader(int id, Bundle args) { return new PrintServicesLoader( (PrintManager) mContext.getSystemService(Context.PRINT_SERVICE), mContext, PrintManager.ALL_SERVICES); } @Override public void onLoadFinished(Loader> loader, List services) { mIndexManager.updateFromClassNameResource(PrintSettingsFragment.class.getName(), true /* includeInSearchResults */); } @Override public void onLoaderReset(Loader> loader) { // nothing to do } // A singleton that monitors input devices changes and updates indexes of physical keyboards. private static class InputDevicesMonitor implements InputManager.InputDeviceListener { // Null if not initialized. @Nullable private DatabaseIndexingManager mIndexManager; private InputManager mInputManager; private InputDevicesMonitor() {} private static class SingletonHolder { private static final InputDevicesMonitor INSTANCE = new InputDevicesMonitor(); } static InputDevicesMonitor getInstance() { return SingletonHolder.INSTANCE; } @VisibleForTesting synchronized void resetForTesting() { if (mIndexManager != null) { mInputManager.unregisterInputDeviceListener(this /* listener */); } mIndexManager = null; } synchronized void initialize(Context context, DatabaseIndexingManager indexManager) { if (mIndexManager != null) return; mIndexManager = indexManager; mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE); buildIndex(); // Watch for input device changes. mInputManager.registerInputDeviceListener(this /* listener */, null /* handler */); } private void buildIndex() { mIndexManager.updateFromClassNameResource(PhysicalKeyboardFragment.class.getName(), true /* includeInSearchResults */); } @Override public void onInputDeviceAdded(int deviceId) { buildIndex(); } @Override public void onInputDeviceRemoved(int deviceId) { buildIndex(); } @Override public void onInputDeviceChanged(int deviceId) { buildIndex(); } } // A singleton that monitors package installing, uninstalling, enabling, and disabling. // Then updates indexes of accessibility services and input methods. private static class PackageChangeMonitor extends PackageMonitor { private static final String TAG = PackageChangeMonitor.class.getSimpleName(); // Null if not initialized. Guarded by {@link #mLock}. @Nullable private PackageManager mPackageManager; private final Object mLock = new Object(); public void registerMonitor(Context context) { synchronized (mLock) { if (mPackageManager != null) { return; } mPackageManager = context.getPackageManager(); // Start tracking packages. Use background thread for monitoring. Note that no need // to unregister this monitor. This should be alive while Settings app is running. super.register(context, null /* thread */, UserHandle.CURRENT, false); } } public void unregisterMonitor() { synchronized (mLock) { if (mPackageManager == null) { return; } super.unregister(); mPackageManager = null; } } // Covers installed, appeared external storage with the package, upgraded. @Override public void onPackageAppeared(String packageName, int reason) { postPackageAvailable(packageName); } // Covers uninstalled, removed external storage with the package. @Override public void onPackageDisappeared(String packageName, int reason) { postPackageUnavailable(packageName); } // Covers enabled, disabled. @Override public void onPackageModified(String packageName) { try { final int state = mPackageManager.getApplicationEnabledSetting(packageName); if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { postPackageAvailable(packageName); } else { postPackageUnavailable(packageName); } } catch (IllegalArgumentException e) { Log.e(TAG, "Package does not exist: " + packageName, e); } } private void postPackageAvailable(final String packageName) { getRegisteredHandler().postDelayed(() -> { InputMethodServicesMonitor.getInstance().onPackageAvailable(packageName); }, DELAY_PROCESS_PACKAGE_CHANGE); } private void postPackageUnavailable(final String packageName) { getRegisteredHandler().postDelayed(() -> { InputMethodServicesMonitor.getInstance().onPackageUnavailable(packageName); }, DELAY_PROCESS_PACKAGE_CHANGE); } } // A singleton that holds list of available input methods and updates search index. // Also it monitors user dictionary changes and updates search index. private static class InputMethodServicesMonitor extends ContentObserver { private static final Uri ENABLED_INPUT_METHODS_CONTENT_URI = Settings.Secure.getUriFor(Settings.Secure.ENABLED_INPUT_METHODS); // Null if not initialized. @Nullable private DatabaseIndexingManager mIndexManager; private PackageManager mPackageManager; private ContentResolver mContentResolver; private final List mInputMethodServices = new ArrayList<>(); private InputMethodServicesMonitor() { // No need for handler because {@link #onChange(boolean,Uri)} is short and quick. super(null /* handler */); } private static class SingletonHolder { private static final InputMethodServicesMonitor INSTANCE = new InputMethodServicesMonitor(); } static InputMethodServicesMonitor getInstance() { return SingletonHolder.INSTANCE; } @VisibleForTesting synchronized void resetForTesting() { if (mIndexManager != null) { mContentResolver.unregisterContentObserver(this /* observer */); } mIndexManager = null; } synchronized void initialize(Context context, DatabaseIndexingManager indexManager) { final boolean hasFeatureIme = context.getPackageManager() .hasSystemFeature(PackageManager.FEATURE_INPUT_METHODS); if (!hasFeatureIme) return; if (mIndexManager != null) return; mIndexManager = indexManager; mPackageManager = context.getPackageManager(); mContentResolver = context.getContentResolver(); mInputMethodServices.clear(); // Build index of {@link UserDictionary}. buildIndex(LanguageAndInputSettings.class); // Build index of IMEs. buildIndex(VirtualKeyboardFragment.class); buildIndex(AvailableVirtualKeyboardFragment.class); // Cache IME service packages to know when they go away. final InputMethodManager inputMethodManager = (InputMethodManager) context .getSystemService(Context.INPUT_METHOD_SERVICE); for (final InputMethodInfo inputMethod : inputMethodManager.getInputMethodList()) { ServiceInfo serviceInfo = inputMethod.getServiceInfo(); if (serviceInfo != null) { mInputMethodServices.add(serviceInfo.packageName); } } // TODO: Implements by JobScheduler with TriggerContentUri parameters. // Watch for related content URIs. mContentResolver.registerContentObserver(UserDictionary.Words.CONTENT_URI, true /* notifyForDescendants */, this /* observer */); // Watch for changing enabled IMEs. mContentResolver.registerContentObserver(ENABLED_INPUT_METHODS_CONTENT_URI, false /* notifyForDescendants */, this /* observer */); } private void buildIndex(Class indexClass) { mIndexManager.updateFromClassNameResource(indexClass.getName(), true /* includeInSearchResults */); } synchronized void onPackageAvailable(String packageName) { if (mIndexManager == null) return; if (mInputMethodServices.contains(packageName)) return; final Intent intent = getIMEServiceIntent(packageName); final List services = mPackageManager .queryIntentServices(intent, 0 /* flags */); if (services == null || services.isEmpty()) return; mInputMethodServices.add(packageName); buildIndex(VirtualKeyboardFragment.class); buildIndex(AvailableVirtualKeyboardFragment.class); } synchronized void onPackageUnavailable(String packageName) { if (mIndexManager == null) return; if (!mInputMethodServices.remove(packageName)) return; buildIndex(VirtualKeyboardFragment.class); buildIndex(AvailableVirtualKeyboardFragment.class); } @Override public void onChange(boolean selfChange, Uri uri) { if (ENABLED_INPUT_METHODS_CONTENT_URI.equals(uri)) { buildIndex(VirtualKeyboardFragment.class); buildIndex(AvailableVirtualKeyboardFragment.class); } else if (UserDictionary.Words.CONTENT_URI.equals(uri)) { buildIndex(LanguageAndInputSettings.class); } } } }