/* * Copyright (C) 2017 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 android.testing; import android.content.BroadcastReceiver; import android.content.ComponentCallbacks; import android.content.ComponentName; import android.content.ContentProviderClient; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageManager; import android.content.res.Resources; import android.net.Uri; import android.os.Handler; import android.os.IBinder; import android.os.UserHandle; import android.provider.Settings; import android.util.ArrayMap; import android.view.LayoutInflater; import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.junit.runners.model.Statement; /** * A ContextWrapper with utilities specifically designed to make Testing easier. * * * *

TestableContext should be defined as a rule on your test so it can clean up after itself. * Like the following:

*
 * @Rule
 * private final TestableContext mContext = new TestableContext(InstrumentationRegister.getContext());
 * 
*/ public class TestableContext extends ContextWrapper implements TestRule { private final TestableContentResolver mTestableContentResolver; private final TestableSettingsProvider mSettingsProvider; private ArrayMap mMockSystemServices; private ArrayMap mMockServices; private ArrayMap mActiveServices; private PackageManager mMockPackageManager; private LeakCheck.Tracker mReceiver; private LeakCheck.Tracker mService; private LeakCheck.Tracker mComponent; private TestableResources mTestableResources; private TestablePermissions mTestablePermissions; public TestableContext(Context base) { this(base, null); } public TestableContext(Context base, LeakCheck check) { super(base); mTestableContentResolver = new TestableContentResolver(base); ContentProviderClient settings = base.getContentResolver() .acquireContentProviderClient(Settings.AUTHORITY); mSettingsProvider = TestableSettingsProvider.getFakeSettingsProvider(settings); mTestableContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider); mSettingsProvider.clearValuesAndCheck(TestableContext.this); mReceiver = check != null ? check.getTracker("receiver") : null; mService = check != null ? check.getTracker("service") : null; mComponent = check != null ? check.getTracker("component") : null; } public void setMockPackageManager(PackageManager mock) { mMockPackageManager = mock; } @Override public PackageManager getPackageManager() { if (mMockPackageManager != null) { return mMockPackageManager; } return super.getPackageManager(); } /** * Makes sure the resources being returned by this TestableContext are a version of * TestableResources. * @see #getResources() */ public void ensureTestableResources() { if (mTestableResources == null) { mTestableResources = new TestableResources(super.getResources()); } } /** * Get (and create if necessary) {@link TestableResources} for this TestableContext. */ public TestableResources getOrCreateTestableResources() { ensureTestableResources(); return mTestableResources; } /** * Returns a Resources instance for the test. * * By default this returns the same resources object that would come from the * {@link ContextWrapper}, but if {@link #ensureTestableResources()} or * {@link #getOrCreateTestableResources()} has been called, it will return resources gotten from * {@link TestableResources}. */ @Override public Resources getResources() { return mTestableResources != null ? mTestableResources.getResources() : super.getResources(); } /** * @see #getSystemService(String) */ public void addMockSystemService(Class service, T mock) { addMockSystemService(getSystemServiceName(service), mock); } /** * @see #getSystemService(String) */ public void addMockSystemService(String name, Object service) { if (mMockSystemServices == null) mMockSystemServices = new ArrayMap<>(); mMockSystemServices.put(name, service); } /** * If a matching mock service has been added through {@link #addMockSystemService} then * that will be returned, otherwise the real service will be acquired from the base * context. */ @Override public Object getSystemService(String name) { if (mMockSystemServices != null && mMockSystemServices.containsKey(name)) { return mMockSystemServices.get(name); } if (name.equals(LAYOUT_INFLATER_SERVICE)) { return getBaseContext().getSystemService(LayoutInflater.class).cloneInContext(this); } return super.getSystemService(name); } TestableSettingsProvider getSettingsProvider() { return mSettingsProvider; } @Override public TestableContentResolver getContentResolver() { return mTestableContentResolver; } /** * Will always return itself for a TestableContext to ensure the testable effects extend * to the application context. */ @Override public Context getApplicationContext() { // Return this so its always a TestableContext. return this; } @Override public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { if (mReceiver != null) mReceiver.getLeakInfo(receiver).addAllocation(new Throwable()); return super.registerReceiver(receiver, filter); } @Override public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter, String broadcastPermission, Handler scheduler) { if (mReceiver != null) mReceiver.getLeakInfo(receiver).addAllocation(new Throwable()); return super.registerReceiver(receiver, filter, broadcastPermission, scheduler); } @Override public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user, IntentFilter filter, String broadcastPermission, Handler scheduler) { if (mReceiver != null) mReceiver.getLeakInfo(receiver).addAllocation(new Throwable()); return super.registerReceiverAsUser(receiver, user, filter, broadcastPermission, scheduler); } @Override public void unregisterReceiver(BroadcastReceiver receiver) { if (mReceiver != null) mReceiver.getLeakInfo(receiver).clearAllocations(); super.unregisterReceiver(receiver); } /** * Adds a mock service to be connected to by a bindService call. *

* Normally a TestableContext will pass through all bind requests to the base context * but when addMockService has been called for a ComponentName being bound, then * TestableContext will immediately trigger a {@link ServiceConnection#onServiceConnected} * with the specified service, and will call {@link ServiceConnection#onServiceDisconnected} * when the service is unbound. *

*/ public void addMockService(ComponentName component, IBinder service) { if (mMockServices == null) mMockServices = new ArrayMap<>(); mMockServices.put(component, service); } /** * @see #addMockService(ComponentName, IBinder) */ @Override public boolean bindService(Intent service, ServiceConnection conn, int flags) { if (mService != null) mService.getLeakInfo(conn).addAllocation(new Throwable()); if (checkMocks(service.getComponent(), conn)) return true; return super.bindService(service, conn, flags); } /** * @see #addMockService(ComponentName, IBinder) */ @Override public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags, Handler handler, UserHandle user) { if (mService != null) mService.getLeakInfo(conn).addAllocation(new Throwable()); if (checkMocks(service.getComponent(), conn)) return true; return super.bindServiceAsUser(service, conn, flags, handler, user); } /** * @see #addMockService(ComponentName, IBinder) */ @Override public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags, UserHandle user) { if (mService != null) mService.getLeakInfo(conn).addAllocation(new Throwable()); if (checkMocks(service.getComponent(), conn)) return true; return super.bindServiceAsUser(service, conn, flags, user); } private boolean checkMocks(ComponentName component, ServiceConnection conn) { if (mMockServices != null && component != null && mMockServices.containsKey(component)) { if (mActiveServices == null) mActiveServices = new ArrayMap<>(); mActiveServices.put(conn, component); conn.onServiceConnected(component, mMockServices.get(component)); return true; } return false; } /** * @see #addMockService(ComponentName, IBinder) */ @Override public void unbindService(ServiceConnection conn) { if (mService != null) mService.getLeakInfo(conn).clearAllocations(); if (mActiveServices != null && mActiveServices.containsKey(conn)) { conn.onServiceDisconnected(mActiveServices.get(conn)); mActiveServices.remove(conn); return; } super.unbindService(conn); } /** * Check if the TestableContext has a mock binding for a specified component. Will return * true between {@link ServiceConnection#onServiceConnected} and * {@link ServiceConnection#onServiceDisconnected} callbacks for a mock service. * * @see #addMockService(ComponentName, IBinder) */ public boolean isBound(ComponentName component) { return mActiveServices != null && mActiveServices.containsValue(component); } @Override public void registerComponentCallbacks(ComponentCallbacks callback) { if (mComponent != null) mComponent.getLeakInfo(callback).addAllocation(new Throwable()); super.registerComponentCallbacks(callback); } @Override public void unregisterComponentCallbacks(ComponentCallbacks callback) { if (mComponent != null) mComponent.getLeakInfo(callback).clearAllocations(); super.unregisterComponentCallbacks(callback); } public TestablePermissions getTestablePermissions() { if (mTestablePermissions == null) { mTestablePermissions = new TestablePermissions(); } return mTestablePermissions; } @Override public int checkCallingOrSelfPermission(String permission) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(permission)) { return mTestablePermissions.check(permission); } return super.checkCallingOrSelfPermission(permission); } @Override public int checkCallingPermission(String permission) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(permission)) { return mTestablePermissions.check(permission); } return super.checkCallingPermission(permission); } @Override public int checkPermission(String permission, int pid, int uid) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(permission)) { return mTestablePermissions.check(permission); } return super.checkPermission(permission, pid, uid); } @Override public int checkPermission(String permission, int pid, int uid, IBinder callerToken) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(permission)) { return mTestablePermissions.check(permission); } return super.checkPermission(permission, pid, uid, callerToken); } @Override public int checkSelfPermission(String permission) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(permission)) { return mTestablePermissions.check(permission); } return super.checkSelfPermission(permission); } @Override public void enforceCallingOrSelfPermission(String permission, String message) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(permission)) { mTestablePermissions.enforce(permission); } else { super.enforceCallingOrSelfPermission(permission, message); } } @Override public void enforceCallingPermission(String permission, String message) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(permission)) { mTestablePermissions.enforce(permission); } else { super.enforceCallingPermission(permission, message); } } @Override public void enforcePermission(String permission, int pid, int uid, String message) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(permission)) { mTestablePermissions.enforce(permission); } else { super.enforcePermission(permission, pid, uid, message); } } @Override public int checkCallingOrSelfUriPermission(Uri uri, int modeFlags) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(uri)) { return mTestablePermissions.check(uri, modeFlags); } return super.checkCallingOrSelfUriPermission(uri, modeFlags); } @Override public int checkCallingUriPermission(Uri uri, int modeFlags) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(uri)) { return mTestablePermissions.check(uri, modeFlags); } return super.checkCallingUriPermission(uri, modeFlags); } @Override public void enforceCallingOrSelfUriPermission(Uri uri, int modeFlags, String message) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(uri)) { mTestablePermissions.enforce(uri, modeFlags); } else { super.enforceCallingOrSelfUriPermission(uri, modeFlags, message); } } @Override public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(uri)) { return mTestablePermissions.check(uri, modeFlags); } return super.checkUriPermission(uri, pid, uid, modeFlags); } @Override public int checkUriPermission(Uri uri, int pid, int uid, int modeFlags, IBinder callerToken) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(uri)) { return mTestablePermissions.check(uri, modeFlags); } return super.checkUriPermission(uri, pid, uid, modeFlags, callerToken); } @Override public int checkUriPermission(Uri uri, String readPermission, String writePermission, int pid, int uid, int modeFlags) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(uri)) { return mTestablePermissions.check(uri, modeFlags); } return super.checkUriPermission(uri, readPermission, writePermission, pid, uid, modeFlags); } @Override public void enforceCallingUriPermission(Uri uri, int modeFlags, String message) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(uri)) { mTestablePermissions.enforce(uri, modeFlags); } else { super.enforceCallingUriPermission(uri, modeFlags, message); } } @Override public void enforceUriPermission(Uri uri, int pid, int uid, int modeFlags, String message) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(uri)) { mTestablePermissions.enforce(uri, modeFlags); } else { super.enforceUriPermission(uri, pid, uid, modeFlags, message); } } @Override public void enforceUriPermission(Uri uri, String readPermission, String writePermission, int pid, int uid, int modeFlags, String message) { if (mTestablePermissions != null && mTestablePermissions.wantsCall(uri)) { mTestablePermissions.enforce(uri, modeFlags); } else { super.enforceUriPermission(uri, readPermission, writePermission, pid, uid, modeFlags, message); } } @Override public Statement apply(Statement base, Description description) { return new TestWatcher() { @Override protected void succeeded(Description description) { mSettingsProvider.clearValuesAndCheck(TestableContext.this); } @Override protected void failed(Throwable e, Description description) { mSettingsProvider.clearValuesAndCheck(TestableContext.this); } }.apply(base, description); } }