1 /* 2 * Copyright (C) 2022 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.app.sdksandbox; 18 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.Context; 22 import android.content.SharedPreferences; 23 import android.os.Bundle; 24 import android.os.RemoteException; 25 import android.preference.PreferenceManager; 26 import android.util.ArrayMap; 27 import android.util.ArraySet; 28 import android.util.Log; 29 30 import com.android.internal.annotations.GuardedBy; 31 import com.android.internal.annotations.VisibleForTesting; 32 33 import java.util.ArrayList; 34 import java.util.List; 35 import java.util.Map; 36 import java.util.Set; 37 38 /** 39 * Syncs specified keys in default {@link SharedPreferences} to Sandbox. 40 * 41 * <p>This class is a singleton since we want to maintain sync between app process and sandbox 42 * process. 43 * 44 * @hide 45 */ 46 public class SharedPreferencesSyncManager { 47 48 private static final String TAG = "SdkSandboxSyncManager"; 49 private static ArrayMap<String, SharedPreferencesSyncManager> sInstanceMap = new ArrayMap<>(); 50 private final ISdkSandboxManager mService; 51 private final Context mContext; 52 private final Object mLock = new Object(); 53 private final ISharedPreferencesSyncCallback mCallback = new SharedPreferencesSyncCallback(); 54 55 @GuardedBy("mLock") 56 private boolean mWaitingForSandbox = false; 57 58 // Set to a listener after initial bulk sync is successful 59 @GuardedBy("mLock") 60 private ChangeListener mListener = null; 61 62 // Set of keys that this manager needs to keep in sync. 63 @GuardedBy("mLock") 64 private ArraySet<String> mKeysToSync = new ArraySet<>(); 65 66 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) SharedPreferencesSyncManager( @onNull Context context, @NonNull ISdkSandboxManager service)67 public SharedPreferencesSyncManager( 68 @NonNull Context context, @NonNull ISdkSandboxManager service) { 69 mContext = context.getApplicationContext(); 70 mService = service; 71 } 72 73 /** 74 * Returns a new instance of this class if there is a new package, otherewise returns a 75 * singleton instance. 76 */ getInstance( @onNull Context context, @NonNull ISdkSandboxManager service)77 public static synchronized SharedPreferencesSyncManager getInstance( 78 @NonNull Context context, @NonNull ISdkSandboxManager service) { 79 final String packageName = context.getPackageName(); 80 if (!sInstanceMap.containsKey(packageName)) { 81 sInstanceMap.put(packageName, new SharedPreferencesSyncManager(context, service)); 82 } 83 return sInstanceMap.get(packageName); 84 } 85 86 /** 87 * Adds keys for syncing from app's default {@link SharedPreferences} to SdkSandbox. 88 * 89 * @see SdkSandboxManager#addSyncedSharedPreferencesKeys(Set) 90 */ addSharedPreferencesSyncKeys(@onNull Set<String> keyNames)91 public void addSharedPreferencesSyncKeys(@NonNull Set<String> keyNames) { 92 // TODO(b/239403323): Validate the parameters in SdkSandboxManager 93 synchronized (mLock) { 94 mKeysToSync.addAll(keyNames); 95 96 if (mListener == null) { 97 mListener = new ChangeListener(); 98 getDefaultSharedPreferences().registerOnSharedPreferenceChangeListener(mListener); 99 } 100 syncData(); 101 } 102 } 103 104 /** 105 * Removes keys from set of keys that have been added using {@link 106 * #addSharedPreferencesSyncKeys(Set)} 107 * 108 * @see SdkSandboxManager#removeSyncedSharedPreferencesKeys(Set) 109 */ removeSharedPreferencesSyncKeys(@onNull Set<String> keys)110 public void removeSharedPreferencesSyncKeys(@NonNull Set<String> keys) { 111 synchronized (mLock) { 112 mKeysToSync.removeAll(keys); 113 114 final ArrayList<SharedPreferencesKey> keysWithTypeBeingRemoved = new ArrayList<>(); 115 116 for (final String key : keys) { 117 keysWithTypeBeingRemoved.add( 118 new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING)); 119 } 120 final SharedPreferencesUpdate update = 121 new SharedPreferencesUpdate(keysWithTypeBeingRemoved, new Bundle()); 122 try { 123 mService.syncDataFromClient( 124 mContext.getPackageName(), 125 /*timeAppCalledSystemServer=*/ System.currentTimeMillis(), 126 update, 127 mCallback); 128 } catch (RemoteException e) { 129 Log.e(TAG, "Couldn't connect to SdkSandboxManagerService: " + e.getMessage()); 130 } 131 } 132 } 133 134 /** 135 * Returns the set of all keys that are being synced from app's default {@link 136 * SharedPreferences} to sandbox. 137 */ getSharedPreferencesSyncKeys()138 public Set<String> getSharedPreferencesSyncKeys() { 139 synchronized (mLock) { 140 return new ArraySet(mKeysToSync); 141 } 142 } 143 144 /** 145 * Returns true if sync is in waiting state. 146 * 147 * <p>Sync transitions into waiting state whenever sdksandbox is unavailable. It resumes syncing 148 * again when SdkSandboxManager notifies us that sdksandbox is available again. 149 */ 150 @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE) isWaitingForSandbox()151 public boolean isWaitingForSandbox() { 152 synchronized (mLock) { 153 return mWaitingForSandbox; 154 } 155 } 156 157 /** 158 * Syncs data to SdkSandbox. 159 * 160 * <p>Syncs values of specified keys {@link #mKeysToSync} from the default {@link 161 * SharedPreferences} of the app. 162 * 163 * <p>Once bulk sync is complete, it also registers listener for updates which maintains the 164 * sync. 165 */ syncData()166 private void syncData() { 167 synchronized (mLock) { 168 // Do not sync if keys have not been specified by the client. 169 if (mKeysToSync.isEmpty()) { 170 return; 171 } 172 173 bulkSyncData(); 174 } 175 } 176 177 @GuardedBy("mLock") bulkSyncData()178 private void bulkSyncData() { 179 // Collect data in a bundle 180 final Bundle data = new Bundle(); 181 final SharedPreferences pref = getDefaultSharedPreferences(); 182 final Map<String, ?> allData = pref.getAll(); 183 final ArrayList<SharedPreferencesKey> keysWithTypeBeingSynced = new ArrayList<>(); 184 185 for (int i = 0; i < mKeysToSync.size(); i++) { 186 final String key = mKeysToSync.valueAt(i); 187 final Object value = allData.get(key); 188 if (value == null) { 189 // Keep the key missing from the bundle; that means key has been removed. 190 // Type of missing key doesn't matter, so we use a random type. 191 keysWithTypeBeingSynced.add( 192 new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING)); 193 continue; 194 } 195 final SharedPreferencesKey keyWithTypeAdded = updateBundle(data, key, value); 196 keysWithTypeBeingSynced.add(keyWithTypeAdded); 197 } 198 199 final SharedPreferencesUpdate update = 200 new SharedPreferencesUpdate(keysWithTypeBeingSynced, data); 201 try { 202 mService.syncDataFromClient( 203 mContext.getPackageName(), 204 /*timeAppCalledSystemServer=*/ System.currentTimeMillis(), 205 update, 206 mCallback); 207 } catch (RemoteException e) { 208 Log.e(TAG, "Couldn't connect to SdkSandboxManagerService: " + e.getMessage()); 209 } 210 } 211 getDefaultSharedPreferences()212 private SharedPreferences getDefaultSharedPreferences() { 213 final Context appContext = mContext.getApplicationContext(); 214 return PreferenceManager.getDefaultSharedPreferences(appContext); 215 } 216 217 private class ChangeListener implements SharedPreferences.OnSharedPreferenceChangeListener { 218 @Override onSharedPreferenceChanged(SharedPreferences pref, @Nullable String key)219 public void onSharedPreferenceChanged(SharedPreferences pref, @Nullable String key) { 220 // Sync specified keys only 221 synchronized (mLock) { 222 // Do not sync if we are in waiting state 223 if (mWaitingForSandbox) { 224 return; 225 } 226 227 if (key == null) { 228 // All keys have been cleared. Bulk sync so that we send null for every key. 229 bulkSyncData(); 230 return; 231 } 232 233 if (!mKeysToSync.contains(key)) { 234 return; 235 } 236 237 final Bundle data = new Bundle(); 238 SharedPreferencesKey keyWithType; 239 final Object value = pref.getAll().get(key); 240 if (value != null) { 241 keyWithType = updateBundle(data, key, value); 242 } else { 243 keyWithType = 244 new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING); 245 } 246 247 final SharedPreferencesUpdate update = 248 new SharedPreferencesUpdate(List.of(keyWithType), data); 249 try { 250 mService.syncDataFromClient( 251 mContext.getPackageName(), 252 /*timeAppCalledSystemServer=*/ System.currentTimeMillis(), 253 update, 254 mCallback); 255 } catch (RemoteException e) { 256 Log.e(TAG, "Couldn't connect to SdkSandboxManagerService: " + e.getMessage()); 257 } 258 } 259 } 260 } 261 262 /** 263 * Adds key to bundle based on type of value 264 * 265 * @return SharedPreferenceKey of the key that has been added 266 */ 267 @GuardedBy("mLock") updateBundle(Bundle data, String key, Object value)268 private SharedPreferencesKey updateBundle(Bundle data, String key, Object value) { 269 final String type = value.getClass().getSimpleName(); 270 try { 271 switch (type) { 272 case "String": 273 data.putString(key, value.toString()); 274 return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING); 275 case "Boolean": 276 data.putBoolean(key, (Boolean) value); 277 return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_BOOLEAN); 278 case "Integer": 279 data.putInt(key, (Integer) value); 280 return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_INTEGER); 281 case "Float": 282 data.putFloat(key, (Float) value); 283 return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_FLOAT); 284 case "Long": 285 data.putLong(key, (Long) value); 286 return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_LONG); 287 case "HashSet": 288 // TODO(b/239403323): Verify the set contains string 289 data.putStringArrayList(key, new ArrayList<>((Set<String>) value)); 290 return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING_SET); 291 default: 292 Log.e( 293 TAG, 294 "Unknown type found in default SharedPreferences for Key: " 295 + key 296 + " type: " 297 + type); 298 } 299 } catch (ClassCastException ignore) { 300 data.remove(key); 301 Log.e( 302 TAG, 303 "Wrong type found in default SharedPreferences for Key: " 304 + key 305 + " Type: " 306 + type); 307 } 308 // By default, assume it's string 309 return new SharedPreferencesKey(key, SharedPreferencesKey.KEY_TYPE_STRING); 310 } 311 312 private class SharedPreferencesSyncCallback extends ISharedPreferencesSyncCallback.Stub { 313 @Override onSandboxStart()314 public void onSandboxStart() { 315 synchronized (mLock) { 316 if (mWaitingForSandbox) { 317 // Retry bulk sync if we were waiting for sandbox to start 318 mWaitingForSandbox = false; 319 bulkSyncData(); 320 } 321 } 322 } 323 324 @Override onError(int errorCode, String errorMsg)325 public void onError(int errorCode, String errorMsg) { 326 synchronized (mLock) { 327 // Transition to waiting state when sandbox is unavailable 328 if (!mWaitingForSandbox 329 && errorCode == ISharedPreferencesSyncCallback.SANDBOX_NOT_AVAILABLE) { 330 Log.w(TAG, "Waiting for SdkSandbox: " + errorMsg); 331 // Wait for sandbox to start. When it starts, server will call onSandboxStart 332 mWaitingForSandbox = true; 333 return; 334 } 335 Log.e(TAG, "errorCode: " + errorCode + " errorMsg: " + errorMsg); 336 } 337 } 338 } 339 } 340