• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.indexer;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.appsearch.AppSearchEnvironmentFactory;
22 import android.app.appsearch.annotation.CanIgnoreReturnValue;
23 import android.app.appsearch.util.LogUtil;
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.content.ComponentName;
29 import android.content.Context;
30 import android.os.CancellationSignal;
31 import android.os.PersistableBundle;
32 import android.os.UserHandle;
33 import android.util.ArrayMap;
34 import android.util.Log;
35 import android.util.Slog;
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.appsearch.contactsindexer.ContactsIndexerMaintenanceService;
41 import com.android.server.appsearch.indexer.IndexerMaintenanceConfig.IndexerType;
42 
43 import java.util.Map;
44 import java.util.Objects;
45 import java.util.concurrent.Executor;
46 import java.util.concurrent.LinkedBlockingQueue;
47 import java.util.concurrent.TimeUnit;
48 
49 /** Dispatches maintenance tasks for various indexers. */
50 public class IndexerMaintenanceService extends JobService {
51     private static final String TAG = "AppSearchIndexerMainten";
52 
53     @VisibleForTesting
54     public static final String EXTRA_USER_ID = "user_id";
55 
56     @VisibleForTesting
57     public static final String INDEXER_TYPE = "indexer_type";
58 
59     /**
60      * A mapping of userHandle-to-CancellationSignal. Since we schedule a separate job for each
61      * user, this JobService might be executing simultaneously for the various users, so we need to
62      * keep track of the cancellation signal for each user update so we stop the appropriate update
63      * when necessary.
64      */
65     @GuardedBy("mSignals")
66     private final Map<UserHandle, CancellationSignal> mSignals = new ArrayMap<>();
67 
68     private final Executor mExecutor =
69             AppSearchEnvironmentFactory.getEnvironmentInstance()
70                     .createExecutorService(
71                             /* corePoolSize= */ 1,
72                             /* maximumPoolSize= */ 1,
73                             /* keepAliveTime= */ 60L,
74                             /* unit= */ TimeUnit.SECONDS,
75                             /* workQueue= */ new LinkedBlockingQueue<>(),
76                             /* priority= */ 0); // priority is unused.
77 
78     /**
79      * Schedules an update job for the given device-user.
80      *
81      * @param userHandle Device user handle for whom the update job should be scheduled.
82      * @param periodic True to indicate that the job should be repeated.
83      * @param indexerType Indicates which {@link IndexerType} to schedule an update for.
84      * @param intervalMillis Millisecond interval for which this job should repeat.
85      */
scheduleUpdateJob( @onNull Context context, @NonNull UserHandle userHandle, @IndexerType int indexerType, boolean periodic, long intervalMillis)86     public static void scheduleUpdateJob(
87             @NonNull Context context,
88             @NonNull UserHandle userHandle,
89             @IndexerType int indexerType,
90             boolean periodic,
91             long intervalMillis) {
92         Objects.requireNonNull(context);
93         Objects.requireNonNull(userHandle);
94         JobInfo jobInfo = createJobInfo(context, userHandle, indexerType, periodic, intervalMillis);
95         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
96         JobInfo pendingJobInfo = jobScheduler.getPendingJob(jobInfo.getId());
97         // Don't reschedule a pending job if the parameters haven't changed.
98         if (jobInfo.equals(pendingJobInfo)) {
99             return;
100         }
101         jobScheduler.schedule(jobInfo);
102         if (LogUtil.DEBUG) {
103             Log.v(TAG, "Scheduled update job " + jobInfo.getId() + " for user " + userHandle);
104         }
105     }
106 
107     /**
108      * Creates a {@link JobInfo} with the given parameters.
109      */
110     @VisibleForTesting
createJobInfo( @onNull Context context, @NonNull UserHandle userHandle, @IndexerType int indexerType, boolean periodic, long intervalMillis)111     public static JobInfo createJobInfo(
112             @NonNull Context context,
113             @NonNull UserHandle userHandle,
114             @IndexerType int indexerType,
115             boolean periodic,
116             long intervalMillis) {
117         int jobId = getJobIdForUser(userHandle, indexerType);
118         // For devices U and below, we have to schedule using ContactsIndexerMaintenanceService
119         // as it has the proper permissions in core/res/AndroidManifest.xml.
120         // IndexerMaintenanceService does not have the proper permissions on U. For simplicity, we
121         // can also use the same component for scheduling maintenance on U+.
122         ComponentName component =
123                 new ComponentName(context, ContactsIndexerMaintenanceService.class);
124         final PersistableBundle extras = new PersistableBundle();
125         extras.putInt(EXTRA_USER_ID, userHandle.getIdentifier());
126         extras.putInt(INDEXER_TYPE, indexerType);
127         JobInfo.Builder jobInfoBuilder =
128                 new JobInfo.Builder(jobId, component)
129                         .setExtras(extras)
130                         .setRequiresBatteryNotLow(true)
131                         .setRequiresDeviceIdle(true)
132                         .setPersisted(true);
133         if (periodic) {
134             // Specify a flex value of 1/2 the interval so that the job is scheduled to run
135             // in the [interval/2, interval) time window, assuming the other conditions are
136             // met. This avoids the scenario where the next update job is started within
137             // a short duration of the previous run.
138             jobInfoBuilder.setPeriodic(intervalMillis, /* flexMillis= */ intervalMillis / 2);
139         }
140         return jobInfoBuilder.build();
141     }
142 
143     /**
144      * Cancel update job for the given user.
145      *
146      * @param userHandle The user handle for whom the update job needs to be cancelled.
147      */
cancelUpdateJob( @onNull Context context, @NonNull UserHandle userHandle, @IndexerType int indexerType)148     private static void cancelUpdateJob(
149             @NonNull Context context,
150             @NonNull UserHandle userHandle,
151             @IndexerType int indexerType) {
152         Objects.requireNonNull(context);
153         Objects.requireNonNull(userHandle);
154         int jobId = getJobIdForUser(userHandle, indexerType);
155         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
156         jobScheduler.cancel(jobId);
157         if (LogUtil.DEBUG) {
158             Log.v(TAG, "Canceled update job " + jobId + " for user " + userHandle);
159         }
160     }
161 
162     /**
163      * Check if a update job is scheduled for the given user.
164      *
165      * @param userHandle The user handle for whom the check for scheduled job needs to be performed
166      * @return true if a scheduled job exists
167      */
isUpdateJobScheduled( @onNull Context context, @NonNull UserHandle userHandle, @IndexerType int indexerType)168     public static boolean isUpdateJobScheduled(
169             @NonNull Context context,
170             @NonNull UserHandle userHandle,
171             @IndexerType int indexerType) {
172         Objects.requireNonNull(context);
173         Objects.requireNonNull(userHandle);
174         int jobId = getJobIdForUser(userHandle, indexerType);
175         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
176         return jobScheduler.getPendingJob(jobId) != null;
177     }
178 
179     /**
180      * Check if an update job is scheduled for the given user with the expected parameters.
181      *
182      * @param userHandle The user handle for whom the check for scheduled job needs to be performed
183      * @return true if a scheduled job exists with the expected parameters
184      */
isUpdateJobScheduledWithExpectedParams( @onNull Context context, @NonNull UserHandle userHandle, @IndexerType int indexerType, long intervalMillis)185     public static boolean isUpdateJobScheduledWithExpectedParams(
186             @NonNull Context context,
187             @NonNull UserHandle userHandle,
188             @IndexerType int indexerType,
189             long intervalMillis) {
190         Objects.requireNonNull(context);
191         Objects.requireNonNull(userHandle);
192         int jobId = getJobIdForUser(userHandle, indexerType);
193         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
194         JobInfo jobInfo = jobScheduler.getPendingJob(jobId);
195         if (jobInfo == null) {
196             return false;
197         }
198         JobInfo periodicJobInfo = createJobInfo(context, userHandle, indexerType, /* periodic= */
199                 true, intervalMillis);
200         JobInfo immediateJobInfo = createJobInfo(context, userHandle, indexerType, /* periodic= */
201                 false, /* intervalMillis= */ -1);
202         return jobInfo.equals(periodicJobInfo) || jobInfo.equals(immediateJobInfo);
203     }
204 
205     /**
206      * Cancel any scheduled update job for the given user. Checks if a update job for the given user
207      * exists before trying to cancel it.
208      *
209      * @param user The user for whom the update job needs to be cancelled.
210      */
cancelUpdateJobIfScheduled( @onNull Context context, @NonNull UserHandle user, @IndexerType int indexerType)211     public static void cancelUpdateJobIfScheduled(
212             @NonNull Context context, @NonNull UserHandle user, @IndexerType int indexerType) {
213         Objects.requireNonNull(context);
214         Objects.requireNonNull(user);
215         try {
216             if (isUpdateJobScheduled(context, user, indexerType)) {
217                 cancelUpdateJob(context, user, indexerType);
218             }
219         } catch (RuntimeException e) {
220             Log.e(TAG, "Failed to cancel pending update job ", e);
221         }
222     }
223 
224     /**
225      * Generate job ids in the range (MIN_INDEXER_JOB_ID, MAX_INDEXER_JOB_ID) to avoid conflicts
226      * with other jobs scheduled by the system service. The range corresponds to 21475 job ids,
227      * which is the maximum number of user ids in the system.
228      *
229      * @see com.android.server.pm.UserManagerService#MAX_USER_ID
230      */
getJobIdForUser( @onNull UserHandle userHandle, @IndexerType int indexerType)231     private static int getJobIdForUser(
232             @NonNull UserHandle userHandle, @IndexerType int indexerType) {
233         Objects.requireNonNull(userHandle);
234         int baseJobId = IndexerMaintenanceConfig.getConfigForIndexer(indexerType).getMinJobId();
235         return baseJobId + userHandle.getIdentifier();
236     }
237 
238     @Override
onStartJob(JobParameters params)239     public boolean onStartJob(JobParameters params) {
240         try {
241             int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue= */ -1);
242             if (userId == -1) {
243                 return false;
244             }
245 
246             if (LogUtil.DEBUG) {
247                 // If the job parameters is missing INDEXER_TYPE, then this job was scheduled on
248                 // a previous version before INDEXER_TYPE was introduced, and therefore the job
249                 // must be for contacts indexer.
250                 @IndexerType int indexerType = params.getExtras().getInt(
251                         INDEXER_TYPE, /* defaultValue= */
252                         IndexerMaintenanceConfig.CONTACTS_INDEXER);
253                 Log.v(TAG, "Update job started for user " + userId + " and indexer type "
254                         + indexerType);
255             }
256 
257             UserHandle userHandle = UserHandle.getUserHandleForUid(userId);
258             final CancellationSignal oldSignal;
259             synchronized (mSignals) {
260                 oldSignal = mSignals.get(userHandle);
261             }
262             if (oldSignal != null) {
263                 // This could happen if we attempt to schedule a new job for the user while there's
264                 // one already running.
265                 Log.w(TAG, "Old update job still running for user " + userHandle);
266                 oldSignal.cancel();
267             }
268             final CancellationSignal signal = new CancellationSignal();
269             synchronized (mSignals) {
270                 mSignals.put(userHandle, signal);
271             }
272             mExecutor.execute(() -> doUpdateForUser(this, params, userHandle, signal));
273             return true;
274         } catch (RuntimeException e) {
275             Slog.wtf(TAG, "IndexerMaintenanceService.onStartJob() failed ", e);
276             return false;
277         }
278     }
279 
280     /**
281      * Triggers update from a background job for the given device-user using {@link
282      * ContactsIndexerManagerService.LocalService} manager.
283      *
284      * @param params Parameters from the job that triggered the update.
285      * @param userHandle Device user handle for whom the update job should be triggered.
286      * @param signal Used to indicate if the update task should be cancelled.
287      * @return A boolean representing whether the update operation completed or encountered an
288      *     issue. This return value is only used for testing purposes.
289      */
290     @VisibleForTesting
291     @CanIgnoreReturnValue
doUpdateForUser( @onNull Context context, @Nullable JobParameters params, @NonNull UserHandle userHandle, @NonNull CancellationSignal signal)292     public boolean doUpdateForUser(
293             @NonNull Context context,
294             @Nullable JobParameters params,
295             @NonNull UserHandle userHandle,
296             @NonNull CancellationSignal signal) {
297         try {
298             Objects.requireNonNull(context);
299             Objects.requireNonNull(userHandle);
300             Objects.requireNonNull(signal);
301 
302             // If the job parameters is missing INDEXER_TYPE, then this job was scheduled on a
303             // previous version before INDEXER_TYPE was introduced, and therefore the job must be
304             // for contacts indexer.
305             @IndexerType int indexerType = params.getExtras().getInt(INDEXER_TYPE,
306                     IndexerMaintenanceConfig.CONTACTS_INDEXER);
307             Class<? extends IndexerLocalService> indexerLocalService =
308                     IndexerMaintenanceConfig.getConfigForIndexer(indexerType).getLocalService();
309             IndexerLocalService service = LocalManagerRegistry.getManager(indexerLocalService);
310             if (service == null) {
311                 Log.e(
312                         TAG,
313                         "Background job failed to trigger Update because "
314                                 + "Indexer.LocalService is not available.");
315                 // If a background update job exists while an indexer is disabled, cancel the
316                 // job after its first run. This will prevent any periodic jobs from being
317                 // unnecessarily triggered repeatedly. If the service is null, it means the indexer
318                 // is disabled. So the local service is not registered during the startup.
319                 cancelUpdateJob(context, userHandle, indexerType);
320                 return false;
321             }
322             service.doUpdateForUser(userHandle, signal);
323         } catch (RuntimeException e) {
324             Log.e(TAG, "Background job failed to trigger Update because ", e);
325             return false;
326         } finally {
327             jobFinished(params, signal.isCanceled());
328             synchronized (mSignals) {
329                 if (signal == mSignals.get(userHandle)) {
330                     mSignals.remove(userHandle);
331                 }
332             }
333         }
334         return true;
335     }
336 
337     @Override
onStopJob(JobParameters params)338     public boolean onStopJob(JobParameters params) {
339         try {
340             final int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue */ -1);
341             if (userId == -1) {
342                 return false;
343             }
344             UserHandle userHandle = UserHandle.getUserHandleForUid(userId);
345             // This will only run on S+ builds, so no need to do a version check.
346             if (LogUtil.DEBUG) {
347                 Log.d(
348                         TAG,
349                         "Stopping update job for user "
350                                 + userId
351                                 + " because "
352                                 + params.getStopReason());
353             }
354             synchronized (mSignals) {
355                 final CancellationSignal signal = mSignals.get(userHandle);
356                 if (signal != null) {
357                     signal.cancel();
358                     mSignals.remove(userHandle);
359                     // We had to stop the job early. Request reschedule.
360                     return true;
361                 }
362             }
363             Log.e(TAG, "JobScheduler stopped an update that wasn't happening...");
364             return false;
365         } catch (RuntimeException e) {
366             Slog.wtf(TAG, "IndexerMaintenanceService.onStopJob() failed ", e);
367             return false;
368         }
369     }
370 }
371