• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 com.android.systemui.people.widget;
18 
19 import static com.android.systemui.people.PeopleBackupFollowUpJob.SHARED_FOLLOW_UP;
20 import static com.android.systemui.people.PeopleSpaceUtils.DEBUG;
21 import static com.android.systemui.people.PeopleSpaceUtils.INVALID_USER_ID;
22 import static com.android.systemui.people.PeopleSpaceUtils.USER_ID;
23 
24 import android.app.backup.BackupDataInputStream;
25 import android.app.backup.BackupDataOutput;
26 import android.app.backup.SharedPreferencesBackupHelper;
27 import android.app.people.IPeopleManager;
28 import android.appwidget.AppWidgetManager;
29 import android.content.ComponentName;
30 import android.content.ContentProvider;
31 import android.content.Context;
32 import android.content.Intent;
33 import android.content.SharedPreferences;
34 import android.content.pm.PackageInfo;
35 import android.content.pm.PackageManager;
36 import android.net.Uri;
37 import android.os.ParcelFileDescriptor;
38 import android.os.ServiceManager;
39 import android.os.UserHandle;
40 import android.preference.PreferenceManager;
41 import android.text.TextUtils;
42 import android.util.Log;
43 
44 import androidx.annotation.Nullable;
45 
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.systemui.people.PeopleBackupFollowUpJob;
48 import com.android.systemui.people.SharedPreferencesHelper;
49 
50 import java.util.ArrayList;
51 import java.util.Collections;
52 import java.util.List;
53 import java.util.Map;
54 import java.util.Set;
55 import java.util.stream.Collectors;
56 
57 /**
58  * Helper class to backup and restore Conversations widgets storage.
59  * It is used by SystemUI's BackupHelper agent.
60  * TODO(b/192334798): Lock access to storage using PeopleSpaceWidgetManager's lock.
61  */
62 public class PeopleBackupHelper extends SharedPreferencesBackupHelper {
63     private static final String TAG = "PeopleBackupHelper";
64 
65     public static final String ADD_USER_ID_TO_URI = "add_user_id_to_uri_";
66     public static final String SHARED_BACKUP = "shared_backup";
67 
68     private final Context mContext;
69     private final UserHandle mUserHandle;
70     private final PackageManager mPackageManager;
71     private final IPeopleManager mIPeopleManager;
72     @Nullable
73     private final AppWidgetManager mAppWidgetManager;
74 
75     /**
76      * Types of entries stored in the default SharedPreferences file for Conversation widgets.
77      * Widget ID corresponds to a pair [widgetId, contactURI].
78      * PeopleTileKey corresponds to a pair [PeopleTileKey, {widgetIds}].
79      * Contact URI corresponds to a pair [Contact URI, {widgetIds}].
80      */
81     enum SharedFileEntryType {
82         UNKNOWN,
83         WIDGET_ID,
84         PEOPLE_TILE_KEY,
85         CONTACT_URI
86     }
87 
88     /**
89      * Returns the file names that should be backed up and restored by SharedPreferencesBackupHelper
90      * infrastructure.
91      */
getFilesToBackup()92     public static List<String> getFilesToBackup() {
93         return Collections.singletonList(SHARED_BACKUP);
94     }
95 
PeopleBackupHelper(Context context, UserHandle userHandle, String[] sharedPreferencesKey)96     public PeopleBackupHelper(Context context, UserHandle userHandle,
97             String[] sharedPreferencesKey) {
98         super(context, sharedPreferencesKey);
99         mContext = context;
100         mUserHandle = userHandle;
101         mPackageManager = context.getPackageManager();
102         mIPeopleManager = IPeopleManager.Stub.asInterface(
103                 ServiceManager.getService(Context.PEOPLE_SERVICE));
104         mAppWidgetManager = AppWidgetManager.getInstance(context);
105     }
106 
107     @VisibleForTesting
PeopleBackupHelper(Context context, UserHandle userHandle, String[] sharedPreferencesKey, PackageManager packageManager, IPeopleManager peopleManager)108     public PeopleBackupHelper(Context context, UserHandle userHandle,
109             String[] sharedPreferencesKey, PackageManager packageManager,
110             IPeopleManager peopleManager) {
111         super(context, sharedPreferencesKey);
112         mContext = context;
113         mUserHandle = userHandle;
114         mPackageManager = packageManager;
115         mIPeopleManager = peopleManager;
116         mAppWidgetManager = AppWidgetManager.getInstance(context);
117     }
118 
119     /**
120      * Reads values from default storage, backs them up appropriately to a specified backup file,
121      * and calls super's performBackup, which backs up the values of the backup file.
122      */
123     @Override
performBackup(ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)124     public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
125             ParcelFileDescriptor newState) {
126         if (DEBUG) Log.d(TAG, "Backing up conversation widgets, writing to: " + SHARED_BACKUP);
127         // Open default value for readings values.
128         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
129         if (sp.getAll().isEmpty()) {
130             if (DEBUG) Log.d(TAG, "No information to be backed up, finishing.");
131             return;
132         }
133 
134         // Open backup file for writing.
135         SharedPreferences backupSp = mContext.getSharedPreferences(
136                 SHARED_BACKUP, Context.MODE_PRIVATE);
137         SharedPreferences.Editor backupEditor = backupSp.edit();
138         backupEditor.clear();
139 
140         // Fetch Conversations widgets corresponding to this user.
141         List<String> existingWidgets = getExistingWidgetsForUser(mUserHandle.getIdentifier());
142         if (existingWidgets.isEmpty()) {
143             if (DEBUG) Log.d(TAG, "No existing Conversations widgets, returning.");
144             return;
145         }
146 
147         // Writes each entry to backup file.
148         sp.getAll().entrySet().forEach(entry -> backupKey(entry, backupEditor, existingWidgets));
149         backupEditor.apply();
150 
151         super.performBackup(oldState, data, newState);
152     }
153 
154     /**
155      * Restores backed up values to backup file via super's restoreEntity, then transfers them
156      * back to regular storage. Restore operations for each users are done in sequence, so we can
157      * safely use the same backup file names.
158      */
159     @Override
restoreEntity(BackupDataInputStream data)160     public void restoreEntity(BackupDataInputStream data) {
161         if (DEBUG) Log.d(TAG, "Restoring Conversation widgets.");
162         super.restoreEntity(data);
163 
164         // Open backup file for reading values.
165         SharedPreferences backupSp = mContext.getSharedPreferences(
166                 SHARED_BACKUP, Context.MODE_PRIVATE);
167 
168         // Open default file and follow-up file for writing.
169         SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
170         SharedPreferences.Editor editor = sp.edit();
171         SharedPreferences followUp = mContext.getSharedPreferences(
172                 SHARED_FOLLOW_UP, Context.MODE_PRIVATE);
173         SharedPreferences.Editor followUpEditor = followUp.edit();
174 
175         // Writes each entry back to default value.
176         boolean shouldScheduleJob = false;
177         for (Map.Entry<String, ?> entry : backupSp.getAll().entrySet()) {
178             boolean restored = restoreKey(entry, editor, followUpEditor, backupSp);
179             if (!restored) {
180                 shouldScheduleJob = true;
181             }
182         }
183 
184         editor.apply();
185         followUpEditor.apply();
186         SharedPreferencesHelper.clear(backupSp);
187 
188         // If any of the widgets is not yet available, schedule a follow-up job to check later.
189         if (shouldScheduleJob) {
190             if (DEBUG) Log.d(TAG, "At least one shortcut is not available, scheduling follow-up.");
191             PeopleBackupFollowUpJob.scheduleJob(mContext);
192         }
193 
194         updateWidgets(mContext);
195     }
196 
197     /** Backs up an entry from default file to backup file. */
backupKey(Map.Entry<String, ?> entry, SharedPreferences.Editor backupEditor, List<String> existingWidgets)198     public void backupKey(Map.Entry<String, ?> entry, SharedPreferences.Editor backupEditor,
199             List<String> existingWidgets) {
200         String key = entry.getKey();
201         if (TextUtils.isEmpty(key)) {
202             return;
203         }
204 
205         SharedFileEntryType entryType = getEntryType(entry);
206         switch(entryType) {
207             case WIDGET_ID:
208                 backupWidgetIdKey(key, String.valueOf(entry.getValue()), backupEditor,
209                         existingWidgets);
210                 break;
211             case PEOPLE_TILE_KEY:
212                 backupPeopleTileKey(key, (Set<String>) entry.getValue(), backupEditor,
213                         existingWidgets);
214                 break;
215             case CONTACT_URI:
216                 backupContactUriKey(key, (Set<String>) entry.getValue(), backupEditor);
217                 break;
218             case UNKNOWN:
219             default:
220                 Log.w(TAG, "Key not identified, skipping: " + key);
221         }
222     }
223 
224     /**
225      * Tries to restore an entry from backup file to default file.
226      * Returns true if restore is finished, false if it needs to be checked later.
227      */
restoreKey(Map.Entry<String, ?> entry, SharedPreferences.Editor editor, SharedPreferences.Editor followUpEditor, SharedPreferences backupSp)228     boolean restoreKey(Map.Entry<String, ?> entry, SharedPreferences.Editor editor,
229             SharedPreferences.Editor followUpEditor, SharedPreferences backupSp) {
230         String key = entry.getKey();
231         SharedFileEntryType keyType = getEntryType(entry);
232         int storedUserId = backupSp.getInt(ADD_USER_ID_TO_URI + key, INVALID_USER_ID);
233         switch (keyType) {
234             case WIDGET_ID:
235                 restoreWidgetIdKey(key, String.valueOf(entry.getValue()), editor, storedUserId);
236                 return true;
237             case PEOPLE_TILE_KEY:
238                 return restorePeopleTileKeyAndCorrespondingWidgetFile(
239                         key, (Set<String>) entry.getValue(), editor, followUpEditor);
240             case CONTACT_URI:
241                 restoreContactUriKey(key, (Set<String>) entry.getValue(), editor, storedUserId);
242                 return true;
243             case UNKNOWN:
244             default:
245                 Log.e(TAG, "Key not identified, skipping:" + key);
246                 return true;
247         }
248     }
249 
250     /**
251      * Backs up a [widgetId, contactURI] pair, if widget id corresponds to current user.
252      * If contact URI has a user id, stores it so it can be re-added on restore.
253      */
backupWidgetIdKey(String key, String uriString, SharedPreferences.Editor editor, List<String> existingWidgets)254     private void backupWidgetIdKey(String key, String uriString, SharedPreferences.Editor editor,
255             List<String> existingWidgets) {
256         if (!existingWidgets.contains(key)) {
257             if (DEBUG) Log.d(TAG, "Widget: " + key + " does't correspond to this user, skipping.");
258             return;
259         }
260         Uri uri = Uri.parse(uriString);
261         if (ContentProvider.uriHasUserId(uri)) {
262             if (DEBUG) Log.d(TAG, "Contact URI value has user ID, removing from: " + uri);
263             int userId = ContentProvider.getUserIdFromUri(uri);
264             editor.putInt(ADD_USER_ID_TO_URI + key, userId);
265             uri = ContentProvider.getUriWithoutUserId(uri);
266         }
267         if (DEBUG) Log.d(TAG, "Backing up widgetId key: " + key + " . Value: " + uri.toString());
268         editor.putString(key, uri.toString());
269     }
270 
271     /** Restores a [widgetId, contactURI] pair, and a potential {@code storedUserId}. */
restoreWidgetIdKey(String key, String uriString, SharedPreferences.Editor editor, int storedUserId)272     private void restoreWidgetIdKey(String key, String uriString, SharedPreferences.Editor editor,
273             int storedUserId) {
274         Uri uri = Uri.parse(uriString);
275         if (storedUserId != INVALID_USER_ID) {
276             uri = ContentProvider.createContentUriForUser(uri, UserHandle.of(storedUserId));
277             if (DEBUG) Log.d(TAG, "UserId was removed from URI on back up, re-adding as:" + uri);
278 
279         }
280         if (DEBUG) Log.d(TAG, "Restoring widgetId key: " + key + " . Value: " + uri.toString());
281         editor.putString(key, uri.toString());
282     }
283 
284     /**
285      * Backs up a [PeopleTileKey, {widgetIds}] pair, if PeopleTileKey's user is the same as current
286      * user, stripping out the user id.
287      */
backupPeopleTileKey(String key, Set<String> widgetIds, SharedPreferences.Editor editor, List<String> existingWidgets)288     private void backupPeopleTileKey(String key, Set<String> widgetIds,
289             SharedPreferences.Editor editor, List<String> existingWidgets) {
290         PeopleTileKey peopleTileKey = PeopleTileKey.fromString(key);
291         if (peopleTileKey.getUserId() != mUserHandle.getIdentifier()) {
292             if (DEBUG) Log.d(TAG, "PeopleTileKey corresponds to different user, skipping backup.");
293             return;
294         }
295 
296         Set<String> filteredWidgets = widgetIds.stream()
297                 .filter(id -> existingWidgets.contains(id))
298                 .collect(Collectors.toSet());
299         if (filteredWidgets.isEmpty()) {
300             return;
301         }
302 
303         peopleTileKey.setUserId(INVALID_USER_ID);
304         if (DEBUG) {
305             Log.d(TAG, "Backing up PeopleTileKey key: " + peopleTileKey.toString() + ". Value: "
306                     + filteredWidgets);
307         }
308         editor.putStringSet(peopleTileKey.toString(), filteredWidgets);
309     }
310 
311     /**
312      * Restores a [PeopleTileKey, {widgetIds}] pair, restoring the user id. Checks if the
313      * corresponding shortcut exists, and if not, we should schedule a follow up to check later.
314      * Also restores corresponding [widgetId, PeopleTileKey], which is not backed up since the
315      * information can be inferred from this.
316      * Returns true if restore is finished, false if we should check if shortcut is available later.
317      */
restorePeopleTileKeyAndCorrespondingWidgetFile(String key, Set<String> widgetIds, SharedPreferences.Editor editor, SharedPreferences.Editor followUpEditor)318     private boolean restorePeopleTileKeyAndCorrespondingWidgetFile(String key,
319             Set<String> widgetIds, SharedPreferences.Editor editor,
320             SharedPreferences.Editor followUpEditor) {
321         PeopleTileKey peopleTileKey = PeopleTileKey.fromString(key);
322         // Should never happen, as type of key has been checked.
323         if (peopleTileKey == null) {
324             if (DEBUG) Log.d(TAG, "PeopleTileKey key to be restored is null, skipping.");
325             return true;
326         }
327 
328         peopleTileKey.setUserId(mUserHandle.getIdentifier());
329         if (!PeopleTileKey.isValid(peopleTileKey)) {
330             if (DEBUG) Log.d(TAG, "PeopleTileKey key to be restored is not valid, skipping.");
331             return true;
332         }
333 
334         boolean restored = isReadyForRestore(
335                 mIPeopleManager, mPackageManager, peopleTileKey);
336         if (!restored) {
337             if (DEBUG) Log.d(TAG, "Adding key to follow-up storage: " + peopleTileKey.toString());
338             // Follow-up file stores shortcuts that need to be checked later, and possibly wiped
339             // from our storage.
340             followUpEditor.putStringSet(peopleTileKey.toString(), widgetIds);
341         }
342 
343         if (DEBUG) {
344             Log.d(TAG, "Restoring PeopleTileKey key: " + peopleTileKey.toString() + " . Value: "
345                     + widgetIds);
346         }
347         editor.putStringSet(peopleTileKey.toString(), widgetIds);
348         restoreWidgetIdFiles(mContext, widgetIds, peopleTileKey);
349         return restored;
350     }
351 
352     /**
353      * Backs up a [contactURI, {widgetIds}] pair. If contactURI contains a userId, we back up
354      * this entry in the corresponding user. If it doesn't, we back it up as user 0.
355      * If contact URI has a user id, stores it so it can be re-added on restore.
356      * We do not take existing widgets for this user into consideration.
357      */
backupContactUriKey(String key, Set<String> widgetIds, SharedPreferences.Editor editor)358     private void backupContactUriKey(String key, Set<String> widgetIds,
359             SharedPreferences.Editor editor) {
360         Uri uri = Uri.parse(String.valueOf(key));
361         if (ContentProvider.uriHasUserId(uri)) {
362             int userId = ContentProvider.getUserIdFromUri(uri);
363             if (DEBUG) Log.d(TAG, "Contact URI has user Id: " + userId);
364             if (userId == mUserHandle.getIdentifier()) {
365                 uri = ContentProvider.getUriWithoutUserId(uri);
366                 if (DEBUG) {
367                     Log.d(TAG, "Backing up contactURI key: " + uri.toString() + " . Value: "
368                             + widgetIds);
369                 }
370                 editor.putInt(ADD_USER_ID_TO_URI + uri.toString(), userId);
371                 editor.putStringSet(uri.toString(), widgetIds);
372             } else {
373                 if (DEBUG) Log.d(TAG, "ContactURI corresponds to different user, skipping.");
374             }
375         } else if (mUserHandle.isSystem()) {
376             if (DEBUG) {
377                 Log.d(TAG, "Backing up contactURI key: " + uri.toString() + " . Value: "
378                         + widgetIds);
379             }
380             editor.putStringSet(uri.toString(), widgetIds);
381         }
382     }
383 
384     /** Restores a [contactURI, {widgetIds}] pair, and a potential {@code storedUserId}. */
restoreContactUriKey(String key, Set<String> widgetIds, SharedPreferences.Editor editor, int storedUserId)385     private void restoreContactUriKey(String key, Set<String> widgetIds,
386             SharedPreferences.Editor editor, int storedUserId) {
387         Uri uri = Uri.parse(key);
388         if (storedUserId != INVALID_USER_ID) {
389             uri = ContentProvider.createContentUriForUser(uri, UserHandle.of(storedUserId));
390             if (DEBUG) Log.d(TAG, "UserId was removed from URI on back up, re-adding as:" + uri);
391         }
392         if (DEBUG) {
393             Log.d(TAG, "Restoring contactURI key: " + uri.toString() + " . Value: " + widgetIds);
394         }
395         editor.putStringSet(uri.toString(), widgetIds);
396     }
397 
398     /** Restores the widget-specific files that contain PeopleTileKey information. */
restoreWidgetIdFiles(Context context, Set<String> widgetIds, PeopleTileKey key)399     public static void restoreWidgetIdFiles(Context context, Set<String> widgetIds,
400             PeopleTileKey key) {
401         for (String id : widgetIds) {
402             if (DEBUG) Log.d(TAG, "Restoring widget Id file: " + id + " . Value: " + key);
403             SharedPreferences dest = context.getSharedPreferences(id, Context.MODE_PRIVATE);
404             SharedPreferencesHelper.setPeopleTileKey(dest, key);
405         }
406     }
407 
getExistingWidgetsForUser(int userId)408     private List<String> getExistingWidgetsForUser(int userId) {
409         List<String> existingWidgets = new ArrayList<>();
410         if (mAppWidgetManager == null) {
411             return existingWidgets;
412         }
413         int[] ids = mAppWidgetManager.getAppWidgetIds(
414                 new ComponentName(mContext, PeopleSpaceWidgetProvider.class));
415         for (int id : ids) {
416             String idString = String.valueOf(id);
417             SharedPreferences sp = mContext.getSharedPreferences(idString, Context.MODE_PRIVATE);
418             if (sp.getInt(USER_ID, INVALID_USER_ID) == userId) {
419                 existingWidgets.add(idString);
420             }
421         }
422         if (DEBUG) Log.d(TAG, "Existing widgets: " + existingWidgets);
423         return existingWidgets;
424     }
425 
426     /**
427      * Returns whether {@code key} corresponds to a shortcut that is ready for restore, either
428      * because it is available or because it never will be. If not ready, we schedule a job to check
429      * again later.
430      */
isReadyForRestore(IPeopleManager peopleManager, PackageManager packageManager, PeopleTileKey key)431     public static boolean isReadyForRestore(IPeopleManager peopleManager,
432             PackageManager packageManager, PeopleTileKey key) {
433         if (DEBUG) Log.d(TAG, "Checking if we should schedule a follow up job : " + key);
434         if (!PeopleTileKey.isValid(key)) {
435             if (DEBUG) Log.d(TAG, "Key is invalid, should not follow up.");
436             return true;
437         }
438 
439         try {
440             PackageInfo info = packageManager.getPackageInfoAsUser(
441                     key.getPackageName(), 0, key.getUserId());
442         } catch (PackageManager.NameNotFoundException e) {
443             if (DEBUG) Log.d(TAG, "Package is not installed, should follow up.");
444             return false;
445         }
446 
447         try {
448             boolean isConversation = peopleManager.isConversation(
449                     key.getPackageName(), key.getUserId(), key.getShortcutId());
450             if (DEBUG) {
451                 Log.d(TAG, "Checked if shortcut exists, should follow up: " + !isConversation);
452             }
453             return isConversation;
454         } catch (Exception e) {
455             if (DEBUG) Log.d(TAG, "Error checking if backed up info is a shortcut.");
456             return false;
457         }
458     }
459 
460     /** Parses default file {@code entry} to determine the entry's type.*/
getEntryType(Map.Entry<String, ?> entry)461     public static SharedFileEntryType getEntryType(Map.Entry<String, ?> entry) {
462         String key = entry.getKey();
463         if (key == null) {
464             return SharedFileEntryType.UNKNOWN;
465         }
466 
467         try {
468             int id = Integer.parseInt(key);
469             try {
470                 String contactUri = (String) entry.getValue();
471             } catch (Exception e) {
472                 Log.w(TAG, "Malformed value, skipping:" + entry.getValue());
473                 return SharedFileEntryType.UNKNOWN;
474             }
475             return SharedFileEntryType.WIDGET_ID;
476         } catch (NumberFormatException ignored) { }
477 
478         try {
479             Set<String> widgetIds = (Set<String>) entry.getValue();
480         } catch (Exception e) {
481             Log.w(TAG, "Malformed value, skipping:" + entry.getValue());
482             return SharedFileEntryType.UNKNOWN;
483         }
484 
485         PeopleTileKey peopleTileKey = PeopleTileKey.fromString(key);
486         if (peopleTileKey != null) {
487             return SharedFileEntryType.PEOPLE_TILE_KEY;
488         }
489 
490         try {
491             Uri uri = Uri.parse(key);
492             return SharedFileEntryType.CONTACT_URI;
493         } catch (Exception e) {
494             return SharedFileEntryType.UNKNOWN;
495         }
496     }
497 
498     /** Sends a broadcast to update the existing Conversation widgets. */
updateWidgets(Context context)499     public static void updateWidgets(Context context) {
500         AppWidgetManager manager = AppWidgetManager.getInstance(context);
501         if (manager == null) {
502             return;
503         }
504         int[] widgetIds = manager
505                 .getAppWidgetIds(new ComponentName(context, PeopleSpaceWidgetProvider.class));
506         if (DEBUG) {
507             for (int id : widgetIds) {
508                 Log.d(TAG, "Calling update to widget: " + id);
509             }
510         }
511         if (widgetIds != null && widgetIds.length != 0) {
512             Intent intent = new Intent(context, PeopleSpaceWidgetProvider.class);
513             intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
514             intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds);
515             context.sendBroadcast(intent);
516         }
517     }
518 }
519