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