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