Refactor DynamicIndexableContentMonitor
Refactor content monitoring code into a few singletons to keep alive while Settings app is running. Bug: 32995210 Test: Manually installing/uninstalling AOSP LatinIME.apk while Settings app is/isn't running, then search AOSP. Test: Connecting/Disconnecting Anker bluetooth keyboard while Settings app is/isn't running, then search Anker. Test: Added Robolectric test for DynamicIndexableContentMonitor. Change-Id: I588e33be169fc9677d41c3daa59ab400f04f6419
This commit is contained in:
@@ -723,7 +723,7 @@ public class SettingsActivity extends SettingsDrawerActivity
|
||||
unregisterReceiver(mBatteryInfoReceiver);
|
||||
unregisterReceiver(mUserAddRemoveReceiver);
|
||||
if (mDynamicIndexableContentMonitor != null) {
|
||||
mDynamicIndexableContentMonitor.unregister();
|
||||
mDynamicIndexableContentMonitor.unregister(this, LOADER_ID_INDEXABLE_CONTENT_MONITOR);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -222,6 +222,7 @@ public class InputMethodAndLanguageSettings extends DashboardFragment
|
||||
InputMethodManager inputMethodManager = (InputMethodManager) context.getSystemService(
|
||||
Context.INPUT_METHOD_SERVICE);
|
||||
|
||||
// TODO: Move to VirtualKeyboardFragment and AvailableVirtualKeyboardFragment.
|
||||
// All other IMEs.
|
||||
List<InputMethodInfo> inputMethods = immValues.getInputMethodList();
|
||||
final int inputMethodCount = (inputMethods == null ? 0 : inputMethods.size());
|
||||
@@ -245,6 +246,7 @@ public class InputMethodAndLanguageSettings extends DashboardFragment
|
||||
indexables.add(indexable);
|
||||
}
|
||||
|
||||
// TODO: Move to PhysicalKeyboardFragment.
|
||||
// Hard keyboards
|
||||
InputManager inputManager = (InputManager) context.getSystemService(
|
||||
Context.INPUT_SERVICE);
|
||||
|
@@ -20,6 +20,7 @@ import android.accessibilityservice.AccessibilityService;
|
||||
import android.accessibilityservice.AccessibilityServiceInfo;
|
||||
import android.app.Activity;
|
||||
import android.app.LoaderManager;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.Loader;
|
||||
@@ -30,17 +31,17 @@ import android.database.ContentObserver;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.Message;
|
||||
import android.os.UserHandle;
|
||||
import android.os.UserManager;
|
||||
import android.print.PrintManager;
|
||||
import android.print.PrintServicesLoader;
|
||||
import android.printservice.PrintServiceInfo;
|
||||
import android.provider.UserDictionary;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.annotation.VisibleForTesting;
|
||||
import android.util.Log;
|
||||
import android.view.accessibility.AccessibilityManager;
|
||||
import android.view.inputmethod.InputMethod;
|
||||
import android.view.inputmethod.InputMethodInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
@@ -52,227 +53,100 @@ import com.android.settings.print.PrintSettingsFragment;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public final class DynamicIndexableContentMonitor extends PackageMonitor implements
|
||||
InputManager.InputDeviceListener,
|
||||
public final class DynamicIndexableContentMonitor implements
|
||||
LoaderManager.LoaderCallbacks<List<PrintServiceInfo>> {
|
||||
private static final String TAG = "DynamicIndexableContentMonitor";
|
||||
// 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;
|
||||
|
||||
private static final int MSG_PACKAGE_AVAILABLE = 1;
|
||||
private static final int MSG_PACKAGE_UNAVAILABLE = 2;
|
||||
|
||||
private final List<String> mAccessibilityServices = new ArrayList<String>();
|
||||
private final List<String> mImeServices = new ArrayList<String>();
|
||||
|
||||
private final Handler mHandler = new Handler() {
|
||||
@Override
|
||||
public void handleMessage(Message msg) {
|
||||
switch (msg.what) {
|
||||
case MSG_PACKAGE_AVAILABLE: {
|
||||
String packageName = (String) msg.obj;
|
||||
handlePackageAvailable(packageName);
|
||||
} break;
|
||||
|
||||
case MSG_PACKAGE_UNAVAILABLE: {
|
||||
String packageName = (String) msg.obj;
|
||||
handlePackageUnavailable(packageName);
|
||||
} break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
private final ContentObserver mUserDictionaryContentObserver =
|
||||
new UserDictionaryContentObserver(mHandler);
|
||||
@VisibleForTesting
|
||||
static final long DELAY_PROCESS_PACKAGE_CHANGE = 2000;
|
||||
|
||||
// Null if not initialized.
|
||||
@Nullable private Index mIndex;
|
||||
private Context mContext;
|
||||
private boolean mHasFeatureIme;
|
||||
private boolean mRegistered;
|
||||
private boolean mHasFeaturePrinting;
|
||||
|
||||
private static Intent getAccessibilityServiceIntent(String packageName) {
|
||||
@VisibleForTesting
|
||||
static Intent getAccessibilityServiceIntent(String packageName) {
|
||||
final Intent intent = new Intent(AccessibilityService.SERVICE_INTERFACE);
|
||||
intent.setPackage(packageName);
|
||||
return intent;
|
||||
}
|
||||
|
||||
private static Intent getIMEServiceIntent(String packageName) {
|
||||
final Intent intent = new Intent("android.view.InputMethod");
|
||||
@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();
|
||||
PackageChangeMonitor.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) {
|
||||
mContext = activity;
|
||||
final boolean isUserUnlocked = activity
|
||||
.getSystemService(UserManager.class)
|
||||
.isUserUnlocked();
|
||||
register(activity, loaderId, Index.getInstance(activity), isUserUnlocked);
|
||||
}
|
||||
|
||||
if (!mContext.getSystemService(UserManager.class).isUserUnlocked()) {
|
||||
/**
|
||||
* For testing to inject {@link Index} 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, Index index, boolean isUserUnlocked) {
|
||||
if (!isUserUnlocked) {
|
||||
Log.w(TAG, "Skipping content monitoring because user is locked");
|
||||
mRegistered = false;
|
||||
return;
|
||||
} else {
|
||||
mRegistered = true;
|
||||
}
|
||||
mContext = activity;
|
||||
mIndex = index;
|
||||
|
||||
boolean hasFeaturePrinting = mContext.getPackageManager().hasSystemFeature(
|
||||
PackageManager.FEATURE_PRINTING);
|
||||
mHasFeatureIme = mContext.getPackageManager().hasSystemFeature(
|
||||
PackageManager.FEATURE_INPUT_METHODS);
|
||||
|
||||
// Cache accessibility service packages to know when they go away.
|
||||
AccessibilityManager accessibilityManager = (AccessibilityManager)
|
||||
mContext.getSystemService(Context.ACCESSIBILITY_SERVICE);
|
||||
List<AccessibilityServiceInfo> accessibilityServices = accessibilityManager
|
||||
.getInstalledAccessibilityServiceList();
|
||||
final int accessibilityServiceCount = accessibilityServices.size();
|
||||
for (int i = 0; i < accessibilityServiceCount; i++) {
|
||||
AccessibilityServiceInfo accessibilityService = accessibilityServices.get(i);
|
||||
ResolveInfo resolveInfo = accessibilityService.getResolveInfo();
|
||||
if (resolveInfo == null || resolveInfo.serviceInfo == null) {
|
||||
continue;
|
||||
}
|
||||
mAccessibilityServices.add(resolveInfo.serviceInfo.packageName);
|
||||
}
|
||||
|
||||
if (hasFeaturePrinting) {
|
||||
activity.getLoaderManager().initLoader(loaderId, null, this);
|
||||
}
|
||||
|
||||
// Cache IME service packages to know when they go away.
|
||||
if (mHasFeatureIme) {
|
||||
InputMethodManager imeManager = (InputMethodManager)
|
||||
mContext.getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
List<InputMethodInfo> inputMethods = imeManager.getInputMethodList();
|
||||
final int inputMethodCount = inputMethods.size();
|
||||
for (int i = 0; i < inputMethodCount; i++) {
|
||||
InputMethodInfo inputMethod = inputMethods.get(i);
|
||||
ServiceInfo serviceInfo = inputMethod.getServiceInfo();
|
||||
if (serviceInfo == null) continue;
|
||||
mImeServices.add(serviceInfo.packageName);
|
||||
}
|
||||
|
||||
// Watch for related content URIs.
|
||||
mContext.getContentResolver().registerContentObserver(
|
||||
UserDictionary.Words.CONTENT_URI, true, mUserDictionaryContentObserver);
|
||||
mHasFeaturePrinting = mContext.getPackageManager()
|
||||
.hasSystemFeature(PackageManager.FEATURE_PRINTING);
|
||||
if (mHasFeaturePrinting) {
|
||||
activity.getLoaderManager().initLoader(loaderId, null /* args */, this /* callbacks */);
|
||||
}
|
||||
|
||||
// Watch for input device changes.
|
||||
InputManager inputManager = (InputManager) activity.getSystemService(
|
||||
Context.INPUT_SERVICE);
|
||||
inputManager.registerInputDeviceListener(this, mHandler);
|
||||
InputDevicesMonitor.getInstance().initialize(mContext, mIndex);
|
||||
|
||||
// Start tracking packages.
|
||||
register(activity, Looper.getMainLooper(), UserHandle.CURRENT, false);
|
||||
PackageChangeMonitor.getInstance().initialize(mContext, mIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister() {
|
||||
if (!mRegistered) return;
|
||||
/**
|
||||
* 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 (mIndex == null) return;
|
||||
|
||||
super.unregister();
|
||||
|
||||
InputManager inputManager = (InputManager) mContext.getSystemService(
|
||||
Context.INPUT_SERVICE);
|
||||
inputManager.unregisterInputDeviceListener(this);
|
||||
|
||||
if (mHasFeatureIme) {
|
||||
mContext.getContentResolver().unregisterContentObserver(
|
||||
mUserDictionaryContentObserver);
|
||||
}
|
||||
|
||||
mAccessibilityServices.clear();
|
||||
mImeServices.clear();
|
||||
}
|
||||
|
||||
// Covers installed, appeared external storage with the package, upgraded.
|
||||
@Override
|
||||
public void onPackageAppeared(String packageName, int uid) {
|
||||
postMessage(MSG_PACKAGE_AVAILABLE, packageName);
|
||||
}
|
||||
|
||||
// Covers uninstalled, removed external storage with the package.
|
||||
@Override
|
||||
public void onPackageDisappeared(String packageName, int uid) {
|
||||
postMessage(MSG_PACKAGE_UNAVAILABLE, packageName);
|
||||
}
|
||||
|
||||
// Covers enabled, disabled.
|
||||
@Override
|
||||
public void onPackageModified(String packageName) {
|
||||
super.onPackageModified(packageName);
|
||||
try {
|
||||
final int state = mContext.getPackageManager().getApplicationEnabledSetting(
|
||||
packageName);
|
||||
if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
|
||||
|| state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
|
||||
postMessage(MSG_PACKAGE_AVAILABLE, packageName);
|
||||
} else {
|
||||
postMessage(MSG_PACKAGE_UNAVAILABLE, packageName);
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
Log.e(TAG, "Package does not exist: " + packageName, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceAdded(int deviceId) {
|
||||
Index.getInstance(mContext).updateFromClassNameResource(
|
||||
InputMethodAndLanguageSettings.class.getName(), false, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceRemoved(int deviceId) {
|
||||
onInputDeviceChanged(deviceId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceChanged(int deviceId) {
|
||||
Index.getInstance(mContext).updateFromClassNameResource(
|
||||
InputMethodAndLanguageSettings.class.getName(), true, true);
|
||||
}
|
||||
|
||||
private void postMessage(int what, String packageName) {
|
||||
Message message = mHandler.obtainMessage(what, packageName);
|
||||
mHandler.sendMessageDelayed(message, DELAY_PROCESS_PACKAGE_CHANGE);
|
||||
}
|
||||
|
||||
private void handlePackageAvailable(String packageName) {
|
||||
if (!mAccessibilityServices.contains(packageName)) {
|
||||
final Intent intent = getAccessibilityServiceIntent(packageName);
|
||||
List<?> services = mContext.getPackageManager().queryIntentServices(intent, 0);
|
||||
if (services != null && !services.isEmpty()) {
|
||||
mAccessibilityServices.add(packageName);
|
||||
Index.getInstance(mContext).updateFromClassNameResource(
|
||||
AccessibilitySettings.class.getName(), false, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (mHasFeatureIme) {
|
||||
if (!mImeServices.contains(packageName)) {
|
||||
Intent intent = getIMEServiceIntent(packageName);
|
||||
List<?> services = mContext.getPackageManager().queryIntentServices(intent, 0);
|
||||
if (services != null && !services.isEmpty()) {
|
||||
mImeServices.add(packageName);
|
||||
Index.getInstance(mContext).updateFromClassNameResource(
|
||||
InputMethodAndLanguageSettings.class.getName(), false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handlePackageUnavailable(String packageName) {
|
||||
final int accessibilityIndex = mAccessibilityServices.indexOf(packageName);
|
||||
if (accessibilityIndex >= 0) {
|
||||
mAccessibilityServices.remove(accessibilityIndex);
|
||||
Index.getInstance(mContext).updateFromClassNameResource(
|
||||
AccessibilitySettings.class.getName(), true, true);
|
||||
}
|
||||
|
||||
if (mHasFeatureIme) {
|
||||
final int imeIndex = mImeServices.indexOf(packageName);
|
||||
if (imeIndex >= 0) {
|
||||
mImeServices.remove(imeIndex);
|
||||
Index.getInstance(mContext).updateFromClassNameResource(
|
||||
InputMethodAndLanguageSettings.class.getName(), true, true);
|
||||
}
|
||||
if (mHasFeaturePrinting) {
|
||||
activity.getLoaderManager().destroyLoader(loaderId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -286,8 +160,8 @@ public final class DynamicIndexableContentMonitor extends PackageMonitor impleme
|
||||
@Override
|
||||
public void onLoadFinished(Loader<List<PrintServiceInfo>> loader,
|
||||
List<PrintServiceInfo> services) {
|
||||
Index.getInstance(mContext).updateFromClassNameResource(
|
||||
PrintSettingsFragment.class.getName(), false, true);
|
||||
mIndex.updateFromClassNameResource(PrintSettingsFragment.class.getName(),
|
||||
false /* rebuild */, true /* includeInSearchResult */);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -295,18 +169,304 @@ public final class DynamicIndexableContentMonitor extends PackageMonitor impleme
|
||||
// nothing to do
|
||||
}
|
||||
|
||||
private final class UserDictionaryContentObserver extends ContentObserver {
|
||||
// A singleton that monitors input devices changes and updates indexes of physical keyboards.
|
||||
private static class InputDevicesMonitor implements InputManager.InputDeviceListener {
|
||||
|
||||
public UserDictionaryContentObserver(Handler handler) {
|
||||
super(handler);
|
||||
// Null if not initialized.
|
||||
@Nullable private Index mIndex;
|
||||
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 (mIndex != null) {
|
||||
mInputManager.unregisterInputDeviceListener(this /* listener */);
|
||||
}
|
||||
mIndex = null;
|
||||
}
|
||||
|
||||
synchronized void initialize(Context context, Index index) {
|
||||
if (mIndex != null) return;
|
||||
mIndex = index;
|
||||
mInputManager = (InputManager) context.getSystemService(Context.INPUT_SERVICE);
|
||||
buildIndex(true /* rebuild */);
|
||||
|
||||
// Watch for input device changes.
|
||||
mInputManager.registerInputDeviceListener(this /* listener */, null /* handler */);
|
||||
}
|
||||
|
||||
private void buildIndex(boolean rebuild) {
|
||||
// TODO: Fix landing page to PhysicalKeyboardFragment.
|
||||
mIndex.updateFromClassNameResource(InputMethodAndLanguageSettings.class.getName(),
|
||||
rebuild, true /* includeInSearchResult */);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceAdded(int deviceId) {
|
||||
buildIndex(false /* rebuild */);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceRemoved(int deviceId) {
|
||||
buildIndex(true /* rebuild */);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onInputDeviceChanged(int deviceId) {
|
||||
buildIndex(true /* rebuild */);
|
||||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
@Nullable private PackageManager mPackageManager;
|
||||
|
||||
private PackageChangeMonitor() {}
|
||||
|
||||
private static class SingletonHolder {
|
||||
private static final PackageChangeMonitor INSTANCE = new PackageChangeMonitor();
|
||||
}
|
||||
|
||||
static PackageChangeMonitor getInstance() {
|
||||
return SingletonHolder.INSTANCE;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
synchronized void resetForTesting() {
|
||||
if (mPackageManager != null) {
|
||||
unregister();
|
||||
}
|
||||
mPackageManager = null;
|
||||
AccessibilityServicesMonitor.getInstance().resetForTesting();
|
||||
InputMethodServicesMonitor.getInstance().resetForTesting();
|
||||
}
|
||||
|
||||
synchronized void initialize(Context context, Index index) {
|
||||
if (mPackageManager != null) return;;
|
||||
mPackageManager = context.getPackageManager();
|
||||
|
||||
AccessibilityServicesMonitor.getInstance().initialize(context, index);
|
||||
InputMethodServicesMonitor.getInstance().initialize(context, index);
|
||||
|
||||
// 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.
|
||||
register(context, null /* thread */, UserHandle.CURRENT, false);
|
||||
}
|
||||
|
||||
// Covers installed, appeared external storage with the package, upgraded.
|
||||
@Override
|
||||
public void onPackageAppeared(String packageName, int uid) {
|
||||
postPackageAvailable(packageName);
|
||||
}
|
||||
|
||||
// Covers uninstalled, removed external storage with the package.
|
||||
@Override
|
||||
public void onPackageDisappeared(String packageName, int uid) {
|
||||
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(() -> {
|
||||
AccessibilityServicesMonitor.getInstance().onPackageAvailable(packageName);
|
||||
InputMethodServicesMonitor.getInstance().onPackageAvailable(packageName);
|
||||
}, DELAY_PROCESS_PACKAGE_CHANGE);
|
||||
}
|
||||
|
||||
private void postPackageUnavailable(final String packageName) {
|
||||
getRegisteredHandler().postDelayed(() -> {
|
||||
AccessibilityServicesMonitor.getInstance().onPackageUnavailable(packageName);
|
||||
InputMethodServicesMonitor.getInstance().onPackageUnavailable(packageName);
|
||||
}, DELAY_PROCESS_PACKAGE_CHANGE);
|
||||
}
|
||||
}
|
||||
|
||||
// A singleton that holds list of available accessibility services and updates search index.
|
||||
private static class AccessibilityServicesMonitor {
|
||||
|
||||
// Null if not initialized.
|
||||
@Nullable private Index mIndex;
|
||||
private PackageManager mPackageManager;
|
||||
private final List<String> mAccessibilityServices = new ArrayList<>();
|
||||
|
||||
private AccessibilityServicesMonitor() {}
|
||||
|
||||
private static class SingletonHolder {
|
||||
private static final AccessibilityServicesMonitor INSTANCE =
|
||||
new AccessibilityServicesMonitor();
|
||||
}
|
||||
|
||||
static AccessibilityServicesMonitor getInstance() {
|
||||
return SingletonHolder.INSTANCE;
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
synchronized void resetForTesting() {
|
||||
mIndex = null;
|
||||
}
|
||||
|
||||
synchronized void initialize(Context context, Index index) {
|
||||
if (mIndex != null) return;
|
||||
mIndex = index;
|
||||
mPackageManager = context.getPackageManager();
|
||||
mAccessibilityServices.clear();
|
||||
buildIndex(true /* rebuild */);
|
||||
|
||||
// Cache accessibility service packages to know when they go away.
|
||||
AccessibilityManager accessibilityManager = (AccessibilityManager) context
|
||||
.getSystemService(Context.ACCESSIBILITY_SERVICE);
|
||||
for (final AccessibilityServiceInfo accessibilityService
|
||||
: accessibilityManager.getInstalledAccessibilityServiceList()) {
|
||||
ResolveInfo resolveInfo = accessibilityService.getResolveInfo();
|
||||
if (resolveInfo != null && resolveInfo.serviceInfo != null) {
|
||||
mAccessibilityServices.add(resolveInfo.serviceInfo.packageName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void buildIndex(boolean rebuild) {
|
||||
mIndex.updateFromClassNameResource(AccessibilitySettings.class.getName(),
|
||||
rebuild, true /* includeInSearchResult */);
|
||||
}
|
||||
|
||||
synchronized void onPackageAvailable(String packageName) {
|
||||
if (mIndex == null) return;
|
||||
if (mAccessibilityServices.contains(packageName)) return;
|
||||
|
||||
final Intent intent = getAccessibilityServiceIntent(packageName);
|
||||
final List<ResolveInfo> services = mPackageManager
|
||||
.queryIntentServices(intent, 0 /* flags */);
|
||||
if (services == null || services.isEmpty()) return;
|
||||
mAccessibilityServices.add(packageName);
|
||||
buildIndex(false /* rebuild */);
|
||||
}
|
||||
|
||||
synchronized void onPackageUnavailable(String packageName) {
|
||||
if (mIndex == null) return;
|
||||
if (!mAccessibilityServices.remove(packageName)) return;
|
||||
buildIndex(true /* rebuild */);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 {
|
||||
|
||||
// Null if not initialized.
|
||||
@Nullable private Index mIndex;
|
||||
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 (mIndex != null) {
|
||||
mContentResolver.unregisterContentObserver(this /* observer */);
|
||||
}
|
||||
mIndex = null;
|
||||
}
|
||||
|
||||
synchronized void initialize(Context context, Index index) {
|
||||
final boolean hasFeatureIme = context.getPackageManager()
|
||||
.hasSystemFeature(PackageManager.FEATURE_INPUT_METHODS);
|
||||
if (!hasFeatureIme) return;
|
||||
|
||||
if (mIndex != null) return;
|
||||
mIndex = index;
|
||||
mPackageManager = context.getPackageManager();
|
||||
mContentResolver = context.getContentResolver();
|
||||
mInputMethodServices.clear();
|
||||
buildIndex(InputMethodAndLanguageSettings.class, true /* rebuild */);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for related content URIs.
|
||||
mContentResolver.registerContentObserver(UserDictionary.Words.CONTENT_URI,
|
||||
true /* notifyForDescendants */, this /* observer */);
|
||||
// TODO: Should monitor android.provider.Settings.Secure.ENABLED_INPUT_METHODS and
|
||||
// update index of AvailableVirtualKeyboardFragment and VirtualKeyboardFragment.
|
||||
}
|
||||
|
||||
private void buildIndex(Class<?> indexClass, boolean rebuild) {
|
||||
mIndex.updateFromClassNameResource(indexClass.getName(), rebuild,
|
||||
true /* includeInSearchResult */);
|
||||
}
|
||||
|
||||
synchronized void onPackageAvailable(String packageName) {
|
||||
if (mIndex == 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);
|
||||
// TODO: Fix landing page to VirtualKeyboardFragment.
|
||||
buildIndex(InputMethodAndLanguageSettings.class, false /* rebuild */);
|
||||
}
|
||||
|
||||
synchronized void onPackageUnavailable(String packageName) {
|
||||
if (mIndex == null) return;
|
||||
if (!mInputMethodServices.remove(packageName)) return;
|
||||
// TODO: Fix landing page to AvailableVirtualKeyboardFragment.
|
||||
buildIndex(InputMethodAndLanguageSettings.class, true /* rebuild */);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChange(boolean selfChange, Uri uri) {
|
||||
if (UserDictionary.Words.CONTENT_URI.equals(uri)) {
|
||||
Index.getInstance(mContext).updateFromClassNameResource(
|
||||
InputMethodAndLanguageSettings.class.getName(), true, true);
|
||||
buildIndex(InputMethodAndLanguageSettings.class, true /* rebuild */);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,601 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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 static com.google.common.truth.Truth.assertThat;
|
||||
|
||||
import static org.mockito.Matchers.any;
|
||||
import static org.mockito.Matchers.anyBoolean;
|
||||
import static org.mockito.Matchers.anyInt;
|
||||
import static org.mockito.Matchers.anyString;
|
||||
import static org.mockito.Matchers.eq;
|
||||
import static org.mockito.Mockito.atLeastOnce;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.only;
|
||||
import static org.mockito.Mockito.reset;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
|
||||
import android.accessibilityservice.AccessibilityServiceInfo;
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.app.LoaderManager;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.Loader;
|
||||
import android.content.pm.ActivityInfo;
|
||||
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.print.PrintManager;
|
||||
import android.print.PrintServicesLoader;
|
||||
import android.printservice.PrintServiceInfo;
|
||||
import android.provider.UserDictionary;
|
||||
import android.view.inputmethod.InputMethodInfo;
|
||||
|
||||
import com.android.internal.content.PackageMonitor;
|
||||
import com.android.settings.SettingsRobolectricTestRunner;
|
||||
import com.android.settings.TestConfig;
|
||||
import com.android.settings.accessibility.AccessibilitySettings;
|
||||
import com.android.settings.inputmethod.InputMethodAndLanguageSettings;
|
||||
import com.android.settings.print.PrintSettingsFragment;
|
||||
import com.android.settings.testutils.shadow.ShadowActivityWithLoadManager;
|
||||
import com.android.settings.testutils.shadow.ShadowContextImplWithRegisterReceiver;
|
||||
import com.android.settings.testutils.shadow.ShadowInputManager;
|
||||
import com.android.settings.testutils.shadow.ShadowInputMethodManagerWithMethodList;
|
||||
import com.android.settings.testutils.shadow.ShadowPackageMonitor;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.mockito.verification.VerificationMode;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.internal.ShadowExtractor;
|
||||
import org.robolectric.res.builder.RobolectricPackageManager;
|
||||
import org.robolectric.shadows.ShadowAccessibilityManager;
|
||||
import org.robolectric.shadows.ShadowApplication;
|
||||
import org.robolectric.shadows.ShadowContentResolver;
|
||||
import org.xmlpull.v1.XmlPullParserException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@RunWith(SettingsRobolectricTestRunner.class)
|
||||
@Config(
|
||||
manifest = TestConfig.MANIFEST_PATH,
|
||||
sdk = TestConfig.SDK_VERSION,
|
||||
shadows = {
|
||||
ShadowActivityWithLoadManager.class,
|
||||
ShadowContextImplWithRegisterReceiver.class,
|
||||
ShadowInputManager.class,
|
||||
ShadowInputMethodManagerWithMethodList.class,
|
||||
ShadowPackageMonitor.class,
|
||||
}
|
||||
)
|
||||
public class DynamicIndexableContentMonitorTest {
|
||||
|
||||
private static final int USER_ID = 5678;
|
||||
private static final int LOADER_ID = 1234;
|
||||
private static final String A11Y_PACKAGE_1 = "a11y-1";
|
||||
private static final String A11Y_PACKAGE_2 = "a11y-2";
|
||||
private static final String IME_PACKAGE_1 = "ime-1";
|
||||
private static final String IME_PACKAGE_2 = "ime-2";
|
||||
|
||||
private LoaderManager mLoaderManager = mock(LoaderManager.class);
|
||||
private Index mIndex = mock(Index.class);
|
||||
|
||||
private Activity mActivity;
|
||||
private InputManager mInputManager;
|
||||
|
||||
private ShadowContextImplWithRegisterReceiver mShadowContextImpl;
|
||||
private ShadowActivityWithLoadManager mShadowActivity;
|
||||
private ShadowAccessibilityManager mShadowAccessibilityManager;
|
||||
private ShadowInputMethodManagerWithMethodList mShadowInputMethodManager;
|
||||
private RobolectricPackageManager mRobolectricPackageManager;
|
||||
|
||||
private final DynamicIndexableContentMonitor mMonitor = new DynamicIndexableContentMonitor();
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
mActivity = Robolectric.buildActivity(Activity.class).get();
|
||||
mInputManager = InputManager.getInstance();
|
||||
|
||||
// Robolectric shadows.
|
||||
mShadowContextImpl = (ShadowContextImplWithRegisterReceiver) ShadowExtractor.extract(
|
||||
((Application) ShadowApplication.getInstance().getApplicationContext())
|
||||
.getBaseContext());
|
||||
mShadowActivity = (ShadowActivityWithLoadManager) ShadowExtractor.extract(mActivity);
|
||||
mShadowAccessibilityManager = (ShadowAccessibilityManager) ShadowExtractor.extract(
|
||||
mActivity.getSystemService(Context.ACCESSIBILITY_SERVICE));
|
||||
mShadowInputMethodManager = (ShadowInputMethodManagerWithMethodList) ShadowExtractor
|
||||
.extract(mActivity.getSystemService(Context.INPUT_METHOD_SERVICE));
|
||||
mRobolectricPackageManager = RuntimeEnvironment.getRobolectricPackageManager();
|
||||
|
||||
// Setup shadows.
|
||||
mShadowContextImpl.setSystemService(Context.PRINT_SERVICE, mock(PrintManager.class));
|
||||
mShadowContextImpl.setSystemService(Context.INPUT_SERVICE, mInputManager);
|
||||
mShadowActivity.setLoaderManager(mLoaderManager);
|
||||
mShadowAccessibilityManager.setInstalledAccessibilityServiceList(Collections.emptyList());
|
||||
mShadowInputMethodManager.setInputMethodList(Collections.emptyList());
|
||||
mRobolectricPackageManager.setSystemFeature(PackageManager.FEATURE_PRINTING, true);
|
||||
mRobolectricPackageManager.setSystemFeature(PackageManager.FEATURE_INPUT_METHODS, true);
|
||||
}
|
||||
|
||||
@After
|
||||
public void shutDown() {
|
||||
DynamicIndexableContentMonitor.resetForTesting();
|
||||
mRobolectricPackageManager.reset();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLockedUser() {
|
||||
mMonitor.register(mActivity, LOADER_ID, mIndex, false /* isUserUnlocked */);
|
||||
|
||||
// No loader procedure happens.
|
||||
verify(mLoaderManager, never()).initLoader(
|
||||
anyInt(), any(Bundle.class), any(LoaderManager.LoaderCallbacks.class));
|
||||
// No indexing happens.
|
||||
verify(mIndex, never()).updateFromClassNameResource(
|
||||
anyString(), anyBoolean(), anyBoolean());
|
||||
|
||||
mMonitor.unregister(mActivity, LOADER_ID);
|
||||
|
||||
// No destroy loader should happen.
|
||||
verify(mLoaderManager, never()).destroyLoader(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testWithNoPrintingFeature() {
|
||||
mRobolectricPackageManager.setSystemFeature(PackageManager.FEATURE_PRINTING, false);
|
||||
|
||||
mMonitor.register(mActivity, LOADER_ID, mIndex, true /* isUserUnlocked */);
|
||||
|
||||
// No loader procedure happens.
|
||||
verify(mLoaderManager, never()).initLoader(
|
||||
anyInt(), any(Bundle.class), any(LoaderManager.LoaderCallbacks.class));
|
||||
verifyNoIndexing(PrintSettingsFragment.class);
|
||||
|
||||
mMonitor.unregister(mActivity, LOADER_ID);
|
||||
|
||||
// No destroy loader should happen.
|
||||
verify(mLoaderManager, never()).destroyLoader(anyInt());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPrinterServiceIndex() {
|
||||
mMonitor.register(mActivity, LOADER_ID, mIndex, true /* isUserUnlocked */);
|
||||
|
||||
// Loader procedure happens.
|
||||
verify(mLoaderManager, only()).initLoader(LOADER_ID, null, mMonitor);
|
||||
|
||||
// Loading print services happens.
|
||||
final Loader<List<PrintServiceInfo>> loader =
|
||||
mMonitor.onCreateLoader(LOADER_ID, null /* args */);
|
||||
assertThat(loader).isInstanceOf(PrintServicesLoader.class);
|
||||
verifyNoIndexing(PrintSettingsFragment.class);
|
||||
|
||||
mMonitor.onLoadFinished(loader, Collections.emptyList());
|
||||
|
||||
verifyIncrementalIndexing(PrintSettingsFragment.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInputDevicesMonitor() {
|
||||
mMonitor.register(mActivity, LOADER_ID, mIndex, true /* isUserUnlocked */);
|
||||
|
||||
// Rebuild indexing should happen.
|
||||
// CAVEAT: Currently InputMethodAndLanuageSettings may be indexed once for input devices and
|
||||
// once for input methods.
|
||||
verifyRebuildIndexing(InputMethodAndLanguageSettings.class, atLeastOnce());
|
||||
// Input monitor should be registered to InputManager.
|
||||
final InputManager.InputDeviceListener listener = extactInputDeviceListener();
|
||||
assertThat(listener).isNotNull();
|
||||
|
||||
/*
|
||||
* Nothing happens on successive register calls.
|
||||
*/
|
||||
reset(mIndex);
|
||||
|
||||
mMonitor.register(mActivity, LOADER_ID, mIndex, true /* isUserUnlocked */);
|
||||
|
||||
verifyNoIndexing(InputMethodAndLanguageSettings.class);
|
||||
assertThat(extactInputDeviceListener()).isEqualTo(listener);
|
||||
|
||||
/*
|
||||
* A device is added.
|
||||
*/
|
||||
reset(mIndex);
|
||||
|
||||
listener.onInputDeviceAdded(1 /* deviceId */);
|
||||
|
||||
verifyIncrementalIndexing(InputMethodAndLanguageSettings.class);
|
||||
|
||||
/*
|
||||
* A device is removed.
|
||||
*/
|
||||
reset(mIndex);
|
||||
|
||||
listener.onInputDeviceRemoved(2 /* deviceId */);
|
||||
|
||||
verifyRebuildIndexing(InputMethodAndLanguageSettings.class);
|
||||
|
||||
/*
|
||||
* A device is changed.
|
||||
*/
|
||||
reset(mIndex);
|
||||
|
||||
listener.onInputDeviceChanged(3 /* deviceId */);
|
||||
|
||||
verifyRebuildIndexing(InputMethodAndLanguageSettings.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAccessibilityServicesMonitor() throws Exception {
|
||||
mMonitor.register(mActivity, LOADER_ID, mIndex, true /* isUserUnlocked */);
|
||||
|
||||
final PackageMonitor packageMonitor = extractPackageMonitor();
|
||||
assertThat(packageMonitor).isNotNull();
|
||||
|
||||
verifyRebuildIndexing(AccessibilitySettings.class);
|
||||
|
||||
/*
|
||||
* When an accessibility service package is installed, incremental indexing happen.
|
||||
*/
|
||||
installAccessibilityService(A11Y_PACKAGE_1);
|
||||
reset(mIndex);
|
||||
|
||||
packageMonitor.onPackageAppeared(A11Y_PACKAGE_1, USER_ID);
|
||||
Robolectric.flushBackgroundThreadScheduler();
|
||||
|
||||
verifyIncrementalIndexing(AccessibilitySettings.class);
|
||||
|
||||
/*
|
||||
* When another accessibility service package is installed, incremental indexing happens.
|
||||
*/
|
||||
installAccessibilityService(A11Y_PACKAGE_2);
|
||||
reset(mIndex);
|
||||
|
||||
packageMonitor.onPackageAppeared(A11Y_PACKAGE_2, USER_ID);
|
||||
Robolectric.flushBackgroundThreadScheduler();
|
||||
|
||||
verifyIncrementalIndexing(AccessibilitySettings.class);
|
||||
|
||||
/*
|
||||
* When an accessibility service is disabled, rebuild indexing happens.
|
||||
*/
|
||||
((PackageManager) mRobolectricPackageManager).setApplicationEnabledSetting(
|
||||
A11Y_PACKAGE_1, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0 /* flags */);
|
||||
reset(mIndex);
|
||||
|
||||
packageMonitor.onPackageModified(A11Y_PACKAGE_1);
|
||||
Robolectric.flushBackgroundThreadScheduler();
|
||||
|
||||
verifyRebuildIndexing(AccessibilitySettings.class);
|
||||
|
||||
/*
|
||||
* When an accessibility service is enabled, incremental indexing happens.
|
||||
*/
|
||||
((PackageManager) mRobolectricPackageManager).setApplicationEnabledSetting(
|
||||
A11Y_PACKAGE_1, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, 0 /* flags */);
|
||||
reset(mIndex);
|
||||
|
||||
packageMonitor.onPackageModified(A11Y_PACKAGE_1);
|
||||
Robolectric.flushBackgroundThreadScheduler();
|
||||
|
||||
verifyIncrementalIndexing(AccessibilitySettings.class);
|
||||
|
||||
/*
|
||||
* When an accessibility service package is uninstalled, rebuild indexing happens.
|
||||
*/
|
||||
uninstallAccessibilityService(A11Y_PACKAGE_1);
|
||||
reset(mIndex);
|
||||
|
||||
packageMonitor.onPackageDisappeared(A11Y_PACKAGE_1, USER_ID);
|
||||
|
||||
verifyRebuildIndexing(AccessibilitySettings.class);
|
||||
|
||||
/*
|
||||
* When an input method service package is installed, nothing happens.
|
||||
*/
|
||||
installInputMethodService(IME_PACKAGE_1);
|
||||
reset(mIndex);
|
||||
|
||||
packageMonitor.onPackageAppeared(IME_PACKAGE_1, USER_ID);
|
||||
|
||||
verifyNoIndexing(AccessibilitySettings.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInputMethodServicesMonitor() throws Exception {
|
||||
mMonitor.register(mActivity, LOADER_ID, mIndex, true /* isUserUnlocked */);
|
||||
|
||||
final PackageMonitor packageMonitor = extractPackageMonitor();
|
||||
assertThat(packageMonitor).isNotNull();
|
||||
|
||||
// CAVEAT: Currently InputMethodAndLanuageSettings may be indexed once for input devices and
|
||||
// once for input methods.
|
||||
verifyRebuildIndexing(InputMethodAndLanguageSettings.class, atLeastOnce());
|
||||
|
||||
/*
|
||||
* When an input method service package is installed, incremental indexing happen.
|
||||
*/
|
||||
installInputMethodService(IME_PACKAGE_1);
|
||||
reset(mIndex);
|
||||
|
||||
packageMonitor.onPackageAppeared(IME_PACKAGE_1, USER_ID);
|
||||
Robolectric.flushBackgroundThreadScheduler();
|
||||
|
||||
verifyIncrementalIndexing(InputMethodAndLanguageSettings.class);
|
||||
|
||||
/*
|
||||
* When another input method service package is installed, incremental indexing happens.
|
||||
*/
|
||||
installInputMethodService(IME_PACKAGE_2);
|
||||
reset(mIndex);
|
||||
|
||||
packageMonitor.onPackageAppeared(IME_PACKAGE_2, USER_ID);
|
||||
Robolectric.flushBackgroundThreadScheduler();
|
||||
|
||||
verifyIncrementalIndexing(InputMethodAndLanguageSettings.class);
|
||||
|
||||
/*
|
||||
* When an input method service is disabled, rebuild indexing happens.
|
||||
*/
|
||||
((PackageManager) mRobolectricPackageManager).setApplicationEnabledSetting(
|
||||
IME_PACKAGE_1, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, 0 /* flags */);
|
||||
reset(mIndex);
|
||||
|
||||
packageMonitor.onPackageModified(IME_PACKAGE_1);
|
||||
Robolectric.flushBackgroundThreadScheduler();
|
||||
|
||||
verifyRebuildIndexing(InputMethodAndLanguageSettings.class);
|
||||
|
||||
/*
|
||||
* When an input method service is enabled, incremental indexing happens.
|
||||
*/
|
||||
((PackageManager) mRobolectricPackageManager).setApplicationEnabledSetting(
|
||||
IME_PACKAGE_1, PackageManager.COMPONENT_ENABLED_STATE_DEFAULT, 0 /* flags */);
|
||||
reset(mIndex);
|
||||
|
||||
packageMonitor.onPackageModified(IME_PACKAGE_1);
|
||||
Robolectric.flushBackgroundThreadScheduler();
|
||||
|
||||
verifyIncrementalIndexing(InputMethodAndLanguageSettings.class);
|
||||
|
||||
/*
|
||||
* When an input method service package is uninstalled, rebuild indexing happens.
|
||||
*/
|
||||
uninstallInputMethodService(IME_PACKAGE_1);
|
||||
reset(mIndex);
|
||||
|
||||
packageMonitor.onPackageDisappeared(IME_PACKAGE_1, USER_ID);
|
||||
|
||||
verifyRebuildIndexing(InputMethodAndLanguageSettings.class);
|
||||
|
||||
/*
|
||||
* When an accessibility service package is installed, nothing happens.
|
||||
*/
|
||||
installAccessibilityService(A11Y_PACKAGE_1);
|
||||
reset(mIndex);
|
||||
|
||||
packageMonitor.onPackageAppeared(A11Y_PACKAGE_1, USER_ID);
|
||||
|
||||
verifyNoIndexing(InputMethodAndLanguageSettings.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUserDictionaryChangeMonitor() throws Exception {
|
||||
mMonitor.register(mActivity, LOADER_ID, mIndex, true /* isUserUnlocked */);
|
||||
|
||||
// Content observer should be registered.
|
||||
final ContentObserver observer = extractContentObserver(UserDictionary.Words.CONTENT_URI);
|
||||
assertThat(observer).isNotNull();
|
||||
|
||||
// CAVEAT: Currently InputMethodAndLanuageSettings may be indexed once for input devices and
|
||||
// once for input methods.
|
||||
verifyRebuildIndexing(InputMethodAndLanguageSettings.class, atLeastOnce());
|
||||
|
||||
/*
|
||||
* When user dictionary content is changed, rebuild indexing happens.
|
||||
*/
|
||||
reset(mIndex);
|
||||
|
||||
observer.onChange(false /* selfChange */, UserDictionary.Words.CONTENT_URI);
|
||||
|
||||
verifyRebuildIndexing(InputMethodAndLanguageSettings.class);
|
||||
}
|
||||
|
||||
/*
|
||||
* Verification helpers.
|
||||
*/
|
||||
|
||||
private void verifyNoIndexing(Class<?> indexingClass) {
|
||||
verify(mIndex, never()).updateFromClassNameResource(eq(indexingClass.getName()),
|
||||
anyBoolean(), anyBoolean());
|
||||
}
|
||||
|
||||
private void verifyRebuildIndexing(Class<?> indexingClass) {
|
||||
verifyRebuildIndexing(indexingClass, times(1));
|
||||
}
|
||||
|
||||
private void verifyRebuildIndexing(Class<?> indexingClass, VerificationMode verificationMode) {
|
||||
verify(mIndex, verificationMode).updateFromClassNameResource(indexingClass.getName(),
|
||||
true /* rebuild */, true /* includeInSearchResults */);
|
||||
verify(mIndex, never()).updateFromClassNameResource(indexingClass.getName(),
|
||||
false /* rebuild */, true /* includeInSearchResults */);
|
||||
}
|
||||
|
||||
private void verifyIncrementalIndexing(Class<?> indexingClass) {
|
||||
verify(mIndex, times(1)).updateFromClassNameResource(indexingClass.getName(),
|
||||
false /* rebuild */, true /* includeInSearchResults */);
|
||||
verify(mIndex, never()).updateFromClassNameResource(indexingClass.getName(),
|
||||
true /* rebuild */, true /* includeInSearchResults */);
|
||||
}
|
||||
|
||||
/*
|
||||
* Testing helper methods.
|
||||
*/
|
||||
|
||||
private InputManager.InputDeviceListener extactInputDeviceListener() {
|
||||
List<InputManager.InputDeviceListener> listeners = ((ShadowInputManager) ShadowExtractor
|
||||
.extract(mInputManager))
|
||||
.getRegisteredInputDeviceListeners();
|
||||
InputManager.InputDeviceListener inputDeviceListener = null;
|
||||
for (InputManager.InputDeviceListener listener : listeners) {
|
||||
if (isUnderTest(listener)) {
|
||||
if (inputDeviceListener != null) {
|
||||
assertThat(listener).isEqualTo(inputDeviceListener);
|
||||
} else {
|
||||
inputDeviceListener = listener;
|
||||
}
|
||||
}
|
||||
}
|
||||
return inputDeviceListener;
|
||||
}
|
||||
|
||||
private PackageMonitor extractPackageMonitor() {
|
||||
List<ShadowApplication.Wrapper> receivers = ShadowApplication.getInstance()
|
||||
.getRegisteredReceivers();
|
||||
PackageMonitor packageMonitor = null;
|
||||
for (ShadowApplication.Wrapper wrapper : receivers) {
|
||||
BroadcastReceiver receiver = wrapper.getBroadcastReceiver();
|
||||
if (isUnderTest(receiver) && receiver instanceof PackageMonitor) {
|
||||
if (packageMonitor != null) {
|
||||
assertThat(receiver).isEqualTo(packageMonitor);
|
||||
} else {
|
||||
packageMonitor = (PackageMonitor) receiver;
|
||||
}
|
||||
}
|
||||
}
|
||||
return packageMonitor;
|
||||
}
|
||||
|
||||
private ContentObserver extractContentObserver(Uri uri) {
|
||||
ShadowContentResolver contentResolver = (ShadowContentResolver) ShadowExtractor
|
||||
.extract(mActivity.getContentResolver());
|
||||
Collection<ContentObserver> observers = contentResolver.getContentObservers(uri);
|
||||
ContentObserver contentObserver = null;
|
||||
for (ContentObserver observer : observers) {
|
||||
if (isUnderTest(observer)) {
|
||||
if (contentObserver != null) {
|
||||
assertThat(observer).isEqualTo(contentObserver);
|
||||
} else {
|
||||
contentObserver = observer;
|
||||
}
|
||||
}
|
||||
}
|
||||
return contentObserver;
|
||||
}
|
||||
|
||||
private void installAccessibilityService(String packageName) throws Exception {
|
||||
final AccessibilityServiceInfo serviceToAdd = buildAccessibilityServiceInfo(packageName);
|
||||
|
||||
final List<AccessibilityServiceInfo> services = new ArrayList<>();
|
||||
services.addAll(mShadowAccessibilityManager.getInstalledAccessibilityServiceList());
|
||||
services.add(serviceToAdd);
|
||||
mShadowAccessibilityManager.setInstalledAccessibilityServiceList(services);
|
||||
|
||||
final Intent intent = DynamicIndexableContentMonitor
|
||||
.getAccessibilityServiceIntent(packageName);
|
||||
mRobolectricPackageManager.addResolveInfoForIntent(intent, serviceToAdd.getResolveInfo());
|
||||
mRobolectricPackageManager.addPackage(packageName);
|
||||
}
|
||||
|
||||
private void uninstallAccessibilityService(String packageName) throws Exception {
|
||||
final AccessibilityServiceInfo serviceToRemove = buildAccessibilityServiceInfo(packageName);
|
||||
|
||||
final List<AccessibilityServiceInfo> services = new ArrayList<>();
|
||||
services.addAll(mShadowAccessibilityManager.getInstalledAccessibilityServiceList());
|
||||
services.remove(serviceToRemove);
|
||||
mShadowAccessibilityManager.setInstalledAccessibilityServiceList(services);
|
||||
|
||||
final Intent intent = DynamicIndexableContentMonitor
|
||||
.getAccessibilityServiceIntent(packageName);
|
||||
mRobolectricPackageManager.removeResolveInfosForIntent(intent, packageName);
|
||||
mRobolectricPackageManager.removePackage(packageName);
|
||||
}
|
||||
|
||||
private void installInputMethodService(String packageName) throws Exception {
|
||||
final ResolveInfo resolveInfoToAdd = buildResolveInfo(packageName, "imeService");
|
||||
final InputMethodInfo serviceToAdd = buildInputMethodInfo(resolveInfoToAdd);
|
||||
|
||||
final List<InputMethodInfo> services = new ArrayList<>();
|
||||
services.addAll(mShadowInputMethodManager.getInputMethodList());
|
||||
services.add(serviceToAdd);
|
||||
mShadowInputMethodManager.setInputMethodList(services);
|
||||
|
||||
final Intent intent = DynamicIndexableContentMonitor.getIMEServiceIntent(packageName);
|
||||
mRobolectricPackageManager.addResolveInfoForIntent(intent, resolveInfoToAdd);
|
||||
mRobolectricPackageManager.addPackage(packageName);
|
||||
}
|
||||
|
||||
private void uninstallInputMethodService(String packageName) throws Exception {
|
||||
final ResolveInfo resolveInfoToRemove = buildResolveInfo(packageName, "imeService");
|
||||
final InputMethodInfo serviceToRemove = buildInputMethodInfo(resolveInfoToRemove);
|
||||
|
||||
final List<InputMethodInfo> services = new ArrayList<>();
|
||||
services.addAll(mShadowInputMethodManager.getInputMethodList());
|
||||
services.remove(serviceToRemove);
|
||||
mShadowInputMethodManager.setInputMethodList(services);
|
||||
|
||||
final Intent intent = DynamicIndexableContentMonitor.getIMEServiceIntent(packageName);
|
||||
mRobolectricPackageManager.removeResolveInfosForIntent(intent, packageName);
|
||||
mRobolectricPackageManager.removePackage(packageName);
|
||||
}
|
||||
|
||||
private AccessibilityServiceInfo buildAccessibilityServiceInfo(String packageName)
|
||||
throws IOException, XmlPullParserException {
|
||||
return new AccessibilityServiceInfo(
|
||||
buildResolveInfo(packageName, "A11yService"), mActivity);
|
||||
}
|
||||
|
||||
private static InputMethodInfo buildInputMethodInfo(ResolveInfo resolveInfo) {
|
||||
return new InputMethodInfo(resolveInfo, false /* isAuxIme */, "SettingsActivity",
|
||||
null /* subtypes */, 0 /* defaultResId */, false /* forceDefault */);
|
||||
}
|
||||
|
||||
private static ResolveInfo buildResolveInfo(String packageName, String className) {
|
||||
final ResolveInfo resolveInfo = new ResolveInfo();
|
||||
resolveInfo.serviceInfo = new ServiceInfo();
|
||||
resolveInfo.serviceInfo.packageName = packageName;
|
||||
resolveInfo.serviceInfo.name = className;
|
||||
// To workaround that RobolectricPackageManager.removeResolveInfosForIntent() only works
|
||||
// for activity/broadcast resolver.
|
||||
resolveInfo.activityInfo = new ActivityInfo();
|
||||
resolveInfo.activityInfo.packageName = packageName;
|
||||
resolveInfo.activityInfo.name = className;
|
||||
|
||||
return resolveInfo;
|
||||
}
|
||||
|
||||
private static boolean isUnderTest(Object object) {
|
||||
return object.getClass().getName().startsWith(
|
||||
DynamicIndexableContentMonitor.class.getName());
|
||||
}
|
||||
}
|
@@ -0,0 +1,43 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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.testutils.shadow;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.LoaderManager;
|
||||
|
||||
import org.robolectric.annotation.Implementation;
|
||||
import org.robolectric.annotation.Implements;
|
||||
import org.robolectric.shadows.ShadowActivity;
|
||||
|
||||
/*
|
||||
* Shadow for {@link Activity} that has LoadManager accessors.
|
||||
*/
|
||||
@Implements(Activity.class)
|
||||
public class ShadowActivityWithLoadManager extends ShadowActivity {
|
||||
|
||||
private LoaderManager mLoaderManager;
|
||||
|
||||
@Implementation
|
||||
public LoaderManager getLoaderManager() {
|
||||
return mLoaderManager;
|
||||
}
|
||||
|
||||
// Non-Android setter.
|
||||
public void setLoaderManager(LoaderManager loaderManager) {
|
||||
mLoaderManager = loaderManager;
|
||||
}
|
||||
}
|
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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.testutils.shadow;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.Handler;
|
||||
import android.os.UserHandle;
|
||||
|
||||
import org.robolectric.annotation.Implementation;
|
||||
import org.robolectric.annotation.Implements;
|
||||
import org.robolectric.shadows.ShadowContextImpl;
|
||||
|
||||
/*
|
||||
* Shadow for {@link ContextImpl} that has registerReceiverAsUser method.
|
||||
*/
|
||||
@Implements(className = ShadowContextImpl.CLASS_NAME)
|
||||
public class ShadowContextImplWithRegisterReceiver extends ShadowContextImpl {
|
||||
|
||||
@Implementation
|
||||
public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
|
||||
IntentFilter filter, String broadcastPermission, Handler scheduler) {
|
||||
return super.registerReceiver(receiver, filter, broadcastPermission, scheduler);
|
||||
}
|
||||
}
|
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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.testutils.shadow;
|
||||
|
||||
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
|
||||
|
||||
import android.hardware.input.IInputManager;
|
||||
import android.hardware.input.InputManager;
|
||||
import android.os.Handler;
|
||||
|
||||
import org.robolectric.annotation.Implementation;
|
||||
import org.robolectric.annotation.Implements;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* Shadow for {@ InputManager} that has assessors for registered {@link InputDeviceListener}s.
|
||||
*/
|
||||
@Implements(value = InputManager.class, callThroughByDefault = false)
|
||||
public class ShadowInputManager {
|
||||
|
||||
private ArrayList<InputManager.InputDeviceListener> mInputDeviceListeners;
|
||||
|
||||
@Implementation
|
||||
public void __constructor__(IInputManager service) {
|
||||
mInputDeviceListeners = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Implementation
|
||||
public static InputManager getInstance() {
|
||||
return ReflectionHelpers.callConstructor(
|
||||
InputManager.class,
|
||||
from(IInputManager.class, null));
|
||||
}
|
||||
|
||||
@Implementation
|
||||
public void registerInputDeviceListener(InputManager.InputDeviceListener listener,
|
||||
Handler handler) {
|
||||
// TODO: Use handler.
|
||||
if (!mInputDeviceListeners.contains(listener)) {
|
||||
mInputDeviceListeners.add(listener);
|
||||
}
|
||||
}
|
||||
|
||||
@Implementation
|
||||
public void unregisterInputDeviceListener(InputManager.InputDeviceListener listener) {
|
||||
if (mInputDeviceListeners.contains(listener)) {
|
||||
mInputDeviceListeners.remove(listener);
|
||||
}
|
||||
}
|
||||
|
||||
// Non-Android accessor.
|
||||
public List<InputManager.InputDeviceListener> getRegisteredInputDeviceListeners() {
|
||||
return Collections.unmodifiableList(mInputDeviceListeners);
|
||||
}
|
||||
}
|
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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.testutils.shadow;
|
||||
|
||||
import android.view.inputmethod.InputMethodInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
|
||||
import org.robolectric.annotation.Implementation;
|
||||
import org.robolectric.annotation.Implements;
|
||||
import org.robolectric.shadows.ShadowInputMethodManager;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/*
|
||||
* Shadow for {@link InputMethodManager} that has accessors for installed input methods.
|
||||
*/
|
||||
@Implements(InputMethodManager.class)
|
||||
public class ShadowInputMethodManagerWithMethodList extends ShadowInputMethodManager {
|
||||
|
||||
private List<InputMethodInfo> mInputMethodInfos = Collections.emptyList();
|
||||
|
||||
@Implementation
|
||||
public static InputMethodManager getInstance() {
|
||||
return ShadowInputMethodManager.peekInstance();
|
||||
}
|
||||
|
||||
@Implementation
|
||||
public List<InputMethodInfo> getInputMethodList() {
|
||||
return mInputMethodInfos;
|
||||
}
|
||||
|
||||
// Non-Android setter.
|
||||
public void setInputMethodList(List<InputMethodInfo> inputMethodInfos) {
|
||||
mInputMethodInfos = inputMethodInfos;
|
||||
}
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
/*
|
||||
* Copyright (C) 2016 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.testutils.shadow;
|
||||
|
||||
import static org.robolectric.util.ReflectionHelpers.ClassParameter.from;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Looper;
|
||||
import android.os.UserHandle;
|
||||
|
||||
import com.android.internal.content.PackageMonitor;
|
||||
import com.android.internal.os.BackgroundThread;
|
||||
|
||||
import org.robolectric.annotation.Implementation;
|
||||
import org.robolectric.annotation.Implements;
|
||||
import org.robolectric.annotation.RealObject;
|
||||
import org.robolectric.internal.Shadow;
|
||||
import org.robolectric.internal.ShadowExtractor;
|
||||
import org.robolectric.shadows.ShadowApplication;
|
||||
import org.robolectric.shadows.ShadowMessageQueue;
|
||||
|
||||
/*
|
||||
* Shadow for hidden {@link PackageMonitor}.
|
||||
*/
|
||||
@Implements(value = PackageMonitor.class, isInAndroidSdk = false)
|
||||
public class ShadowPackageMonitor {
|
||||
|
||||
@RealObject
|
||||
private PackageMonitor mPackageMonitor;
|
||||
|
||||
@Implementation
|
||||
public void register(Context context, Looper thread, UserHandle user, boolean externalStorage) {
|
||||
// Call through to @RealObject's method.
|
||||
Shadow.directlyOn(mPackageMonitor, PackageMonitor.class, "register",
|
||||
from(Context.class, context), from(Looper.class, thread),
|
||||
from(UserHandle.class, user), from(Boolean.TYPE, externalStorage));
|
||||
// When <code>thread</code> is null, the {@link BackgroundThread} is used. Here we have to
|
||||
// setup background Robolectric scheduler for it.
|
||||
if (thread == null) {
|
||||
setupBackgroundThreadScheduler();
|
||||
}
|
||||
}
|
||||
|
||||
private static void setupBackgroundThreadScheduler() {
|
||||
ShadowMessageQueue shadowMessageQueue = ((ShadowMessageQueue) ShadowExtractor.extract(
|
||||
BackgroundThread.getHandler().getLooper().getQueue()));
|
||||
shadowMessageQueue.setScheduler(
|
||||
ShadowApplication.getInstance().getBackgroundThreadScheduler());
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user