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