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