1 /*
2  * Copyright (C) 2023 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.federatedcompute.services.encryption;
18 
19 import static com.android.adservices.service.stats.AdServicesStatsLog.AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON;
20 
21 import android.app.job.JobInfo;
22 import android.app.job.JobParameters;
23 import android.app.job.JobScheduler;
24 import android.app.job.JobService;
25 import android.content.ComponentName;
26 import android.content.Context;
27 
28 import com.android.federatedcompute.internal.util.LogUtil;
29 import com.android.federatedcompute.services.common.FederatedComputeExecutors;
30 import com.android.federatedcompute.services.common.FederatedComputeJobInfo;
31 import com.android.federatedcompute.services.common.FederatedComputeJobUtil;
32 import com.android.federatedcompute.services.common.Flags;
33 import com.android.federatedcompute.services.common.FlagsFactory;
34 import com.android.federatedcompute.services.data.FederatedComputeEncryptionKey;
35 import com.android.federatedcompute.services.statsd.joblogging.FederatedComputeJobServiceLogger;
36 import com.android.internal.annotations.VisibleForTesting;
37 
38 import com.google.common.util.concurrent.FutureCallback;
39 import com.google.common.util.concurrent.Futures;
40 import com.google.common.util.concurrent.ListeningExecutorService;
41 
42 import java.util.List;
43 import java.util.concurrent.ExecutionException;
44 import java.util.concurrent.TimeoutException;
45 
46 public class BackgroundKeyFetchJobService extends JobService {
47     private static final String TAG = BackgroundKeyFetchJobService.class.getSimpleName();
48 
49     private static final int ENCRYPTION_KEY_FETCH_JOB_ID =
50             FederatedComputeJobInfo.ENCRYPTION_KEY_FETCH_JOB_ID;
51 
52     static class Injector {
getExecutor()53         ListeningExecutorService getExecutor() {
54             return FederatedComputeExecutors.getBackgroundExecutor();
55         }
56 
getLightWeightExecutor()57         ListeningExecutorService getLightWeightExecutor() {
58             return FederatedComputeExecutors.getLightweightExecutor();
59         }
60 
getEncryptionKeyManager(Context context)61         FederatedComputeEncryptionKeyManager getEncryptionKeyManager(Context context) {
62             return FederatedComputeEncryptionKeyManager.getInstance(context);
63         }
64     }
65 
66     private final Injector mInjector;
67 
BackgroundKeyFetchJobService()68     public BackgroundKeyFetchJobService() {
69         mInjector = new Injector();
70     }
71 
72     @VisibleForTesting
BackgroundKeyFetchJobService(Injector injector)73     BackgroundKeyFetchJobService(Injector injector) {
74         mInjector = injector;
75     }
76 
77     /** Runs the background key fetch and persist keys job. The method is for testing only. */
78     @VisibleForTesting
run(JobParameters params)79     public void run(JobParameters params) {
80         var unused = Futures.submit(() -> onStartJob(params), mInjector.getExecutor());
81     }
82 
83     @Override
onStartJob(JobParameters params)84     public boolean onStartJob(JobParameters params) {
85         LogUtil.d(TAG, "BackgroundKeyFetchJobService.onStartJob %d", params.getJobId());
86         FederatedComputeJobServiceLogger.getInstance(this)
87                 .recordOnStartJob(ENCRYPTION_KEY_FETCH_JOB_ID);
88         if (FlagsFactory.getFlags().getGlobalKillSwitch()) {
89             LogUtil.d(TAG, "GlobalKillSwitch enabled, finishing job.");
90             return FederatedComputeJobUtil.cancelAndFinishJob(this, params,
91                     ENCRYPTION_KEY_FETCH_JOB_ID,
92                     AD_SERVICES_BACKGROUND_JOBS_EXECUTION_REPORTED__EXECUTION_RESULT_CODE__SKIP_FOR_KILL_SWITCH_ON);
93         }
94         mInjector
95                 .getEncryptionKeyManager(this)
96                 .fetchAndPersistActiveKeys(FederatedComputeEncryptionKey.KEY_TYPE_ENCRYPTION,
97                         /* isScheduledJob= */ true)
98                 .addCallback(
99                         new FutureCallback<List<FederatedComputeEncryptionKey>>() {
100                             @Override
101                             public void onSuccess(
102                                     List<FederatedComputeEncryptionKey>
103                                             federatedComputeEncryptionKeys) {
104                                 LogUtil.d(
105                                         TAG,
106                                         "BackgroundKeyFetchJobService %d is done, fetched %d keys",
107                                         params.getJobId(),
108                                         federatedComputeEncryptionKeys.size());
109                                 boolean wantsReschedule = false;
110                                 FederatedComputeJobServiceLogger.getInstance(
111                                         BackgroundKeyFetchJobService.this)
112                                         .recordJobFinished(
113                                                 ENCRYPTION_KEY_FETCH_JOB_ID,
114                                                 /* isSuccessful= */ true,
115                                                 wantsReschedule);
116                                 jobFinished(params, wantsReschedule);
117                             }
118 
119                             @Override
120                             public void onFailure(Throwable throwable) {
121                                 LogUtil.e(
122                                         TAG,
123                                         "Failed to run job %d to fetch key and delete expired keys",
124                                         params.getJobId());
125                                 if (throwable instanceof ExecutionException) {
126                                     LogUtil.e(
127                                             TAG,
128                                             "Background key fetch failed due to internal error");
129                                 } else if (throwable instanceof TimeoutException) {
130                                     LogUtil.e(
131                                             TAG,
132                                             "Background key fetch failed due to timeout error");
133                                 } else if (throwable instanceof InterruptedException) {
134                                     LogUtil.e(
135                                             TAG,
136                                             "Background key fetch failed due to interruption "
137                                             + "error");
138                                 } else if (throwable instanceof IllegalArgumentException) {
139                                     LogUtil.e(
140                                             TAG,
141                                             "Background key fetch failed due to illegal argument "
142                                             + "error");
143                                 } else {
144                                     LogUtil.e(
145                                             TAG,
146                                             "Background key fetch failed due to unexpected error");
147                                 }
148                                 boolean wantsReschedule = false;
149                                 FederatedComputeJobServiceLogger.getInstance(
150                                         BackgroundKeyFetchJobService.this)
151                                         .recordJobFinished(
152                                                 ENCRYPTION_KEY_FETCH_JOB_ID,
153                                                 /* isSuccessful= */ false,
154                                                 wantsReschedule);
155                                 jobFinished(params, wantsReschedule);
156                             }
157                         },
158                         mInjector.getLightWeightExecutor());
159         return true;
160     }
161 
162     @Override
onStopJob(JobParameters params)163     public boolean onStopJob(JobParameters params) {
164         LogUtil.d(TAG, "BackgroundKeyFetchJobService.onStopJob %d", params.getJobId());
165         boolean wantsReschedule = false;
166         FederatedComputeJobServiceLogger.getInstance(this)
167                 .recordOnStopJob(
168                         params,
169                         ENCRYPTION_KEY_FETCH_JOB_ID,
170                         wantsReschedule);
171         return wantsReschedule;
172     }
173 
174     /** Schedule the periodic background key fetch and delete job if it is not scheduled. */
scheduleJobIfNeeded(Context context, Flags flags)175     public static boolean scheduleJobIfNeeded(Context context, Flags flags) {
176         if (!flags.getEnableBackgroundEncryptionKeyFetch()) {
177             LogUtil.d(
178                     TAG,
179                     "Schedule encryption key job fetch is not enable in flags.");
180             return false;
181         }
182         final JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
183         if (jobScheduler == null) {
184             LogUtil.e(TAG, "Failed to get job scheduler from system service.");
185             return false;
186         }
187 
188         final JobInfo scheduledJob = jobScheduler.getPendingJob(ENCRYPTION_KEY_FETCH_JOB_ID);
189         final JobInfo jobInfo =
190                 new JobInfo.Builder(
191                                 ENCRYPTION_KEY_FETCH_JOB_ID,
192                                 new ComponentName(context, BackgroundKeyFetchJobService.class))
193                         .setPeriodic(
194                                 flags.getEncryptionKeyFetchPeriodSeconds()
195                                         * 1000) // convert to milliseconds
196                         .setRequiresBatteryNotLow(true)
197                         .setRequiresDeviceIdle(true)
198                         .setRequiredNetworkType(JobInfo.NETWORK_TYPE_UNMETERED)
199                         .setPersisted(true)
200                         .build();
201 
202         if (!jobInfo.equals(scheduledJob)) {
203             jobScheduler.schedule(jobInfo);
204             LogUtil.d(
205                     TAG,
206                     "Scheduled job BackgroundKeyFetchJobService id %d",
207                     ENCRYPTION_KEY_FETCH_JOB_ID);
208             return true;
209         } else {
210             LogUtil.d(
211                     TAG,
212                     "Already scheduled job BackgroundKeyFetchJobService id %d",
213                     ENCRYPTION_KEY_FETCH_JOB_ID);
214             return false;
215         }
216     }
217 }
218