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