blob: e356b7762813d9c8211a538748b2c3fa66740c82 [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 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.preference.PreferenceManager;
import com.android.internal.annotations.GuardedBy;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* Syncs all keys in default {@link SharedPreferences} containing string values to Sdk Sandbox.
*
* @hide
*/
public class SharedPreferencesSyncManager {
private static final String TAG = "SdkSandboxManager";
private final ISdkSandboxManager mService;
private final Context mContext;
private final ChangeListener mListener = new ChangeListener();
private final Object mLock = new Object();
// TODO(b/239403323): Maintain a dynamic sync status based on lifecycle events
@GuardedBy("mLock")
private boolean mInitialSyncComplete = false;
// List of keys that this manager needs to keep in sync.
@Nullable
@GuardedBy("mLock")
private Set<String> mKeysToSync = null;
public SharedPreferencesSyncManager(
@NonNull Context context, @NonNull ISdkSandboxManager service) {
mContext = context.getApplicationContext();
mService = service;
}
/**
* Set of keys which the sync manager should be syncing to Sandbox.
*
* <p>Keys outside of this list will be ignored. This method should be called only once.
* Subsequent calls won't update the list of keys being synced.
*
* @param keysToSync set of keys that will be synced to Sandbox. Must not be null.
* @return true if set of keys have been successfully updated, otherwise returns false.
*/
public boolean setKeysToSync(@NonNull Set<String> keysToSync) {
// TODO(b/239403323): Validate keysToSync does not contain null.
Objects.requireNonNull(keysToSync, "keysToSync must not be null");
synchronized (mLock) {
// TODO(b/239403323): Allow updating mKeysToSync
if (mKeysToSync == null) {
mKeysToSync = keysToSync;
return true;
} else {
return false;
}
}
}
// TODO(b/239403323): On sandbox restart, we need to sync again.
// TODO(b/239403323): Also sync non-string values.
/**
* Sync data to SdkSandbox.
*
* <p>Currently syncs all string values from the default {@link SharedPreferences} of the app.
*
* <p>Once bulk sync is complete, it also registers listener for updates which maintains the
* sync.
*
* <p>This method is idempotent. Calling it multiple times has same affect as calling it once.
*/
public void syncData() {
synchronized (mLock) {
// Do not sync if keys have not been specified by the client.
if (mKeysToSync == null || mKeysToSync.isEmpty()) {
return;
}
if (!mInitialSyncComplete) {
bulkSyncData();
// Register listener for syncing future updates
getDefaultSharedPreferences().registerOnSharedPreferenceChangeListener(mListener);
// TODO(b/239403323): We can get out of sync if listener fails to propagate live
// updates.
mInitialSyncComplete = true;
}
}
}
@GuardedBy("mLock")
private void bulkSyncData() {
final Bundle data = new Bundle();
final SharedPreferences pref = getDefaultSharedPreferences();
final Map<String, ?> allData = pref.getAll();
for (Map.Entry<String, ?> entry : allData.entrySet()) {
final String key = entry.getKey();
// Sync only specified keys
if (!mKeysToSync.contains(key)) {
continue;
}
if (entry.getValue() instanceof String) {
data.putString(key, pref.getString(key, ""));
}
}
// No need to sync if there data is empty
if (data.isEmpty()) {
return;
}
try {
mService.syncDataFromClient(mContext.getPackageName(), data);
} catch (RemoteException ignore) {
// TODO(b/239403323): Sandbox isn't available. We need to retry when it restarts.
}
}
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) {
if (key == null || mKeysToSync == null || !mKeysToSync.contains(key)) {
return;
}
}
final Bundle data = new Bundle();
final Object value = pref.getAll().get(key);
if (!(value instanceof String)) {
// TODO(b/239403323): Add support for non-string values
return;
}
// TODO(b/239403323): Support removal of keys
data.putString(key, pref.getString(key, ""));
try {
mService.syncDataFromClient(mContext.getPackageName(), data);
} catch (RemoteException e) {
// TODO(b/239403323): Sandbox isn't available. We need to retry when it restarts.
}
}
}
}