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