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.data.user;
18 
19 import static android.app.job.JobScheduler.RESULT_FAILURE;
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.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED;
23 import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.USER_DATA_COLLECTION_ID;
24 
25 import android.app.job.JobInfo;
26 import android.app.job.JobParameters;
27 import android.app.job.JobScheduler;
28 import android.app.job.JobService;
29 import android.content.ComponentName;
30 import android.content.Context;
31 
32 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
33 import com.android.ondevicepersonalization.services.FlagsFactory;
34 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
35 import com.android.ondevicepersonalization.services.statsd.joblogging.OdpJobServiceLogger;
36 
37 import com.google.common.util.concurrent.FutureCallback;
38 import com.google.common.util.concurrent.Futures;
39 import com.google.common.util.concurrent.ListenableFuture;
40 
41 /**
42  * JobService to collect user data in the background thread.
43  */
44 public class UserDataCollectionJobService extends JobService {
45     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
46     private static final String TAG = "UserDataCollectionJobService";
47     // 4-hour interval.
48     private static final long PERIOD_SECONDS = 14400;
49     private ListenableFuture<Void> mFuture;
50     private UserDataCollector mUserDataCollector;
51     private RawUserData mUserData;
52 
53     /**
54      * Schedules a unique instance of UserDataCollectionJobService to be run.
55      */
schedule(Context context)56     public static int schedule(Context context) {
57         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
58         if (jobScheduler.getPendingJob(
59                 USER_DATA_COLLECTION_ID) != null) {
60             sLogger.d(TAG + ": Job is already scheduled. Doing nothing,");
61             return RESULT_FAILURE;
62         }
63         ComponentName serviceComponent = new ComponentName(context,
64                 UserDataCollectionJobService.class);
65         JobInfo.Builder builder = new JobInfo.Builder(
66                 USER_DATA_COLLECTION_ID, serviceComponent);
67 
68         // Constraints
69         builder.setRequiresDeviceIdle(true);
70         builder.setRequiresBatteryNotLow(true);
71         builder.setRequiresStorageNotLow(true);
72         builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE);
73         builder.setPeriodic(1000 * PERIOD_SECONDS); // JobScheduler uses Milliseconds.
74         // persist this job across boots
75         builder.setPersisted(true);
76 
77         return jobScheduler.schedule(builder.build());
78     }
79 
80     @Override
onStartJob(JobParameters params)81     public boolean onStartJob(JobParameters params) {
82         sLogger.d(TAG + ": onStartJob()");
83         OdpJobServiceLogger.getInstance(this)
84                 .recordOnStartJob(USER_DATA_COLLECTION_ID);
85         if (FlagsFactory.getFlags().getGlobalKillSwitch()) {
86             sLogger.d(TAG + ": GlobalKillSwitch enabled, finishing job.");
87             return cancelAndFinishJob(params,
88                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON);
89         }
90         if (!UserPrivacyStatus.getInstance().isProtectedAudienceEnabled()
91                         && !UserPrivacyStatus.getInstance().isMeasurementEnabled()) {
92             sLogger.d(TAG + ": user control is revoked, "
93                             + "deleting existing user data and finishing job.");
94             mUserDataCollector = UserDataCollector.getInstance(this);
95             mUserData = RawUserData.getInstance();
96             mUserDataCollector.clearUserData(mUserData);
97             mUserDataCollector.clearMetadata();
98             OdpJobServiceLogger.getInstance(this).recordJobSkipped(
99                     USER_DATA_COLLECTION_ID,
100                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED);
101             jobFinished(params, /* wantsReschedule = */ false);
102             return true;
103         }
104         mUserDataCollector = UserDataCollector.getInstance(this);
105         mUserData = RawUserData.getInstance();
106         mFuture = Futures.submit(new Runnable() {
107             @Override
108             public void run() {
109                 sLogger.d(TAG + ": Running user data collection job");
110                 try {
111                     // TODO(b/262749958): add multi-threading support if necessary.
112                     mUserDataCollector.updateUserData(mUserData);
113                 } catch (Exception e) {
114                     sLogger.e(TAG + ": Failed to collect user data", e);
115                 }
116             }
117         }, OnDevicePersonalizationExecutors.getBackgroundExecutor());
118 
119         Futures.addCallback(
120                 mFuture,
121                 new FutureCallback<Void>() {
122                     @Override
123                     public void onSuccess(Void result) {
124                         sLogger.d(TAG + ": User data collection job completed.");
125                         boolean wantsReschedule = false;
126                         OdpJobServiceLogger.getInstance(UserDataCollectionJobService.this)
127                                 .recordJobFinished(
128                                         USER_DATA_COLLECTION_ID,
129                                         /* isSuccessful= */ true,
130                                         wantsReschedule);
131                         jobFinished(params, wantsReschedule);
132                     }
133 
134                     @Override
135                     public void onFailure(Throwable t) {
136                         sLogger.e(TAG + ": Failed to handle JobService: " + params.getJobId(), t);
137                         boolean wantsReschedule = false;
138                         OdpJobServiceLogger.getInstance(UserDataCollectionJobService.this)
139                                 .recordJobFinished(
140                                         USER_DATA_COLLECTION_ID,
141                                         /* isSuccessful= */ false,
142                                         wantsReschedule);
143                         //  When failure, also tell the JobScheduler that the job has completed and
144                         // does not need to be rescheduled.
145                         jobFinished(params, wantsReschedule);
146                     }
147                 },
148                 OnDevicePersonalizationExecutors.getBackgroundExecutor());
149 
150         return true;
151     }
152 
153     @Override
onStopJob(JobParameters params)154     public boolean onStopJob(JobParameters params) {
155         if (mFuture != null) {
156             mFuture.cancel(true);
157         }
158         // Reschedule the job since it ended before finishing
159         boolean wantsReschedule = true;
160         OdpJobServiceLogger.getInstance(this)
161                 .recordOnStopJob(
162                         params,
163                         USER_DATA_COLLECTION_ID,
164                         wantsReschedule);
165         return wantsReschedule;
166     }
167 
cancelAndFinishJob(final JobParameters params, int skipReason)168     private boolean cancelAndFinishJob(final JobParameters params, int skipReason) {
169         JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
170         if (jobScheduler != null) {
171             jobScheduler.cancel(USER_DATA_COLLECTION_ID);
172         }
173         OdpJobServiceLogger.getInstance(this).recordJobSkipped(
174                 USER_DATA_COLLECTION_ID,
175                 skipReason);
176         jobFinished(params, /* wantsReschedule = */ false);
177         return true;
178     }
179 }
180