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.federatedcompute.services.scheduling;
18 
19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON;
20 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_FAILED;
21 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SKIPPED;
22 import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SUCCESSFUL;
23 
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 
31 import com.android.adservices.shared.spe.JobServiceConstants.JobSchedulingResultCode;
32 import com.android.federatedcompute.internal.util.LogUtil;
33 import com.android.federatedcompute.services.common.FederatedComputeExecutors;
34 import com.android.federatedcompute.services.common.FederatedComputeJobInfo;
35 import com.android.federatedcompute.services.common.FederatedComputeJobUtil;
36 import com.android.federatedcompute.services.common.Flags;
37 import com.android.federatedcompute.services.common.FlagsFactory;
38 import com.android.federatedcompute.services.data.FederatedTrainingTaskDao;
39 import com.android.federatedcompute.services.data.ODPAuthorizationTokenDao;
40 import com.android.federatedcompute.services.statsd.joblogging.FederatedComputeJobServiceLogger;
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.odp.module.common.Clock;
43 import com.android.odp.module.common.MonotonicClock;
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 import com.google.common.util.concurrent.ListeningExecutorService;
49 
50 import java.util.List;
51 
52 public class DeleteExpiredJobService extends JobService {
53 
54     private static final String TAG = DeleteExpiredJobService.class.getSimpleName();
55 
56     private static final int DELETE_EXPIRED_JOB_ID = FederatedComputeJobInfo.DELETE_EXPIRED_JOB_ID;
57 
58     private final Injector mInjector;
59 
DeleteExpiredJobService()60     public DeleteExpiredJobService() {
61         mInjector = new Injector();
62     }
63 
64     @VisibleForTesting
DeleteExpiredJobService(Injector injector)65     public DeleteExpiredJobService(Injector injector) {
66         mInjector = injector;
67     }
68 
69     static class Injector {
getExecutor()70         ListeningExecutorService getExecutor() {
71             return FederatedComputeExecutors.getBackgroundExecutor();
72         }
73 
getODPAuthorizationTokenDao(Context context)74         ODPAuthorizationTokenDao getODPAuthorizationTokenDao(Context context) {
75             return ODPAuthorizationTokenDao.getInstance(context);
76         }
77 
getTrainingTaskDao(Context context)78         FederatedTrainingTaskDao getTrainingTaskDao(Context context) {
79             return FederatedTrainingTaskDao.getInstance(context);
80         }
81 
getClock()82         Clock getClock() {
83             return MonotonicClock.getInstance();
84         }
85 
getFlags()86         Flags getFlags() {
87             return FlagsFactory.getFlags();
88         }
89     }
90 
91     @Override
onStartJob(JobParameters params)92     public boolean onStartJob(JobParameters params) {
93         LogUtil.d(TAG, "DeleteExpiredJobService.onStartJob %d", params.getJobId());
94         FederatedComputeJobServiceLogger.getInstance(this).recordOnStartJob(DELETE_EXPIRED_JOB_ID);
95         Flags flags = mInjector.getFlags();
96 
97         if (flags.getGlobalKillSwitch()) {
98             LogUtil.d(TAG, "GlobalKillSwitch is enabled, finishing job.");
99             return FederatedComputeJobUtil.cancelAndFinishJob(
100                     this,
101                     params,
102                     DELETE_EXPIRED_JOB_ID,
103                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON);
104         }
105 
106         // Reschedule jobs with SPE if it's enabled. Note scheduled jobs by this
107         // DeleteExpiredJobService will be cancelled for the same job ID.
108         //
109         // Note the job without a flex period will execute immediately after rescheduling with the
110         // same ID. Therefore, ending the execution here and let it run in the new SPE job.
111         if (flags.getSpePilotJobEnabled()) {
112             LogUtil.d(
113                     TAG,
114                     "SPE is enabled. Reschedule DeleteExpiredJobService with"
115                             + " DeleteExpiredJob.");
116             DeleteExpiredJob.schedule(this, flags);
117             return false;
118         }
119 
120         ListenableFuture<Integer> deleteExpiredAuthTokenFuture =
121                 Futures.submit(
122                         () ->
123                                 mInjector
124                                         .getODPAuthorizationTokenDao(this)
125                                         .deleteExpiredAuthorizationTokens(),
126                         mInjector.getExecutor());
127         ListenableFuture<Integer> deleteExpiredTaskHistoryFuture =
128                 Futures.submit(
129                         () -> {
130                             long deleteTime =
131                                     mInjector.getClock().currentTimeMillis()
132                                             - flags.getTaskHistoryTtl();
133                             return mInjector
134                                     .getTrainingTaskDao(this)
135                                     .deleteExpiredTaskHistory(deleteTime);
136                         },
137                         mInjector.getExecutor());
138         ListenableFuture<List<Integer>> futuresList =
139                 Futures.allAsList(deleteExpiredAuthTokenFuture, deleteExpiredTaskHistoryFuture);
140         Futures.addCallback(
141                 futuresList,
142                 new FutureCallback<List<Integer>>() {
143                     @Override
144                     public void onSuccess(List<Integer> result) {
145                         LogUtil.d(TAG, "Deleted expired records %s", result.toString());
146                         boolean wantsReschedule = false;
147                         FederatedComputeJobServiceLogger.getInstance(DeleteExpiredJobService.this)
148                                 .recordJobFinished(
149                                         DELETE_EXPIRED_JOB_ID,
150                                         /* isSuccessful= */ true,
151                                         wantsReschedule);
152                         jobFinished(params, /* wantsReschedule= */ wantsReschedule);
153                     }
154 
155                     @Override
156                     public void onFailure(Throwable t) {
157                         LogUtil.e(TAG, t, "Exception encountered when deleting expired records");
158                         boolean wantsReschedule = false;
159                         FederatedComputeJobServiceLogger.getInstance(DeleteExpiredJobService.this)
160                                 .recordJobFinished(
161                                         DELETE_EXPIRED_JOB_ID,
162                                         /* isSuccessful= */ false,
163                                         wantsReschedule);
164                         jobFinished(params, /* wantsReschedule= */ wantsReschedule);
165                     }
166                 },
167                 FederatedComputeExecutors.getLightweightExecutor());
168         return true;
169     }
170 
171     @Override
onStopJob(JobParameters params)172     public boolean onStopJob(JobParameters params) {
173         LogUtil.d(TAG, "DeleteExpiredJobService.onStopJob %d", params.getJobId());
174         boolean wantsReschedule = false;
175         FederatedComputeJobServiceLogger.getInstance(this)
176                 .recordOnStopJob(params, DELETE_EXPIRED_JOB_ID, wantsReschedule);
177         return wantsReschedule;
178     }
179 
180     /** Schedule the periodic deletion job if it is not scheduled. */
181     @JobSchedulingResultCode
scheduleJobIfNeeded(Context context, Flags flags, boolean forceSchedule)182     public static int scheduleJobIfNeeded(Context context, Flags flags, boolean forceSchedule) {
183         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
184         if (jobScheduler == null) {
185             LogUtil.e(TAG, "Failed to get job scheduler from system service.");
186             return SCHEDULING_RESULT_CODE_FAILED;
187         }
188 
189         final JobInfo scheduledJob = jobScheduler.getPendingJob(DELETE_EXPIRED_JOB_ID);
190         final JobInfo jobInfo =
191                 new JobInfo.Builder(
192                                 DELETE_EXPIRED_JOB_ID,
193                                 new ComponentName(context, DeleteExpiredJobService.class))
194                         .setPeriodic(
195                                 flags.getAuthorizationTokenDeletionPeriodSeconds()
196                                         * 1000) // convert to milliseconds
197                         .setRequiresBatteryNotLow(true)
198                         .setRequiresDeviceIdle(true)
199                         .setPersisted(true)
200                         .build();
201 
202         if (forceSchedule || !jobInfo.equals(scheduledJob)) {
203             jobScheduler.schedule(jobInfo);
204             LogUtil.d(TAG, "Scheduled job DeleteExpiredJobService id %d", DELETE_EXPIRED_JOB_ID);
205             return SCHEDULING_RESULT_CODE_SUCCESSFUL;
206         } else {
207             LogUtil.d(
208                     TAG,
209                     "Already scheduled job DeleteExpiredJobService id %d",
210                     DELETE_EXPIRED_JOB_ID);
211             return SCHEDULING_RESULT_CODE_SKIPPED;
212         }
213     }
214 }
215