This is necessary to kill DynamicContentMonitor later - Removed all logic related to indexing accesiblitysetting from the monitor class and AccessibilitySetting page itself - Created a loader to search against A11yServices at runtime I noticed adding a loader in SearchResultsAdapter is rather manual. It's something we should consider refactor in the future. Bug: 64310452 Test: robotests Change-Id: Iff31aff65ce000991229433f294e2ec69af99da2
416 lines
16 KiB
Java
416 lines
16 KiB
Java
/*
|
|
* 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<List<PrintServiceInfo>> {
|
|
// 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.
|
|
* <code>mIndex</code> 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<List<PrintServiceInfo>> onCreateLoader(int id, Bundle args) {
|
|
return new PrintServicesLoader(
|
|
(PrintManager) mContext.getSystemService(Context.PRINT_SERVICE), mContext,
|
|
PrintManager.ALL_SERVICES);
|
|
}
|
|
|
|
@Override
|
|
public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
|
|
List<PrintServiceInfo> services) {
|
|
mIndexManager.updateFromClassNameResource(PrintSettingsFragment.class.getName(),
|
|
true /* includeInSearchResults */);
|
|
}
|
|
|
|
@Override
|
|
public void onLoaderReset(Loader<List<PrintServiceInfo>> 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<String> 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<ResolveInfo> 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);
|
|
}
|
|
}
|
|
}
|
|
}
|