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.server.healthconnect;
18 
19 import static android.health.connect.Constants.DEFAULT_INT;
20 
21 import static com.android.server.healthconnect.HealthConnectDailyJobs.HC_DAILY_JOB;
22 import static com.android.server.healthconnect.exportimport.ExportImportJobs.PERIODIC_EXPORT_JOB_NAME;
23 import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_COMPLETE_JOB_NAME;
24 import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_PAUSE_JOB_NAME;
25 
26 import android.annotation.NonNull;
27 import android.annotation.UserIdInt;
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.health.connect.Constants;
33 import android.util.Slog;
34 
35 import com.android.server.healthconnect.exportimport.ExportImportJobs;
36 import com.android.server.healthconnect.exportimport.ExportManager;
37 import com.android.server.healthconnect.migration.MigrationStateChangeJob;
38 
39 import java.time.Clock;
40 import java.util.Objects;
41 
42 /**
43  * Health Connect wrapper around JobService.
44  *
45  * @hide
46  */
47 public class HealthConnectDailyService extends JobService {
48     public static final String EXTRA_USER_ID = "user_id";
49     public static final String EXTRA_JOB_NAME_KEY = "job_name";
50     private static final String TAG = "HealthConnectDailyService";
51     @UserIdInt private static volatile int sCurrentUserId;
52 
53     /**
54      * Routes the job to the right place based on the job name, after performing common checks.,
55      *
56      * <p>Please handle exceptions for each task within the task. Do not crash the job as it might
57      * result in failure of other tasks being triggered from the job.
58      */
59     @Override
onStartJob(@onNull JobParameters params)60     public boolean onStartJob(@NonNull JobParameters params) {
61         int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue= */ DEFAULT_INT);
62         String jobName = params.getExtras().getString(EXTRA_JOB_NAME_KEY);
63         if (userId == DEFAULT_INT || userId != sCurrentUserId) {
64             // This job is no longer valid, the service for this user should have been stopped.
65             // Just ignore this request in case we still got the request.
66             return false;
67         }
68 
69         if (Objects.isNull(jobName)) {
70             return false;
71         }
72 
73         // This service executes each incoming job on a Handler running on the application's
74         // main thread. This means that we must offload the execution logic to background executor.
75         switch (jobName) {
76             case HC_DAILY_JOB:
77                 HealthConnectThreadScheduler.scheduleInternalTask(
78                         () -> {
79                             HealthConnectDailyJobs.execute(getApplicationContext(), params);
80                             jobFinished(params, false);
81                         });
82                 return true;
83             case MIGRATION_COMPLETE_JOB_NAME:
84                 HealthConnectThreadScheduler.scheduleInternalTask(
85                         () -> {
86                             MigrationStateChangeJob.executeMigrationCompletionJob(
87                                     getApplicationContext());
88                             jobFinished(params, false);
89                         });
90                 return true;
91             case MIGRATION_PAUSE_JOB_NAME:
92                 HealthConnectThreadScheduler.scheduleInternalTask(
93                         () -> {
94                             MigrationStateChangeJob.executeMigrationPauseJob(
95                                     getApplicationContext());
96                             jobFinished(params, false);
97                         });
98                 return true;
99             case PERIODIC_EXPORT_JOB_NAME:
100                 HealthConnectThreadScheduler.scheduleInternalTask(
101                         () -> {
102                             boolean isExportSuccessful =
103                                     ExportImportJobs.executePeriodicExportJob(
104                                             new ExportManager(
105                                                     getApplicationContext(), Clock.systemUTC()));
106                             // If the export is not successful, reschedule the job.
107                             jobFinished(params, !isExportSuccessful);
108                         });
109                 return true;
110             default:
111                 Slog.w(TAG, "Job name " + jobName + " is not supported.");
112                 break;
113         }
114         return false;
115     }
116 
117     /** Called when job needs to be stopped. Don't do anything here and let the job be killed. */
118     @Override
onStopJob(@onNull JobParameters params)119     public boolean onStopJob(@NonNull JobParameters params) {
120         return false;
121     }
122 
123     /** Start periodically scheduling this service for {@code userId}. */
schedule( @onNull JobScheduler jobScheduler, @UserIdInt int userId, @NonNull JobInfo jobInfo)124     public static void schedule(
125             @NonNull JobScheduler jobScheduler, @UserIdInt int userId, @NonNull JobInfo jobInfo) {
126         Objects.requireNonNull(jobScheduler);
127         sCurrentUserId = userId;
128 
129         int result = jobScheduler.schedule(jobInfo);
130         if (result != JobScheduler.RESULT_SUCCESS) {
131             Slog.e(
132                     TAG,
133                     "Failed to schedule the job: "
134                             + jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY));
135         } else if (Constants.DEBUG) {
136             Slog.d(
137                     TAG,
138                     "Scheduled a job successfully: "
139                             + jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY));
140         }
141     }
142 }
143