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