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
diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java
index cc203f7..b9be118 100644
--- a/src/com/android/settings/SettingsActivity.java
+++ b/src/com/android/settings/SettingsActivity.java
@@ -723,7 +723,7 @@
         unregisterReceiver(mBatteryInfoReceiver);
         unregisterReceiver(mUserAddRemoveReceiver);
         if (mDynamicIndexableContentMonitor != null) {
-            mDynamicIndexableContentMonitor.unregister();
+            mDynamicIndexableContentMonitor.unregister(this, LOADER_ID_INDEXABLE_CONTENT_MONITOR);
         }
     }
 
diff --git a/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java b/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java
index 44bf435..e122244 100644
--- a/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java
+++ b/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java
@@ -222,6 +222,7 @@
             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 @@
                 indexables.add(indexable);
             }
 
+            // TODO: Move to PhysicalKeyboardFragment.
             // Hard keyboards
             InputManager inputManager = (InputManager) context.getSystemService(
                     Context.INPUT_SERVICE);
diff --git a/src/com/android/settings/search/DynamicIndexableContentMonitor.java b/src/com/android/settings/search/DynamicIndexableContentMonitor.java
index a24ec50..34cdeba 100644
--- a/src/com/android/settings/search/DynamicIndexableContentMonitor.java
+++ b/src/com/android/settings/search/DynamicIndexableContentMonitor.java
@@ -20,6 +20,7 @@
 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.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 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;
+    @VisibleForTesting
+    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);
-
+    // 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 @@
     @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 @@
         // 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 */);
             }
-        };
+        }
     }
 }
diff --git a/tests/robotests/src/com/android/settings/search/DynamicIndexableContentMonitorTest.java b/tests/robotests/src/com/android/settings/search/DynamicIndexableContentMonitorTest.java
new file mode 100644
index 0000000..28fe8b0
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/search/DynamicIndexableContentMonitorTest.java
@@ -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());
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowActivityWithLoadManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowActivityWithLoadManager.java
new file mode 100644
index 0000000..0125b77
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowActivityWithLoadManager.java
@@ -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;
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowContextImplWithRegisterReceiver.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowContextImplWithRegisterReceiver.java
new file mode 100644
index 0000000..c1f8293
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowContextImplWithRegisterReceiver.java
@@ -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);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowInputManager.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowInputManager.java
new file mode 100644
index 0000000..5b09645
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowInputManager.java
@@ -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);
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowInputMethodManagerWithMethodList.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowInputMethodManagerWithMethodList.java
new file mode 100644
index 0000000..0e59fec
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowInputMethodManagerWithMethodList.java
@@ -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;
+    }
+}
diff --git a/tests/robotests/src/com/android/settings/testutils/shadow/ShadowPackageMonitor.java b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowPackageMonitor.java
new file mode 100644
index 0000000..b93b035
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/testutils/shadow/ShadowPackageMonitor.java
@@ -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());
+    }
+}