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