diff options
17 files changed, 2010 insertions, 44 deletions
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml index 604310a9e905..8963ddaece35 100644 --- a/packages/SystemUI/AndroidManifest.xml +++ b/packages/SystemUI/AndroidManifest.xml @@ -281,6 +281,8 @@ <!-- Permission for Smartspace. --> <uses-permission android:name="android.permission.MANAGE_SMARTSPACE" /> + <uses-permission android:name="android.permission.READ_PEOPLE_DATA" /> + <protected-broadcast android:name="com.android.settingslib.action.REGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settingslib.action.UNREGISTER_SLICE_RECEIVER" /> <protected-broadcast android:name="com.android.settings.flashlight.action.FLASHLIGHT_CHANGED" /> @@ -604,7 +606,8 @@ </activity> <activity android:name=".people.widget.LaunchConversationActivity" - android:windowDisablePreview="true" /> + android:windowDisablePreview="true" + android:theme="@android:style/Theme.Translucent.NoTitleBar.Fullscreen" /> <!-- People Space Widget --> <receiver @@ -630,6 +633,9 @@ android:permission="android.permission.GET_PEOPLE_TILE_PREVIEW"> </provider> + <service android:name=".people.PeopleBackupFollowUpJob" + android:permission="android.permission.BIND_JOB_SERVICE"/> + <!-- a gallery of delicious treats --> <service android:name=".DessertCaseDream" diff --git a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt index fe31a7b75c06..c9e67715decb 100644 --- a/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/backup/BackupHelper.kt @@ -29,6 +29,7 @@ import android.os.UserHandle import android.util.Log import com.android.systemui.controls.controller.AuxiliaryPersistenceWrapper import com.android.systemui.controls.controller.ControlsFavoritePersistenceWrapper +import com.android.systemui.people.widget.PeopleBackupHelper /** * Helper for backing up elements in SystemUI @@ -45,18 +46,29 @@ class BackupHelper : BackupAgentHelper() { private const val TAG = "BackupHelper" internal const val CONTROLS = ControlsFavoritePersistenceWrapper.FILE_NAME private const val NO_OVERWRITE_FILES_BACKUP_KEY = "systemui.files_no_overwrite" + private const val PEOPLE_TILES_BACKUP_KEY = "systemui.people.shared_preferences" val controlsDataLock = Any() const val ACTION_RESTORE_FINISHED = "com.android.systemui.backup.RESTORE_FINISHED" private const val PERMISSION_SELF = "com.android.systemui.permission.SELF" } - override fun onCreate() { + override fun onCreate(userHandle: UserHandle, operationType: Int) { super.onCreate() // The map in mapOf is guaranteed to be order preserving val controlsMap = mapOf(CONTROLS to getPPControlsFile(this)) NoOverwriteFileBackupHelper(controlsDataLock, this, controlsMap).also { addHelper(NO_OVERWRITE_FILES_BACKUP_KEY, it) } + + // Conversations widgets backup only works for system user, because widgets' information is + // stored in system user's SharedPreferences files and we can't open those from other users. + if (!userHandle.isSystem) { + return + } + + val keys = PeopleBackupHelper.getFilesToBackup() + addHelper(PEOPLE_TILES_BACKUP_KEY, PeopleBackupHelper( + this, userHandle, keys.toTypedArray())) } override fun onRestoreFinished() { diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleBackupFollowUpJob.java b/packages/SystemUI/src/com/android/systemui/people/PeopleBackupFollowUpJob.java new file mode 100644 index 000000000000..452484f599a8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleBackupFollowUpJob.java @@ -0,0 +1,229 @@ +/* + * 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; + +import static com.android.systemui.people.PeopleSpaceUtils.DEBUG; +import static com.android.systemui.people.PeopleSpaceUtils.EMPTY_STRING; +import static com.android.systemui.people.PeopleSpaceUtils.removeSharedPreferencesStorageForTile; +import static com.android.systemui.people.widget.PeopleBackupHelper.isReadyForRestore; + +import android.app.job.JobInfo; +import android.app.job.JobParameters; +import android.app.job.JobScheduler; +import android.app.job.JobService; +import android.app.people.IPeopleManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.os.PersistableBundle; +import android.os.ServiceManager; +import android.preference.PreferenceManager; +import android.util.Log; + +import androidx.annotation.VisibleForTesting; + +import com.android.systemui.people.widget.PeopleBackupHelper; +import com.android.systemui.people.widget.PeopleTileKey; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * Follow-up job that runs after a Conversations widgets restore operation. Check if shortcuts that + * were not available before are now available. If any shortcut doesn't become available after + * 1 day, we clean up its storage. + */ +public class PeopleBackupFollowUpJob extends JobService { + private static final String TAG = "PeopleBackupFollowUpJob"; + private static final String START_DATE = "start_date"; + + /** Follow-up job id. */ + public static final int JOB_ID = 74823873; + + private static final long JOB_PERIODIC_DURATION = Duration.ofHours(6).toMillis(); + private static final long CLEAN_UP_STORAGE_AFTER_DURATION = Duration.ofHours(24).toMillis(); + + /** SharedPreferences file name for follow-up specific storage.*/ + public static final String SHARED_FOLLOW_UP = "shared_follow_up"; + + private final Object mLock = new Object(); + private Context mContext; + private PackageManager mPackageManager; + private IPeopleManager mIPeopleManager; + private JobScheduler mJobScheduler; + + /** Schedules a PeopleBackupFollowUpJob every 2 hours. */ + public static void scheduleJob(Context context) { + JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); + PersistableBundle bundle = new PersistableBundle(); + bundle.putLong(START_DATE, System.currentTimeMillis()); + JobInfo jobInfo = new JobInfo + .Builder(JOB_ID, new ComponentName(context, PeopleBackupFollowUpJob.class)) + .setPeriodic(JOB_PERIODIC_DURATION) + .setExtras(bundle) + .build(); + jobScheduler.schedule(jobInfo); + } + + @Override + public void onCreate() { + super.onCreate(); + mContext = getApplicationContext(); + mPackageManager = getApplicationContext().getPackageManager(); + mIPeopleManager = IPeopleManager.Stub.asInterface( + ServiceManager.getService(Context.PEOPLE_SERVICE)); + mJobScheduler = mContext.getSystemService(JobScheduler.class); + + } + + /** Sets necessary managers for testing. */ + @VisibleForTesting + public void setManagers(Context context, PackageManager packageManager, + IPeopleManager iPeopleManager, JobScheduler jobScheduler) { + mContext = context; + mPackageManager = packageManager; + mIPeopleManager = iPeopleManager; + mJobScheduler = jobScheduler; + } + + @Override + public boolean onStartJob(JobParameters params) { + if (DEBUG) Log.d(TAG, "Starting job."); + synchronized (mLock) { + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = sp.edit(); + SharedPreferences followUp = this.getSharedPreferences( + SHARED_FOLLOW_UP, Context.MODE_PRIVATE); + SharedPreferences.Editor followUpEditor = followUp.edit(); + + // Remove from SHARED_FOLLOW_UP storage all widgets that are now ready to be updated. + Map<String, Set<String>> remainingWidgets = + processFollowUpFile(followUp, followUpEditor); + + // Check if all widgets were restored or if enough time elapsed to cancel the job. + long start = params.getExtras().getLong(START_DATE); + long now = System.currentTimeMillis(); + if (shouldCancelJob(remainingWidgets, start, now)) { + cancelJobAndClearRemainingWidgets(remainingWidgets, followUpEditor, sp); + } + + editor.apply(); + followUpEditor.apply(); + } + + // Ensure all widgets modified from SHARED_FOLLOW_UP storage are now updated. + PeopleBackupHelper.updateWidgets(mContext); + return false; + } + + /** + * Iterates through follow-up file entries and checks which shortcuts are now available. + * Returns a map of shortcuts that should be checked at a later time. + */ + public Map<String, Set<String>> processFollowUpFile(SharedPreferences followUp, + SharedPreferences.Editor followUpEditor) { + Map<String, Set<String>> remainingWidgets = new HashMap<>(); + Map<String, ?> all = followUp.getAll(); + for (Map.Entry<String, ?> entry : all.entrySet()) { + String key = entry.getKey(); + + PeopleTileKey peopleTileKey = PeopleTileKey.fromString(key); + boolean restored = isReadyForRestore(mIPeopleManager, mPackageManager, peopleTileKey); + if (restored) { + if (DEBUG) Log.d(TAG, "Removing key from follow-up: " + key); + followUpEditor.remove(key); + continue; + } + + if (DEBUG) Log.d(TAG, "Key should not be restored yet, try later: " + key); + try { + remainingWidgets.put(entry.getKey(), (Set<String>) entry.getValue()); + } catch (Exception e) { + Log.e(TAG, "Malformed entry value: " + entry.getValue()); + } + } + return remainingWidgets; + } + + /** Returns whether all shortcuts were restored or if enough time elapsed to cancel the job. */ + public boolean shouldCancelJob(Map<String, Set<String>> remainingWidgets, + long start, long now) { + if (remainingWidgets.isEmpty()) { + if (DEBUG) Log.d(TAG, "All widget storage was successfully restored."); + return true; + } + + boolean oneDayHasPassed = (now - start) > CLEAN_UP_STORAGE_AFTER_DURATION; + if (oneDayHasPassed) { + if (DEBUG) { + Log.w(TAG, "One or more widgets were not properly restored, " + + "but cancelling job because it has been a day."); + } + return true; + } + if (DEBUG) Log.d(TAG, "There are still non-restored widgets, run job again."); + return false; + } + + /** Cancels job and removes storage of any shortcut that was not restored. */ + public void cancelJobAndClearRemainingWidgets(Map<String, Set<String>> remainingWidgets, + SharedPreferences.Editor followUpEditor, SharedPreferences sp) { + if (DEBUG) Log.d(TAG, "Cancelling follow up job."); + removeUnavailableShortcutsFromSharedStorage(remainingWidgets, sp); + followUpEditor.clear(); + mJobScheduler.cancel(JOB_ID); + } + + private void removeUnavailableShortcutsFromSharedStorage(Map<String, + Set<String>> remainingWidgets, SharedPreferences sp) { + for (Map.Entry<String, Set<String>> entry : remainingWidgets.entrySet()) { + PeopleTileKey peopleTileKey = PeopleTileKey.fromString(entry.getKey()); + if (!PeopleTileKey.isValid(peopleTileKey)) { + Log.e(TAG, "Malformed peopleTileKey in follow-up file: " + entry.getKey()); + continue; + } + Set<String> widgetIds; + try { + widgetIds = (Set<String>) entry.getValue(); + } catch (Exception e) { + Log.e(TAG, "Malformed widget ids in follow-up file: " + e); + continue; + } + for (String id : widgetIds) { + int widgetId; + try { + widgetId = Integer.parseInt(id); + } catch (NumberFormatException ex) { + Log.e(TAG, "Malformed widget id in follow-up file: " + ex); + continue; + } + + String contactUriString = sp.getString(String.valueOf(widgetId), EMPTY_STRING); + removeSharedPreferencesStorageForTile( + mContext, peopleTileKey, widgetId, contactUriString); + } + } + } + + @Override + public boolean onStopJob(JobParameters params) { + return false; + } +} diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java index 917a060f1f1d..dcab86bac406 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleSpaceUtils.java @@ -25,6 +25,7 @@ import static com.android.systemui.people.NotificationHelper.shouldMatchNotifica import android.annotation.Nullable; import android.app.Notification; +import android.app.backup.BackupManager; import android.app.people.ConversationChannel; import android.app.people.IPeopleManager; import android.app.people.PeopleSpaceTile; @@ -89,7 +90,7 @@ public class PeopleSpaceUtils { /** Returns stored widgets for the conversation specified. */ public static Set<String> getStoredWidgetIds(SharedPreferences sp, PeopleTileKey key) { - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { return new HashSet<>(); } return new HashSet<>(sp.getStringSet(key.toString(), new HashSet<>())); @@ -97,19 +98,16 @@ public class PeopleSpaceUtils { /** Sets all relevant storage for {@code appWidgetId} association to {@code tile}. */ public static void setSharedPreferencesStorageForTile(Context context, PeopleTileKey key, - int appWidgetId, Uri contactUri) { - if (!key.isValid()) { + int appWidgetId, Uri contactUri, BackupManager backupManager) { + if (!PeopleTileKey.isValid(key)) { Log.e(TAG, "Not storing for invalid key"); return; } // Write relevant persisted storage. SharedPreferences widgetSp = context.getSharedPreferences(String.valueOf(appWidgetId), Context.MODE_PRIVATE); - SharedPreferences.Editor widgetEditor = widgetSp.edit(); - widgetEditor.putString(PeopleSpaceUtils.PACKAGE_NAME, key.getPackageName()); - widgetEditor.putString(PeopleSpaceUtils.SHORTCUT_ID, key.getShortcutId()); - widgetEditor.putInt(PeopleSpaceUtils.USER_ID, key.getUserId()); - widgetEditor.apply(); + SharedPreferencesHelper.setPeopleTileKey(widgetSp, key); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences.Editor editor = sp.edit(); String contactUriString = contactUri == null ? EMPTY_STRING : contactUri.toString(); @@ -117,14 +115,18 @@ public class PeopleSpaceUtils { // Don't overwrite existing widgets with the same key. addAppWidgetIdForKey(sp, editor, appWidgetId, key.toString()); - addAppWidgetIdForKey(sp, editor, appWidgetId, contactUriString); + if (!TextUtils.isEmpty(contactUriString)) { + addAppWidgetIdForKey(sp, editor, appWidgetId, contactUriString); + } editor.apply(); + backupManager.dataChanged(); } /** Removes stored data when tile is deleted. */ public static void removeSharedPreferencesStorageForTile(Context context, PeopleTileKey key, int widgetId, String contactUriString) { // Delete widgetId mapping to key. + if (DEBUG) Log.d(TAG, "Removing widget info from sharedPrefs"); SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences.Editor editor = sp.edit(); editor.remove(String.valueOf(widgetId)); @@ -230,7 +232,7 @@ public class PeopleSpaceUtils { */ public static PeopleSpaceTile augmentTileFromNotification(Context context, PeopleSpaceTile tile, PeopleTileKey key, NotificationEntry notificationEntry, int messagesCount, - Optional<Integer> appWidgetId) { + Optional<Integer> appWidgetId, BackupManager backupManager) { if (notificationEntry == null || notificationEntry.getSbn().getNotification() == null) { if (DEBUG) Log.d(TAG, "Tile key: " + key.toString() + ". Notification is null"); return removeNotificationFields(tile); @@ -246,7 +248,7 @@ public class PeopleSpaceUtils { Uri contactUri = Uri.parse(uriFromNotification); // Update storage. setSharedPreferencesStorageForTile(context, new PeopleTileKey(tile), appWidgetId.get(), - contactUri); + contactUri, backupManager); // Update cached tile in-memory. updatedTile.setContactUri(contactUri); } diff --git a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java index 844a8c6b6583..730e85004936 100644 --- a/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java +++ b/packages/SystemUI/src/com/android/systemui/people/PeopleTileViewHelper.java @@ -330,11 +330,8 @@ public class PeopleTileViewHelper { R.layout.people_tile_suppressed_layout); } Drawable appIcon = mContext.getDrawable(R.drawable.ic_conversation_icon); - Bitmap appIconAsBitmap = convertDrawableToBitmap(appIcon); - FastBitmapDrawable drawable = new FastBitmapDrawable(appIconAsBitmap); - drawable.setIsDisabled(true); - Bitmap convertedBitmap = convertDrawableToBitmap(drawable); - views.setImageViewBitmap(R.id.icon, convertedBitmap); + Bitmap disabledBitmap = convertDrawableToDisabledBitmap(appIcon); + views.setImageViewBitmap(R.id.icon, disabledBitmap); return views; } @@ -504,6 +501,11 @@ public class PeopleTileViewHelper { } private RemoteViews setLaunchIntents(RemoteViews views) { + if (!PeopleTileKey.isValid(mKey) || mTile == null) { + if (DEBUG) Log.d(TAG, "Skipping launch intent, Null tile or invalid key: " + mKey); + return views; + } + try { Intent activityIntent = new Intent(mContext, LaunchConversationActivity.class); activityIntent.addFlags( @@ -1067,7 +1069,8 @@ public class PeopleTileViewHelper { Icon icon = tile.getUserIcon(); if (icon == null) { - return null; + Drawable placeholder = context.getDrawable(R.drawable.ic_avatar_with_badge); + return convertDrawableToDisabledBitmap(placeholder); } PeopleStoryIconFactory storyIcon = new PeopleStoryIconFactory(context, context.getPackageManager(), @@ -1179,4 +1182,11 @@ public class PeopleTileViewHelper { mAvatarSize = avatarSize; } } + + private static Bitmap convertDrawableToDisabledBitmap(Drawable icon) { + Bitmap appIconAsBitmap = convertDrawableToBitmap(icon); + FastBitmapDrawable drawable = new FastBitmapDrawable(appIconAsBitmap); + drawable.setIsDisabled(true); + return convertDrawableToBitmap(drawable); + } } diff --git a/packages/SystemUI/src/com/android/systemui/people/SharedPreferencesHelper.java b/packages/SystemUI/src/com/android/systemui/people/SharedPreferencesHelper.java new file mode 100644 index 000000000000..aef08fb421d8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/SharedPreferencesHelper.java @@ -0,0 +1,59 @@ +/* + * 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; + +import static com.android.systemui.people.PeopleSpaceUtils.INVALID_USER_ID; +import static com.android.systemui.people.PeopleSpaceUtils.PACKAGE_NAME; +import static com.android.systemui.people.PeopleSpaceUtils.SHORTCUT_ID; +import static com.android.systemui.people.PeopleSpaceUtils.USER_ID; + +import android.content.SharedPreferences; + +import com.android.systemui.people.widget.PeopleTileKey; + +/** Helper class for Conversations widgets SharedPreferences storage. */ +public class SharedPreferencesHelper { + /** Clears all storage from {@code sp}. */ + public static void clear(SharedPreferences sp) { + SharedPreferences.Editor editor = sp.edit(); + editor.clear(); + editor.apply(); + } + + /** Sets {@code sp}'s storage to identify a {@link PeopleTileKey}. */ + public static void setPeopleTileKey(SharedPreferences sp, PeopleTileKey key) { + setPeopleTileKey(sp, key.getShortcutId(), key.getUserId(), key.getPackageName()); + } + + /** Sets {@code sp}'s storage to identify a {@link PeopleTileKey}. */ + public static void setPeopleTileKey(SharedPreferences sp, String shortcutId, int userId, + String packageName) { + SharedPreferences.Editor editor = sp.edit(); + editor.putString(SHORTCUT_ID, shortcutId); + editor.putInt(USER_ID, userId); + editor.putString(PACKAGE_NAME, packageName); + editor.apply(); + } + + /** Returns a {@link PeopleTileKey} based on storage from {@code sp}. */ + public static PeopleTileKey getPeopleTileKey(SharedPreferences sp) { + String shortcutId = sp.getString(SHORTCUT_ID, null); + String packageName = sp.getString(PACKAGE_NAME, null); + int userId = sp.getInt(USER_ID, INVALID_USER_ID); + return new PeopleTileKey(shortcutId, userId, packageName); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java b/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java index b031637e4016..79318d69837d 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/LaunchConversationActivity.java @@ -152,7 +152,7 @@ public class LaunchConversationActivity extends Activity { launcherApps.startShortcut( packageName, tileId, null, null, userHandle); } catch (Exception e) { - Log.e(TAG, "Exception:" + e); + Log.e(TAG, "Exception launching shortcut:" + e); } } else { if (DEBUG) Log.d(TAG, "Trying to launch conversation with null shortcutInfo."); diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java new file mode 100644 index 000000000000..d8c96dd182b4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleBackupHelper.java @@ -0,0 +1,508 @@ +/* + * 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); + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java index 62a0df270698..72cddd0b4b3f 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java @@ -22,6 +22,7 @@ import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL; import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE; import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY; import static android.content.Intent.ACTION_BOOT_COMPLETED; +import static android.content.Intent.ACTION_PACKAGE_ADDED; import static android.content.Intent.ACTION_PACKAGE_REMOVED; import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE; @@ -29,6 +30,7 @@ import static com.android.systemui.people.NotificationHelper.getContactUri; import static com.android.systemui.people.NotificationHelper.getHighestPriorityNotification; import static com.android.systemui.people.NotificationHelper.shouldFilterOut; import static com.android.systemui.people.NotificationHelper.shouldMatchNotificationByUri; +import static com.android.systemui.people.PeopleBackupFollowUpJob.SHARED_FOLLOW_UP; import static com.android.systemui.people.PeopleSpaceUtils.EMPTY_STRING; import static com.android.systemui.people.PeopleSpaceUtils.INVALID_USER_ID; import static com.android.systemui.people.PeopleSpaceUtils.PACKAGE_NAME; @@ -38,6 +40,7 @@ import static com.android.systemui.people.PeopleSpaceUtils.augmentTileFromNotifi import static com.android.systemui.people.PeopleSpaceUtils.getMessagesCount; import static com.android.systemui.people.PeopleSpaceUtils.getNotificationsByUri; import static com.android.systemui.people.PeopleSpaceUtils.removeNotificationFields; +import static com.android.systemui.people.widget.PeopleBackupHelper.getEntryType; import android.annotation.NonNull; import android.annotation.Nullable; @@ -46,6 +49,8 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Person; +import android.app.backup.BackupManager; +import android.app.job.JobScheduler; import android.app.people.ConversationChannel; import android.app.people.IPeopleManager; import android.app.people.PeopleManager; @@ -84,8 +89,10 @@ import com.android.systemui.broadcast.BroadcastDispatcher; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Background; import com.android.systemui.people.NotificationHelper; +import com.android.systemui.people.PeopleBackupFollowUpJob; import com.android.systemui.people.PeopleSpaceUtils; import com.android.systemui.people.PeopleTileViewHelper; +import com.android.systemui.people.SharedPreferencesHelper; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationListener.NotificationHandler; import com.android.systemui.statusbar.notification.NotificationEntryManager; @@ -93,6 +100,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntry; import com.android.wm.shell.bubbles.Bubbles; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -126,6 +134,7 @@ public class PeopleSpaceWidgetManager { private Optional<Bubbles> mBubblesOptional; private UserManager mUserManager; private PeopleSpaceWidgetManager mManager; + private BackupManager mBackupManager; public UiEventLogger mUiEventLogger = new UiEventLoggerImpl(); private NotificationManager mNotificationManager; private BroadcastDispatcher mBroadcastDispatcher; @@ -164,6 +173,7 @@ public class PeopleSpaceWidgetManager { ServiceManager.getService(Context.NOTIFICATION_SERVICE)); mBubblesOptional = bubblesOptional; mUserManager = userManager; + mBackupManager = new BackupManager(context); mNotificationManager = notificationManager; mManager = this; mBroadcastDispatcher = broadcastDispatcher; @@ -189,6 +199,7 @@ public class PeopleSpaceWidgetManager { null /* executor */, UserHandle.ALL); IntentFilter perAppFilter = new IntentFilter(ACTION_PACKAGE_REMOVED); + perAppFilter.addAction(ACTION_PACKAGE_ADDED); perAppFilter.addDataScheme("package"); // BroadcastDispatcher doesn't allow data schemes. mContext.registerReceiver(mBaseBroadcastReceiver, perAppFilter); @@ -224,7 +235,7 @@ public class PeopleSpaceWidgetManager { AppWidgetManager appWidgetManager, IPeopleManager iPeopleManager, PeopleManager peopleManager, LauncherApps launcherApps, NotificationEntryManager notificationEntryManager, PackageManager packageManager, - Optional<Bubbles> bubblesOptional, UserManager userManager, + Optional<Bubbles> bubblesOptional, UserManager userManager, BackupManager backupManager, INotificationManager iNotificationManager, NotificationManager notificationManager, @Background Executor executor) { mContext = context; @@ -236,6 +247,7 @@ public class PeopleSpaceWidgetManager { mPackageManager = packageManager; mBubblesOptional = bubblesOptional; mUserManager = userManager; + mBackupManager = backupManager; mINotificationManager = iNotificationManager; mNotificationManager = notificationManager; mManager = this; @@ -257,8 +269,6 @@ public class PeopleSpaceWidgetManager { if (DEBUG) Log.d(TAG, "no widgets to update"); return; } - - if (DEBUG) Log.d(TAG, "updating " + widgetIds.length + " widgets: " + widgetIds); synchronized (mLock) { updateSingleConversationWidgets(widgetIds); } @@ -274,6 +284,7 @@ public class PeopleSpaceWidgetManager { public void updateSingleConversationWidgets(int[] appWidgetIds) { Map<Integer, PeopleSpaceTile> widgetIdToTile = new HashMap<>(); for (int appWidgetId : appWidgetIds) { + if (DEBUG) Log.d(TAG, "Updating widget: " + appWidgetId); PeopleSpaceTile tile = getTileForExistingWidget(appWidgetId); if (tile == null) { Log.e(TAG, "Matching conversation not found for shortcut ID"); @@ -293,7 +304,8 @@ public class PeopleSpaceWidgetManager { private void updateAppWidgetViews(int appWidgetId, PeopleSpaceTile tile, Bundle options) { PeopleTileKey key = getKeyFromStorageByWidgetId(appWidgetId); if (DEBUG) Log.d(TAG, "Widget: " + appWidgetId + " for: " + key.toString()); - if (!key.isValid()) { + + if (!PeopleTileKey.isValid(key)) { Log.e(TAG, "Cannot update invalid widget"); return; } @@ -301,6 +313,7 @@ public class PeopleSpaceWidgetManager { options, key); // Tell the AppWidgetManager to perform an update on the current app widget. + if (DEBUG) Log.d(TAG, "Calling update widget for widgetId: " + appWidgetId); mAppWidgetManager.updateAppWidget(appWidgetId, views); } @@ -314,6 +327,9 @@ public class PeopleSpaceWidgetManager { /** Updates tile in app widget options and the current view. */ public void updateAppWidgetOptionsAndView(int appWidgetId, PeopleSpaceTile tile) { + if (tile == null) { + if (DEBUG) Log.w(TAG, "Storing null tile"); + } synchronized (mTiles) { mTiles.put(appWidgetId, tile); } @@ -368,7 +384,7 @@ public class PeopleSpaceWidgetManager { @Nullable public PeopleSpaceTile getTileFromPersistentStorage(PeopleTileKey key, int appWidgetId) throws PackageManager.NameNotFoundException { - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { Log.e(TAG, "PeopleTileKey invalid: " + key.toString()); return null; } @@ -430,7 +446,8 @@ public class PeopleSpaceWidgetManager { try { PeopleTileKey key = new PeopleTileKey( sbn.getShortcutId(), sbn.getUser().getIdentifier(), sbn.getPackageName()); - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { + Log.d(TAG, "Sbn doesn't contain valid PeopleTileKey: " + key.toString()); return; } int[] widgetIds = mAppWidgetManager.getAppWidgetIds( @@ -561,7 +578,7 @@ public class PeopleSpaceWidgetManager { if (DEBUG) Log.d(TAG, "Augmenting tile from notification, key: " + key.toString()); return augmentTileFromNotification(mContext, tile, key, highestPriority, messagesCount, - appWidgetId); + appWidgetId, mBackupManager); } /** Returns an augmented tile for an existing widget. */ @@ -588,7 +605,7 @@ public class PeopleSpaceWidgetManager { /** Returns stored widgets for the conversation specified. */ public Set<String> getMatchingKeyWidgetIds(PeopleTileKey key) { - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { return new HashSet<>(); } return new HashSet<>(mSharedPrefs.getStringSet(key.toString(), new HashSet<>())); @@ -776,7 +793,7 @@ public class PeopleSpaceWidgetManager { // PeopleTileKey arguments. if (DEBUG) Log.d(TAG, "onAppWidgetOptionsChanged called for widget: " + appWidgetId); PeopleTileKey optionsKey = AppWidgetOptionsHelper.getPeopleTileKeyFromBundle(newOptions); - if (optionsKey.isValid()) { + if (PeopleTileKey.isValid(optionsKey)) { if (DEBUG) { Log.d(TAG, "PeopleTileKey was present in Options, shortcutId: " + optionsKey.getShortcutId()); @@ -808,7 +825,7 @@ public class PeopleSpaceWidgetManager { existingKeyIfStored = getKeyFromStorageByWidgetId(appWidgetId); } // Delete previous storage if the widget already existed and is just reconfigured. - if (existingKeyIfStored.isValid()) { + if (PeopleTileKey.isValid(existingKeyIfStored)) { if (DEBUG) Log.d(TAG, "Remove previous storage for widget: " + appWidgetId); deleteWidgets(new int[]{appWidgetId}); } else { @@ -820,7 +837,7 @@ public class PeopleSpaceWidgetManager { synchronized (mLock) { if (DEBUG) Log.d(TAG, "Add storage for : " + key.toString()); PeopleSpaceUtils.setSharedPreferencesStorageForTile(mContext, key, appWidgetId, - tile.getContactUri()); + tile.getContactUri(), mBackupManager); } if (DEBUG) Log.d(TAG, "Ensure listener is registered for widget: " + appWidgetId); registerConversationListenerIfNeeded(appWidgetId, key); @@ -838,7 +855,7 @@ public class PeopleSpaceWidgetManager { /** Registers a conversation listener for {@code appWidgetId} if not already registered. */ public void registerConversationListenerIfNeeded(int widgetId, PeopleTileKey key) { // Retrieve storage needed for registration. - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { if (DEBUG) Log.w(TAG, "Could not register listener for widget: " + widgetId); return; } @@ -887,7 +904,7 @@ public class PeopleSpaceWidgetManager { widgetSp.getString(SHORTCUT_ID, null), widgetSp.getInt(USER_ID, INVALID_USER_ID), widgetSp.getString(PACKAGE_NAME, null)); - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { if (DEBUG) Log.e(TAG, "Could not delete " + widgetId); return; } @@ -1053,6 +1070,7 @@ public class PeopleSpaceWidgetManager { return; } for (int appWidgetId : appWidgetIds) { + if (DEBUG) Log.d(TAG, "Updating widget from broadcast, widget id: " + appWidgetId); PeopleSpaceTile existingTile = null; PeopleSpaceTile updatedTile = null; try { @@ -1060,7 +1078,7 @@ public class PeopleSpaceWidgetManager { existingTile = getTileForExistingWidgetThrowing(appWidgetId); if (existingTile == null) { Log.e(TAG, "Matching conversation not found for shortcut ID"); - return; + continue; } updatedTile = getTileWithCurrentState(existingTile, entryPoint); updateAppWidgetOptionsAndView(appWidgetId, updatedTile); @@ -1068,6 +1086,14 @@ public class PeopleSpaceWidgetManager { } catch (PackageManager.NameNotFoundException e) { // Delete data for uninstalled widgets. Log.e(TAG, "Package no longer found for tile: " + e); + JobScheduler jobScheduler = mContext.getSystemService(JobScheduler.class); + if (jobScheduler != null + && jobScheduler.getPendingJob(PeopleBackupFollowUpJob.JOB_ID) != null) { + if (DEBUG) { + Log.d(TAG, "Device was recently restored, wait before deleting storage."); + } + continue; + } synchronized (mLock) { updateAppWidgetOptionsAndView(appWidgetId, updatedTile); } @@ -1185,4 +1211,139 @@ public class PeopleSpaceWidgetManager { return PeopleSpaceTile.BLOCK_CONVERSATIONS; } } + + /** + * Modifies widgets storage after a restore operation, since widget ids get remapped on restore. + * This is guaranteed to run after the PeopleBackupHelper restore operation. + */ + public void remapWidgets(int[] oldWidgetIds, int[] newWidgetIds) { + if (DEBUG) { + Log.d(TAG, "Remapping widgets, old: " + Arrays.toString(oldWidgetIds) + ". new: " + + Arrays.toString(newWidgetIds)); + } + + Map<String, String> widgets = new HashMap<>(); + for (int i = 0; i < oldWidgetIds.length; i++) { + widgets.put(String.valueOf(oldWidgetIds[i]), String.valueOf(newWidgetIds[i])); + } + + remapWidgetFiles(widgets); + remapSharedFile(widgets); + remapFollowupFile(widgets); + + int[] widgetIds = mAppWidgetManager.getAppWidgetIds( + new ComponentName(mContext, PeopleSpaceWidgetProvider.class)); + Bundle b = new Bundle(); + b.putBoolean(AppWidgetManager.OPTION_APPWIDGET_RESTORE_COMPLETED, true); + for (int id : widgetIds) { + if (DEBUG) Log.d(TAG, "Setting widget as restored, widget id:" + id); + mAppWidgetManager.updateAppWidgetOptions(id, b); + } + + updateWidgets(widgetIds); + } + + /** Remaps widget ids in widget specific files. */ + public void remapWidgetFiles(Map<String, String> widgets) { + if (DEBUG) Log.d(TAG, "Remapping widget files"); + for (Map.Entry<String, String> entry : widgets.entrySet()) { + String from = String.valueOf(entry.getKey()); + String to = String.valueOf(entry.getValue()); + + SharedPreferences src = mContext.getSharedPreferences(from, Context.MODE_PRIVATE); + PeopleTileKey key = SharedPreferencesHelper.getPeopleTileKey(src); + if (PeopleTileKey.isValid(key)) { + if (DEBUG) { + Log.d(TAG, "Moving PeopleTileKey: " + key.toString() + " from file: " + + from + ", to file: " + to); + } + SharedPreferences dest = mContext.getSharedPreferences(to, Context.MODE_PRIVATE); + SharedPreferencesHelper.setPeopleTileKey(dest, key); + SharedPreferencesHelper.clear(src); + } + } + } + + /** Remaps widget ids in default shared storage. */ + public void remapSharedFile(Map<String, String> widgets) { + if (DEBUG) Log.d(TAG, "Remapping shared file"); + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); + SharedPreferences.Editor editor = sp.edit(); + Map<String, ?> all = sp.getAll(); + for (Map.Entry<String, ?> entry : all.entrySet()) { + String key = entry.getKey(); + PeopleBackupHelper.SharedFileEntryType keyType = getEntryType(entry); + if (DEBUG) Log.d(TAG, "Remapping key:" + key); + switch (keyType) { + case WIDGET_ID: + String newId = widgets.get(key); + if (TextUtils.isEmpty(newId)) { + Log.w(TAG, "Key is widget id without matching new id, skipping: " + key); + break; + } + if (DEBUG) Log.d(TAG, "Key is widget id: " + key + ", replace with: " + newId); + try { + editor.putString(newId, (String) entry.getValue()); + } catch (Exception e) { + Log.e(TAG, "Malformed entry value: " + entry.getValue()); + } + editor.remove(key); + break; + case PEOPLE_TILE_KEY: + case CONTACT_URI: + Set<String> oldWidgetIds; + try { + oldWidgetIds = (Set<String>) entry.getValue(); + } catch (Exception e) { + Log.e(TAG, "Malformed entry value: " + entry.getValue()); + editor.remove(key); + break; + } + Set<String> newWidgets = getNewWidgets(oldWidgetIds, widgets); + if (DEBUG) { + Log.d(TAG, "Key is PeopleTileKey or contact URI: " + key + + ", replace values with new ids: " + newWidgets); + } + editor.putStringSet(key, newWidgets); + break; + case UNKNOWN: + Log.e(TAG, "Key not identified:" + key); + } + } + editor.apply(); + } + + /** Remaps widget ids in follow-up job file. */ + public void remapFollowupFile(Map<String, String> widgets) { + if (DEBUG) Log.d(TAG, "Remapping follow up file"); + SharedPreferences followUp = mContext.getSharedPreferences( + SHARED_FOLLOW_UP, Context.MODE_PRIVATE); + SharedPreferences.Editor followUpEditor = followUp.edit(); + Map<String, ?> followUpAll = followUp.getAll(); + for (Map.Entry<String, ?> entry : followUpAll.entrySet()) { + String key = entry.getKey(); + Set<String> oldWidgetIds; + try { + oldWidgetIds = (Set<String>) entry.getValue(); + } catch (Exception e) { + Log.e(TAG, "Malformed entry value: " + entry.getValue()); + followUpEditor.remove(key); + continue; + } + Set<String> newWidgets = getNewWidgets(oldWidgetIds, widgets); + if (DEBUG) { + Log.d(TAG, "Follow up key: " + key + ", replace with new ids: " + newWidgets); + } + followUpEditor.putStringSet(key, newWidgets); + } + followUpEditor.apply(); + } + + private Set<String> getNewWidgets(Set<String> oldWidgets, Map<String, String> widgetsMapping) { + return oldWidgets + .stream() + .map(widgetsMapping::get) + .filter(id -> !TextUtils.isEmpty(id)) + .collect(Collectors.toSet()); + } } diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetPinnedReceiver.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetPinnedReceiver.java index a28da43a80b6..c4be197504be 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetPinnedReceiver.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetPinnedReceiver.java @@ -75,7 +75,7 @@ public class PeopleSpaceWidgetPinnedReceiver extends BroadcastReceiver { String packageName = intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME); int userId = intent.getIntExtra(Intent.EXTRA_USER_ID, INVALID_USER_ID); PeopleTileKey key = new PeopleTileKey(shortcutId, userId, packageName); - if (!key.isValid()) { + if (!PeopleTileKey.isValid(key)) { if (DEBUG) Log.w(TAG, "Skipping: key is not valid: " + key.toString()); return; } diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetProvider.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetProvider.java index 3522b76e6460..36939b735a07 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetProvider.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetProvider.java @@ -70,6 +70,13 @@ public class PeopleSpaceWidgetProvider extends AppWidgetProvider { mPeopleSpaceWidgetManager.deleteWidgets(appWidgetIds); } + @Override + public void onRestored(Context context, int[] oldWidgetIds, int[] newWidgetIds) { + super.onRestored(context, oldWidgetIds, newWidgetIds); + ensurePeopleSpaceWidgetManagerInitialized(); + mPeopleSpaceWidgetManager.remapWidgets(oldWidgetIds, newWidgetIds); + } + private void ensurePeopleSpaceWidgetManagerInitialized() { mPeopleSpaceWidgetManager.init(); } diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleTileKey.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleTileKey.java index 319df85b4872..6e6ca254dee0 100644 --- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleTileKey.java +++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleTileKey.java @@ -25,6 +25,8 @@ import android.text.TextUtils; import com.android.systemui.statusbar.notification.collection.NotificationEntry; import java.util.Objects; +import java.util.regex.Matcher; +import java.util.regex.Pattern; /** Class that encapsulates fields identifying a Conversation. */ public class PeopleTileKey { @@ -32,6 +34,8 @@ public class PeopleTileKey { private int mUserId; private String mPackageName; + private static final Pattern KEY_PATTERN = Pattern.compile("(.+)/(-?\\d+)/(\\p{L}.*)"); + public PeopleTileKey(String shortcutId, int userId, String packageName) { mShortcutId = shortcutId; mUserId = userId; @@ -66,8 +70,12 @@ public class PeopleTileKey { return mPackageName; } + public void setUserId(int userId) { + mUserId = userId; + } + /** Returns whether PeopleTileKey is valid/well-formed. */ - public boolean isValid() { + private boolean validate() { return !TextUtils.isEmpty(mShortcutId) && !TextUtils.isEmpty(mPackageName) && mUserId >= 0; } @@ -88,10 +96,31 @@ public class PeopleTileKey { */ @Override public String toString() { - if (!isValid()) return EMPTY_STRING; return mShortcutId + "/" + mUserId + "/" + mPackageName; } + /** Parses {@code key} into a {@link PeopleTileKey}. */ + public static PeopleTileKey fromString(String key) { + if (key == null) { + return null; + } + Matcher m = KEY_PATTERN.matcher(key); + if (m.find()) { + try { + int userId = Integer.parseInt(m.group(2)); + return new PeopleTileKey(m.group(1), userId, m.group(3)); + } catch (NumberFormatException e) { + return null; + } + } + return null; + } + + /** Returns whether {@code key} is a valid {@link PeopleTileKey}. */ + public static boolean isValid(PeopleTileKey key) { + return key != null && key.validate(); + } + @Override public boolean equals(Object other) { if (this == other) { diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleBackupFollowUpJobTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/PeopleBackupFollowUpJobTest.java new file mode 100644 index 000000000000..00e012e5d30c --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/people/PeopleBackupFollowUpJobTest.java @@ -0,0 +1,202 @@ +/* + * 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; + +import static com.android.systemui.people.PeopleBackupFollowUpJob.SHARED_FOLLOW_UP; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.job.JobScheduler; +import android.app.people.IPeopleManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.RemoteException; +import android.preference.PreferenceManager; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.people.widget.PeopleTileKey; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.time.Duration; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class PeopleBackupFollowUpJobTest extends SysuiTestCase { + private static final String SHORTCUT_ID_1 = "101"; + private static final String PACKAGE_NAME_1 = "package_name"; + private static final int USER_ID_1 = 0; + + private static final PeopleTileKey PEOPLE_TILE_KEY = + new PeopleTileKey(SHORTCUT_ID_1, USER_ID_1, PACKAGE_NAME_1); + + private static final String WIDGET_ID_STRING = "3"; + private static final String SECOND_WIDGET_ID_STRING = "12"; + private static final Set<String> WIDGET_IDS = new HashSet<>( + Arrays.asList(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING)); + + private static final Uri URI = Uri.parse("fake_uri"); + + @Mock + private PackageManager mPackageManager; + @Mock + private PackageInfo mPackageInfo; + @Mock + private IPeopleManager mIPeopleManager; + @Mock + private JobScheduler mJobScheduler; + + private final SharedPreferences mSp = PreferenceManager.getDefaultSharedPreferences(mContext); + private final SharedPreferences.Editor mEditor = mSp.edit(); + private final SharedPreferences mFollowUpSp = mContext.getSharedPreferences( + SHARED_FOLLOW_UP, Context.MODE_PRIVATE); + private final SharedPreferences.Editor mFollowUpEditor = mFollowUpSp.edit(); + private final SharedPreferences mWidgetIdSp = mContext.getSharedPreferences( + WIDGET_ID_STRING, Context.MODE_PRIVATE); + private final SharedPreferences mSecondWidgetIdSp = mContext.getSharedPreferences( + SECOND_WIDGET_ID_STRING, Context.MODE_PRIVATE); + + private PeopleBackupFollowUpJob mPeopleBackupFollowUpJob; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + + when(mPackageManager.getPackageInfoAsUser(any(), anyInt(), anyInt())) + .thenReturn(mPackageInfo); + when(mIPeopleManager.isConversation(any(), anyInt(), any())).thenReturn(true); + + mPeopleBackupFollowUpJob = new PeopleBackupFollowUpJob(); + mPeopleBackupFollowUpJob.setManagers( + mContext, mPackageManager, mIPeopleManager, mJobScheduler); + } + + @After + public void tearDown() { + mEditor.clear().commit(); + mFollowUpEditor.clear().commit(); + mWidgetIdSp.edit().clear().commit(); + mSecondWidgetIdSp.edit().clear().commit(); + } + + @Test + public void testProcessFollowUpFile_shouldFollowUp() throws RemoteException { + when(mIPeopleManager.isConversation(any(), anyInt(), any())).thenReturn(false); + mFollowUpEditor.putStringSet(PEOPLE_TILE_KEY.toString(), WIDGET_IDS); + mFollowUpEditor.apply(); + + Map<String, Set<String>> remainingWidgets = + mPeopleBackupFollowUpJob.processFollowUpFile(mFollowUpSp, mFollowUpEditor); + mEditor.apply(); + mFollowUpEditor.apply(); + + assertThat(remainingWidgets.size()).isEqualTo(1); + assertThat(remainingWidgets.get(PEOPLE_TILE_KEY.toString())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + assertThat(mFollowUpSp.getStringSet(PEOPLE_TILE_KEY.toString(), new HashSet<>())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + } + + @Test + public void testProcessFollowUpFile_shouldRestore() { + mFollowUpEditor.putStringSet(PEOPLE_TILE_KEY.toString(), WIDGET_IDS); + mFollowUpEditor.apply(); + + Map<String, Set<String>> remainingWidgets = + mPeopleBackupFollowUpJob.processFollowUpFile(mFollowUpSp, mFollowUpEditor); + mEditor.apply(); + mFollowUpEditor.apply(); + + assertThat(remainingWidgets).isEmpty(); + assertThat(mFollowUpSp.getStringSet(PEOPLE_TILE_KEY.toString(), new HashSet<>())).isEmpty(); + } + + @Test + public void testShouldCancelJob_noRemainingWidgets_shouldCancel() { + assertThat(mPeopleBackupFollowUpJob.shouldCancelJob( + new HashMap<>(), 10, Duration.ofMinutes(1).toMillis())).isTrue(); + } + + @Test + public void testShouldCancelJob_noRemainingWidgets_longTimeElapsed_shouldCancel() { + assertThat(mPeopleBackupFollowUpJob.shouldCancelJob( + new HashMap<>(), 10, Duration.ofHours(25).toMillis())).isTrue(); + } + + @Test + public void testShouldCancelJob_remainingWidgets_shortTimeElapsed_shouldNotCancel() { + Map<String, Set<String>> remainingWidgets = new HashMap<>(); + remainingWidgets.put(PEOPLE_TILE_KEY.toString(), WIDGET_IDS); + assertThat(mPeopleBackupFollowUpJob.shouldCancelJob(remainingWidgets, 10, 1000)).isFalse(); + } + + @Test + public void testShouldCancelJob_remainingWidgets_longTimeElapsed_shouldCancel() { + Map<String, Set<String>> remainingWidgets = new HashMap<>(); + remainingWidgets.put(PEOPLE_TILE_KEY.toString(), WIDGET_IDS); + assertThat(mPeopleBackupFollowUpJob.shouldCancelJob( + remainingWidgets, 10, 1000 * 60 * 60 * 25)).isTrue(); + } + + @Test + public void testCancelJobAndClearRemainingWidgets() { + SharedPreferencesHelper.setPeopleTileKey(mWidgetIdSp, PEOPLE_TILE_KEY); + SharedPreferencesHelper.setPeopleTileKey(mSecondWidgetIdSp, PEOPLE_TILE_KEY); + mEditor.putStringSet(URI.toString(), WIDGET_IDS); + mEditor.putString(WIDGET_ID_STRING, URI.toString()); + mEditor.putString(SECOND_WIDGET_ID_STRING, URI.toString()); + mEditor.apply(); + Map<String, Set<String>> remainingWidgets = new HashMap<>(); + remainingWidgets.put(PEOPLE_TILE_KEY.toString(), WIDGET_IDS); + + mPeopleBackupFollowUpJob.cancelJobAndClearRemainingWidgets( + remainingWidgets, mFollowUpEditor, mSp); + mEditor.apply(); + mFollowUpEditor.apply(); + + verify(mJobScheduler, times(1)).cancel(anyInt()); + assertThat(mFollowUpSp.getAll()).isEmpty(); + assertThat(mWidgetIdSp.getAll()).isEmpty(); + assertThat(mSecondWidgetIdSp.getAll()).isEmpty(); + assertThat(mSp.getStringSet(PEOPLE_TILE_KEY.toString(), new HashSet<>())).isEmpty(); + assertThat(mSp.getStringSet(URI.toString(), new HashSet<>())).isEmpty(); + assertThat(mSp.getString(WIDGET_ID_STRING, null)).isNull(); + assertThat(mSp.getString(SECOND_WIDGET_ID_STRING, null)).isNull(); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleSpaceUtilsTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/PeopleSpaceUtilsTest.java index 33c7a571ce27..fba19861b006 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/PeopleSpaceUtilsTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/people/PeopleSpaceUtilsTest.java @@ -33,6 +33,7 @@ import static org.mockito.Mockito.when; import android.app.INotificationManager; import android.app.Notification; import android.app.Person; +import android.app.backup.BackupManager; import android.app.people.IPeopleManager; import android.app.people.PeopleSpaceTile; import android.appwidget.AppWidgetManager; @@ -198,6 +199,8 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { private NotificationEntryManager mNotificationEntryManager; @Mock private PeopleSpaceWidgetManager mPeopleSpaceWidgetManager; + @Mock + private BackupManager mBackupManager; private Bundle mOptions; @@ -252,7 +255,7 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { PeopleTileKey key = new PeopleTileKey(tile); PeopleSpaceTile actual = PeopleSpaceUtils .augmentTileFromNotification(mContext, tile, key, mNotificationEntry1, 0, - Optional.empty()); + Optional.empty(), mBackupManager); assertThat(actual.getNotificationContent().toString()).isEqualTo(NOTIFICATION_TEXT_2); assertThat(actual.getNotificationSender()).isEqualTo(null); @@ -292,7 +295,7 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { PeopleTileKey key = new PeopleTileKey(tile); PeopleSpaceTile actual = PeopleSpaceUtils .augmentTileFromNotification(mContext, tile, key, notificationEntry, 0, - Optional.empty()); + Optional.empty(), mBackupManager); assertThat(actual.getNotificationContent().toString()).isEqualTo(NOTIFICATION_TEXT_2); assertThat(actual.getNotificationSender().toString()).isEqualTo("name"); @@ -325,7 +328,7 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { PeopleTileKey key = new PeopleTileKey(tile); PeopleSpaceTile actual = PeopleSpaceUtils .augmentTileFromNotification(mContext, tile, key, notificationEntry, 0, - Optional.empty()); + Optional.empty(), mBackupManager); assertThat(actual.getNotificationContent().toString()).isEqualTo(NOTIFICATION_TEXT_1); assertThat(actual.getNotificationDataUri()).isEqualTo(URI); @@ -358,7 +361,7 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { PeopleTileKey key = new PeopleTileKey(tile); PeopleSpaceTile actual = PeopleSpaceUtils .augmentTileFromNotification(mContext, tile, key, notificationEntry, 0, - Optional.empty()); + Optional.empty(), mBackupManager); assertThat(actual.getNotificationContent().toString()).isEqualTo(NOTIFICATION_TEXT_1); assertThat(actual.getNotificationDataUri()).isNull(); @@ -376,7 +379,7 @@ public class PeopleSpaceUtilsTest extends SysuiTestCase { PeopleTileKey key = new PeopleTileKey(tile); PeopleSpaceTile actual = PeopleSpaceUtils .augmentTileFromNotification(mContext, tile, key, mNotificationEntry3, 0, - Optional.empty()); + Optional.empty(), mBackupManager); assertThat(actual.getNotificationContent()).isEqualTo(null); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/SharedPreferencesHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/SharedPreferencesHelperTest.java new file mode 100644 index 000000000000..7cd5e22a9b97 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/people/SharedPreferencesHelperTest.java @@ -0,0 +1,100 @@ +/* + * 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; + +import static com.android.systemui.people.PeopleSpaceUtils.INVALID_USER_ID; +import static com.android.systemui.people.PeopleSpaceUtils.PACKAGE_NAME; +import static com.android.systemui.people.PeopleSpaceUtils.SHORTCUT_ID; +import static com.android.systemui.people.PeopleSpaceUtils.USER_ID; + +import static com.google.common.truth.Truth.assertThat; + +import android.content.Context; +import android.content.SharedPreferences; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.people.widget.PeopleTileKey; + +import org.junit.Test; +import org.junit.runner.RunWith; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class SharedPreferencesHelperTest extends SysuiTestCase { + private static final String SHORTCUT_ID_1 = "101"; + private static final String PACKAGE_NAME_1 = "package_name"; + private static final int USER_ID_1 = 0; + + private static final PeopleTileKey PEOPLE_TILE_KEY = + new PeopleTileKey(SHORTCUT_ID_1, USER_ID_1, PACKAGE_NAME_1); + + private static final int WIDGET_ID = 1; + + private void setStorageForTile(PeopleTileKey peopleTileKey, int widgetId) { + SharedPreferences widgetSp = mContext.getSharedPreferences( + String.valueOf(widgetId), + Context.MODE_PRIVATE); + SharedPreferences.Editor widgetEditor = widgetSp.edit(); + widgetEditor.putString(PeopleSpaceUtils.PACKAGE_NAME, peopleTileKey.getPackageName()); + widgetEditor.putString(PeopleSpaceUtils.SHORTCUT_ID, peopleTileKey.getShortcutId()); + widgetEditor.putInt(PeopleSpaceUtils.USER_ID, peopleTileKey.getUserId()); + widgetEditor.apply(); + } + + @Test + public void testGetPeopleTileKey() { + setStorageForTile(PEOPLE_TILE_KEY, WIDGET_ID); + + SharedPreferences sp = mContext.getSharedPreferences( + String.valueOf(WIDGET_ID), + Context.MODE_PRIVATE); + PeopleTileKey actual = SharedPreferencesHelper.getPeopleTileKey(sp); + + assertThat(actual.getPackageName()).isEqualTo(PACKAGE_NAME_1); + assertThat(actual.getShortcutId()).isEqualTo(SHORTCUT_ID_1); + assertThat(actual.getUserId()).isEqualTo(USER_ID_1); + } + + @Test + public void testSetPeopleTileKey() { + SharedPreferences sp = mContext.getSharedPreferences( + String.valueOf(WIDGET_ID), + Context.MODE_PRIVATE); + SharedPreferencesHelper.setPeopleTileKey(sp, PEOPLE_TILE_KEY); + + assertThat(sp.getString(SHORTCUT_ID, null)).isEqualTo(SHORTCUT_ID_1); + assertThat(sp.getString(PACKAGE_NAME, null)).isEqualTo(PACKAGE_NAME_1); + assertThat(sp.getInt(USER_ID, INVALID_USER_ID)).isEqualTo(USER_ID_1); + } + + @Test + public void testClear() { + setStorageForTile(PEOPLE_TILE_KEY, WIDGET_ID); + + SharedPreferences sp = mContext.getSharedPreferences( + String.valueOf(WIDGET_ID), + Context.MODE_PRIVATE); + SharedPreferencesHelper.clear(sp); + + assertThat(sp.getString(SHORTCUT_ID, null)).isEqualTo(null); + assertThat(sp.getString(PACKAGE_NAME, null)).isEqualTo(null); + assertThat(sp.getInt(USER_ID, INVALID_USER_ID)).isEqualTo(INVALID_USER_ID); + + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleBackupHelperTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleBackupHelperTest.java new file mode 100644 index 000000000000..5d526e102b91 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleBackupHelperTest.java @@ -0,0 +1,537 @@ +/* + * 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.INVALID_USER_ID; +import static com.android.systemui.people.widget.PeopleBackupHelper.ADD_USER_ID_TO_URI; +import static com.android.systemui.people.widget.PeopleBackupHelper.SHARED_BACKUP; +import static com.android.systemui.people.widget.PeopleBackupHelper.SharedFileEntryType; +import static com.android.systemui.people.widget.PeopleBackupHelper.getEntryType; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.when; + +import android.app.people.IPeopleManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.RemoteException; +import android.os.UserHandle; +import android.preference.PreferenceManager; +import android.testing.AndroidTestingRunner; + +import androidx.test.filters.SmallTest; + +import com.android.systemui.SysuiTestCase; +import com.android.systemui.people.SharedPreferencesHelper; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.AbstractMap; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class PeopleBackupHelperTest extends SysuiTestCase { + private static final String SHORTCUT_ID_1 = "101"; + private static final String PACKAGE_NAME_1 = "package_name"; + private static final int USER_ID_0 = 0; + private static final int USER_ID_10 = 10; + + private static final PeopleTileKey PEOPLE_TILE_KEY = + new PeopleTileKey(SHORTCUT_ID_1, USER_ID_0, PACKAGE_NAME_1); + private static final PeopleTileKey OTHER_PEOPLE_TILE_KEY = + new PeopleTileKey(SHORTCUT_ID_1, USER_ID_10, PACKAGE_NAME_1); + private static final PeopleTileKey INVALID_USER_ID_PEOPLE_TILE_KEY = + new PeopleTileKey(SHORTCUT_ID_1, INVALID_USER_ID, PACKAGE_NAME_1); + + private static final String WIDGET_ID_STRING = "3"; + private static final String SECOND_WIDGET_ID_STRING = "12"; + private static final String OTHER_WIDGET_ID_STRING = "7"; + private static final Set<String> WIDGET_IDS = new HashSet<>( + Arrays.asList(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING)); + + private static final String URI_STRING = "content://mms"; + private static final String URI_WITH_USER_ID_0 = "content://0@mms"; + private static final String URI_WITH_USER_ID_10 = "content://10@mms"; + + private final SharedPreferences mBackupSp = mContext.getSharedPreferences( + SHARED_BACKUP, Context.MODE_PRIVATE); + private final SharedPreferences.Editor mBackupEditor = mBackupSp.edit(); + private final SharedPreferences mSp = PreferenceManager.getDefaultSharedPreferences(mContext); + private final SharedPreferences.Editor mEditor = mSp.edit(); + private final SharedPreferences mFollowUpSp = mContext.getSharedPreferences( + SHARED_FOLLOW_UP, Context.MODE_PRIVATE); + private final SharedPreferences.Editor mFollowUpEditor = mFollowUpSp.edit(); + private final SharedPreferences mWidgetIdSp = mContext.getSharedPreferences( + WIDGET_ID_STRING, Context.MODE_PRIVATE); + private final SharedPreferences mSecondWidgetIdSp = mContext.getSharedPreferences( + SECOND_WIDGET_ID_STRING, Context.MODE_PRIVATE); + + @Mock + private PackageManager mPackageManager; + @Mock + private PackageInfo mPackageInfo; + @Mock + private IPeopleManager mIPeopleManager; + + private PeopleBackupHelper mHelper; + private PeopleBackupHelper mOtherHelper; + + @Before + public void setUp() throws Exception { + MockitoAnnotations.initMocks(this); + mHelper = new PeopleBackupHelper(mContext, + UserHandle.of(0), new String[]{SHARED_BACKUP}, mPackageManager, mIPeopleManager); + mOtherHelper = new PeopleBackupHelper(mContext, + UserHandle.of(10), new String[]{SHARED_BACKUP}, mPackageManager, mIPeopleManager); + + when(mPackageManager.getPackageInfoAsUser(any(), anyInt(), anyInt())) + .thenReturn(mPackageInfo); + when(mIPeopleManager.isConversation(any(), anyInt(), any())).thenReturn(true); + } + + @After + public void tearDown() { + mBackupEditor.clear().commit(); + mEditor.clear().commit(); + mFollowUpEditor.clear().commit(); + mWidgetIdSp.edit().clear().commit(); + mSecondWidgetIdSp.edit().clear().commit(); + } + + @Test + public void testGetKeyType_widgetId() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(WIDGET_ID_STRING, "contact"); + assertThat(getEntryType(entry)).isEqualTo(SharedFileEntryType.WIDGET_ID); + } + + @Test + public void testGetKeyType_widgetId_twoDigits() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + SECOND_WIDGET_ID_STRING, URI_STRING); + assertThat(getEntryType(entry)).isEqualTo(SharedFileEntryType.WIDGET_ID); + } + + @Test + public void testGetKeyType_peopleTileKey_valid() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + "shortcut_id/12/com.android.systemui", WIDGET_IDS); + assertThat(getEntryType(entry)).isEqualTo(SharedFileEntryType.PEOPLE_TILE_KEY); + } + + @Test + public void testGetKeyType_peopleTileKey_validWithSlashes() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + "shortcut_id/with/slashes/12/com.android.systemui2", WIDGET_IDS); + assertThat(getEntryType(entry)).isEqualTo(SharedFileEntryType.PEOPLE_TILE_KEY); + } + + @Test + public void testGetKeyType_peopleTileKey_negativeNumber() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + "shortcut_id/with/slashes/-1/com.android.systemui2", WIDGET_IDS); + assertThat(getEntryType(entry)).isEqualTo(SharedFileEntryType.PEOPLE_TILE_KEY); + } + + @Test + public void testGetKeyType_contactUri() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + "shortcut_id/1f/com.android.systemui2", WIDGET_IDS); + assertThat(getEntryType(entry)).isEqualTo(SharedFileEntryType.CONTACT_URI); + } + + @Test + public void testGetKeyType_contactUri_valid() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + "http://content.fake", WIDGET_IDS); + assertThat(getEntryType(entry)).isEqualTo(SharedFileEntryType.CONTACT_URI); + } + + @Test + public void testGetKeyType_contactUri_invalidPackageName() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + "shortcut_id/with/slashes/12/2r/com.android.systemui2", WIDGET_IDS); + assertThat(getEntryType(entry)).isEqualTo(SharedFileEntryType.CONTACT_URI); + } + + @Test + public void testGetKeyType_unknown_unexpectedValueForPeopleTileKey() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + "shortcut_id/12/com.android.systemui", URI_STRING); + assertThat(getEntryType(entry)).isEqualTo(SharedFileEntryType.UNKNOWN); + } + + @Test + public void testGetKeyType_unknown_unexpectedValueForContactUri() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + URI_STRING, "12"); + assertThat(getEntryType(entry)).isEqualTo(SharedFileEntryType.UNKNOWN); + } + + @Test + public void testGetKeyType_unknown() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + null, WIDGET_IDS); + assertThat(getEntryType(entry)).isEqualTo(SharedFileEntryType.UNKNOWN); + } + + @Test + public void testBackupKey_widgetIdKey_containsWidget_noUserIdInUri() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(WIDGET_ID_STRING, URI_STRING); + + mHelper.backupKey(entry, mBackupEditor, Collections.singletonList(WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getString(WIDGET_ID_STRING, null)).isEqualTo(URI_STRING); + } + + @Test + public void testBackupKey_widgetIdKey_doesNotContainWidget_noUserIdInUri() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(WIDGET_ID_STRING, URI_STRING); + + mHelper.backupKey(entry, mBackupEditor, Collections.singletonList(OTHER_WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getString(WIDGET_ID_STRING, null)).isNull(); + } + + @Test + public void testBackupKey_widgetIdKey_containsOneWidget_differentUserIdInUri() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(WIDGET_ID_STRING, + URI_WITH_USER_ID_10); + + mHelper.backupKey(entry, mBackupEditor, Collections.singletonList(WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getString(WIDGET_ID_STRING, null)).isEqualTo(URI_STRING); + assertThat(mBackupSp.getInt(ADD_USER_ID_TO_URI + WIDGET_ID_STRING, INVALID_USER_ID)) + .isEqualTo(USER_ID_10); + } + + @Test + public void testBackupKey_widgetIdKey_containsWidget_SameUserIdInUri() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + WIDGET_ID_STRING, URI_WITH_USER_ID_10); + + mOtherHelper.backupKey(entry, mBackupEditor, Collections.singletonList(WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getString(WIDGET_ID_STRING, null)).isEqualTo(URI_STRING); + assertThat(mBackupSp.getInt(ADD_USER_ID_TO_URI + WIDGET_ID_STRING, INVALID_USER_ID)) + .isEqualTo(USER_ID_10); + } + + @Test + public void testBackupKey_contactUriKey_ignoresExistingWidgets() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(URI_STRING, WIDGET_IDS); + + mHelper.backupKey(entry, mBackupEditor, Collections.singletonList(WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getStringSet(URI_STRING, new HashSet<>())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + } + + @Test + public void testBackupKey_contactUriKey_ignoresExistingWidgets_otherWidget() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(URI_STRING, WIDGET_IDS); + + mHelper.backupKey(entry, mBackupEditor, Collections.singletonList(WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getStringSet(URI_STRING, new HashSet<>())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + } + + @Test + public void testBackupKey_contactUriKey_noUserId_otherUser_doesntBackup() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(URI_STRING, WIDGET_IDS); + + mOtherHelper.backupKey(entry, mBackupEditor, Collections.singletonList(WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getStringSet(URI_STRING, new HashSet<>())).isEmpty(); + } + + @Test + public void testBackupKey_contactUriKey_sameUserId() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(URI_WITH_USER_ID_10, WIDGET_IDS); + + mOtherHelper.backupKey(entry, mBackupEditor, Collections.singletonList(WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getStringSet(URI_STRING, new HashSet<>())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + assertThat(mBackupSp.getInt(ADD_USER_ID_TO_URI + URI_STRING, INVALID_USER_ID)) + .isEqualTo(USER_ID_10); + } + + @Test + public void testBackupKey_contactUriKey_differentUserId_runningAsUser0() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(URI_WITH_USER_ID_10, WIDGET_IDS); + + mHelper.backupKey(entry, mBackupEditor, Collections.singletonList(WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getStringSet(URI_STRING, new HashSet<>())).isEmpty(); + assertThat(mBackupSp.getInt(ADD_USER_ID_TO_URI + URI_STRING, INVALID_USER_ID)) + .isEqualTo(INVALID_USER_ID); + } + + @Test + public void testBackupKey_contactUriKey_differentUserId_runningAsUser10() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(URI_WITH_USER_ID_0, WIDGET_IDS); + + mOtherHelper.backupKey(entry, mBackupEditor, Collections.singletonList(WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getStringSet(URI_STRING, new HashSet<>())).isEmpty(); + assertThat(mBackupSp.getInt(ADD_USER_ID_TO_URI + URI_STRING, INVALID_USER_ID)) + .isEqualTo(INVALID_USER_ID); + } + + @Test + public void testBackupKey_peopleTileKey_containsWidget() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + PEOPLE_TILE_KEY.toString(), WIDGET_IDS); + + mHelper.backupKey(entry, mBackupEditor, Collections.singletonList(WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getStringSet( + INVALID_USER_ID_PEOPLE_TILE_KEY.toString(), new HashSet<>())) + .containsExactly(WIDGET_ID_STRING); + } + + @Test + public void testBackupKey_peopleTileKey_containsBothWidgets() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + PEOPLE_TILE_KEY.toString(), WIDGET_IDS); + + mHelper.backupKey(entry, mBackupEditor, + Arrays.asList(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat( + mBackupSp.getStringSet(INVALID_USER_ID_PEOPLE_TILE_KEY.toString(), new HashSet<>())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + } + + @Test + public void testBackupKey_peopleTileKey_doesNotContainWidget() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + PEOPLE_TILE_KEY.toString(), WIDGET_IDS); + + mHelper.backupKey(entry, mBackupEditor, Collections.singletonList(OTHER_WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getStringSet( + INVALID_USER_ID_PEOPLE_TILE_KEY.toString(), new HashSet<>())).isEmpty(); + } + + @Test + public void testBackupKey_peopleTileKey_differentUserId() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + OTHER_PEOPLE_TILE_KEY.toString(), WIDGET_IDS); + + mHelper.backupKey(entry, mBackupEditor, Collections.singletonList(WIDGET_ID_STRING)); + mBackupEditor.apply(); + + assertThat(mBackupSp.getStringSet( + INVALID_USER_ID_PEOPLE_TILE_KEY.toString(), new HashSet<>())).isEmpty(); + } + + @Test + public void testRestoreKey_widgetIdKey_noUserIdInUri() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(WIDGET_ID_STRING, URI_STRING); + + boolean restored = mHelper.restoreKey(entry, mEditor, mFollowUpEditor, mBackupSp); + mEditor.apply(); + mFollowUpEditor.apply(); + + assertThat(restored).isTrue(); + assertThat(mSp.getString(WIDGET_ID_STRING, null)).isEqualTo(URI_STRING); + assertThat(mFollowUpSp.getAll()).isEmpty(); + } + + @Test + public void testRestoreKey_widgetIdKey_sameUserInUri() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(WIDGET_ID_STRING, URI_STRING); + mBackupEditor.putInt(ADD_USER_ID_TO_URI + WIDGET_ID_STRING, USER_ID_0); + mBackupEditor.apply(); + + boolean restored = mHelper.restoreKey(entry, mEditor, mFollowUpEditor, mBackupSp); + mEditor.apply(); + mFollowUpEditor.apply(); + + assertThat(restored).isTrue(); + assertThat(mSp.getString(WIDGET_ID_STRING, null)).isEqualTo(URI_WITH_USER_ID_0); + assertThat(mFollowUpSp.getAll()).isEmpty(); + } + + @Test + public void testRestoreKey_widgetIdKey_differentUserInUri() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(WIDGET_ID_STRING, URI_STRING); + mBackupEditor.putInt(ADD_USER_ID_TO_URI + WIDGET_ID_STRING, USER_ID_10); + mBackupEditor.apply(); + + boolean restored = mHelper.restoreKey(entry, mEditor, mFollowUpEditor, mBackupSp); + mEditor.apply(); + mFollowUpEditor.apply(); + + assertThat(restored).isTrue(); + assertThat(mSp.getString(WIDGET_ID_STRING, null)).isEqualTo(URI_WITH_USER_ID_10); + assertThat(mFollowUpSp.getAll()).isEmpty(); + } + + @Test + public void testRestoreKey_widgetIdKey_nonSystemUser_differentUser() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(WIDGET_ID_STRING, URI_STRING); + mBackupEditor.putInt(ADD_USER_ID_TO_URI + WIDGET_ID_STRING, USER_ID_0); + mBackupEditor.apply(); + + boolean restored = mOtherHelper.restoreKey(entry, mEditor, mFollowUpEditor, mBackupSp); + mEditor.apply(); + mFollowUpEditor.apply(); + + assertThat(restored).isTrue(); + assertThat(mSp.getString(WIDGET_ID_STRING, null)).isEqualTo(URI_WITH_USER_ID_0); + assertThat(mFollowUpSp.getAll()).isEmpty(); + } + + @Test + public void testRestoreKey_contactUriKey_noUserIdInUri() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(URI_STRING, WIDGET_IDS); + + boolean restored = mHelper.restoreKey(entry, mEditor, mFollowUpEditor, mBackupSp); + mEditor.apply(); + mFollowUpEditor.apply(); + + assertThat(restored).isTrue(); + assertThat(mSp.getStringSet(URI_STRING, new HashSet<>())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + assertThat(mFollowUpSp.getAll()).isEmpty(); + } + + @Test + public void testRestoreKey_contactUriKey_sameUserInUri() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(URI_STRING, WIDGET_IDS); + mBackupEditor.putInt(ADD_USER_ID_TO_URI + URI_STRING, USER_ID_0); + mBackupEditor.apply(); + + boolean restored = mHelper.restoreKey(entry, mEditor, mFollowUpEditor, mBackupSp); + mEditor.apply(); + mFollowUpEditor.apply(); + + assertThat(restored).isTrue(); + assertThat(mSp.getStringSet(URI_WITH_USER_ID_0, new HashSet<>())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + assertThat(mSp.getStringSet(URI_STRING, new HashSet<>())).isEmpty(); + assertThat(mFollowUpSp.getAll()).isEmpty(); + } + + @Test + public void testRestoreKey_contactUriKey_differentUserInUri() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(URI_STRING, WIDGET_IDS); + mBackupEditor.putInt(ADD_USER_ID_TO_URI + URI_STRING, USER_ID_10); + mBackupEditor.apply(); + + boolean restored = mHelper.restoreKey(entry, mEditor, mFollowUpEditor, mBackupSp); + mEditor.apply(); + mFollowUpEditor.apply(); + + assertThat(restored).isTrue(); + assertThat(mSp.getStringSet(URI_WITH_USER_ID_10, new HashSet<>())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + assertThat(mSp.getStringSet(URI_STRING, new HashSet<>())).isEmpty(); + assertThat(mFollowUpSp.getAll()).isEmpty(); + } + + @Test + public void testRestoreKey_contactUriKey_nonSystemUser_differentUser() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>(URI_STRING, WIDGET_IDS); + mBackupEditor.putInt(ADD_USER_ID_TO_URI + URI_STRING, USER_ID_0); + mBackupEditor.apply(); + + boolean restored = mOtherHelper.restoreKey(entry, mEditor, mFollowUpEditor, mBackupSp); + mEditor.apply(); + mFollowUpEditor.apply(); + + assertThat(restored).isTrue(); + assertThat(mSp.getStringSet(URI_WITH_USER_ID_0, new HashSet<>())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + assertThat(mSp.getStringSet(URI_STRING, new HashSet<>())).isEmpty(); + assertThat(mFollowUpSp.getAll()).isEmpty(); + } + + @Test + public void testRestoreKey_peopleTileKey_shouldNotFollowUp() { + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + INVALID_USER_ID_PEOPLE_TILE_KEY.toString(), WIDGET_IDS); + + boolean restored = mHelper.restoreKey(entry, mEditor, mFollowUpEditor, mBackupSp); + mEditor.apply(); + mFollowUpEditor.apply(); + + assertThat(restored).isTrue(); + assertThat(mSp.getStringSet(PEOPLE_TILE_KEY.toString(), new HashSet<>())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + assertThat(SharedPreferencesHelper.getPeopleTileKey(mWidgetIdSp)) + .isEqualTo(PEOPLE_TILE_KEY); + assertThat(SharedPreferencesHelper.getPeopleTileKey(mSecondWidgetIdSp)) + .isEqualTo(PEOPLE_TILE_KEY); + assertThat(mFollowUpSp.getAll()).isEmpty(); + } + + @Test + public void testRestoreKey_peopleTileKey_shortcutNotYetRestored_shouldFollowUpBoth() + throws RemoteException { + when(mIPeopleManager.isConversation(any(), anyInt(), any())).thenReturn(false); + + Map.Entry<String, ?> entry = new AbstractMap.SimpleEntry<>( + INVALID_USER_ID_PEOPLE_TILE_KEY.toString(), WIDGET_IDS); + + boolean restored = mHelper.restoreKey(entry, mEditor, mFollowUpEditor, mBackupSp); + mEditor.apply(); + mFollowUpEditor.apply(); + + assertThat(restored).isFalse(); + assertThat(mSp.getStringSet(PEOPLE_TILE_KEY.toString(), new HashSet<>())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + assertThat(SharedPreferencesHelper.getPeopleTileKey(mWidgetIdSp)) + .isEqualTo(PEOPLE_TILE_KEY); + assertThat(SharedPreferencesHelper.getPeopleTileKey(mSecondWidgetIdSp)) + .isEqualTo(PEOPLE_TILE_KEY); + + assertThat(mFollowUpSp.getStringSet(PEOPLE_TILE_KEY.toString(), new HashSet<>())) + .containsExactly(WIDGET_ID_STRING, SECOND_WIDGET_ID_STRING); + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java index c48f26b8c853..ddad7581899b 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java @@ -73,6 +73,7 @@ import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; import android.app.Person; +import android.app.backup.BackupManager; import android.app.people.ConversationChannel; import android.app.people.ConversationStatus; import android.app.people.IPeopleManager; @@ -102,7 +103,9 @@ import androidx.test.filters.SmallTest; import com.android.systemui.R; import com.android.systemui.SysuiTestCase; +import com.android.systemui.people.PeopleBackupFollowUpJob; import com.android.systemui.people.PeopleSpaceUtils; +import com.android.systemui.people.SharedPreferencesHelper; import com.android.systemui.statusbar.NotificationListener; import com.android.systemui.statusbar.NotificationListener.NotificationHandler; import com.android.systemui.statusbar.SbnBuilder; @@ -151,6 +154,11 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { private static final int WIDGET_ID_WITH_KEY_IN_OPTIONS = 4; private static final int WIDGET_ID_WITH_SAME_URI = 5; private static final int WIDGET_ID_WITH_DIFFERENT_URI = 6; + private static final int WIDGET_ID_8 = 8; + private static final int WIDGET_ID_9 = 9; + private static final int WIDGET_ID_11 = 11; + private static final int WIDGET_ID_14 = 14; + private static final int WIDGET_ID_15 = 15; private static final String SHORTCUT_ID = "101"; private static final String OTHER_SHORTCUT_ID = "102"; private static final String NOTIFICATION_KEY = "0|com.android.systemui.tests|0|null|0"; @@ -195,6 +203,14 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { | SUPPRESSED_EFFECT_NOTIFICATION_LIST; private static final long SBN_POST_TIME = 567L; + private static final Map<String, String> WIDGETS_MAPPING = Map.of( + String.valueOf(WIDGET_ID_8), String.valueOf(WIDGET_ID_WITH_SHORTCUT), + String.valueOf(WIDGET_ID_9), String.valueOf(WIDGET_ID_WITHOUT_SHORTCUT), + String.valueOf(WIDGET_ID_11), String.valueOf(WIDGET_ID_WITH_KEY_IN_OPTIONS), + String.valueOf(WIDGET_ID_14), String.valueOf(WIDGET_ID_WITH_SAME_URI), + String.valueOf(WIDGET_ID_15), String.valueOf(WIDGET_ID_WITH_DIFFERENT_URI) + ); + private ShortcutInfo mShortcutInfo; private NotificationEntry mNotificationEntry; @@ -228,6 +244,8 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { private NotificationManager.Policy mNotificationPolicy; @Mock private Bubbles mBubbles; + @Mock + private BackupManager mBackupManager; @Captor private ArgumentCaptor<NotificationHandler> mListenerCaptor; @@ -246,8 +264,8 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { mDependency.injectTestDependency(NotificationEntryManager.class, mNotificationEntryManager); mManager = new PeopleSpaceWidgetManager(mContext, mAppWidgetManager, mIPeopleManager, mPeopleManager, mLauncherApps, mNotificationEntryManager, mPackageManager, - Optional.of(mBubbles), mUserManager, mINotificationManager, mNotificationManager, - mFakeExecutor); + Optional.of(mBubbles), mUserManager, mBackupManager, mINotificationManager, + mNotificationManager, mFakeExecutor); mManager.attach(mListenerService); verify(mListenerService).addNotificationHandler(mListenerCaptor.capture()); @@ -1410,6 +1428,89 @@ public class PeopleSpaceWidgetManagerTest extends SysuiTestCase { assertThat(tile.getNotificationPolicyState()).isEqualTo(expected | SHOW_CONVERSATIONS); } + @Test + public void testRemapWidgetFiles() { + setStorageForTile(SHORTCUT_ID, TEST_PACKAGE_A, WIDGET_ID_8, URI); + setStorageForTile(OTHER_SHORTCUT_ID, TEST_PACKAGE_B, WIDGET_ID_11, URI); + + mManager.remapWidgetFiles(WIDGETS_MAPPING); + + SharedPreferences sp1 = mContext.getSharedPreferences( + String.valueOf(WIDGET_ID_WITH_SHORTCUT), Context.MODE_PRIVATE); + PeopleTileKey key1 = SharedPreferencesHelper.getPeopleTileKey(sp1); + assertThat(key1.getShortcutId()).isEqualTo(SHORTCUT_ID); + assertThat(key1.getPackageName()).isEqualTo(TEST_PACKAGE_A); + + SharedPreferences sp4 = mContext.getSharedPreferences( + String.valueOf(WIDGET_ID_WITH_KEY_IN_OPTIONS), Context.MODE_PRIVATE); + PeopleTileKey key4 = SharedPreferencesHelper.getPeopleTileKey(sp4); + assertThat(key4.getShortcutId()).isEqualTo(OTHER_SHORTCUT_ID); + assertThat(key4.getPackageName()).isEqualTo(TEST_PACKAGE_B); + + SharedPreferences sp8 = mContext.getSharedPreferences( + String.valueOf(WIDGET_ID_8), Context.MODE_PRIVATE); + PeopleTileKey key8 = SharedPreferencesHelper.getPeopleTileKey(sp8); + assertThat(key8.getShortcutId()).isNull(); + assertThat(key8.getPackageName()).isNull(); + + SharedPreferences sp11 = mContext.getSharedPreferences( + String.valueOf(WIDGET_ID_11), Context.MODE_PRIVATE); + PeopleTileKey key11 = SharedPreferencesHelper.getPeopleTileKey(sp11); + assertThat(key11.getShortcutId()).isNull(); + assertThat(key11.getPackageName()).isNull(); + } + + @Test + public void testRemapSharedFile() { + setStorageForTile(SHORTCUT_ID, TEST_PACKAGE_A, WIDGET_ID_8, URI); + setStorageForTile(OTHER_SHORTCUT_ID, TEST_PACKAGE_B, WIDGET_ID_11, URI); + + mManager.remapSharedFile(WIDGETS_MAPPING); + + SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(mContext); + + assertThat(sp.getString(String.valueOf(WIDGET_ID_8), null)).isNull(); + assertThat(sp.getString(String.valueOf(WIDGET_ID_11), null)).isNull(); + assertThat(sp.getString(String.valueOf(WIDGET_ID_WITH_SHORTCUT), null)) + .isEqualTo(URI.toString()); + assertThat(sp.getString(String.valueOf(WIDGET_ID_WITH_KEY_IN_OPTIONS), null)) + .isEqualTo(URI.toString()); + + assertThat(sp.getStringSet(URI.toString(), new HashSet<>())).containsExactly( + String.valueOf(WIDGET_ID_WITH_SHORTCUT), + String.valueOf(WIDGET_ID_WITH_KEY_IN_OPTIONS)); + + PeopleTileKey key8 = new PeopleTileKey(SHORTCUT_ID, 0, TEST_PACKAGE_A); + assertThat(sp.getStringSet(key8.toString(), new HashSet<>())).containsExactly( + String.valueOf(WIDGET_ID_WITH_SHORTCUT)); + + PeopleTileKey key11 = new PeopleTileKey(OTHER_SHORTCUT_ID, 0, TEST_PACKAGE_B); + assertThat(sp.getStringSet(key11.toString(), new HashSet<>())).containsExactly( + String.valueOf(WIDGET_ID_WITH_KEY_IN_OPTIONS)); + } + + @Test + public void testRemapFollowupFile() { + PeopleTileKey key8 = new PeopleTileKey(SHORTCUT_ID, 0, TEST_PACKAGE_A); + PeopleTileKey key11 = new PeopleTileKey(OTHER_SHORTCUT_ID, 0, TEST_PACKAGE_B); + Set<String> set8 = new HashSet<>(Collections.singleton(String.valueOf(WIDGET_ID_8))); + Set<String> set11 = new HashSet<>(Collections.singleton(String.valueOf(WIDGET_ID_11))); + + SharedPreferences followUp = mContext.getSharedPreferences( + PeopleBackupFollowUpJob.SHARED_FOLLOW_UP, Context.MODE_PRIVATE); + SharedPreferences.Editor followUpEditor = followUp.edit(); + followUpEditor.putStringSet(key8.toString(), set8); + followUpEditor.putStringSet(key11.toString(), set11); + followUpEditor.apply(); + + mManager.remapFollowupFile(WIDGETS_MAPPING); + + assertThat(followUp.getStringSet(key8.toString(), new HashSet<>())).containsExactly( + String.valueOf(WIDGET_ID_WITH_SHORTCUT)); + assertThat(followUp.getStringSet(key11.toString(), new HashSet<>())).containsExactly( + String.valueOf(WIDGET_ID_WITH_KEY_IN_OPTIONS)); + } + private void setFinalField(String fieldName, int value) { try { Field field = NotificationManager.Policy.class.getDeclaredField(fieldName); |