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;
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.util.Log;
32 import android.util.Slog;
33 import android.util.SparseArray;
34 
35 import com.android.internal.annotations.GuardedBy;
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.server.LocalManagerRegistry;
38 
39 import java.util.Objects;
40 import java.util.concurrent.Executor;
41 import java.util.concurrent.Executors;
42 
43 public class AppSearchMaintenanceService extends JobService {
44     private static final String TAG = "AppSearchMaintenanceSer";
45 
46     private static final Executor EXECUTOR = Executors.newSingleThreadExecutor();
47     private static final String EXTRA_USER_ID = "user_id";
48 
49     /**
50      * Generate job ids in the range (MIN_APPSEARCH_MAINTENANCE_JOB_ID,
51      * MIN_APPSEARCH_MAINTENANCE_JOB_ID + MAX_USER_ID) to avoid conflicts with other jobs scheduled
52      * by the system service. The range corresponds to 21475 job ids, which is the maximum number of
53      * user ids in the system.
54      *
55      * @see com.android.server.pm.UserManagerService#MAX_USER_ID
56      */
57     public static final int MIN_APPSEARCH_MAINTENANCE_JOB_ID = 461234957; // 0x1B7DE30D
58 
59     /**
60      * A mapping of userId-to-CancellationSignal. Since we schedule a separate job for each user,
61      * this JobService might be executing simultaneously for the various users, so we need to keep
62      * track of the cancellation signal for each user update so we stop the appropriate update when
63      * necessary.
64      */
65     @GuardedBy("mSignalsLocked")
66     private final SparseArray<CancellationSignal> mSignalsLocked = new SparseArray<>();
67 
68     /**
69      * Schedule the daily fully persist job for the given user.
70      *
71      * <p>The job will persists all pending mutation operation to disk.
72      */
scheduleFullyPersistJob( @onNull Context context, @UserIdInt int userId, long intervalMillis)73     static void scheduleFullyPersistJob(
74             @NonNull Context context, @UserIdInt int userId, long intervalMillis) {
75         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
76 
77         final PersistableBundle extras = new PersistableBundle();
78         extras.putInt(EXTRA_USER_ID, userId);
79         JobInfo jobInfo =
80                 new JobInfo.Builder(
81                                 MIN_APPSEARCH_MAINTENANCE_JOB_ID
82                                         + userId, // must be unique across uid
83                                 new ComponentName(context, AppSearchMaintenanceService.class))
84                         .setPeriodic(intervalMillis) // run once a day, at most
85                         .setExtras(extras)
86                         .setPersisted(true) // persist across reboots
87                         .setRequiresBatteryNotLow(true)
88                         .setRequiresCharging(true)
89                         .setRequiresDeviceIdle(true)
90                         .build();
91         jobScheduler.schedule(jobInfo);
92         if (LogUtil.DEBUG) {
93             Log.v(TAG, "Scheduling the daily AppSearch full persist job");
94         }
95     }
96 
97     @Override
onStartJob(JobParameters params)98     public boolean onStartJob(JobParameters params) {
99         try {
100             int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue= */ -1);
101             if (userId == -1) {
102                 return false;
103             }
104 
105             final CancellationSignal signal;
106             synchronized (mSignalsLocked) {
107                 CancellationSignal oldSignal = mSignalsLocked.get(userId);
108                 if (oldSignal != null) {
109                     // This could happen if we attempt to schedule a new job for the user while
110                     // there's
111                     // one already running.
112                     Log.w(TAG, "Old maintenance job still running for user " + userId);
113                     oldSignal.cancel();
114                 }
115                 signal = new CancellationSignal();
116                 mSignalsLocked.put(userId, signal);
117             }
118             EXECUTOR.execute(() -> doFullyPersistJobForUser(this, params, userId, signal));
119             return true;
120         } catch (RuntimeException e) {
121             Slog.wtf(TAG, "AppSearchMaintenanceService.onStartJob() failed ", e);
122             return false;
123         }
124     }
125 
126     /** Triggers full persist job for the given user directly. */
127     @VisibleForTesting
128     @CanIgnoreReturnValue
doFullyPersistJobForUser( Context context, JobParameters params, int userId, CancellationSignal signal)129     protected boolean doFullyPersistJobForUser(
130             Context context, JobParameters params, int userId, CancellationSignal signal) {
131         try {
132             AppSearchManagerService.LocalService service =
133                     LocalManagerRegistry.getManager(AppSearchManagerService.LocalService.class);
134             if (service == null) {
135                 Log.e(
136                         TAG,
137                         "Background job failed to trigger Full persist because "
138                                 + "AppSearchManagerService.LocalService is not available.");
139                 // Cancel unnecessary background full persist job if AppSearch local service is not
140                 // registered
141                 cancelFullyPersistJobIfScheduled(context, userId);
142                 return false;
143             }
144             service.doFullyPersistForUser(userId);
145         } catch (Throwable t) {
146             Log.e(TAG, "Run Daily optimize job failed.", t);
147             jobFinished(params, /* wantsReschedule= */ true);
148             return false;
149         } finally {
150             jobFinished(params, /* wantsReschedule= */ false);
151             synchronized (mSignalsLocked) {
152                 if (signal == mSignalsLocked.get(userId)) {
153                     mSignalsLocked.remove(userId);
154                 }
155             }
156         }
157         return true;
158     }
159 
160     @Override
onStopJob(JobParameters params)161     public boolean onStopJob(JobParameters params) {
162         try {
163             final int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue */ -1);
164             if (userId == -1) {
165                 return false;
166             }
167             if (LogUtil.DEBUG) {
168                 Log.d(
169                         TAG,
170                         "AppSearch maintenance job is stopped; id="
171                                 + params.getJobId()
172                                 + ", reason="
173                                 + params.getStopReason());
174             }
175             synchronized (mSignalsLocked) {
176                 final CancellationSignal signal = mSignalsLocked.get(userId);
177                 if (signal != null) {
178                     signal.cancel();
179                     mSignalsLocked.remove(userId);
180                     // We had to stop the job early. Request reschedule.
181                     return true;
182                 }
183             }
184             Log.e(TAG, "JobScheduler stopped an update that wasn't happening...");
185             return false;
186         } catch (RuntimeException e) {
187             Slog.wtf(TAG, "AppSearchMaintenanceService.onStopJob() failed ", e);
188         }
189         return false;
190     }
191 
192     /**
193      * Cancel full persist job for the given user.
194      *
195      * @param userId The user id for whom the full persist job needs to be cancelled.
196      */
cancelFullyPersistJobIfScheduled( @onNull Context context, @UserIdInt int userId)197     public static void cancelFullyPersistJobIfScheduled(
198             @NonNull Context context, @UserIdInt int userId) {
199         Objects.requireNonNull(context);
200         int jobId = MIN_APPSEARCH_MAINTENANCE_JOB_ID + userId;
201         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
202         if (jobScheduler.getPendingJob(jobId) != null) {
203             jobScheduler.cancel(jobId);
204             if (LogUtil.DEBUG) {
205                 Log.v(TAG, "Canceled job " + jobId + " for user " + userId);
206             }
207         }
208     }
209 }
210