• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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