diff --git a/src/com/android/settings/SettingsActivity.java b/src/com/android/settings/SettingsActivity.java index cc203f7fd28..b9be118d7c9 100644 --- a/src/com/android/settings/SettingsActivity.java +++ b/src/com/android/settings/SettingsActivity.java @@ -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); } } diff --git a/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java b/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java index 44bf4355a51..e122244625e 100644 --- a/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java +++ b/src/com/android/settings/inputmethod/InputMethodAndLanguageSettings.java @@ -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 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); diff --git a/src/com/android/settings/search/DynamicIndexableContentMonitor.java b/src/com/android/settings/search/DynamicIndexableContentMonitor.java index a24ec50baa6..34cdeba9e46 100644 --- a/src/com/android/settings/search/DynamicIndexableContentMonitor.java +++ b/src/com/android/settings/search/DynamicIndexableContentMonitor.java @@ -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> { - 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 mAccessibilityServices = new ArrayList(); - private final List mImeServices = new ArrayList(); - - 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. + * mIndex has non-null value after successfully initialized. + * + * @param activity used to get {@link LoaderManager}. + * @param loaderId id for loading print services. + */ public void register(Activity activity, int loaderId) { - 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 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 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> loader, List 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 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 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 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 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 00000000000..28fe8b00b16 --- /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> 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 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 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 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 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 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 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 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 00000000000..0125b77fe29 --- /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 00000000000..c1f82933b93 --- /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 00000000000..5b09645ac99 --- /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 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 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 00000000000..0e59fec30ce --- /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 mInputMethodInfos = Collections.emptyList(); + + @Implementation + public static InputMethodManager getInstance() { + return ShadowInputMethodManager.peekInstance(); + } + + @Implementation + public List getInputMethodList() { + return mInputMethodInfos; + } + + // Non-Android setter. + public void setInputMethodList(List 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 00000000000..b93b035ccf3 --- /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 thread 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()); + } +}