blob: 1d3418f91e71b04fdef9d5fb2bbd07d5ec37b6d4 [file] [log] [blame]
/*
* Copyright (C) 2008 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.provider.cts.settings;
import static android.provider.Settings.Config.SYNC_DISABLED_MODE_NONE;
import static android.provider.Settings.RESET_MODE_PACKAGE_DEFAULTS;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.provider.Settings;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import com.android.compatibility.common.util.PollingCheck;
import com.android.internal.annotations.GuardedBy;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
@RunWith(AndroidJUnit4.class)
public class Settings_ConfigTest {
private static final String NAMESPACE1 = "namespace1";
private static final String NAMESPACE2 = "namespace2";
private static final String EMPTY_NAMESPACE = "empty_namespace";
private static final String KEY1 = "key1";
private static final String KEY2 = "key2";
private static final String VALUE1 = "value1";
private static final String VALUE2 = "value2";
private static final String VALUE3 = "value3";
private static final String VALUE4 = "value4";
private static final String DEFAULT_VALUE = "default_value";
private static final String TAG = "ContentResolverTest";
private static final Uri TABLE1_URI = Uri.parse("content://"
+ Settings.AUTHORITY + "/config");
private static final String TEST_PACKAGE_NAME = "android.content.cts";
private static final long OPERATION_TIMEOUT_MS = 5000;
private static final Context CONTEXT = InstrumentationRegistry.getContext();
private static final long WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS = 2000; // 2 sec
private final Object mLock = new Object();
private static final String WRITE_DEVICE_CONFIG_PERMISSION =
"android.permission.WRITE_DEVICE_CONFIG";
private static final String READ_DEVICE_CONFIG_PERMISSION =
"android.permission.READ_DEVICE_CONFIG";
private static final String MONITOR_DEVICE_CONFIG_ACCESS =
"android.permission.MONITOR_DEVICE_CONFIG_ACCESS";
private static ContentResolver sContentResolver;
private static Context sContext;
/**
* Get necessary permissions to access Setting.Config API and set up context
*/
@Before
public void setUpContext() throws Exception {
InstrumentationRegistry.getInstrumentation().getUiAutomation().adoptShellPermissionIdentity(
WRITE_DEVICE_CONFIG_PERMISSION, READ_DEVICE_CONFIG_PERMISSION,
MONITOR_DEVICE_CONFIG_ACCESS);
sContext = InstrumentationRegistry.getContext();
sContentResolver = sContext.getContentResolver();
}
/**
* Clean up the namespaces, sync mode and permissions.
*/
@After
public void cleanUp() throws Exception {
Settings.Config.setSyncDisabledMode(SYNC_DISABLED_MODE_NONE);
deleteProperty(NAMESPACE1, KEY1);
deleteProperty(NAMESPACE2, KEY1);
deleteProperty(NAMESPACE1, KEY2);
deleteProperty(NAMESPACE2, KEY2);
InstrumentationRegistry.getInstrumentation().getUiAutomation()
.dropShellPermissionIdentity();
}
/**
* Checks that getting string which does not exist returns null.
*/
@Test
public void testGetString_empty() {
String result = Settings.Config.getString(KEY1);
assertNull("Request for non existent flag name in Settings.Config API should return null "
+ "while " + result + " was returned", result);
}
/**
* Checks that getting strings which does not exist returns empty map.
*/
@Test
public void testGetStrings_empty() {
Map<String, String> result = Settings.Config
.getStrings(EMPTY_NAMESPACE, Arrays.asList(KEY1));
assertTrue("Request for non existent flag name in Settings.Config API should return "
+ "empty map while " + result.toString() + " was returned", result.isEmpty());
}
/**
* Checks that setting and getting string from the same namespace return correct value.
*/
@Test
public void testSetAndGetString_sameNamespace() {
Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/false);
String result = Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1);
assertEquals("Value read from Settings.Config API does not match written value.", VALUE1,
result);
}
/**
* Checks that setting a string in one namespace does not set the same string in a different
* namespace.
*/
@Test
public void testSetAndGetString_differentNamespace() {
Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/false);
String result = Settings.Config.getStrings(NAMESPACE2, Arrays.asList(KEY1)).get(KEY1);
assertNull("Value for same keys written to different namespaces must not clash", result);
}
/**
* Checks that different namespaces can keep different values for the same key.
*/
@Test
public void testSetAndGetString_multipleNamespaces() {
Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/false);
Settings.Config.putString(NAMESPACE2, KEY1, VALUE2, /*makeDefault=*/false);
String result = Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1);
assertEquals("Value read from Settings.Config API does not match written value.", VALUE1,
result);
result = Settings.Config.getStrings(NAMESPACE2, Arrays.asList(KEY1)).get(KEY1);
assertEquals("Value read from Settings.Config API does not match written value.", VALUE2,
result);
}
/**
* Checks that saving value twice keeps the last value.
*/
@Test
public void testSetAndGetString_overrideValue() {
Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/false);
Settings.Config.putString(NAMESPACE1, KEY1, VALUE2, /*makeDefault=*/false);
String result = Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1);
assertEquals("New value written to the same namespace/key did not override previous"
+ " value.", VALUE2, result);
}
/**
* Checks that putString() fails with NullPointerException when called with null namespace.
*/
@Test
public void testPutString_nullNamespace() {
try {
Settings.Config.putString(null, KEY1, DEFAULT_VALUE, /*makeDefault=*/false);
fail("Settings.Config.putString() with null namespace must result in "
+ "NullPointerException");
} catch (NullPointerException e) {
// expected
}
}
/**
* Checks that putString() fails with NullPointerException when called with null name.
*/
@Test
public void testPutString_nullName() {
try {
Settings.Config.putString(NAMESPACE1, null, DEFAULT_VALUE, /*makeDefault=*/false);
fail("Settings.Config.putString() with null name must result in NullPointerException");
} catch (NullPointerException e) {
// expected
}
}
/**
* Checks that setting and getting strings from the same namespace return correct values.
*/
@Test
public void testSetAndGetStrings_sameNamespace() throws Exception {
assertNull(Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
assertNull(Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY2));
Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
put(KEY1, VALUE1);
put(KEY2, VALUE2);
}});
assertEquals(VALUE1, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
assertEquals(VALUE2, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY2)).get(KEY2));
}
/**
* Checks that setting strings in one namespace does not set the same strings in a
* different namespace.
*/
@Test
public void testSetAndGetStrings_differentNamespace() throws Exception {
Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
put(KEY1, VALUE1);
put(KEY2, VALUE2);
}});
assertNull(Settings.Config.getStrings(NAMESPACE2, Arrays.asList(KEY1)).get(KEY1));
assertNull(Settings.Config.getStrings(NAMESPACE2, Arrays.asList(KEY2)).get(KEY2));
}
/**
* Checks that different namespaces can keep different values for the same keys.
*/
@Test
public void testSetAndGetStrings_multipleNamespaces() throws Exception {
Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
put(KEY1, VALUE1);
put(KEY2, VALUE2);
}});
Settings.Config.setStrings(NAMESPACE2, new HashMap<String, String>() {{
put(KEY1, VALUE3);
put(KEY2, VALUE4);
}});
Map<String, String> namespace1Values = Settings.Config
.getStrings(NAMESPACE1, Arrays.asList(KEY1, KEY2));
Map<String, String> namespace2Values = Settings.Config
.getStrings(NAMESPACE2, Arrays.asList(KEY1, KEY2));
assertEquals(namespace1Values.toString(), VALUE1, namespace1Values.get(KEY1));
assertEquals(namespace1Values.toString(), VALUE2, namespace1Values.get(KEY2));
assertEquals(namespace2Values.toString(), VALUE3, namespace2Values.get(KEY1));
assertEquals(namespace2Values.toString(), VALUE4, namespace2Values.get(KEY2));
}
/**
* Checks that saving values twice keeps the last values.
*/
@Test
public void testSetAndGetStrings_overrideValue() throws Exception {
Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
put(KEY1, VALUE1);
put(KEY2, VALUE2);
}});
Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
put(KEY1, VALUE3);
put(KEY2, VALUE4);
}});
assertEquals(VALUE3, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
assertEquals(VALUE4, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY2)).get(KEY2));
}
/**
* Checks that deleteString() fails with NullPointerException when called with null namespace.
*/
@Test
public void testDeleteString_nullKey() {
try {
Settings.Config.deleteString(null, KEY1);
fail("Settings.Config.deleteString() with null namespace must result in "
+ "NullPointerException");
} catch (NullPointerException e) {
// expected
}
}
/**
* Checks that deleteString() fails with NullPointerException when called with null key.
*/
@Test
public void testDeleteString_nullNamespace() {
try {
Settings.Config.deleteString(NAMESPACE1, null);
fail("Settings.Config.deleteString() with null key must result in "
+ "NullPointerException");
} catch (NullPointerException e) {
// expected
}
}
/**
* Checks delete string.
*/
@Test
public void testDeleteString() {
Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/false);
assertEquals(VALUE1, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
Settings.Config.deleteString(NAMESPACE1, KEY1);
assertNull(Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
}
/**
* Test that reset to package default successfully resets values.
*/
@Test
public void testResetToPackageDefaults() {
Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/true);
Settings.Config.putString(NAMESPACE1, KEY1, VALUE2, /*makeDefault=*/false);
assertEquals(VALUE2, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
Settings.Config.resetToDefaults(RESET_MODE_PACKAGE_DEFAULTS, NAMESPACE1);
assertEquals(VALUE1, Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1)).get(KEY1));
}
/**
* Test updating syncDisabledMode.
*/
@Test
public void testSetSyncDisabledMode() {
assertEquals(SYNC_DISABLED_MODE_NONE, Settings.Config.getSyncDisabledMode());
Settings.Config.setSyncDisabledMode(RESET_MODE_PACKAGE_DEFAULTS);
assertEquals(RESET_MODE_PACKAGE_DEFAULTS, Settings.Config.getSyncDisabledMode());
}
/**
* Test register content observer.
*/
@Test
public void testRegisterContentObserver() {
final MockContentObserver mco = new MockContentObserver();
Settings.Config.registerContentObserver(NAMESPACE1, true, mco);
assertFalse(mco.hadOnChanged());
Settings.Config.putString(NAMESPACE1, KEY1, VALUE2, /*makeDefault=*/false);
new PollingCheck() {
@Override
protected boolean check() {
return mco.hadOnChanged();
}
}.run();
mco.reset();
Settings.Config.unregisterContentObserver(mco);
assertFalse(mco.hadOnChanged());
Settings.Config.putString(NAMESPACE1, KEY1, VALUE1, /*makeDefault=*/false);
assertFalse(mco.hadOnChanged());
try {
Settings.Config.registerContentObserver(null, false, mco);
fail("did not throw Exceptionwhen uri is null.");
} catch (NullPointerException e) {
//expected.
} catch (IllegalArgumentException e) {
// also expected
}
try {
Settings.Config.registerContentObserver(NAMESPACE1, false, null);
fail("did not throw Exception when register null content observer.");
} catch (NullPointerException e) {
//expected.
}
try {
sContentResolver.unregisterContentObserver(null);
fail("did not throw NullPointerException when unregister null content observer.");
} catch (NullPointerException e) {
//expected.
}
}
/**
* Test set monitor callback.
*/
@Test
public void testSetMonitorCallback() {
final CountDownLatch latch = new CountDownLatch(2);
final TestMonitorCallback callback = new TestMonitorCallback(latch);
Settings.Config.setMonitorCallback(sContentResolver,
Executors.newSingleThreadExecutor(), callback);
// Reading properties triggers the monitor callback function.
Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1));
try {
Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
put(KEY1, VALUE1);
put(KEY2, VALUE2);
}});
} catch (DeviceConfig.BadConfigException e) {
fail("Callback set strings" + e.toString());
}
try {
if (!latch.await(OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
fail("Callback function was not called");
}
} catch (InterruptedException e) {
// this part is executed when an exception (in this example InterruptedException) occurs
fail("Callback function was not called due to interruption" + e.toString());
}
assertEquals(callback.onNamespaceUpdateCalls, 1);
assertEquals(callback.onDeviceConfigAccessCalls, 1);
}
/**
* Test clear monitor callback.
*/
@Test
public void testClearMonitorCallback() {
final CountDownLatch latch = new CountDownLatch(2);
final TestMonitorCallback callback = new TestMonitorCallback(latch);
Settings.Config.setMonitorCallback(sContentResolver,
Executors.newSingleThreadExecutor(), callback);
Settings.Config.clearMonitorCallback(sContentResolver);
// Reading properties triggers the monitor callback function.
Settings.Config.getStrings(NAMESPACE1, Arrays.asList(KEY1));
try {
Settings.Config.setStrings(NAMESPACE1, new HashMap<String, String>() {{
put(KEY1, VALUE1);
put(KEY2, VALUE2);
}});
} catch (DeviceConfig.BadConfigException e) {
fail("Callback set strings" + e.toString());
}
try {
if (latch.await(OPERATION_TIMEOUT_MS, TimeUnit.MILLISECONDS)) {
fail("Callback function was called while it has been cleared");
}
} catch (InterruptedException e) {
// this part is executed when an exception (in this example InterruptedException) occurs
fail("un expected interruption occur" + e.toString());
}
assertEquals(callback.onNamespaceUpdateCalls, 0);
assertEquals(callback.onDeviceConfigAccessCalls, 0);
}
private class TestMonitorCallback implements DeviceConfig.MonitorCallback {
public int onNamespaceUpdateCalls = 0;
public int onDeviceConfigAccessCalls = 0;
public CountDownLatch latch;
TestMonitorCallback(CountDownLatch latch) {
this.latch = latch;
}
public void onNamespaceUpdate(@NonNull String updatedNamespace) {
onNamespaceUpdateCalls++;
latch.countDown();
}
public void onDeviceConfigAccess(@NonNull String callingPackage,
@NonNull String namespace) {
onDeviceConfigAccessCalls++;
latch.countDown();
}
}
private static void deleteProperty(String namespace, String key) {
Settings.Config.deleteString(namespace, key);
}
private static class MockContentObserver extends ContentObserver {
private boolean mHadOnChanged = false;
private List<Change> mChanges = new ArrayList<>();
MockContentObserver() {
super(null);
}
@Override
public boolean deliverSelfNotifications() {
return true;
}
@Override
public synchronized void onChange(boolean selfChange, Collection<Uri> uris, int flags) {
doOnChangeLocked(selfChange, uris, flags, /*userId=*/ -1);
}
@Override
public synchronized void onChange(boolean selfChange, @NonNull Collection<Uri> uris,
@ContentResolver.NotifyFlags int flags, UserHandle user) {
doOnChangeLocked(selfChange, uris, flags, user.getIdentifier());
}
public synchronized boolean hadOnChanged() {
return mHadOnChanged;
}
public synchronized void reset() {
mHadOnChanged = false;
}
public synchronized boolean hadChanges(Collection<Change> changes) {
return mChanges.containsAll(changes);
}
@GuardedBy("this")
private void doOnChangeLocked(boolean selfChange, @NonNull Collection<Uri> uris,
@ContentResolver.NotifyFlags int flags, @UserIdInt int userId) {
final Change change = new Change(selfChange, uris, flags, userId);
Log.v(TAG, change.toString());
mHadOnChanged = true;
mChanges.add(change);
}
}
public static class Change {
public final boolean selfChange;
public final Iterable<Uri> uris;
public final int flags;
@UserIdInt
public final int userId;
public Change(boolean selfChange, Iterable<Uri> uris, int flags) {
this.selfChange = selfChange;
this.uris = uris;
this.flags = flags;
this.userId = -1;
}
public Change(boolean selfChange, Iterable<Uri> uris, int flags, @UserIdInt int userId) {
this.selfChange = selfChange;
this.uris = uris;
this.flags = flags;
this.userId = userId;
}
@Override
public String toString() {
return String.format("onChange(%b, %s, %d, %d)",
selfChange, asSet(uris).toString(), flags, userId);
}
@Override
public boolean equals(Object other) {
if (other instanceof Change) {
final Change change = (Change) other;
return change.selfChange == selfChange
&& Objects.equals(asSet(change.uris), asSet(uris))
&& change.flags == flags
&& change.userId == userId;
} else {
return false;
}
}
private static Set<Uri> asSet(Iterable<Uri> uris) {
final Set<Uri> asSet = new HashSet<>();
uris.forEach(asSet::add);
return asSet;
}
}
}