/* * Copyright (C) 2022 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.ondevicepersonalization.services.data.user; import static android.app.job.JobScheduler.RESULT_SUCCESS; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON; import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED; import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_FAILED; import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SKIPPED; import static com.android.adservices.shared.spe.JobServiceConstants.SCHEDULING_RESULT_CODE_SUCCESSFUL; import static com.android.ondevicepersonalization.services.OnDevicePersonalizationConfig.USER_DATA_COLLECTION_ID; import android.app.job.JobInfo; import android.app.job.JobParameters; import android.app.job.JobScheduler; import android.app.job.JobService; import android.content.ComponentName; import android.content.Context; import com.android.adservices.shared.spe.JobServiceConstants; import com.android.internal.annotations.VisibleForTesting; import com.android.ondevicepersonalization.internal.util.LoggerFactory; import com.android.ondevicepersonalization.services.Flags; import com.android.ondevicepersonalization.services.FlagsFactory; import com.android.ondevicepersonalization.services.OnDevicePersonalizationExecutors; import com.android.ondevicepersonalization.services.statsd.joblogging.OdpJobServiceLogger; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; /** JobService to collect user data in the background thread. */ public class UserDataCollectionJobService extends JobService { private static final LoggerFactory.Logger sLogger = LoggerFactory.getLogger(); private static final String TAG = "UserDataCollectionJobService"; // 4-hour interval. private static final long PERIOD_SECONDS = 14400; private ListenableFuture mFuture; private UserDataCollector mUserDataCollector; private RawUserData mUserData; private final Injector mInjector; public UserDataCollectionJobService() { mInjector = new Injector(); } @VisibleForTesting public UserDataCollectionJobService(Injector injector) { mInjector = injector; } static class Injector { ListeningExecutorService getExecutor() { return OnDevicePersonalizationExecutors.getBackgroundExecutor(); } Flags getFlags() { return FlagsFactory.getFlags(); } } /** Schedules a unique instance of UserDataCollectionJobService to be run. */ @JobServiceConstants.JobSchedulingResultCode public static int schedule(Context context, boolean forceSchedule) { JobScheduler jobScheduler = context.getSystemService(JobScheduler.class); if (jobScheduler == null) { sLogger.e(TAG, "Failed to get job scheduler from system service."); return SCHEDULING_RESULT_CODE_FAILED; } if (!forceSchedule && jobScheduler.getPendingJob(USER_DATA_COLLECTION_ID) != null) { sLogger.d(TAG + ": Job is already scheduled. Doing nothing,"); return SCHEDULING_RESULT_CODE_SKIPPED; } ComponentName serviceComponent = new ComponentName(context, UserDataCollectionJobService.class); JobInfo.Builder builder = new JobInfo.Builder(USER_DATA_COLLECTION_ID, serviceComponent); // Constraints builder.setRequiresDeviceIdle(true); builder.setRequiresBatteryNotLow(true); builder.setRequiresStorageNotLow(true); builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_NONE); builder.setPeriodic(1000 * PERIOD_SECONDS); // JobScheduler uses Milliseconds. // persist this job across boots builder.setPersisted(true); int schedulingResult = jobScheduler.schedule(builder.build()); return RESULT_SUCCESS == schedulingResult ? SCHEDULING_RESULT_CODE_SUCCESSFUL : SCHEDULING_RESULT_CODE_FAILED; } @Override public boolean onStartJob(JobParameters params) { sLogger.d(TAG + ": onStartJob()"); OdpJobServiceLogger.getInstance(this).recordOnStartJob(USER_DATA_COLLECTION_ID); if (mInjector.getFlags().getGlobalKillSwitch()) { sLogger.d(TAG + ": GlobalKillSwitch enabled, finishing job."); return cancelAndFinishJob( params, AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON); } // Reschedule jobs with SPE if it's enabled. Note scheduled jobs by this // UserDataCollectionJobService will be cancelled for the same job ID. if (mInjector.getFlags().getSpeOnUserDataCollectionJobEnabled()) { sLogger.i( "SPE is enabled. Reschedule UserDataCollectionJobService with" + " UserDataCollectionJob."); UserDataCollectionJob.schedule(/* context */ this); return false; } runPrivacyStatusChecksInBackground(params); return true; } private void runPrivacyStatusChecksInBackground(final JobParameters params) { OnDevicePersonalizationExecutors.getHighPriorityBackgroundExecutor().execute(() -> { boolean isProtectedAudienceAndMeasurementBothDisabled = UserPrivacyStatus.getInstance() .isProtectedAudienceAndMeasurementBothDisabled(); sLogger.d(TAG + ": is ProtectedAudience and Measurement both disabled: %s", isProtectedAudienceAndMeasurementBothDisabled); if (isProtectedAudienceAndMeasurementBothDisabled) { handlePrivacyControlsRevoked(params); } else { startUserDataCollectionJob(params); } }); } private void handlePrivacyControlsRevoked(JobParameters params) { sLogger.d(TAG + ": user control is revoked, deleting existing user data and finishing job."); mUserDataCollector = UserDataCollector.getInstance(this); mUserData = RawUserData.getInstance(); mUserDataCollector.clearUserData(mUserData); mUserDataCollector.clearMetadata(); OdpJobServiceLogger.getInstance(this) .recordJobSkipped( USER_DATA_COLLECTION_ID, AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_PERSONALIZATION_NOT_ENABLED); jobFinished(params, /* wantsReschedule= */ false); } private void startUserDataCollectionJob(final JobParameters params) { mUserDataCollector = UserDataCollector.getInstance(this); mUserData = RawUserData.getInstance(); mFuture = Futures.submit(new Runnable() { @Override public void run() { sLogger.d(TAG + ": Running user data collection job"); try { mUserDataCollector.updateUserData(mUserData); } catch (Exception e) { sLogger.e(TAG + ": Failed to collect user data", e); } } }, mInjector.getExecutor()); Futures.addCallback( mFuture, new FutureCallback() { @Override public void onSuccess(Void result) { sLogger.d(TAG + ": User data collection job completed."); handleJobCompletion(params, /* isSuccessful= */ true); } @Override public void onFailure(Throwable t) { sLogger.e(t, TAG + ": Failed to handle JobService: " + params.getJobId()); handleJobCompletion(params, /* isSuccessful= */ false); } }, mInjector.getExecutor() ); } private void handleJobCompletion(JobParameters params, boolean isSuccessful) { boolean wantsReschedule = false; OdpJobServiceLogger.getInstance(UserDataCollectionJobService.this) .recordJobFinished( USER_DATA_COLLECTION_ID, isSuccessful, wantsReschedule); jobFinished(params, wantsReschedule); } @Override public boolean onStopJob(JobParameters params) { if (mFuture != null) { mFuture.cancel(true); } // Reschedule the job since it ended before finishing boolean wantsReschedule = true; OdpJobServiceLogger.getInstance(this) .recordOnStopJob(params, USER_DATA_COLLECTION_ID, wantsReschedule); return wantsReschedule; } private boolean cancelAndFinishJob(final JobParameters params, int skipReason) { JobScheduler jobScheduler = this.getSystemService(JobScheduler.class); if (jobScheduler != null) { jobScheduler.cancel(USER_DATA_COLLECTION_ID); } OdpJobServiceLogger.getInstance(this).recordJobSkipped(USER_DATA_COLLECTION_ID, skipReason); jobFinished(params, /* wantsReschedule= */ false); return true; } }