• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.server.appsearch.contactsindexer;
18 
19 import android.annotation.NonNull;
20 import android.annotation.UserIdInt;
21 import android.app.appsearch.annotation.CanIgnoreReturnValue;
22 import android.app.appsearch.util.LogUtil;
23 import android.app.job.JobInfo;
24 import android.app.job.JobParameters;
25 import android.app.job.JobScheduler;
26 import android.app.job.JobService;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.os.CancellationSignal;
30 import android.os.PersistableBundle;
31 import android.os.UserHandle;
32 import android.util.Log;
33 import android.util.Slog;
34 import android.util.SparseArray;
35 
36 
37 import com.android.internal.annotations.GuardedBy;
38 import com.android.internal.annotations.VisibleForTesting;
39 import com.android.server.LocalManagerRegistry;
40 import com.android.server.SystemService;
41 
42 import java.util.Objects;
43 import java.util.concurrent.Executor;
44 import java.util.concurrent.LinkedBlockingQueue;
45 import java.util.concurrent.ThreadPoolExecutor;
46 import java.util.concurrent.TimeUnit;
47 
48 public class ContactsIndexerMaintenanceService extends JobService {
49     private static final String TAG = "ContactsIndexerMaintena";
50 
51     /**
52      * Generate job ids in the range (MIN_INDEXER_JOB_ID, MAX_INDEXER_JOB_ID) to avoid conflicts
53      * with other jobs scheduled by the system service. The range corresponds to 21475 job ids,
54      * which is the maximum number of user ids in the system.
55      *
56      * @see com.android.server.pm.UserManagerService#MAX_USER_ID
57      */
58     public static final int MIN_INDEXER_JOB_ID = 16942831; // corresponds to ag/16942831
59     private static final int MAX_INDEXER_JOB_ID = 16964306; // 16942831 + 21475
60 
61     private static final String EXTRA_USER_ID = "user_id";
62 
63     private static final Executor EXECUTOR = new ThreadPoolExecutor(/*corePoolSize=*/ 1,
64             /*maximumPoolSize=*/ 1, /*keepAliveTime=*/ 60L, TimeUnit.SECONDS,
65             new LinkedBlockingQueue<>());
66 
67     /**
68      * A mapping of userId-to-CancellationSignal. Since we schedule a separate job for each user,
69      * this JobService might be executing simultaneously for the various users, so we need to keep
70      * track of the cancellation signal for each user update so we stop the appropriate update
71      * when necessary.
72      */
73     @GuardedBy("mSignals")
74     private final SparseArray<CancellationSignal> mSignals = new SparseArray<>();
75 
76     /**
77      * Schedules a full update job for the given device-user.
78      *
79      * @param userId Device user id for whom the full update job should be scheduled.
80      * @param periodic True to indicate that the job should be repeated.
81      * @param intervalMillis Millisecond interval for which this job should repeat.
82      */
scheduleFullUpdateJob(Context context, @UserIdInt int userId, boolean periodic, long intervalMillis)83     static void scheduleFullUpdateJob(Context context, @UserIdInt int userId,
84             boolean periodic, long intervalMillis) {
85         int jobId = getJobIdForUser(userId);
86         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
87         ComponentName component =
88                 new ComponentName(context, ContactsIndexerMaintenanceService.class);
89         final PersistableBundle extras = new PersistableBundle();
90         extras.putInt(EXTRA_USER_ID, userId);
91         JobInfo.Builder jobInfoBuilder =
92                 new JobInfo.Builder(jobId, component)
93                         .setExtras(extras)
94                         .setRequiresBatteryNotLow(true)
95                         .setRequiresDeviceIdle(true)
96                         .setPersisted(true);
97 
98         if (periodic) {
99             // Specify a flex value of 1/2 the interval so that the job is scheduled to run
100             // in the [interval/2, interval) time window, assuming the other conditions are
101             // met. This avoids the scenario where the next full-update job is started within
102             // a short duration of the previous run.
103             jobInfoBuilder.setPeriodic(intervalMillis, /*flexMillis=*/ intervalMillis/2);
104         }
105         JobInfo jobInfo = jobInfoBuilder.build();
106         JobInfo pendingJobInfo = jobScheduler.getPendingJob(jobId);
107         // Don't reschedule a pending job if the parameters haven't changed.
108         if (jobInfo.equals(pendingJobInfo)) {
109             return;
110         }
111         jobScheduler.schedule(jobInfo);
112         if (LogUtil.DEBUG) {
113             Log.v(TAG, "Scheduled full update job " + jobId + " for user " + userId);
114         }
115     }
116 
117     /**
118      * Cancel full update job for the given user.
119      *
120      * @param userId The user id for whom the full update job needs to be cancelled.
121      */
cancelFullUpdateJob(@onNull Context context, @UserIdInt int userId)122     private static void cancelFullUpdateJob(@NonNull Context context, @UserIdInt int userId) {
123         Objects.requireNonNull(context);
124         int jobId = getJobIdForUser(userId);
125         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
126         jobScheduler.cancel(jobId);
127         if (LogUtil.DEBUG) {
128             Log.v(TAG, "Canceled full update job " + jobId + " for user " + userId);
129         }
130     }
131 
132     /**
133      * Check if a full update job is scheduled for the given user.
134      *
135      * @param userId The user id for whom the check for scheduled job needs to be performed
136      *
137      * @return true if a scheduled job exists
138      */
isFullUpdateJobScheduled(@onNull Context context, @UserIdInt int userId)139     public static boolean isFullUpdateJobScheduled(@NonNull Context context,
140             @UserIdInt int userId) {
141         Objects.requireNonNull(context);
142         int jobId = getJobIdForUser(userId);
143         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
144         return jobScheduler.getPendingJob(jobId) != null;
145     }
146 
147     /**
148      * Cancel any scheduled full update job for the given user. Checks if a full update job for the
149      * given user exists before trying to cancel it.
150      *
151      * @param user The user for whom the full update job needs to be cancelled.
152      */
cancelFullUpdateJobIfScheduled(@onNull Context context, UserHandle user)153     public static void cancelFullUpdateJobIfScheduled(@NonNull Context context, UserHandle user) {
154         try {
155             if (isFullUpdateJobScheduled(context, user.getIdentifier())) {
156                 cancelFullUpdateJob(context, user.getIdentifier());
157             }
158         } catch (RuntimeException e) {
159             Log.e(TAG, "Failed to cancel pending full update job ", e);
160         }
161     }
162 
getJobIdForUser(int userId)163     private static int getJobIdForUser(int userId) {
164         return MIN_INDEXER_JOB_ID + userId;
165     }
166 
167     @Override
onStartJob(JobParameters params)168     public boolean onStartJob(JobParameters params) {
169         try {
170             int userId = params.getExtras().getInt(EXTRA_USER_ID, /*defaultValue=*/ -1);
171             if (userId == -1) {
172                 return false;
173             }
174 
175             if (LogUtil.DEBUG) {
176                 Log.v(TAG, "Full update job started for user " + userId);
177             }
178             final CancellationSignal oldSignal;
179             synchronized (mSignals) {
180                 oldSignal = mSignals.get(userId);
181             }
182             if (oldSignal != null) {
183                 // This could happen if we attempt to schedule a new job for the user while there's
184                 // one already running.
185                 Log.w(TAG, "Old update job still running for user " + userId);
186                 oldSignal.cancel();
187             }
188             final CancellationSignal signal = new CancellationSignal();
189             synchronized (mSignals) {
190                 mSignals.put(userId, signal);
191             }
192             EXECUTOR.execute(() -> doFullUpdateForUser(this, params, userId, signal));
193             return true;
194         } catch (RuntimeException e) {
195             Slog.wtf(TAG, "ContactsIndexerMaintenanceService.onStartJob() failed ", e);
196             return false;
197         }
198     }
199 
200     /**
201      * Triggers full update from a background job for the given device-user using
202      * {@link ContactsIndexerManagerService.LocalService} manager.
203      *
204      * @param params Parameters from the job that triggered the full update.
205      * @param userId Device user id for whom the full update job should be triggered.
206      * @param signal Used to indicate if the full update task should be cancelled.
207      * @return A boolean representing whether the update operation
208      * completed or encountered an issue. This return value is only used for testing purposes.
209      */
210     @VisibleForTesting
211     @CanIgnoreReturnValue
doFullUpdateForUser(Context context, JobParameters params, int userId, CancellationSignal signal)212     protected boolean doFullUpdateForUser(Context context, JobParameters params, int userId,
213             CancellationSignal signal) {
214         try {
215             ContactsIndexerManagerService.LocalService service =
216                     LocalManagerRegistry.getManager(
217                             ContactsIndexerManagerService.LocalService.class);
218             if (service == null) {
219                 Log.e(TAG, "Background job failed to trigger FullUpdate because "
220                         + "ContactsIndexerManagerService.LocalService is not available.");
221                 // If a background full update job exists while ContactsIndexer is disabled, cancel
222                 // the job after its first run. This will prevent any periodic jobs from being
223                 // unnecessarily triggered repeatedly. If the service is null, it means the contacts
224                 // indexer is disabled. So the local service is not registered during the startup.
225                 cancelFullUpdateJob(context, userId);
226                 return false;
227             }
228             service.doFullUpdateForUser(userId, signal);
229         } catch (RuntimeException e) {
230             Log.e(TAG, "Background job failed to trigger FullUpdate because ", e);
231             return false;
232         } finally {
233             jobFinished(params, signal.isCanceled());
234             synchronized (mSignals) {
235                 if (signal == mSignals.get(userId)) {
236                     mSignals.remove(userId);
237                 }
238             }
239         }
240         return true;
241     }
242 
243     @Override
onStopJob(JobParameters params)244     public boolean onStopJob(JobParameters params) {
245         try {
246             final int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue */ -1);
247             if (userId == -1) {
248                 return false;
249             }
250             // This will only run on S+ builds, so no need to do a version check.
251             if (LogUtil.DEBUG) {
252                 Log.d(TAG,
253                         "Stopping update job for user " + userId + " because "
254                                 + params.getStopReason());
255             }
256             synchronized (mSignals) {
257                 final CancellationSignal signal = mSignals.get(userId);
258                 if (signal != null) {
259                     signal.cancel();
260                     mSignals.remove(userId);
261                     // We had to stop the job early. Request reschedule.
262                     return true;
263                 }
264             }
265             Log.e(TAG, "JobScheduler stopped an update that wasn't happening...");
266             return false;
267         } catch (RuntimeException e) {
268             Slog.wtf(TAG, "ContactsIndexerMaintenanceService.onStopJob() failed ", e);
269             return false;
270         }
271     }
272 }
273