From 6d553f6dfdcc188fa6b17d4abb11d6222009a8fb Mon Sep 17 00:00:00 2001 From: Lorenzo Colitti Date: Sun, 5 Jun 2016 02:20:29 +0900 Subject: [PATCH 1/4] Add a FakeSettingsProvider and use it in ConnectivityServiceTest. This class makes it easier to test code that uses Settings: 1. Real device or emulator settings don't affect the code under test; all settings always start off empty. 2. It's possible to change settings from the test without affecting system settings. 3. No changes are needed to the code under test. The changes to the tests are simple: just add a fake ContentResolver to whatever mock Context is already used by the test, and make that ContentResolver use the fake provider. Bug: 23113288 Change-Id: I5e7e5a87571444ae49ccf551705620675a36cd17 --- .../internal/util/FakeSettingsProvider.java | 130 ++++++++++++++++++ .../util/FakeSettingsProviderTest.java | 58 ++++++++ .../server/ConnectivityServiceTest.java | 15 +- 3 files changed, 202 insertions(+), 1 deletion(-) create mode 100644 services/tests/servicestests/src/com/android/internal/util/FakeSettingsProvider.java create mode 100644 services/tests/servicestests/src/com/android/internal/util/FakeSettingsProviderTest.java diff --git a/services/tests/servicestests/src/com/android/internal/util/FakeSettingsProvider.java b/services/tests/servicestests/src/com/android/internal/util/FakeSettingsProvider.java new file mode 100644 index 0000000000000..808f4dd7b84ef --- /dev/null +++ b/services/tests/servicestests/src/com/android/internal/util/FakeSettingsProvider.java @@ -0,0 +1,130 @@ +/* + * 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.internal.util; + +import android.net.Uri; +import android.os.Bundle; +import android.provider.Settings; +import android.test.mock.MockContentProvider; +import android.util.Log; + +import java.util.HashMap; + +/** + * Fake for system settings. + * + * To use, ensure that the Context used by the test code returns a ContentResolver that uses this + * provider for the Settings authority: + * + * class MyTestContext extends MockContext { + * ... + * private final MockContentResolver mContentResolver; + * public MyTestContext(...) { + * ... + * mContentResolver = new MockContentResolver(); + * mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + * } + * ... + * @Override + * public ContentResolver getContentResolver() { + * return mContentResolver; + * } + * + * As long as the code under test is using the test Context, the actual code under test does not + * need to be modified, and can access Settings using the normal static methods: + * + * Settings.Global.getInt(cr, "my_setting", 0); // Returns 0. + * Settings.Global.putInt(cr, "my_setting", 5); + * Settings.Global.getInt(cr, "my_setting", 0); // Returns 5. + * + * Note that this class cannot be used in the same process as real settings. This is because it + * works by passing an alternate ContentResolver to Settings operations. Unfortunately, the Settings + * class only fetches the content provider from the passed-in ContentResolver the first time it's + * used, and after that stores it in a per-process static. + * + * TODO: evaluate implementing settings change notifications. This would require: + * + * 1. Making ContentResolver#registerContentObserver non-final and overriding it in + * MockContentResolver. + * 2. Making FakeSettingsProvider take a ContentResolver argument. + * 3. Calling ContentResolver#notifyChange(getUriFor(table, arg), ...) on every settings change. + */ +public class FakeSettingsProvider extends MockContentProvider { + + private static final String TAG = FakeSettingsProvider.class.getSimpleName(); + private static final boolean DBG = false; + private static final String[] TABLES = { "system", "secure", "global" }; + + private final HashMap> mTables = new HashMap<>(); + + public FakeSettingsProvider() { + for (int i = 0; i < TABLES.length; i++) { + mTables.put(TABLES[i], new HashMap()); + } + } + + private Uri getUriFor(String table, String key) { + switch (table) { + case "system": + return Settings.System.getUriFor(key); + case "secure": + return Settings.Secure.getUriFor(key); + case "global": + return Settings.Global.getUriFor(key); + default: + throw new UnsupportedOperationException("Unknown settings table " + table); + } + } + + public Bundle call(String method, String arg, Bundle extras) { + // Methods are "GET_system", "GET_global", "PUT_secure", etc. + String[] commands = method.split("_", 2); + String op = commands[0]; + String table = commands[1]; + + Bundle out = new Bundle(); + String value; + switch (op) { + case "GET": + value = mTables.get(table).get(arg); + if (value != null) { + if (DBG) { + Log.d(TAG, String.format("Returning fake setting %s.%s = %s", + table, arg, value)); + } + out.putString(Settings.NameValueTable.VALUE, value); + } + break; + case "PUT": + value = extras.getString(Settings.NameValueTable.VALUE, null); + if (DBG) { + Log.d(TAG, String.format("Inserting fake setting %s.%s = %s", + table, arg, value)); + } + if (value != null) { + mTables.get(table).put(arg, value); + } else { + mTables.get(table).remove(arg); + } + break; + default: + throw new UnsupportedOperationException("Unknown command " + method); + } + + return out; + } +} diff --git a/services/tests/servicestests/src/com/android/internal/util/FakeSettingsProviderTest.java b/services/tests/servicestests/src/com/android/internal/util/FakeSettingsProviderTest.java new file mode 100644 index 0000000000000..a18737c42e529 --- /dev/null +++ b/services/tests/servicestests/src/com/android/internal/util/FakeSettingsProviderTest.java @@ -0,0 +1,58 @@ +/* + * 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.internal.util; + +import android.content.ContentResolver; +import android.database.ContentObserver; +import android.net.Uri; +import android.provider.Settings; +import android.test.AndroidTestCase; +import android.test.mock.MockContentResolver; +import android.test.mock.MockContext; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.Log; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Unit tests for FakeSettingsProvider. + */ +public class FakeSettingsProviderTest extends AndroidTestCase { + + private MockContentResolver mCr; + + @Override + public void setUp() throws Exception { + mCr = new MockContentResolver(); + mCr.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); + } + + @SmallTest + public void testBasicOperation() throws Exception { + String settingName = Settings.System.SCREEN_BRIGHTNESS; + + try { + Settings.System.getInt(mCr, settingName); + fail("FakeSettingsProvider should start off empty."); + } catch (Settings.SettingNotFoundException expected) {} + + // Check that fake settings can be written and read back. + Settings.System.putInt(mCr, settingName, 123); + assertEquals(123, Settings.System.getInt(mCr, settingName)); + } +} diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java index 4fae4a732aad3..00ba06988d869 100644 --- a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.mock; import android.app.PendingIntent; import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; @@ -61,12 +62,15 @@ import android.os.Messenger; import android.os.MessageQueue.IdleHandler; import android.os.Process; import android.os.SystemClock; +import android.provider.Settings; import android.test.AndroidTestCase; +import android.test.mock.MockContentResolver; import android.test.suitebuilder.annotation.LargeTest; import android.test.suitebuilder.annotation.SmallTest; import android.util.Log; import android.util.LogPrinter; +import com.android.internal.util.FakeSettingsProvider; import com.android.internal.util.WakeupMessage; import com.android.server.connectivity.NetworkAgentInfo; import com.android.server.connectivity.NetworkMonitor; @@ -118,8 +122,12 @@ public class ConnectivityServiceTest extends AndroidTestCase { } private class MockContext extends BroadcastInterceptingContext { + private final MockContentResolver mContentResolver; + MockContext(Context base) { super(base); + mContentResolver = new MockContentResolver(); + mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); } @Override @@ -135,10 +143,15 @@ public class ConnectivityServiceTest extends AndroidTestCase { } @Override - public Object getSystemService (String name) { + public Object getSystemService(String name) { if (name == Context.CONNECTIVITY_SERVICE) return mCm; return super.getSystemService(name); } + + @Override + public ContentResolver getContentResolver() { + return mContentResolver; + } } /** From 762ea7aab7636b33daeee2c4ba169f163c61347f Mon Sep 17 00:00:00 2001 From: Lorenzo Colitti Date: Sun, 5 Jun 2016 21:00:23 +0900 Subject: [PATCH 2/4] Add a test for mobile data always on. Bug: 23113288 Change-Id: Ia8649061a797367d135bb5576600a7cdc85a822e --- .../android/server/ConnectivityService.java | 11 +++ .../server/ConnectivityServiceTest.java | 67 +++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/services/core/java/com/android/server/ConnectivityService.java b/services/core/java/com/android/server/ConnectivityService.java index 5118b3f2ab7ac..7e0e8d4961db7 100644 --- a/services/core/java/com/android/server/ConnectivityService.java +++ b/services/core/java/com/android/server/ConnectivityService.java @@ -786,6 +786,17 @@ public class ConnectivityService extends IConnectivityManager.Stub return new NetworkRequest(netCap, TYPE_NONE, nextNetworkRequestId()); } + // Used only for testing. + // TODO: Delete this and either: + // 1. Give Fake SettingsProvider the ability to send settings change notifications (requires + // changing ContentResolver to make registerContentObserver non-final). + // 2. Give FakeSettingsProvider an alternative notification mechanism and have the test use it + // by subclassing SettingsObserver. + @VisibleForTesting + void updateMobileDataAlwaysOn() { + mHandler.sendEmptyMessage(EVENT_CONFIGURE_MOBILE_DATA_ALWAYS_ON); + } + private void handleMobileDataAlwaysOn() { final boolean enable = (Settings.Global.getInt( mContext.getContentResolver(), Settings.Global.MOBILE_DATA_ALWAYS_ON, 0) == 1); diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java index 00ba06988d869..9668d95c3e758 100644 --- a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java @@ -1490,6 +1490,73 @@ public class ConnectivityServiceTest extends AndroidTestCase { defaultNetworkCallback.expectCallback(CallbackState.LOST, mCellNetworkAgent); } + @SmallTest + public void testMobileDataAlwaysOn() throws Exception { + final TestNetworkCallback cellNetworkCallback = new TestNetworkCallback(); + final NetworkRequest cellRequest = new NetworkRequest.Builder() + .addTransportType(TRANSPORT_CELLULAR).build(); + mCm.registerNetworkCallback(cellRequest, cellNetworkCallback); + + final HandlerThread handlerThread = new HandlerThread("MobileDataAlwaysOnFactory"); + handlerThread.start(); + NetworkCapabilities filter = new NetworkCapabilities() + .addTransportType(TRANSPORT_CELLULAR) + .addCapability(NET_CAPABILITY_INTERNET); + final MockNetworkFactory testFactory = new MockNetworkFactory(handlerThread.getLooper(), + mServiceContext, "testFactory", filter); + testFactory.setScoreFilter(40); + + // Register the factory and expect it to start looking for a network. + testFactory.expectAddRequests(1); + testFactory.register(); + testFactory.waitForNetworkRequests(1); + assertTrue(testFactory.getMyStartRequested()); + + // Bring up wifi. The factory stops looking for a network. + mWiFiNetworkAgent = new MockNetworkAgent(TRANSPORT_WIFI); + testFactory.expectAddRequests(2); // Because the default request changes score twice. + mWiFiNetworkAgent.connect(true); + testFactory.waitForNetworkRequests(1); + assertFalse(testFactory.getMyStartRequested()); + + ContentResolver cr = mServiceContext.getContentResolver(); + + // Turn on mobile data always on. The factory starts looking again. + testFactory.expectAddRequests(1); + Settings.Global.putInt(cr, Settings.Global.MOBILE_DATA_ALWAYS_ON, 1); + mService.updateMobileDataAlwaysOn(); + testFactory.waitForNetworkRequests(2); + assertTrue(testFactory.getMyStartRequested()); + + // Bring up cell data and check that the factory stops looking. + assertEquals(1, mCm.getAllNetworks().length); + mCellNetworkAgent = new MockNetworkAgent(TRANSPORT_CELLULAR); + testFactory.expectAddRequests(2); // Because the cell request changes score twice. + mCellNetworkAgent.connect(true); + cellNetworkCallback.expectCallback(CallbackState.AVAILABLE); + testFactory.waitForNetworkRequests(2); + assertFalse(testFactory.getMyStartRequested()); // Because the cell network outscores us. + + // Check that cell data stays up. + mService.waitForIdle(); + verifyActiveNetwork(TRANSPORT_WIFI); + assertEquals(2, mCm.getAllNetworks().length); + + // Turn off mobile data always on and expect the request to disappear... + testFactory.expectRemoveRequests(1); + Settings.Global.putInt(cr, Settings.Global.MOBILE_DATA_ALWAYS_ON, 0); + mService.updateMobileDataAlwaysOn(); + testFactory.waitForNetworkRequests(1); + + // ... and cell data to be torn down. + cellNetworkCallback.expectCallback(CallbackState.LOST); + assertEquals(1, mCm.getAllNetworks().length); + + testFactory.unregister(); + mCm.unregisterNetworkCallback(cellNetworkCallback); + handlerThread.quit(); + } + private static class TestKeepaliveCallback extends PacketKeepaliveCallback { public static enum CallbackType { ON_STARTED, ON_STOPPED, ON_ERROR }; From 9acda9c82577972b8291853978650c24c20e950c Mon Sep 17 00:00:00 2001 From: Lorenzo Colitti Date: Sun, 5 Jun 2016 23:45:25 +0900 Subject: [PATCH 3/4] Don't treat the lingerExpired broadcast specially. NetworkMonitor no longer uses the broadcast for lingering, it uses WakeupMessage instead. Bug: 23113288 Change-Id: Idb0c55fc68cb8f45b3213c7134213904f227852e --- .../com/android/server/ConnectivityServiceTest.java | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java index 9668d95c3e758..68a35f1b9140c 100644 --- a/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java @@ -130,18 +130,6 @@ public class ConnectivityServiceTest extends AndroidTestCase { mContentResolver.addProvider(Settings.AUTHORITY, new FakeSettingsProvider()); } - @Override - public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) { - // PendingIntents sent by the AlarmManager are not intercepted by - // BroadcastInterceptingContext so we must really register the receiver. - // This shouldn't effect the real NetworkMonitors as the action contains a random token. - if (filter.getAction(0).startsWith("android.net.netmon.lingerExpired")) { - return getBaseContext().registerReceiver(receiver, filter); - } else { - return super.registerReceiver(receiver, filter); - } - } - @Override public Object getSystemService(String name) { if (name == Context.CONNECTIVITY_SERVICE) return mCm; @@ -654,7 +642,6 @@ public class ConnectivityServiceTest extends AndroidTestCase { public void waitForIdle() { waitForIdle(TIMEOUT_MS); } - } private interface Criteria { From eab17da5882e59caff25c73aa6bcd0587aca9dd4 Mon Sep 17 00:00:00 2001 From: Lorenzo Colitti Date: Tue, 7 Jun 2016 16:27:10 +0900 Subject: [PATCH 4/4] Give WakeupMessage the ability to transport an object as well. Bug: 23113288 Change-Id: Ic98e3bcb3ea8b0b28a309bd647fb4178311c46a3 --- .../com/android/internal/util/WakeupMessage.java | 16 ++++++++++++---- .../android/internal/util/WakeupMessageTest.java | 4 +++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/core/java/com/android/internal/util/WakeupMessage.java b/core/java/com/android/internal/util/WakeupMessage.java index 26537451992c6..7d222c74ac3be 100644 --- a/core/java/com/android/internal/util/WakeupMessage.java +++ b/core/java/com/android/internal/util/WakeupMessage.java @@ -45,24 +45,32 @@ public class WakeupMessage implements AlarmManager.OnAlarmListener { protected final String mCmdName; @VisibleForTesting protected final int mCmd, mArg1, mArg2; + @VisibleForTesting + protected final Object mObj; private boolean mScheduled; public WakeupMessage(Context context, Handler handler, - String cmdName, int cmd, int arg1, int arg2) { + String cmdName, int cmd, int arg1, int arg2, Object obj) { mAlarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); mHandler = handler; mCmdName = cmdName; mCmd = cmd; mArg1 = arg1; mArg2 = arg2; + mObj = obj; } public WakeupMessage(Context context, Handler handler, String cmdName, int cmd, int arg1) { - this(context, handler, cmdName, cmd, arg1, 0); + this(context, handler, cmdName, cmd, arg1, 0, null); + } + + public WakeupMessage(Context context, Handler handler, + String cmdName, int cmd, int arg1, int arg2) { + this(context, handler, cmdName, cmd, arg1, arg2, null); } public WakeupMessage(Context context, Handler handler, String cmdName, int cmd) { - this(context, handler, cmdName, cmd, 0, 0); + this(context, handler, cmdName, cmd, 0, 0, null); } /** @@ -99,7 +107,7 @@ public class WakeupMessage implements AlarmManager.OnAlarmListener { mScheduled = false; } if (stillScheduled) { - Message msg = mHandler.obtainMessage(mCmd, mArg1, mArg2); + Message msg = mHandler.obtainMessage(mCmd, mArg1, mArg2, mObj); mHandler.handleMessage(msg); msg.recycle(); } diff --git a/core/tests/utiltests/src/com/android/internal/util/WakeupMessageTest.java b/core/tests/utiltests/src/com/android/internal/util/WakeupMessageTest.java index da8bc1d0945d9..7935880d12a91 100644 --- a/core/tests/utiltests/src/com/android/internal/util/WakeupMessageTest.java +++ b/core/tests/utiltests/src/com/android/internal/util/WakeupMessageTest.java @@ -45,6 +45,7 @@ public class WakeupMessageTest { private static final int TEST_CMD = 18; private static final int TEST_ARG1 = 33; private static final int TEST_ARG2 = 182; + private static final Object TEST_OBJ = "hello"; @Mock AlarmManager mAlarmManager; WakeupMessage mMessage; @@ -92,7 +93,7 @@ public class WakeupMessageTest { mListenerCaptor.capture(), any(Handler.class)); mMessage = new WakeupMessage(context, mHandler, TEST_CMD_NAME, TEST_CMD, TEST_ARG1, - TEST_ARG2); + TEST_ARG2, TEST_OBJ); } /** @@ -114,6 +115,7 @@ public class WakeupMessageTest { assertEquals("what", TEST_CMD, mHandler.getLastMessage().what); assertEquals("arg1", TEST_ARG1, mHandler.getLastMessage().arg1); assertEquals("arg2", TEST_ARG2, mHandler.getLastMessage().arg2); + assertEquals("obj", TEST_OBJ, mHandler.getLastMessage().obj); } /**