| /* |
| * 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(48).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; |
| } |
| } |