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