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.ondevicepersonalization.services.maintenance; 18 19 import static android.app.job.JobScheduler.RESULT_SUCCESS; 20 21 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON; 22 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_FAILED; 23 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SKIPPED; 24 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SUCCESSFUL; 25 import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.MAINTENANCE_TASK_JOB_ID; 26 27 import android.app.job.JobInfo; 28 import android.app.job.JobParameters; 29 import android.app.job.JobScheduler; 30 import android.app.job.JobService; 31 import android.content.ComponentName; 32 import android.content.Context; 33 34 import com.android.adservices.shared.spe.JobServiceConstants.JobSchedulingResultCode; 35 import com.android.internal.annotations.VisibleForTesting; 36 import com.android.ondevicepersonalization.internal.util.LoggerFactory; 37 import com.android.ondevicepersonalization.services.FlagsFactory; 38 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors; 39 import com.android.ondevicepersonalization.services.data.errors.AggregatedErrorCodesLogger; 40 import com.android.ondevicepersonalization.services.data.events.EventsDao; 41 import com.android.ondevicepersonalization.services.data.vendor.OnDevicePersonalizationVendorDataDao; 42 import com.android.ondevicepersonalization.services.manifest.AppManifestConfigHelper; 43 import com.android.ondevicepersonalization.services.statsd.joblogging.OdpJobServiceLogger; 44 45 import com.google.common.util.concurrent.FutureCallback; 46 import com.google.common.util.concurrent.Futures; 47 import com.google.common.util.concurrent.ListenableFuture; 48 49 import java.util.List; 50 51 /** JobService to handle the OnDevicePersonalization maintenance */ 52 public class OnDevicePersonalizationMaintenanceJobService extends JobService { 53 private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger(); 54 private static final String TAG = "OnDevicePersonalizationMaintenanceJobService"; 55 56 // Every 24hrs. 57 private static final long PERIOD_SECONDS = 86400; 58 59 // The maximum deletion timeframe is 63 days. 60 // Set parameter to 60 days to account for job scheduler delays. 61 private static final long MAXIMUM_DELETION_TIMEFRAME_MILLIS = 5184000000L; 62 private ListenableFuture<Void> mFuture; 63 64 /** Schedules a unique instance of OnDevicePersonalizationMaintenanceJobService to be run. */ 65 @JobSchedulingResultCode schedule(Context context, boolean forceSchedule)66 public static int schedule(Context context, boolean forceSchedule) { 67 JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); 68 if (!forceSchedule && jobScheduler.getPendingJob(MAINTENANCE_TASK_JOB_ID) != null) { 69 sLogger.d(TAG + ": Job is already scheduled. Doing nothing,"); 70 return SCHEDULING_RESULT_CODE_SKIPPED; 71 } 72 ComponentName serviceComponent = 73 new ComponentName(context, OnDevicePersonalizationMaintenanceJobService.class); 74 JobInfo.Builder builder = new JobInfo.Builder(MAINTENANCE_TASK_JOB_ID, serviceComponent); 75 76 // Constraints. 77 builder.setRequiresDeviceIdle(true); 78 builder.setRequiresBatteryNotLow(true); 79 builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE); 80 builder.setPeriodic(1000 * PERIOD_SECONDS); // JobScheduler uses Milliseconds. 81 // persist this job across boots 82 builder.setPersisted(true); 83 84 int schedulingResult = 85 jobScheduler.schedule(builder.build()) == RESULT_SUCCESS 86 ? SCHEDULING_RESULT_CODE_SUCCESSFUL 87 : SCHEDULING_RESULT_CODE_FAILED; 88 sLogger.d( 89 TAG + ": OnDevicePersonalizationMaintenanceJobService scheduling result is %s.", 90 schedulingResult == SCHEDULING_RESULT_CODE_SUCCESSFUL 91 ? "SCHEDULING_RESULT_CODE_SUCCESSFUL" 92 : "SCHEDULING_RESULT_CODE_FAILED"); 93 return schedulingResult; 94 } 95 deleteEventsAndQueries(Context context)96 private static void deleteEventsAndQueries(Context context) throws Exception { 97 EventsDao eventsDao = EventsDao.getInstance(context); 98 // Cleanup event and queries table. 99 eventsDao.deleteEventsAndQueries( 100 System.currentTimeMillis() - MAXIMUM_DELETION_TIMEFRAME_MILLIS); 101 } 102 103 @VisibleForTesting cleanupVendorData(Context context)104 static void cleanupVendorData(Context context) throws Exception { 105 List<ComponentName> services = 106 AppManifestConfigHelper.getOdpServices(context, /* enrolledOnly= */ true); 107 108 OnDevicePersonalizationVendorDataDao.deleteVendorTables(context, services); 109 deleteEventsAndQueries(context); 110 AggregatedErrorCodesLogger.cleanupErrorData(context); 111 } 112 113 @Override onStartJob(JobParameters params)114 public boolean onStartJob(JobParameters params) { 115 sLogger.d(TAG + ": onStartJob()"); 116 OdpJobServiceLogger.getInstance(this).recordOnStartJob(MAINTENANCE_TASK_JOB_ID); 117 if (FlagsFactory.getFlags().getGlobalKillSwitch()) { 118 sLogger.d(TAG + ": GlobalKillSwitch enabled, finishing job."); 119 return cancelAndFinishJob( 120 params, 121 AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON); 122 } 123 124 Context context = this; 125 126 // Reschedule jobs with SPE if it's enabled. Note scheduled jobs by this 127 // OnDevicePersonalizationMaintenanceJobService will be cancelled for the same job ID. 128 // 129 // Note the job without a flex period will execute immediately after rescheduling with the 130 // same ID. Therefore, ending the execution here and let it run in the new SPE job. 131 if (FlagsFactory.getFlags().getSpePilotJobEnabled()) { 132 sLogger.d( 133 "SPE is enabled. Reschedule OnDevicePersonalizationMaintenanceJobService with" 134 + " OnDevicePersonalizationMaintenanceJob."); 135 OnDevicePersonalizationMaintenanceJob.schedule(context); 136 return false; 137 } 138 139 mFuture = 140 Futures.submit( 141 () -> { 142 sLogger.d(TAG + ": Running maintenance job"); 143 try { 144 cleanupVendorData(context); 145 } catch (Exception e) { 146 sLogger.e(TAG + ": Failed to cleanup vendorData", e); 147 } 148 }, 149 OnDevicePersonalizationExecutors.getBackgroundExecutor()); 150 151 Futures.addCallback( 152 mFuture, 153 new FutureCallback<Void>() { 154 @Override 155 public void onSuccess(Void result) { 156 sLogger.d(TAG + ": Maintenance job completed."); 157 boolean wantsReschedule = false; 158 OdpJobServiceLogger.getInstance(context) 159 .recordJobFinished( 160 MAINTENANCE_TASK_JOB_ID, 161 /* isSuccessful= */ true, 162 wantsReschedule); 163 // Tell the JobScheduler that the job has completed and does not needs to be 164 // rescheduled. 165 jobFinished(params, wantsReschedule); 166 } 167 168 @Override 169 public void onFailure(Throwable t) { 170 sLogger.e(TAG + ": Failed to handle JobService: " + params.getJobId(), t); 171 boolean wantsReschedule = false; 172 OdpJobServiceLogger.getInstance(context) 173 .recordJobFinished( 174 MAINTENANCE_TASK_JOB_ID, 175 /* isSuccessful= */ false, 176 wantsReschedule); 177 // When failure, also tell the JobScheduler that the job has completed and 178 // does not need to be rescheduled. 179 jobFinished(params, wantsReschedule); 180 } 181 }, 182 OnDevicePersonalizationExecutors.getBackgroundExecutor()); 183 184 return true; 185 } 186 187 @Override onStopJob(JobParameters params)188 public boolean onStopJob(JobParameters params) { 189 if (mFuture != null) { 190 mFuture.cancel(true); 191 } 192 // Reschedule the job since it ended before finishing 193 boolean wantsReschedule = true; 194 OdpJobServiceLogger.getInstance(this) 195 .recordOnStopJob( 196 params, 197 MAINTENANCE_TASK_JOB_ID, 198 wantsReschedule); 199 return wantsReschedule; 200 } 201 cancelAndFinishJob(final JobParameters params, int skipReason)202 private boolean cancelAndFinishJob(final JobParameters params, int skipReason) { 203 JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); 204 if (jobScheduler != null) { 205 jobScheduler.cancel(MAINTENANCE_TASK_JOB_ID); 206 } 207 OdpJobServiceLogger.getInstance(this).recordJobSkipped( 208 MAINTENANCE_TASK_JOB_ID, 209 skipReason); 210 jobFinished(params, /* wantsReschedule = */ false); 211 return true; 212 } 213 } 214