• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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