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