/* * 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. * *

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 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 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 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 keys) { synchronized (mLock) { mKeysToSync.removeAll(keys); final ArrayList 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 getSharedPreferencesSyncKeys() { synchronized (mLock) { return new ArraySet(mKeysToSync); } } /** * Returns true if sync is in waiting state. * *

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. * *

Syncs values of specified keys {@link #mKeysToSync} from the default {@link * SharedPreferences} of the app. * *

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 allData = pref.getAll(); final ArrayList 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) 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); } } } }