• 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.data.user;
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.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED;
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.USER_DATA_COLLECTION_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 
35 import com.android.adservices.shared.spe.JobServiceConstants;
36 import com.android.internal.annotations.VisibleForTesting;
37 import com.android.ondevicepersonalization.internal.util.LoggerFactory;
38 import com.android.ondevicepersonalization.services.Flags;
39 import com.android.ondevicepersonalization.services.FlagsFactory;
40 import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors;
41 import com.android.ondevicepersonalization.services.statsd.joblogging.OdpJobServiceLogger;
42 
43 import com.google.common.util.concurrent.FutureCallback;
44 import com.google.common.util.concurrent.Futures;
45 import com.google.common.util.concurrent.ListenableFuture;
46 import com.google.common.util.concurrent.ListeningExecutorService;
47 
48 /** JobService to collect user data in the background thread. */
49 public class UserDataCollectionJobService extends JobService {
50     private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger();
51     private static final String TAG = "UserDataCollectionJobService";
52     // 4-hour interval.
53     private static final long PERIOD_SECONDS = 14400;
54     private ListenableFuture<Void> mFuture;
55     private UserDataCollector mUserDataCollector;
56     private RawUserData mUserData;
57 
58     private final Injector mInjector;
59 
UserDataCollectionJobService()60     public UserDataCollectionJobService() {
61         mInjector = new Injector();
62     }
63 
64     @VisibleForTesting
UserDataCollectionJobService(Injector injector)65     public UserDataCollectionJobService(Injector injector) {
66         mInjector = injector;
67     }
68 
69     static class Injector {
getExecutor()70         ListeningExecutorService getExecutor() {
71             return OnDevicePersonalizationExecutors.getBackgroundExecutor();
72         }
73 
getFlags()74         Flags getFlags() {
75             return FlagsFactory.getFlags();
76         }
77     }
78 
79     /** Schedules a unique instance of UserDataCollectionJobService to be run. */
80     @JobServiceConstants.JobSchedulingResultCode
schedule(Context context, boolean forceSchedule)81     public static int schedule(Context context, boolean forceSchedule) {
82         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
83         if (jobScheduler == null) {
84             sLogger.e(TAG, "Failed to get job scheduler from system service.");
85             return SCHEDULING_RESULT_CODE_FAILED;
86         }
87         if (!forceSchedule && jobScheduler.getPendingJob(USER_DATA_COLLECTION_ID) != null) {
88             sLogger.d(TAG + ": Job is already scheduled. Doing nothing,");
89             return SCHEDULING_RESULT_CODE_SKIPPED;
90         }
91         ComponentName serviceComponent =
92                 new ComponentName(context, UserDataCollectionJobService.class);
93         JobInfo.Builder builder = new JobInfo.Builder(USER_DATA_COLLECTION_ID, serviceComponent);
94 
95         // Constraints
96         builder.setRequiresDeviceIdle(true);
97         builder.setRequiresBatteryNotLow(true);
98         builder.setRequiresStorageNotLow(true);
99         builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE);
100         builder.setPeriodic(1000 * PERIOD_SECONDS); // JobScheduler uses Milliseconds.
101         // persist this job across boots
102         builder.setPersisted(true);
103 
104         int schedulingResult = jobScheduler.schedule(builder.build());
105         return RESULT_SUCCESS == schedulingResult ? SCHEDULING_RESULT_CODE_SUCCESSFUL
106                 : SCHEDULING_RESULT_CODE_FAILED;
107     }
108 
109     @Override
onStartJob(JobParameters params)110     public boolean onStartJob(JobParameters params) {
111         sLogger.d(TAG + ": onStartJob()");
112         OdpJobServiceLogger.getInstance(this).recordOnStartJob(USER_DATA_COLLECTION_ID);
113         if (mInjector.getFlags().getGlobalKillSwitch()) {
114             sLogger.d(TAG + ": GlobalKillSwitch enabled, finishing job.");
115             return cancelAndFinishJob(
116                     params,
117                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON);
118         }
119         // Reschedule jobs with SPE if it's enabled. Note scheduled jobs by this
120         // UserDataCollectionJobService will be cancelled for the same job ID.
121         if (mInjector.getFlags().getSpeOnUserDataCollectionJobEnabled()) {
122             sLogger.i(
123                     "SPE is enabled. Reschedule UserDataCollectionJobService with"
124                             + " UserDataCollectionJob.");
125             UserDataCollectionJob.schedule(/* context */ this);
126             return false;
127         }
128         runPrivacyStatusChecksInBackground(params);
129         return true;
130     }
131 
runPrivacyStatusChecksInBackground(final JobParameters params)132     private void runPrivacyStatusChecksInBackground(final JobParameters params) {
133         OnDevicePersonalizationExecutors.getHighPriorityBackgroundExecutor().execute(() -> {
134             boolean isProtectedAudienceAndMeasurementBothDisabled =
135                     UserPrivacyStatus.getInstance()
136                             .isProtectedAudienceAndMeasurementBothDisabled();
137             sLogger.d(TAG + ": is ProtectedAudience and Measurement both disabled: %s",
138                     isProtectedAudienceAndMeasurementBothDisabled);
139             if (isProtectedAudienceAndMeasurementBothDisabled) {
140                 handlePrivacyControlsRevoked(params);
141             } else {
142                 startUserDataCollectionJob(params);
143             }
144         });
145     }
146 
handlePrivacyControlsRevoked(JobParameters params)147     private void handlePrivacyControlsRevoked(JobParameters params) {
148         sLogger.d(TAG
149                 + ": user control is revoked, deleting existing user data and finishing job.");
150         mUserDataCollector = UserDataCollector.getInstance(this);
151         mUserData = RawUserData.getInstance();
152         mUserDataCollector.clearUserData(mUserData);
153         mUserDataCollector.clearMetadata();
154         OdpJobServiceLogger.getInstance(this)
155                 .recordJobSkipped(
156                         USER_DATA_COLLECTION_ID,
157                         AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED);
158         jobFinished(params, /* wantsReschedule= */ false);
159     }
160 
startUserDataCollectionJob(final JobParameters params)161     private void startUserDataCollectionJob(final JobParameters params) {
162         mUserDataCollector = UserDataCollector.getInstance(this);
163         mUserData = RawUserData.getInstance();
164         mFuture = Futures.submit(new Runnable() {
165             @Override
166             public void run() {
167                 sLogger.d(TAG + ": Running user data collection job");
168                 try {
169                     mUserDataCollector.updateUserData(mUserData);
170                 } catch (Exception e) {
171                     sLogger.e(TAG + ": Failed to collect user data", e);
172                 }
173             }
174         }, mInjector.getExecutor());
175 
176         Futures.addCallback(
177                 mFuture,
178                 new FutureCallback<Void>() {
179                     @Override
180                     public void onSuccess(Void result) {
181                         sLogger.d(TAG + ": User data collection job completed.");
182                         handleJobCompletion(params, /* isSuccessful= */ true);
183                     }
184 
185                     @Override
186                     public void onFailure(Throwable t) {
187                         sLogger.e(t, TAG + ": Failed to handle JobService: " + params.getJobId());
188                         handleJobCompletion(params, /* isSuccessful= */ false);
189                     }
190                 },
191                 mInjector.getExecutor()
192         );
193     }
194 
handleJobCompletion(JobParameters params, boolean isSuccessful)195     private void handleJobCompletion(JobParameters params, boolean isSuccessful) {
196         boolean wantsReschedule = false;
197         OdpJobServiceLogger.getInstance(UserDataCollectionJobService.this)
198                 .recordJobFinished(
199                         USER_DATA_COLLECTION_ID,
200                         isSuccessful,
201                         wantsReschedule);
202         jobFinished(params, wantsReschedule);
203     }
204 
205     @Override
onStopJob(JobParameters params)206     public boolean onStopJob(JobParameters params) {
207         if (mFuture != null) {
208             mFuture.cancel(true);
209         }
210         // Reschedule the job since it ended before finishing
211         boolean wantsReschedule = true;
212         OdpJobServiceLogger.getInstance(this)
213                 .recordOnStopJob(params, USER_DATA_COLLECTION_ID, wantsReschedule);
214         return wantsReschedule;
215     }
216 
cancelAndFinishJob(final JobParameters params, int skipReason)217     private boolean cancelAndFinishJob(final JobParameters params, int skipReason) {
218         JobScheduler jobScheduler = this.getSystemService(JobScheduler.class);
219         if (jobScheduler != null) {
220             jobScheduler.cancel(USER_DATA_COLLECTION_ID);
221         }
222         OdpJobServiceLogger.getInstance(this).recordJobSkipped(USER_DATA_COLLECTION_ID, skipReason);
223         jobFinished(params, /* wantsReschedule= */ false);
224         return true;
225     }
226 }
227