341 lines
13 KiB
Java
341 lines
13 KiB
Java
/*
|
|
* 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 android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.content.Context;
|
|
import android.content.SharedPreferences;
|
|
import android.os.Bundle;
|
|
import android.os.RemoteException;
|
|
import android.os.SystemClock;
|
|
import android.preference.PreferenceManager;
|
|
import android.util.ArrayMap;
|
|
import android.util.ArraySet;
|
|
import android.util.Log;
|
|
|
|
import com.android.internal.annotations.GuardedBy;
|
|
import com.android.internal.annotations.VisibleForTesting;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.Set;
|
|
|
|
/**
|
|
* Syncs specified keys in default {@link SharedPreferences} to Sandbox.
|
|
*
|
|
* <p>This class is a singleton since we want to maintain sync between app process and sandbox
|
|
* process.
|
|
*
|
|
* @hide
|
|
*/
|
|
public class SharedPreferencesSyncManager {
|
|
|
|
private static final String TAG = "SdkSandboxSyncManager";
|
|
private static ArrayMap<String, SharedPreferencesSyncManager> sInstanceMap = new ArrayMap<>();
|
|
private final ISdkSandboxManager mService;
|
|
private final Context mContext;
|
|
private final Object mLock = new Object();
|
|
private final ISharedPreferencesSyncCallback mCallback = new SharedPreferencesSyncCallback();
|
|
|
|
@GuardedBy("mLock")
|
|
private boolean mWaitingForSandbox = false;
|
|
|
|
// Set to a listener after initial bulk sync is successful
|
|
@GuardedBy("mLock")
|
|
private ChangeListener mListener = null;
|
|
|
|
// Set of keys that this manager needs to keep in sync.
|
|
@GuardedBy("mLock")
|
|
private ArraySet<String> mKeysToSync = new ArraySet<>();
|
|
|
|
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
|
|
public SharedPreferencesSyncManager(
|
|
@NonNull Context context, @NonNull ISdkSandboxManager service) {
|
|
mContext = context.getApplicationContext();
|
|
mService = service;
|
|
}
|
|
|
|
/**
|
|
* Returns a new instance of this class if there is a new package, otherewise returns a
|
|
* singleton instance.
|
|
*/
|
|
public static synchronized SharedPreferencesSyncManager getInstance(
|
|
@NonNull Context context, @NonNull ISdkSandboxManager service) {
|
|
final String packageName = context.getPackageName();
|
|
if (!sInstanceMap.containsKey(packageName)) {
|
|
sInstanceMap.put(packageName, new SharedPreferencesSyncManager(context, service));
|
|
}
|
|
return sInstanceMap.get(packageName);
|
|
}
|
|
|
|
/**
|
|
* Adds keys for syncing from app's default {@link SharedPreferences} to SdkSandbox.
|
|
*
|
|
* @see SdkSandboxManager#addSyncedSharedPreferencesKeys(Set)
|
|
*/
|
|
public void addSharedPreferencesSyncKeys(@NonNull Set<String> keyNames) {
|
|
// TODO(b/239403323): Validate the parameters in SdkSandboxManager
|
|
synchronized (mLock) {
|
|
mKeysToSync.addAll(keyNames);
|
|
|
|
if (mListener == null) {
|
|
mListener = new ChangeListener();
|
|
getDefaultSharedPreferences().registerOnSharedPreferenceChangeListener(mListener);
|
|
}
|
|
syncData();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Removes keys from set of keys that have been added using {@link
|
|
* #addSharedPreferencesSyncKeys(Set)}
|
|
*
|
|
* @see SdkSandboxManager#removeSyncedSharedPreferencesKeys(Set)
|
|
*/
|
|
public void removeSharedPreferencesSyncKeys(@NonNull Set<String> keys) {
|
|
synchronized (mLock) {
|
|
mKeysToSync.removeAll(keys);
|
|
|
|
final ArrayList<SharedPreferencesKey> keysWithTypeBeingRemoved = new ArrayList<>();
|
|
|
|
for (final String key : keys) {
|
|
keysWithTypeBeingRemoved.add(
|
|
new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING));
|
|
}
|
|
final SharedPreferencesUpdate update =
|
|
new SharedPreferencesUpdate(keysWithTypeBeingRemoved, new Bundle());
|
|
try {
|
|
SandboxLatencyInfo sandboxLatencyInfo =
|
|
new SandboxLatencyInfo(SandboxLatencyInfo.METHOD_SYNC_DATA_FROM_CLIENT);
|
|
sandboxLatencyInfo.setTimeAppCalledSystemServer(SystemClock.elapsedRealtime());
|
|
mService.syncDataFromClient(
|
|
mContext.getPackageName(), sandboxLatencyInfo, update, mCallback);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Couldn't connect to SdkSandboxManagerService: " + e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the set of all keys that are being synced from app's default {@link
|
|
* SharedPreferences} to sandbox.
|
|
*/
|
|
public Set<String> getSharedPreferencesSyncKeys() {
|
|
synchronized (mLock) {
|
|
return new ArraySet(mKeysToSync);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns true if sync is in waiting state.
|
|
*
|
|
* <p>Sync transitions into waiting state whenever sdksandbox is unavailable. It resumes syncing
|
|
* again when SdkSandboxManager notifies us that sdksandbox is available again.
|
|
*/
|
|
@VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
|
|
public boolean isWaitingForSandbox() {
|
|
synchronized (mLock) {
|
|
return mWaitingForSandbox;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Syncs data to SdkSandbox.
|
|
*
|
|
* <p>Syncs values of specified keys {@link #mKeysToSync} from the default {@link
|
|
* SharedPreferences} of the app.
|
|
*
|
|
* <p>Once bulk sync is complete, it also registers listener for updates which maintains the
|
|
* sync.
|
|
*/
|
|
private void syncData() {
|
|
synchronized (mLock) {
|
|
// Do not sync if keys have not been specified by the client.
|
|
if (mKeysToSync.isEmpty()) {
|
|
return;
|
|
}
|
|
|
|
bulkSyncData();
|
|
}
|
|
}
|
|
|
|
@GuardedBy("mLock")
|
|
private void bulkSyncData() {
|
|
// Collect data in a bundle
|
|
final Bundle data = new Bundle();
|
|
final SharedPreferences pref = getDefaultSharedPreferences();
|
|
final Map<String, ?> allData = pref.getAll();
|
|
final ArrayList<SharedPreferencesKey> keysWithTypeBeingSynced = new ArrayList<>();
|
|
|
|
for (int i = 0; i < mKeysToSync.size(); i++) {
|
|
final String key = mKeysToSync.valueAt(i);
|
|
final Object value = allData.get(key);
|
|
if (value == null) {
|
|
// Keep the key missing from the bundle; that means key has been removed.
|
|
// Type of missing key doesn't matter, so we use a random type.
|
|
keysWithTypeBeingSynced.add(
|
|
new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING));
|
|
continue;
|
|
}
|
|
final SharedPreferencesKey keyWithTypeAdded = updateBundle(data, key, value);
|
|
keysWithTypeBeingSynced.add(keyWithTypeAdded);
|
|
}
|
|
|
|
final SharedPreferencesUpdate update =
|
|
new SharedPreferencesUpdate(keysWithTypeBeingSynced, data);
|
|
try {
|
|
SandboxLatencyInfo sandboxLatencyInfo =
|
|
new SandboxLatencyInfo(SandboxLatencyInfo.METHOD_SYNC_DATA_FROM_CLIENT);
|
|
sandboxLatencyInfo.setTimeAppCalledSystemServer(SystemClock.elapsedRealtime());
|
|
mService.syncDataFromClient(
|
|
mContext.getPackageName(), sandboxLatencyInfo, update, mCallback);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Couldn't connect to SdkSandboxManagerService: " + e.getMessage());
|
|
}
|
|
}
|
|
|
|
private SharedPreferences getDefaultSharedPreferences() {
|
|
final Context appContext = mContext.getApplicationContext();
|
|
return PreferenceManager.getDefaultSharedPreferences(appContext);
|
|
}
|
|
|
|
private class ChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener {
|
|
@Override
|
|
public void onSharedPreferenceChanged(SharedPreferences pref, @Nullable String key) {
|
|
// Sync specified keys only
|
|
synchronized (mLock) {
|
|
// Do not sync if we are in waiting state
|
|
if (mWaitingForSandbox) {
|
|
return;
|
|
}
|
|
|
|
if (key == null) {
|
|
// All keys have been cleared. Bulk sync so that we send null for every key.
|
|
bulkSyncData();
|
|
return;
|
|
}
|
|
|
|
if (!mKeysToSync.contains(key)) {
|
|
return;
|
|
}
|
|
|
|
final Bundle data = new Bundle();
|
|
SharedPreferencesKey keyWithType;
|
|
final Object value = pref.getAll().get(key);
|
|
if (value != null) {
|
|
keyWithType = updateBundle(data, key, value);
|
|
} else {
|
|
keyWithType =
|
|
new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING);
|
|
}
|
|
|
|
final SharedPreferencesUpdate update =
|
|
new SharedPreferencesUpdate(List.of(keyWithType), data);
|
|
try {
|
|
SandboxLatencyInfo sandboxLatencyInfo =
|
|
new SandboxLatencyInfo(SandboxLatencyInfo.METHOD_SYNC_DATA_FROM_CLIENT);
|
|
sandboxLatencyInfo.setTimeAppCalledSystemServer(SystemClock.elapsedRealtime());
|
|
mService.syncDataFromClient(
|
|
mContext.getPackageName(), sandboxLatencyInfo, update, mCallback);
|
|
} catch (RemoteException e) {
|
|
Log.e(TAG, "Couldn't connect to SdkSandboxManagerService: " + e.getMessage());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds key to bundle based on type of value
|
|
*
|
|
* @return SharedPreferenceKey of the key that has been added
|
|
*/
|
|
@GuardedBy("mLock")
|
|
private SharedPreferencesKey updateBundle(Bundle data, String key, Object value) {
|
|
final String type = value.getClass().getSimpleName();
|
|
try {
|
|
switch (type) {
|
|
case "String":
|
|
data.putString(key, value.toString());
|
|
return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING);
|
|
case "Boolean":
|
|
data.putBoolean(key, (Boolean) value);
|
|
return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_BOOLEAN);
|
|
case "Integer":
|
|
data.putInt(key, (Integer) value);
|
|
return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_INTEGER);
|
|
case "Float":
|
|
data.putFloat(key, (Float) value);
|
|
return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_FLOAT);
|
|
case "Long":
|
|
data.putLong(key, (Long) value);
|
|
return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_LONG);
|
|
case "HashSet":
|
|
// TODO(b/239403323): Verify the set contains string
|
|
data.putStringArrayList(key, new ArrayList<>((Set<String>) value));
|
|
return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING_SET);
|
|
default:
|
|
Log.e(
|
|
TAG,
|
|
"Unknown type found in default SharedPreferences for Key: "
|
|
+ key
|
|
+ " type: "
|
|
+ type);
|
|
}
|
|
} catch (ClassCastException ignore) {
|
|
data.remove(key);
|
|
Log.e(
|
|
TAG,
|
|
"Wrong type found in default SharedPreferences for Key: "
|
|
+ key
|
|
+ " Type: "
|
|
+ type);
|
|
}
|
|
// By default, assume it's string
|
|
return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING);
|
|
}
|
|
|
|
private class SharedPreferencesSyncCallback extends ISharedPreferencesSyncCallback.Stub {
|
|
@Override
|
|
public void onSandboxStart() {
|
|
synchronized (mLock) {
|
|
if (mWaitingForSandbox) {
|
|
// Retry bulk sync if we were waiting for sandbox to start
|
|
mWaitingForSandbox = false;
|
|
bulkSyncData();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onError(int errorCode, String errorMsg) {
|
|
synchronized (mLock) {
|
|
// Transition to waiting state when sandbox is unavailable
|
|
if (!mWaitingForSandbox
|
|
&& errorCode == ISharedPreferencesSyncCallback.SANDBOX_NOT_AVAILABLE) {
|
|
Log.w(TAG, "Waiting for SdkSandbox: " + errorMsg);
|
|
// Wait for sandbox to start. When it starts, server will call onSandboxStart
|
|
mWaitingForSandbox = true;
|
|
return;
|
|
}
|
|
Log.e(TAG, "errorCode: " + errorCode + " errorMsg: " + errorMsg);
|
|
}
|
|
}
|
|
}
|
|
}
|