blob: d8c96dd182b4462b953c94a397d72838d61b2362 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.systemui.people.widget;
import static com.android.systemui.people.PeopleBackupFollowUpJob.SHARED_FOLLOW_UP;
import static com.android.systemui.people.PeopleSpaceUtils.DEBUG;
import static com.android.systemui.people.PeopleSpaceUtils.INVALID_USER_ID;
import static com.android.systemui.people.PeopleSpaceUtils.USER_ID;
import android.app.backup.BackupDataInputStream;
import android.app.backup.BackupDataOutput;
import android.app.backup.SharedPreferencesBackupHelper;
import android.app.people.IPeopleManager;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.ContentProvider;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.os.ServiceManager;
import android.os.UserHandle;
import android.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;
import com.android.internal.annotations.VisibleForTesting;
import com.android.systemui.people.PeopleBackupFollowUpJob;
import com.android.systemui.people.SharedPreferencesHelper;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* Helper class to backup and restore Conversations widgets storage.
* It is used by SystemUI's BackupHelper agent.
* TODO(b/192334798): Lock access to storage using PeopleSpaceWidgetManager's lock.
*/
public class PeopleBackupHelper extends SharedPreferencesBackupHelper {
private static final String TAG = "PeopleBackupHelper";
public static final String ADD_USER_ID_TO_URI = "add_user_id_to_uri_";
public static final String SHARED_BACKUP = "shared_backup";
private final Context mContext;
private final UserHandle mUserHandle;
private final PackageManager mPackageManager;
private final IPeopleManager mIPeopleManager;
private final AppWidgetManager mAppWidgetManager;
/**
* Types of entries stored in the default SharedPreferences file for Conversation widgets.
* Widget ID corresponds to a pair [widgetId, contactURI].
* PeopleTileKey corresponds to a pair [PeopleTileKey, {widgetIds}].
* Contact URI corresponds to a pair [Contact URI, {widgetIds}].
*/
enum SharedFileEntryType {
UNKNOWN,
WIDGET_ID,
PEOPLE_TILE_KEY,
CONTACT_URI
}
/**
* Returns the file names that should be backed up and restored by SharedPreferencesBackupHelper
* infrastructure.
*/
public static List<String> getFilesToBackup() {
return Collections.singletonList(SHARED_BACKUP);
}
public PeopleBackupHelper(Context context, UserHandle userHandle,
String[] sharedPreferencesKey) {
super(context, sharedPreferencesKey);
mContext = context;
mUserHandle = userHandle;
mPackageManager = context.getPackageManager();
mIPeopleManager = IPeopleManager.Stub.asInterface(
ServiceManager.getService(Context.PEOPLE_SERVICE));
mAppWidgetManager = AppWidgetManager.getInstance(context);
}
@VisibleForTesting
public PeopleBackupHelper(Context context, UserHandle userHandle,
String[] sharedPreferencesKey, PackageManager packageManager,
IPeopleManager peopleManager) {
super(context, sharedPreferencesKey);
mContext = context;
mUserHandle = userHandle;
mPackageManager = packageManager;
mIPeopleManager = peopleManager;
mAppWidgetManager = AppWidgetManager.getInstance(context);
}
/**
* Reads values from default storage, backs them up appropriately to a specified backup file,
* and calls super's performBackup, which backs up the values of the backup file.
*/
@Override
public void performBackup(ParcelFileDescriptor oldState, BackupDataOutput data,
ParcelFileDescriptor newState) {
if (DEBUG) Log.d(TAG, "Backing up conversation widgets, writing to: " + SHARED_BACKUP);
// Open default value for readings values.
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
if (sp.getAll().isEmpty()) {
if (DEBUG) Log.d(TAG, "No information to be backed up, finishing.");
return;
}
// Open backup file for writing.
SharedPreferences backupSp = mContext.getSharedPreferences(
SHARED_BACKUP, Context.MODE_PRIVATE);
SharedPreferences.Editor backupEditor = backupSp.edit();
backupEditor.clear();
// Fetch Conversations widgets corresponding to this user.
List<String> existingWidgets = getExistingWidgetsForUser(mUserHandle.getIdentifier());
if (existingWidgets.isEmpty()) {
if (DEBUG) Log.d(TAG, "No existing Conversations widgets, returning.");
return;
}
// Writes each entry to backup file.
sp.getAll().entrySet().forEach(entry -> backupKey(entry, backupEditor, existingWidgets));
backupEditor.apply();
super.performBackup(oldState, data, newState);
}
/**
* Restores backed up values to backup file via super's restoreEntity, then transfers them
* back to regular storage. Restore operations for each users are done in sequence, so we can
* safely use the same backup file names.
*/
@Override
public void restoreEntity(BackupDataInputStream data) {
if (DEBUG) Log.d(TAG, "Restoring Conversation widgets.");
super.restoreEntity(data);
// Open backup file for reading values.
SharedPreferences backupSp = mContext.getSharedPreferences(
SHARED_BACKUP, Context.MODE_PRIVATE);
// Open default file and follow-up file for writing.
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext);
SharedPreferences.Editor editor = sp.edit();
SharedPreferences followUp = mContext.getSharedPreferences(
SHARED_FOLLOW_UP, Context.MODE_PRIVATE);
SharedPreferences.Editor followUpEditor = followUp.edit();
// Writes each entry back to default value.
boolean shouldScheduleJob = false;
for (Map.Entry<String, ?> entry : backupSp.getAll().entrySet()) {
boolean restored = restoreKey(entry, editor, followUpEditor, backupSp);
if (!restored) {
shouldScheduleJob = true;
}
}
editor.apply();
followUpEditor.apply();
SharedPreferencesHelper.clear(backupSp);
// If any of the widgets is not yet available, schedule a follow-up job to check later.
if (shouldScheduleJob) {
if (DEBUG) Log.d(TAG, "At least one shortcut is not available, scheduling follow-up.");
PeopleBackupFollowUpJob.scheduleJob(mContext);
}
updateWidgets(mContext);
}
/** Backs up an entry from default file to backup file. */
public void backupKey(Map.Entry<String, ?> entry, SharedPreferences.Editor backupEditor,
List<String> existingWidgets) {
String key = entry.getKey();
if (TextUtils.isEmpty(key)) {
return;
}
SharedFileEntryType entryType = getEntryType(entry);
switch(entryType) {
case WIDGET_ID:
backupWidgetIdKey(key, String.valueOf(entry.getValue()), backupEditor,
existingWidgets);
break;
case PEOPLE_TILE_KEY:
backupPeopleTileKey(key, (Set<String>) entry.getValue(), backupEditor,
existingWidgets);
break;
case CONTACT_URI:
backupContactUriKey(key, (Set<String>) entry.getValue(), backupEditor);
break;
case UNKNOWN:
default:
Log.w(TAG, "Key not identified, skipping: " + key);
}
}
/**
* Tries to restore an entry from backup file to default file.
* Returns true if restore is finished, false if it needs to be checked later.
*/
boolean restoreKey(Map.Entry<String, ?> entry, SharedPreferences.Editor editor,
SharedPreferences.Editor followUpEditor, SharedPreferences backupSp) {
String key = entry.getKey();
SharedFileEntryType keyType = getEntryType(entry);
int storedUserId = backupSp.getInt(ADD_USER_ID_TO_URI + key, INVALID_USER_ID);
switch (keyType) {
case WIDGET_ID:
restoreWidgetIdKey(key, String.valueOf(entry.getValue()), editor, storedUserId);
return true;
case PEOPLE_TILE_KEY:
return restorePeopleTileKeyAndCorrespondingWidgetFile(
key, (Set<String>) entry.getValue(), editor, followUpEditor);
case CONTACT_URI:
restoreContactUriKey(key, (Set<String>) entry.getValue(), editor, storedUserId);
return true;
case UNKNOWN:
default:
Log.e(TAG, "Key not identified, skipping:" + key);
return true;
}
}
/**
* Backs up a [widgetId, contactURI] pair, if widget id corresponds to current user.
* If contact URI has a user id, stores it so it can be re-added on restore.
*/
private void backupWidgetIdKey(String key, String uriString, SharedPreferences.Editor editor,
List<String> existingWidgets) {
if (!existingWidgets.contains(key)) {
if (DEBUG) Log.d(TAG, "Widget: " + key + " does't correspond to this user, skipping.");
return;
}
Uri uri = Uri.parse(uriString);
if (ContentProvider.uriHasUserId(uri)) {
if (DEBUG) Log.d(TAG, "Contact URI value has user ID, removing from: " + uri);
int userId = ContentProvider.getUserIdFromUri(uri);
editor.putInt(ADD_USER_ID_TO_URI + key, userId);
uri = ContentProvider.getUriWithoutUserId(uri);
}
if (DEBUG) Log.d(TAG, "Backing up widgetId key: " + key + " . Value: " + uri.toString());
editor.putString(key, uri.toString());
}
/** Restores a [widgetId, contactURI] pair, and a potential {@code storedUserId}. */
private void restoreWidgetIdKey(String key, String uriString, SharedPreferences.Editor editor,
int storedUserId) {
Uri uri = Uri.parse(uriString);
if (storedUserId != INVALID_USER_ID) {
uri = ContentProvider.createContentUriForUser(uri, UserHandle.of(storedUserId));
if (DEBUG) Log.d(TAG, "UserId was removed from URI on back up, re-adding as:" + uri);
}
if (DEBUG) Log.d(TAG, "Restoring widgetId key: " + key + " . Value: " + uri.toString());
editor.putString(key, uri.toString());
}
/**
* Backs up a [PeopleTileKey, {widgetIds}] pair, if PeopleTileKey's user is the same as current
* user, stripping out the user id.
*/
private void backupPeopleTileKey(String key, Set<String> widgetIds,
SharedPreferences.Editor editor, List<String> existingWidgets) {
PeopleTileKey peopleTileKey = PeopleTileKey.fromString(key);
if (peopleTileKey.getUserId() != mUserHandle.getIdentifier()) {
if (DEBUG) Log.d(TAG, "PeopleTileKey corresponds to different user, skipping backup.");
return;
}
Set<String> filteredWidgets = widgetIds.stream()
.filter(id -> existingWidgets.contains(id))
.collect(Collectors.toSet());
if (filteredWidgets.isEmpty()) {
return;
}
peopleTileKey.setUserId(INVALID_USER_ID);
if (DEBUG) {
Log.d(TAG, "Backing up PeopleTileKey key: " + peopleTileKey.toString() + ". Value: "
+ filteredWidgets);
}
editor.putStringSet(peopleTileKey.toString(), filteredWidgets);
}
/**
* Restores a [PeopleTileKey, {widgetIds}] pair, restoring the user id. Checks if the
* corresponding shortcut exists, and if not, we should schedule a follow up to check later.
* Also restores corresponding [widgetId, PeopleTileKey], which is not backed up since the
* information can be inferred from this.
* Returns true if restore is finished, false if we should check if shortcut is available later.
*/
private boolean restorePeopleTileKeyAndCorrespondingWidgetFile(String key,
Set<String> widgetIds, SharedPreferences.Editor editor,
SharedPreferences.Editor followUpEditor) {
PeopleTileKey peopleTileKey = PeopleTileKey.fromString(key);
// Should never happen, as type of key has been checked.
if (peopleTileKey == null) {
if (DEBUG) Log.d(TAG, "PeopleTileKey key to be restored is null, skipping.");
return true;
}
peopleTileKey.setUserId(mUserHandle.getIdentifier());
if (!PeopleTileKey.isValid(peopleTileKey)) {
if (DEBUG) Log.d(TAG, "PeopleTileKey key to be restored is not valid, skipping.");
return true;
}
boolean restored = isReadyForRestore(
mIPeopleManager, mPackageManager, peopleTileKey);
if (!restored) {
if (DEBUG) Log.d(TAG, "Adding key to follow-up storage: " + peopleTileKey.toString());
// Follow-up file stores shortcuts that need to be checked later, and possibly wiped
// from our storage.
followUpEditor.putStringSet(peopleTileKey.toString(), widgetIds);
}
if (DEBUG) {
Log.d(TAG, "Restoring PeopleTileKey key: " + peopleTileKey.toString() + " . Value: "
+ widgetIds);
}
editor.putStringSet(peopleTileKey.toString(), widgetIds);
restoreWidgetIdFiles(mContext, widgetIds, peopleTileKey);
return restored;
}
/**
* Backs up a [contactURI, {widgetIds}] pair. If contactURI contains a userId, we back up
* this entry in the corresponding user. If it doesn't, we back it up as user 0.
* If contact URI has a user id, stores it so it can be re-added on restore.
* We do not take existing widgets for this user into consideration.
*/
private void backupContactUriKey(String key, Set<String> widgetIds,
SharedPreferences.Editor editor) {
Uri uri = Uri.parse(String.valueOf(key));
if (ContentProvider.uriHasUserId(uri)) {
int userId = ContentProvider.getUserIdFromUri(uri);
if (DEBUG) Log.d(TAG, "Contact URI has user Id: " + userId);
if (userId == mUserHandle.getIdentifier()) {
uri = ContentProvider.getUriWithoutUserId(uri);
if (DEBUG) {
Log.d(TAG, "Backing up contactURI key: " + uri.toString() + " . Value: "
+ widgetIds);
}
editor.putInt(ADD_USER_ID_TO_URI + uri.toString(), userId);
editor.putStringSet(uri.toString(), widgetIds);
} else {
if (DEBUG) Log.d(TAG, "ContactURI corresponds to different user, skipping.");
}
} else if (mUserHandle.isSystem()) {
if (DEBUG) {
Log.d(TAG, "Backing up contactURI key: " + uri.toString() + " . Value: "
+ widgetIds);
}
editor.putStringSet(uri.toString(), widgetIds);
}
}
/** Restores a [contactURI, {widgetIds}] pair, and a potential {@code storedUserId}. */
private void restoreContactUriKey(String key, Set<String> widgetIds,
SharedPreferences.Editor editor, int storedUserId) {
Uri uri = Uri.parse(key);
if (storedUserId != INVALID_USER_ID) {
uri = ContentProvider.createContentUriForUser(uri, UserHandle.of(storedUserId));
if (DEBUG) Log.d(TAG, "UserId was removed from URI on back up, re-adding as:" + uri);
}
if (DEBUG) {
Log.d(TAG, "Restoring contactURI key: " + uri.toString() + " . Value: " + widgetIds);
}
editor.putStringSet(uri.toString(), widgetIds);
}
/** Restores the widget-specific files that contain PeopleTileKey information. */
public static void restoreWidgetIdFiles(Context context, Set<String> widgetIds,
PeopleTileKey key) {
for (String id : widgetIds) {
if (DEBUG) Log.d(TAG, "Restoring widget Id file: " + id + " . Value: " + key);
SharedPreferences dest = context.getSharedPreferences(id, Context.MODE_PRIVATE);
SharedPreferencesHelper.setPeopleTileKey(dest, key);
}
}
private List<String> getExistingWidgetsForUser(int userId) {
List<String> existingWidgets = new ArrayList<>();
int[] ids = mAppWidgetManager.getAppWidgetIds(
new ComponentName(mContext, PeopleSpaceWidgetProvider.class));
for (int id : ids) {
String idString = String.valueOf(id);
SharedPreferences sp = mContext.getSharedPreferences(idString, Context.MODE_PRIVATE);
if (sp.getInt(USER_ID, INVALID_USER_ID) == userId) {
existingWidgets.add(idString);
}
}
if (DEBUG) Log.d(TAG, "Existing widgets: " + existingWidgets);
return existingWidgets;
}
/**
* Returns whether {@code key} corresponds to a shortcut that is ready for restore, either
* because it is available or because it never will be. If not ready, we schedule a job to check
* again later.
*/
public static boolean isReadyForRestore(IPeopleManager peopleManager,
PackageManager packageManager, PeopleTileKey key) {
if (DEBUG) Log.d(TAG, "Checking if we should schedule a follow up job : " + key);
if (!PeopleTileKey.isValid(key)) {
if (DEBUG) Log.d(TAG, "Key is invalid, should not follow up.");
return true;
}
try {
PackageInfo info = packageManager.getPackageInfoAsUser(
key.getPackageName(), 0, key.getUserId());
} catch (PackageManager.NameNotFoundException e) {
if (DEBUG) Log.d(TAG, "Package is not installed, should follow up.");
return false;
}
try {
boolean isConversation = peopleManager.isConversation(
key.getPackageName(), key.getUserId(), key.getShortcutId());
if (DEBUG) {
Log.d(TAG, "Checked if shortcut exists, should follow up: " + !isConversation);
}
return isConversation;
} catch (Exception e) {
if (DEBUG) Log.d(TAG, "Error checking if backed up info is a shortcut.");
return false;
}
}
/** Parses default file {@code entry} to determine the entry's type.*/
public static SharedFileEntryType getEntryType(Map.Entry<String, ?> entry) {
String key = entry.getKey();
if (key == null) {
return SharedFileEntryType.UNKNOWN;
}
try {
int id = Integer.parseInt(key);
try {
String contactUri = (String) entry.getValue();
} catch (Exception e) {
Log.w(TAG, "Malformed value, skipping:" + entry.getValue());
return SharedFileEntryType.UNKNOWN;
}
return SharedFileEntryType.WIDGET_ID;
} catch (NumberFormatException ignored) { }
try {
Set<String> widgetIds = (Set<String>) entry.getValue();
} catch (Exception e) {
Log.w(TAG, "Malformed value, skipping:" + entry.getValue());
return SharedFileEntryType.UNKNOWN;
}
PeopleTileKey peopleTileKey = PeopleTileKey.fromString(key);
if (peopleTileKey != null) {
return SharedFileEntryType.PEOPLE_TILE_KEY;
}
try {
Uri uri = Uri.parse(key);
return SharedFileEntryType.CONTACT_URI;
} catch (Exception e) {
return SharedFileEntryType.UNKNOWN;
}
}
/** Sends a broadcast to update the existing Conversation widgets. */
public static void updateWidgets(Context context) {
int[] widgetIds = AppWidgetManager.getInstance(context)
.getAppWidgetIds(new ComponentName(context, PeopleSpaceWidgetProvider.class));
if (DEBUG) {
for (int id : widgetIds) {
Log.d(TAG, "Calling update to widget: " + id);
}
}
if (widgetIds != null && widgetIds.length != 0) {
Intent intent = new Intent(context, PeopleSpaceWidgetProvider.class);
intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds);
context.sendBroadcast(intent);
}
}
}