blob: 50af6c18276f4bc816cf97540c7a1f258df54473 [file] [log] [blame]
/*
* Copyright (C) 2022 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.app.sdksandbox;
import static com.google.common.truth.Truth.assertThat;
import android.annotation.Nullable;
import android.app.sdksandbox.testutils.StubSdkSandboxManagerService;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.ArraySet;
import androidx.test.InstrumentationRegistry;
import com.android.internal.annotations.GuardedBy;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/** Tests {@link SharedPreferencesSyncManager} APIs. */
@RunWith(JUnit4.class)
public class SharedPreferencesSyncManagerUnitTest {
private SharedPreferencesSyncManager mSyncManager;
private FakeSdkSandboxManagerService mSdkSandboxManagerService;
private Context mContext;
// TODO(b/239403323): Write test where we try to sync non-string values like null or object.
private static final Map<String, String> TEST_DATA =
Map.of("hello1", "world1", "hello2", "world2", "empty", "");
@Before
public void setUp() throws Exception {
mContext = InstrumentationRegistry.getContext();
mSdkSandboxManagerService = new FakeSdkSandboxManagerService();
mSyncManager = new SharedPreferencesSyncManager(mContext, mSdkSandboxManagerService);
}
@After
public void tearDown() throws Exception {
getDefaultSharedPreferences().edit().clear().commit();
}
@Test
public void test_syncData_doesNotSyncIfKeysNotSpecified() throws Exception {
// Populate default shared preference with test data
populateDefaultSharedPreference(TEST_DATA);
// Sync data without specifying list of keys to sync
mSyncManager.syncData();
// Verify that sync manager does not try to sync at all
assertThat(mSdkSandboxManagerService.getNumberOfUpdatesReceived()).isEqualTo(0);
}
@Test
public void test_syncData_doesNotSyncEmptyUpdates() throws Exception {
// Unpopulated shared preference. There is nothing to sync.
mSyncManager.setKeysToSync(TEST_DATA.keySet());
mSyncManager.syncData();
// Verify that sync manager does not try to sync with empty data
assertThat(mSdkSandboxManagerService.getNumberOfUpdatesReceived()).isEqualTo(0);
}
@Test
public void test_syncData_syncSpecifiedKeys() throws Exception {
// Populate default shared preference with test data
populateDefaultSharedPreference(TEST_DATA);
// Set specific shared keys that we want to sync
mSyncManager.setKeysToSync(TEST_DATA.keySet());
mSyncManager.syncData();
// Verify that sync manager passes the correct data to SdkSandboxManager
final Bundle capturedData = mSdkSandboxManagerService.getLastUpdate();
assertThat(mSdkSandboxManagerService.getCallingPackageName())
.isEqualTo(mContext.getPackageName());
assertThat(capturedData.keySet()).containsExactlyElementsIn(TEST_DATA.keySet());
for (String key : TEST_DATA.keySet()) {
assertThat(capturedData.getString(key)).isEqualTo(TEST_DATA.get(key));
}
}
@Test
public void test_syncData_ignoreUnspecifiedKeys() throws Exception {
// Populate default shared preference and set specific keys for sycing
populateDefaultSharedPreference(TEST_DATA);
mSyncManager.setKeysToSync(TEST_DATA.keySet());
// Populate extra data outside of shared key list
populateDefaultSharedPreference(Map.of("extraKey", "notSpecifiedByApi"));
mSyncManager.syncData();
// Verify that sync manager passes the correct data to SdkSandboxManager
final Bundle capturedData = mSdkSandboxManagerService.getLastUpdate();
assertThat(capturedData.keySet()).containsExactlyElementsIn(TEST_DATA.keySet());
}
@Test
public void test_syncData_multipleCalls() throws Exception {
// Populate default shared preference and set specific keys for sycing
populateDefaultSharedPreference(TEST_DATA);
mSyncManager.setKeysToSync(TEST_DATA.keySet());
// Sync data multiple times
mSyncManager.syncData();
mSyncManager.syncData();
// Verify that SyncManager bulk syncs only once
assertThat(mSdkSandboxManagerService.getNumberOfUpdatesReceived()).isEqualTo(1);
}
@Test
public void test_syncData_ignoresUnsupportedValues() throws Exception {
// Populate default shared preference with test data
populateDefaultSharedPreference(TEST_DATA);
// Populate default shared preference with invalid data
final SharedPreferences.Editor editor = getDefaultSharedPreferences().edit();
editor.putBoolean("boolean", true);
editor.putFloat("float", 1.2f);
editor.putInt("int", 1);
editor.putLong("long", 1L);
editor.putStringSet("set", Set.of("value"));
editor.commit();
// Set keys to sync and then sync data
final Set<String> keysToSync =
new ArraySet<>(Arrays.asList("boolean", "float", "int", "long", "set"));
keysToSync.addAll(TEST_DATA.keySet());
mSyncManager.setKeysToSync(keysToSync);
mSyncManager.syncData();
// Verify that sync manager passes the correct data to SdkSandboxManager
final Bundle capturedData = mSdkSandboxManagerService.getLastUpdate();
assertThat(capturedData.keySet()).containsExactlyElementsIn(TEST_DATA.keySet());
for (String key : TEST_DATA.keySet()) {
assertThat(capturedData.getString(key)).isEqualTo(TEST_DATA.get(key));
}
}
// TODO(b/239403323): We probably want to allow client update this the list dynamically.
@Test
public void test_setKeysToSync_canBeSetOnlyOnce() throws Exception {
// Populate default shared preference and set specific keys for sycing
populateDefaultSharedPreference(TEST_DATA);
// Setting keys to sync for the first time should return true
assertThat(mSyncManager.setKeysToSync(TEST_DATA.keySet())).isTrue();
// Try to update keys to sync again
assertThat(mSyncManager.setKeysToSync(Collections.emptySet())).isFalse();
mSyncManager.syncData();
// Verify that sync manager is still using first set of keys
final Bundle capturedData = mSdkSandboxManagerService.getLastUpdate();
assertThat(capturedData.keySet()).containsExactlyElementsIn(TEST_DATA.keySet());
}
@Test
public void test_updateListener_syncsFurtherUpdates() throws Exception {
// Set specified keys for sycing and register listener
mSyncManager.setKeysToSync(TEST_DATA.keySet());
mSyncManager.syncData();
// Update the SharedPreference to trigger listeners
final String keyToUpdate = TEST_DATA.keySet().toArray()[0].toString();
getDefaultSharedPreferences().edit().putString(keyToUpdate, "update").commit();
// Verify we registered a listener that called SdkSandboxManagerService
mSdkSandboxManagerService.blockForReceivingUpdates(1);
final Bundle capturedData = mSdkSandboxManagerService.getLastUpdate();
assertThat(capturedData.keySet()).containsExactly(keyToUpdate);
assertThat(capturedData.getString(keyToUpdate)).isEqualTo("update");
}
@Test
public void test_updateListener_ignoresUnspecifiedKeys() throws Exception {
// Set specified keys for sycing and register listener
mSyncManager.setKeysToSync(TEST_DATA.keySet());
mSyncManager.syncData();
// Update the SharedPreference to trigger listeners
getDefaultSharedPreferences().edit().putString("unspecified_key", "update").commit();
// Verify SdkSandboxManagerService does not receive the update for unspecified key
Thread.sleep(5000);
assertThat(mSdkSandboxManagerService.getNumberOfUpdatesReceived()).isEqualTo(0);
}
/** Test that listener for live update is registered only once */
@Test
public void test_updateListener_registersOnlyOnce() throws Exception {
// Populate default shared preference and set specific keys for sycing
populateDefaultSharedPreference(TEST_DATA);
mSyncManager.setKeysToSync(TEST_DATA.keySet());
// Sync data multiple times
mSyncManager.syncData();
mSyncManager.syncData();
// Update the SharedPreference to trigger listeners
final String keyToUpdate = TEST_DATA.keySet().toArray()[0].toString();
getDefaultSharedPreferences().edit().putString(keyToUpdate, "update").commit();
// Verify that SyncManager tried to sync only twice: once for bulk and once for live update.
mSdkSandboxManagerService.blockForReceivingUpdates(2);
assertThat(mSdkSandboxManagerService.getNumberOfUpdatesReceived()).isEqualTo(2);
}
/** Write all key-values provided in the map to app's default SharedPreferences */
private void populateDefaultSharedPreference(Map<String, String> data) {
final SharedPreferences.Editor editor = getDefaultSharedPreferences().edit();
for (Map.Entry<String, String> entry : data.entrySet()) {
editor.putString(entry.getKey(), entry.getValue());
}
editor.apply();
}
private SharedPreferences getDefaultSharedPreferences() {
final Context appContext = mContext.getApplicationContext();
return PreferenceManager.getDefaultSharedPreferences(appContext);
}
private static class FakeSdkSandboxManagerService extends StubSdkSandboxManagerService {
@GuardedBy("this")
private ArrayList<Bundle> mDataCache = new ArrayList<>();
@GuardedBy("this")
private String mCallingPackageName = null;
/** Gets updated when {@link blockForReceivingUpdates} is called. */
private CountDownLatch mWaitForMoreUpdates = new CountDownLatch(0);
@Override
public synchronized void syncDataFromClient(String callingPackageName, Bundle data) {
if (mCallingPackageName == null) {
mCallingPackageName = callingPackageName;
} else {
assertThat(mCallingPackageName).isEqualTo(callingPackageName);
}
mDataCache.add(data);
mWaitForMoreUpdates.countDown();
}
public synchronized String getCallingPackageName() {
return mCallingPackageName;
}
@Nullable
public synchronized Bundle getLastUpdate() {
if (mDataCache.isEmpty()) {
throw new AssertionError(
"Fake SdkSandboxManagerService did not receive any update");
}
return mDataCache.get(mDataCache.size() - 1);
}
public synchronized int getNumberOfUpdatesReceived() {
return mDataCache.size();
}
public void blockForReceivingUpdates(int numberOfUpdates) throws Exception {
synchronized (this) {
final int updatesNeeded = numberOfUpdates - getNumberOfUpdatesReceived();
if (updatesNeeded <= 0) {
return;
}
mWaitForMoreUpdates = new CountDownLatch(updatesNeeded);
}
if (!mWaitForMoreUpdates.await(5000, TimeUnit.MILLISECONDS)) {
throw new TimeoutException(
"Failed to receive required number of updates. Required: "
+ numberOfUpdates
+ ", but found: "
+ getNumberOfUpdatesReceived());
}
}
}
}