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