1 /* 2 * Copyright (C) 2021 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.systemui.people; 18 19 import static com.android.systemui.people.PeopleSpaceUtils.DEBUG; 20 import static com.android.systemui.people.PeopleSpaceUtils.EMPTY_STRING; 21 import static com.android.systemui.people.PeopleSpaceUtils.removeSharedPreferencesStorageForTile; 22 import static com.android.systemui.people.widget.PeopleBackupHelper.isReadyForRestore; 23 24 import android.app.job.JobInfo; 25 import android.app.job.JobParameters; 26 import android.app.job.JobScheduler; 27 import android.app.job.JobService; 28 import android.app.people.IPeopleManager; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.content.SharedPreferences; 32 import android.content.pm.PackageManager; 33 import android.os.PersistableBundle; 34 import android.os.ServiceManager; 35 import android.preference.PreferenceManager; 36 import android.util.Log; 37 38 import androidx.annotation.VisibleForTesting; 39 40 import com.android.systemui.people.widget.PeopleBackupHelper; 41 import com.android.systemui.people.widget.PeopleTileKey; 42 43 import java.time.Duration; 44 import java.util.HashMap; 45 import java.util.Map; 46 import java.util.Set; 47 48 /** 49 * Follow-up job that runs after a Conversations widgets restore operation. Check if shortcuts that 50 * were not available before are now available. If any shortcut doesn't become available after 51 * 1 day, we clean up its storage. 52 */ 53 public class PeopleBackupFollowUpJob extends JobService { 54 private static final String TAG = "PeopleBackupFollowUpJob"; 55 private static final String START_DATE = "start_date"; 56 57 /** Follow-up job id. */ 58 public static final int JOB_ID = 74823873; 59 60 private static final long JOB_PERIODIC_DURATION = Duration.ofHours(6).toMillis(); 61 private static final long CLEAN_UP_STORAGE_AFTER_DURATION = Duration.ofHours(48).toMillis(); 62 63 /** SharedPreferences file name for follow-up specific storage.*/ 64 public static final String SHARED_FOLLOW_UP = "shared_follow_up"; 65 66 private final Object mLock = new Object(); 67 private Context mContext; 68 private PackageManager mPackageManager; 69 private IPeopleManager mIPeopleManager; 70 private JobScheduler mJobScheduler; 71 72 /** Schedules a PeopleBackupFollowUpJob every 2 hours. */ scheduleJob(Context context)73 public static void scheduleJob(Context context) { 74 JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 75 PersistableBundle bundle = new PersistableBundle(); 76 bundle.putLong(START_DATE, System.currentTimeMillis()); 77 JobInfo jobInfo = new JobInfo 78 .Builder(JOB_ID, new ComponentName(context, PeopleBackupFollowUpJob.class)) 79 .setPeriodic(JOB_PERIODIC_DURATION) 80 .setExtras(bundle) 81 .build(); 82 jobScheduler.schedule(jobInfo); 83 } 84 85 @Override onCreate()86 public void onCreate() { 87 super.onCreate(); 88 mContext = getApplicationContext(); 89 mPackageManager = getApplicationContext().getPackageManager(); 90 mIPeopleManager = IPeopleManager.Stub.asInterface( 91 ServiceManager.getService(Context.PEOPLE_SERVICE)); 92 mJobScheduler = mContext.getSystemService(JobScheduler.class); 93 94 } 95 96 /** Sets necessary managers for testing. */ 97 @VisibleForTesting setManagers(Context context, PackageManager packageManager, IPeopleManager iPeopleManager, JobScheduler jobScheduler)98 public void setManagers(Context context, PackageManager packageManager, 99 IPeopleManager iPeopleManager, JobScheduler jobScheduler) { 100 mContext = context; 101 mPackageManager = packageManager; 102 mIPeopleManager = iPeopleManager; 103 mJobScheduler = jobScheduler; 104 } 105 106 @Override onStartJob(JobParameters params)107 public boolean onStartJob(JobParameters params) { 108 if (DEBUG) Log.d(TAG, "Starting job."); 109 synchronized (mLock) { 110 SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(this); 111 SharedPreferences.Editor editor = sp.edit(); 112 SharedPreferences followUp = this.getSharedPreferences( 113 SHARED_FOLLOW_UP, Context.MODE_PRIVATE); 114 SharedPreferences.Editor followUpEditor = followUp.edit(); 115 116 // Remove from SHARED_FOLLOW_UP storage all widgets that are now ready to be updated. 117 Map<String, Set<String>> remainingWidgets = 118 processFollowUpFile(followUp, followUpEditor); 119 120 // Check if all widgets were restored or if enough time elapsed to cancel the job. 121 long start = params.getExtras().getLong(START_DATE); 122 long now = System.currentTimeMillis(); 123 if (shouldCancelJob(remainingWidgets, start, now)) { 124 cancelJobAndClearRemainingWidgets(remainingWidgets, followUpEditor, sp); 125 } 126 127 editor.apply(); 128 followUpEditor.apply(); 129 } 130 131 // Ensure all widgets modified from SHARED_FOLLOW_UP storage are now updated. 132 PeopleBackupHelper.updateWidgets(mContext); 133 return false; 134 } 135 136 /** 137 * Iterates through follow-up file entries and checks which shortcuts are now available. 138 * Returns a map of shortcuts that should be checked at a later time. 139 */ processFollowUpFile(SharedPreferences followUp, SharedPreferences.Editor followUpEditor)140 public Map<String, Set<String>> processFollowUpFile(SharedPreferences followUp, 141 SharedPreferences.Editor followUpEditor) { 142 Map<String, Set<String>> remainingWidgets = new HashMap<>(); 143 Map<String, ?> all = followUp.getAll(); 144 for (Map.Entry<String, ?> entry : all.entrySet()) { 145 String key = entry.getKey(); 146 147 PeopleTileKey peopleTileKey = PeopleTileKey.fromString(key); 148 boolean restored = isReadyForRestore(mIPeopleManager, mPackageManager, peopleTileKey); 149 if (restored) { 150 if (DEBUG) Log.d(TAG, "Removing key from follow-up: " + key); 151 followUpEditor.remove(key); 152 continue; 153 } 154 155 if (DEBUG) Log.d(TAG, "Key should not be restored yet, try later: " + key); 156 try { 157 remainingWidgets.put(entry.getKey(), (Set<String>) entry.getValue()); 158 } catch (Exception e) { 159 Log.e(TAG, "Malformed entry value: " + entry.getValue()); 160 } 161 } 162 return remainingWidgets; 163 } 164 165 /** Returns whether all shortcuts were restored or if enough time elapsed to cancel the job. */ shouldCancelJob(Map<String, Set<String>> remainingWidgets, long start, long now)166 public boolean shouldCancelJob(Map<String, Set<String>> remainingWidgets, 167 long start, long now) { 168 if (remainingWidgets.isEmpty()) { 169 if (DEBUG) Log.d(TAG, "All widget storage was successfully restored."); 170 return true; 171 } 172 173 boolean oneDayHasPassed = (now - start) > CLEAN_UP_STORAGE_AFTER_DURATION; 174 if (oneDayHasPassed) { 175 if (DEBUG) { 176 Log.w(TAG, "One or more widgets were not properly restored, " 177 + "but cancelling job because it has been a day."); 178 } 179 return true; 180 } 181 if (DEBUG) Log.d(TAG, "There are still non-restored widgets, run job again."); 182 return false; 183 } 184 185 /** Cancels job and removes storage of any shortcut that was not restored. */ cancelJobAndClearRemainingWidgets(Map<String, Set<String>> remainingWidgets, SharedPreferences.Editor followUpEditor, SharedPreferences sp)186 public void cancelJobAndClearRemainingWidgets(Map<String, Set<String>> remainingWidgets, 187 SharedPreferences.Editor followUpEditor, SharedPreferences sp) { 188 if (DEBUG) Log.d(TAG, "Cancelling follow up job."); 189 removeUnavailableShortcutsFromSharedStorage(remainingWidgets, sp); 190 followUpEditor.clear(); 191 mJobScheduler.cancel(JOB_ID); 192 } 193 removeUnavailableShortcutsFromSharedStorage(Map<String, Set<String>> remainingWidgets, SharedPreferences sp)194 private void removeUnavailableShortcutsFromSharedStorage(Map<String, 195 Set<String>> remainingWidgets, SharedPreferences sp) { 196 for (Map.Entry<String, Set<String>> entry : remainingWidgets.entrySet()) { 197 PeopleTileKey peopleTileKey = PeopleTileKey.fromString(entry.getKey()); 198 if (!PeopleTileKey.isValid(peopleTileKey)) { 199 Log.e(TAG, "Malformed peopleTileKey in follow-up file: " + entry.getKey()); 200 continue; 201 } 202 Set<String> widgetIds; 203 try { 204 widgetIds = (Set<String>) entry.getValue(); 205 } catch (Exception e) { 206 Log.e(TAG, "Malformed widget ids in follow-up file: " + e); 207 continue; 208 } 209 for (String id : widgetIds) { 210 int widgetId; 211 try { 212 widgetId = Integer.parseInt(id); 213 } catch (NumberFormatException ex) { 214 Log.e(TAG, "Malformed widget id in follow-up file: " + ex); 215 continue; 216 } 217 218 String contactUriString = sp.getString(String.valueOf(widgetId), EMPTY_STRING); 219 removeSharedPreferencesStorageForTile( 220 mContext, peopleTileKey, widgetId, contactUriString); 221 } 222 } 223 } 224 225 @Override onStopJob(JobParameters params)226 public boolean onStopJob(JobParameters params) { 227 return false; 228 } 229 } 230