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