From e97892844a5c21c91c7f82b96f82202b18a1a24d Mon Sep 17 00:00:00 2001 From: Jason Monk Date: Wed, 9 Nov 2016 08:59:56 -0500 Subject: [PATCH] android.provider.Setting test utilities Add a way to fake values for settings in tests. Since the content provider is cached in the NameValueCache, there is one static FakeSettingsProvider that passes through all values to the real SettingsProvider by default. Values that are required for the test can be acquired and locked for the duration of the test easily. Test: runtest systemui Change-Id: Ibc31ac8509fb31a22c522358a9c1bae6ec63553b --- .../systemui/BatteryMeterDrawable.java | 2 +- .../keyguard/KeyguardMessageAreaTest.java | 20 +- .../systemui/BatteryMeterDrawableTest.java | 9 +- .../com/android/systemui/SysuiTestCase.java | 13 +- .../power/PowerNotificationWarningsTest.java | 10 +- .../android/systemui/qs/TileLayoutTest.java | 12 +- .../qs/external/TileLifecycleManagerTest.java | 19 +- .../systemui/utils/FakeContentResolver.java | 131 ++++++++ .../systemui/utils/FakeSettingsProvider.java | 298 ++++++++++++++++++ .../utils/FakeSettingsProviderTest.java | 175 ++++++++++ .../systemui/utils/TestableContext.java | 52 +++ 11 files changed, 700 insertions(+), 41 deletions(-) create mode 100644 packages/SystemUI/tests/src/com/android/systemui/utils/FakeContentResolver.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProvider.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProviderTest.java create mode 100644 packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java diff --git a/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java b/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java index e1d6a94fcf07b..57e324548a349 100755 --- a/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/BatteryMeterDrawable.java @@ -32,8 +32,8 @@ import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Handler; -import android.os.Looper; import android.provider.Settings; + import com.android.systemui.statusbar.policy.BatteryController; public class BatteryMeterDrawable extends Drawable implements diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaTest.java b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaTest.java index fccb2a2406602..f3be945d5002e 100644 --- a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaTest.java +++ b/packages/SystemUI/tests/src/com/android/keyguard/KeyguardMessageAreaTest.java @@ -16,26 +16,24 @@ package com.android.keyguard; +import static junit.framework.Assert.assertEquals; + +import static org.mockito.Mockito.mock; + +import android.os.Handler; +import android.os.Looper; +import android.support.test.runner.AndroidJUnit4; +import android.test.suitebuilder.annotation.SmallTest; + import com.android.systemui.SysuiTestCase; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.support.test.InstrumentationRegistry; -import android.support.test.runner.AndroidJUnit4; -import android.test.suitebuilder.annotation.SmallTest; - -import static junit.framework.Assert.*; -import static org.mockito.Mockito.mock; - @SmallTest @RunWith(AndroidJUnit4.class) public class KeyguardMessageAreaTest extends SysuiTestCase { - private Context mContext = InstrumentationRegistry.getTargetContext(); private Handler mHandler = new Handler(Looper.getMainLooper()); private KeyguardMessageArea mMessageArea; diff --git a/packages/SystemUI/tests/src/com/android/systemui/BatteryMeterDrawableTest.java b/packages/SystemUI/tests/src/com/android/systemui/BatteryMeterDrawableTest.java index 5cb5e68fe6f87..cb0f7a388d011 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/BatteryMeterDrawableTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/BatteryMeterDrawableTest.java @@ -17,7 +17,7 @@ package com.android.systemui; import static junit.framework.Assert.assertEquals; -import static junit.framework.Assert.assertTrue; + import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyFloat; import static org.mockito.Mockito.anyString; @@ -28,27 +28,24 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; -import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) -public class BatteryMeterDrawableTest { +public class BatteryMeterDrawableTest extends SysuiTestCase { - private Context mContext; private Resources mResources; private BatteryMeterDrawable mBatteryMeter; @Before public void setUp() throws Exception { - mContext = InstrumentationRegistry.getTargetContext(); mResources = mContext.getResources(); mBatteryMeter = new BatteryMeterDrawable(mContext, 0); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java index d943eb6a1a609..5dac8e54aa215 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java +++ b/packages/SystemUI/tests/src/com/android/systemui/SysuiTestCase.java @@ -20,6 +20,10 @@ import android.support.test.InstrumentationRegistry; import android.os.Handler; import android.os.Looper; import android.os.MessageQueue; + +import com.android.systemui.utils.TestableContext; + +import org.junit.After; import org.junit.Before; /** @@ -28,11 +32,16 @@ import org.junit.Before; public class SysuiTestCase { private Handler mHandler; - protected Context mContext; + protected TestableContext mContext; @Before public void SysuiSetup() throws Exception { - mContext = InstrumentationRegistry.getTargetContext(); + mContext = new TestableContext(InstrumentationRegistry.getTargetContext()); + } + + @After + public void cleanup() throws Exception { + mContext.getSettingsProvider().clearOverrides(this); } protected Context getContext() { diff --git a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java index 39b64129f64de..5c87fb01e23ca 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/power/PowerNotificationWarningsTest.java @@ -17,9 +17,11 @@ package com.android.systemui.power; import static android.test.MoreAsserts.assertNotEqual; + import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; + import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; @@ -29,9 +31,11 @@ import static org.mockito.Mockito.verify; import android.app.Notification; import android.app.NotificationManager; -import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; + +import com.android.systemui.SysuiTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -39,7 +43,7 @@ import org.mockito.ArgumentCaptor; @SmallTest @RunWith(AndroidJUnit4.class) -public class PowerNotificationWarningsTest { +public class PowerNotificationWarningsTest extends SysuiTestCase { private final NotificationManager mMockNotificationManager = mock(NotificationManager.class); private PowerNotificationWarnings mPowerNotificationWarnings; @@ -47,7 +51,7 @@ public class PowerNotificationWarningsTest { public void setUp() throws Exception { // Test Instance. mPowerNotificationWarnings = new PowerNotificationWarnings( - InstrumentationRegistry.getTargetContext(), mMockNotificationManager, null); + mContext, mMockNotificationManager, null); } @Test diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java index 8eecfcf00d468..5401c30def544 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/TileLayoutTest.java @@ -18,6 +18,7 @@ package com.android.systemui.qs; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertTrue; + import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyBoolean; import static org.mockito.Mockito.anyInt; @@ -26,11 +27,12 @@ import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import android.content.Context; -import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; + import com.android.systemui.R; +import com.android.systemui.SysuiTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -38,13 +40,13 @@ import org.mockito.ArgumentCaptor; @SmallTest @RunWith(AndroidJUnit4.class) -public class TileLayoutTest { - private Context mContext = InstrumentationRegistry.getTargetContext(); - private final TileLayout mTileLayout = new TileLayout(mContext); +public class TileLayoutTest extends SysuiTestCase { + private TileLayout mTileLayout; private int mLayoutSizeForOneTile; @Before public void setUp() throws Exception { + mTileLayout = new TileLayout(mContext); // Layout needs to leave space for the tile margins. Three times the margin size is // sufficient for any number of columns. mLayoutSizeForOneTile = diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java index d7ff04f539abb..782a4890ba551 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/external/TileLifecycleManagerTest.java @@ -16,7 +16,7 @@ package com.android.systemui.qs.external; import static junit.framework.Assert.assertTrue; -import static junit.framework.Assert.fail; + import static org.mockito.Mockito.any; import static org.mockito.Mockito.anyInt; import static org.mockito.Mockito.anyString; @@ -25,12 +25,9 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import android.app.Service; -import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; -import android.content.IntentFilter; import android.content.ServiceConnection; import android.content.pm.PackageInfo; import android.content.pm.ServiceInfo; @@ -38,19 +35,16 @@ import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.HandlerThread; -import android.os.IBinder; -import android.os.Process; -import android.os.RemoteException; import android.os.UserHandle; import android.service.quicksettings.IQSService; import android.service.quicksettings.IQSTileService; import android.service.quicksettings.Tile; import android.service.quicksettings.TileService; -import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; -import android.util.ArraySet; -import android.util.Log; + +import com.android.systemui.SysuiTestCase; + import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -61,7 +55,7 @@ import org.mockito.stubbing.Answer; @SmallTest @RunWith(AndroidJUnit4.class) -public class TileLifecycleManagerTest { +public class TileLifecycleManagerTest extends SysuiTestCase { private static final int TEST_FAIL_TIMEOUT = 5000; private final Context mMockContext = Mockito.mock(Context.class); @@ -78,8 +72,7 @@ public class TileLifecycleManagerTest { @Before public void setUp() throws Exception { setPackageEnabled(true); - mTileServiceComponentName = new ComponentName( - InstrumentationRegistry.getTargetContext(), "FakeTileService.class"); + mTileServiceComponentName = new ComponentName(mContext, "FakeTileService.class"); // Stub.asInterface will just return itself. when(mMockTileService.queryLocalInterface(anyString())).thenReturn(mMockTileService); diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/FakeContentResolver.java b/packages/SystemUI/tests/src/com/android/systemui/utils/FakeContentResolver.java new file mode 100644 index 0000000000000..34f2e019761d5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/FakeContentResolver.java @@ -0,0 +1,131 @@ +/* + * 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.systemui.utils; + +import android.content.ContentProvider; +import android.content.ContentResolver; +import android.content.Context; +import android.content.IContentProvider; +import android.database.ContentObserver; +import android.net.Uri; +import android.util.ArraySet; + +import com.google.android.collect.Maps; + +import java.util.Map; + +/** + * Alternative to a MockContentResolver that falls back to real providers. + */ +public class FakeContentResolver extends ContentResolver { + + private final Map mProviders = Maps.newHashMap(); + private final ContentResolver mParent; + private final ArraySet mInUse = new ArraySet<>(); + private boolean mFallbackToExisting; + + public FakeContentResolver(Context context) { + super(context); + mParent = context.getContentResolver(); + mFallbackToExisting = true; + } + + /** + * Sets whether existing providers should be returned when a mock does not exist. + * The default is true. + */ + public void setFallbackToExisting(boolean fallbackToExisting) { + mFallbackToExisting = fallbackToExisting; + } + + /** + * Adds access to a provider based on its authority + * + * @param name The authority name associated with the provider. + * @param provider An instance of {@link android.content.ContentProvider} or one of its + * subclasses, or null. + */ + public void addProvider(String name, ContentProvider provider) { + mProviders.put(name, provider); + } + + @Override + protected IContentProvider acquireProvider(Context context, String name) { + final ContentProvider provider = mProviders.get(name); + if (provider != null) { + return provider.getIContentProvider(); + } else { + return mFallbackToExisting ? mParent.acquireProvider(name) : null; + } + } + + @Override + protected IContentProvider acquireExistingProvider(Context context, String name) { + final ContentProvider provider = mProviders.get(name); + if (provider != null) { + return provider.getIContentProvider(); + } else { + return mFallbackToExisting ? mParent.acquireExistingProvider( + new Uri.Builder().authority(name).build()) : null; + } + } + + @Override + public boolean releaseProvider(IContentProvider provider) { + if (!mFallbackToExisting) return true; + if (mInUse.contains(provider)) { + mInUse.remove(provider); + return true; + } + return mParent.releaseProvider(provider); + } + + @Override + protected IContentProvider acquireUnstableProvider(Context c, String name) { + final ContentProvider provider = mProviders.get(name); + if (provider != null) { + return provider.getIContentProvider(); + } else { + return mFallbackToExisting ? mParent.acquireUnstableProvider(name) : null; + } + } + + @Override + public boolean releaseUnstableProvider(IContentProvider icp) { + if (!mFallbackToExisting) return true; + if (mInUse.contains(icp)) { + mInUse.remove(icp); + return true; + } + return mParent.releaseUnstableProvider(icp); + } + + @Override + public void unstableProviderDied(IContentProvider icp) { + if (!mFallbackToExisting) return; + if (mInUse.contains(icp)) { + return; + } + mParent.unstableProviderDied(icp); + } + + @Override + public void notifyChange(Uri uri, ContentObserver observer, boolean syncToNetwork) { + if (!mFallbackToExisting) return; + if (!mProviders.containsKey(uri.getAuthority())) { + super.notifyChange(uri, observer, syncToNetwork); + } + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProvider.java b/packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProvider.java new file mode 100644 index 0000000000000..f40fe4cb24506 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProvider.java @@ -0,0 +1,298 @@ +/* + * 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.systemui.utils; + +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.os.Bundle; +import android.os.RemoteException; +import android.provider.Settings; +import android.support.annotation.VisibleForTesting; +import android.test.mock.MockContentProvider; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.utils.FakeSettingsProvider.SettingOverrider.Builder; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Allows calls to android.provider.Settings to be tested easier. A SettingOverride + * can be acquired and a set of specific settings can be set to a value (and not changed + * in the system when set), so that they can be tested without breaking the test device. + *

+ * To use, in the before method acquire the override add all settings that will affect if + * your test passes or not. + * + *

+ * {@literal
+ * mSettingOverride = mTestableContext.getSettingsProvider().acquireOverridesBuilder()
+ *         .addSetting("secure", Secure.USER_SETUP_COMPLETE, "0")
+ *         .build();
+ * }
+ * 
+ * + * Then in the after free up the settings. + * + *
+ * {@literal
+ * mSettingOverride.release();
+ * }
+ * 
+ */ +public class FakeSettingsProvider extends MockContentProvider { + + private static final String TAG = "FakeSettingsProvider"; + private static final boolean DEBUG = false; + + // Number of times to try to acquire a setting if in use. + private static final int MAX_TRIES = 10; + // Time to wait for each setting. WAIT_TIMEOUT * MAX_TRIES will be the maximum wait time + // for a setting. + private static final long WAIT_TIMEOUT = 1000; + + private final Map mOverrideMap = new ArrayMap<>(); + private final Map> mOwners = new ArrayMap<>(); + + private static FakeSettingsProvider sInstance; + private final ContentProviderClient mSettings; + private final ContentResolver mResolver; + + private FakeSettingsProvider(ContentProviderClient settings, ContentResolver resolver) { + mSettings = settings; + mResolver = resolver; + } + + public Builder acquireOverridesBuilder(SysuiTestCase test) { + return new Builder(this, test); + } + + public void clearOverrides(SysuiTestCase test) { + List overrides = mOwners.remove(test); + if (overrides != null) { + overrides.forEach(override -> override.ensureReleased()); + } + } + + public Bundle call(String method, String arg, Bundle extras) { + // Methods are "GET_system", "GET_global", "PUT_secure", etc. + final String[] commands = method.split("_", 2); + final String op = commands[0]; + final String table = commands[1]; + + synchronized (mOverrideMap) { + SettingOverrider overrider = mOverrideMap.get(key(table, arg)); + if (overrider == null) { + // Fall through to real settings. + try { + if (DEBUG) Log.d(TAG, "Falling through to real settings " + method); + // TODO: Add our own version of caching to handle this. + Bundle call = mSettings.call(method, arg, extras); + call.remove(Settings.CALL_METHOD_TRACK_GENERATION_KEY); + return call; + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + String value; + Bundle out = new Bundle(); + switch (op) { + case "GET": + value = overrider.get(table, arg); + if (value != null) { + out.putString(Settings.NameValueTable.VALUE, value); + } + break; + case "PUT": + value = extras.getString(Settings.NameValueTable.VALUE, null); + if (value != null) { + overrider.put(table, arg, value); + } else { + overrider.remove(table, arg); + } + break; + default: + throw new UnsupportedOperationException("Unknown command " + method); + } + return out; + } + } + + private void acquireSettings(SettingOverrider overridder, Set keys, + SysuiTestCase owner) throws AcquireTimeoutException { + synchronized (mOwners) { + List list = mOwners.get(owner); + if (list == null) { + list = new ArrayList<>(); + mOwners.put(owner, list); + } + list.add(overridder); + } + synchronized (mOverrideMap) { + for (int i = 0; i < MAX_TRIES; i++) { + if (checkKeys(keys, false)) break; + try { + if (DEBUG) Log.d(TAG, "Waiting for contention to finish"); + mOverrideMap.wait(WAIT_TIMEOUT); + } catch (InterruptedException e) { + } + } + checkKeys(keys, true); + for (String key : keys) { + if (DEBUG) Log.d(TAG, "Acquiring " + key); + mOverrideMap.put(key, overridder); + } + } + } + + private void releaseSettings(Set keys) { + synchronized (mOverrideMap) { + for (String key : keys) { + if (DEBUG) Log.d(TAG, "Releasing " + key); + mOverrideMap.remove(key); + } + if (DEBUG) Log.d(TAG, "Notifying"); + mOverrideMap.notify(); + } + } + + @VisibleForTesting + public Object getLock() { + return mOverrideMap; + } + + private boolean checkKeys(Set keys, boolean shouldThrow) + throws AcquireTimeoutException { + for (String key : keys) { + if (mOverrideMap.containsKey(key)) { + if (shouldThrow) { + throw new AcquireTimeoutException("Could not acquire " + key); + } + return false; + } + } + return true; + } + + public static class SettingOverrider { + private final Set mValidKeys; + private final Map mValueMap = new ArrayMap<>(); + private final FakeSettingsProvider mProvider; + private boolean mReleased; + + private SettingOverrider(Set keys, FakeSettingsProvider provider) { + mValidKeys = new ArraySet<>(keys); + mProvider = provider; + } + + private void ensureReleased() { + if (!mReleased) { + release(); + } + } + + public void release() { + mProvider.releaseSettings(mValidKeys); + mReleased = true; + } + + private void putDirect(String key, String value) { + mValueMap.put(key, value); + } + + public void put(String table, String key, String value) { + if (!mValidKeys.contains(key(table, key))) { + throw new IllegalArgumentException("Key " + table + " " + key + + " not acquired for this overrider"); + } + mValueMap.put(key(table, key), value); + } + + public void remove(String table, String key) { + if (!mValidKeys.contains(key(table, key))) { + throw new IllegalArgumentException("Key " + table + " " + key + + " not acquired for this overrider"); + } + mValueMap.remove(key(table, key)); + } + + public String get(String table, String key) { + if (!mValidKeys.contains(key(table, key))) { + throw new IllegalArgumentException("Key " + table + " " + key + + " not acquired for this overrider"); + } + Log.d(TAG, "Get " + table + " " + key + " " + mValueMap.get(key(table, key))); + return mValueMap.get(key(table, key)); + } + + public static class Builder { + private final FakeSettingsProvider mProvider; + private final SysuiTestCase mOwner; + private Set mKeys = new ArraySet<>(); + private Map mValues = new ArrayMap<>(); + + private Builder(FakeSettingsProvider provider, SysuiTestCase test) { + mProvider = provider; + mOwner = test; + } + + public Builder addSetting(String table, String key) { + mKeys.add(key(table, key)); + return this; + } + + public Builder addSetting(String table, String key, String value) { + addSetting(table, key); + mValues.put(key(table, key), value); + return this; + } + + public SettingOverrider build() throws AcquireTimeoutException { + SettingOverrider overrider = new SettingOverrider(mKeys, mProvider); + mProvider.acquireSettings(overrider, mKeys, mOwner); + mValues.forEach((key, value) -> overrider.putDirect(key, value)); + return overrider; + } + } + } + + public static class AcquireTimeoutException extends Exception { + public AcquireTimeoutException(String str) { + super(str); + } + } + + private static String key(String table, String key) { + return table + "_" + key; + } + + /** + * Since the settings provider is cached inside android.provider.Settings, this must + * be gotten statically to ensure there is only one instance referenced. + * @param settings + */ + public static FakeSettingsProvider getFakeSettingsProvider(ContentProviderClient settings, + ContentResolver resolver) { + if (sInstance == null) { + sInstance = new FakeSettingsProvider(settings, resolver); + } + return sInstance; + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProviderTest.java b/packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProviderTest.java new file mode 100644 index 0000000000000..63bb5e7f0e4a5 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/FakeSettingsProviderTest.java @@ -0,0 +1,175 @@ +/* + * 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.systemui.utils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; + +import android.content.ContentResolver; +import android.os.Handler; +import android.os.HandlerThread; +import android.provider.Settings; +import android.provider.Settings.Global; +import android.provider.Settings.Secure; +import android.support.test.runner.AndroidJUnit4; +import android.util.Log; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.utils.FakeSettingsProvider.AcquireTimeoutException; +import com.android.systemui.utils.FakeSettingsProvider.SettingOverrider; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidJUnit4.class) +public class FakeSettingsProviderTest extends SysuiTestCase { + + public static final String NONEXISTENT_SETTING = "nonexistent_setting"; + private static final String TAG = "FakeSettingsProviderTest"; + private SettingOverrider mOverrider; + private ContentResolver mContentResolver; + + @Before + public void setup() throws AcquireTimeoutException { + mOverrider = mContext.getSettingsProvider().acquireOverridesBuilder(this) + .addSetting("secure", NONEXISTENT_SETTING) + .addSetting("global", NONEXISTENT_SETTING, "initial value") + .addSetting("global", Global.DEVICE_PROVISIONED) + .build(); + mContentResolver = mContext.getContentResolver(); + } + + @After + public void teardown() { + if (mOverrider != null) { + mOverrider.release(); + } + } + + @Test + public void testInitialValueSecure() { + String value = Secure.getString(mContentResolver, NONEXISTENT_SETTING); + assertNull(value); + } + + @Test + public void testInitialValueGlobal() { + String value = Global.getString(mContentResolver, NONEXISTENT_SETTING); + assertEquals("initial value", value); + } + + @Test + public void testSeparateTables() { + Secure.putString(mContentResolver, NONEXISTENT_SETTING, "something"); + Global.putString(mContentResolver, NONEXISTENT_SETTING, "else"); + assertEquals("something", Secure.getString(mContentResolver, NONEXISTENT_SETTING)); + assertEquals("something", mOverrider.get("secure", NONEXISTENT_SETTING)); + assertEquals("else", Global.getString(mContentResolver, NONEXISTENT_SETTING)); + assertEquals("else", mOverrider.get("global", NONEXISTENT_SETTING)); + } + + @Test + public void testPassThrough() { + // Grab the value of a setting that is not overridden. + assertTrue(Secure.getInt(mContentResolver, Secure.USER_SETUP_COMPLETE, 0) != 0); + } + + @Test + public void testOverrideExisting() { + // Grab the value of a setting that is overridden and will be different than the actual + // value. + assertNull(Global.getString(mContentResolver, Global.DEVICE_PROVISIONED)); + } + + @Test + public void testRelease() { + // Verify different value. + assertNull(Global.getString(mContentResolver, Global.DEVICE_PROVISIONED)); + mOverrider.release(); + mOverrider = null; + // Verify actual value after release. + assertEquals("1", Global.getString(mContentResolver, Global.DEVICE_PROVISIONED)); + } + + @Test + public void testAutoRelease() throws Exception { + super.cleanup(); + mContext.getSettingsProvider().acquireOverridesBuilder(this) + .addSetting("global", Global.DEVICE_PROVISIONED) + .build(); + } + + @Test + public void testContention() throws AcquireTimeoutException, InterruptedException { + SettingOverrider[] overriders = new SettingOverrider[2]; + Object lock = new Object(); + String secure = "secure"; + String key = "something shared"; + String[] result = new String[1]; + overriders[0] = mContext.getSettingsProvider().acquireOverridesBuilder(this) + .addSetting(secure, key, "Some craziness") + .build(); + synchronized (lock) { + HandlerThread t = runOnHandler(() -> { + try { + // Grab the lock that will be used for the settings ownership to ensure + // we have some contention going on. + synchronized (mContext.getSettingsProvider().getLock()) { + synchronized (lock) { + // Let the other thread know to release the settings, but it won't + // be able to until this thread waits in the build() method. + lock.notify(); + } + overriders[1] = mContext.getSettingsProvider() + .acquireOverridesBuilder(FakeSettingsProviderTest.this) + .addSetting(secure, key, "default value") + .build(); + // Ensure that the default is the one we set, and not left over from + // the other setting override. + result[0] = Settings.Secure.getString(mContentResolver, key); + synchronized (lock) { + // Let the main thread know we are done. + lock.notify(); + } + } + } catch (AcquireTimeoutException e) { + Log.e(TAG, "Couldn't acquire setting", e); + } + }); + // Wait for the thread to hold the acquire lock, then release the settings. + lock.wait(); + overriders[0].release(); + // Wait for the thread to be done getting the value. + lock.wait(); + // Quit and cleanup. + t.quitSafely(); + assertNotNull(overriders[1]); + overriders[1].release(); + } + // Verify the value was the expected one from the thread's SettingOverride. + assertEquals("default value", result[0]); + } + + private HandlerThread runOnHandler(Runnable r) { + HandlerThread t = new HandlerThread("Test Thread"); + t.start(); + new Handler(t.getLooper()).post(r); + return t; + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java b/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java new file mode 100644 index 0000000000000..517982361424c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/utils/TestableContext.java @@ -0,0 +1,52 @@ +/* + * 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.systemui.utils; + +import android.content.ContentProviderClient; +import android.content.ContentResolver; +import android.content.Context; +import android.content.ContextWrapper; +import android.provider.Settings; + +public class TestableContext extends ContextWrapper { + + private final FakeContentResolver mFakeContentResolver; + private final FakeSettingsProvider mSettingsProvider; + + public TestableContext(Context base) { + super(base); + mFakeContentResolver = new FakeContentResolver(base); + ContentProviderClient settings = base.getContentResolver() + .acquireContentProviderClient(Settings.AUTHORITY); + mSettingsProvider = FakeSettingsProvider.getFakeSettingsProvider(settings, + mFakeContentResolver); + mFakeContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider); + } + + public FakeSettingsProvider getSettingsProvider() { + return mSettingsProvider; + } + + @Override + public FakeContentResolver getContentResolver() { + return mFakeContentResolver; + } + + @Override + public Context getApplicationContext() { + // Return this so its always a TestableContext. + return this; + } +}