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