• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Nullable;
27 import android.app.job.JobInfo;
28 import android.app.job.JobParameters;
29 import android.app.job.JobScheduler;
30 import android.app.job.JobService;
31 import android.content.Context;
32 import android.health.connect.Constants;
33 import android.os.UserHandle;
34 import android.util.Slog;
35 
36 import com.android.server.healthconnect.exportimport.ExportImportJobs;
37 import com.android.server.healthconnect.exportimport.ExportManager;
38 import com.android.server.healthconnect.injector.HealthConnectInjector;
39 import com.android.server.healthconnect.logging.EcosystemStatsCollector;
40 import com.android.server.healthconnect.logging.UsageStatsCollector;
41 import com.android.server.healthconnect.migration.MigrationStateChangeJob;
42 import com.android.server.healthconnect.migration.MigrationStateManager;
43 import com.android.server.healthconnect.storage.DailyCleanupJob;
44 import com.android.server.healthconnect.storage.ExportImportSettingsStorage;
45 import com.android.server.healthconnect.storage.HealthConnectContext;
46 import com.android.server.healthconnect.storage.datatypehelpers.DatabaseStatsCollector;
47 import com.android.server.healthconnect.storage.datatypehelpers.PreferenceHelper;
48 
49 import java.util.Objects;
50 
51 /**
52  * Health Connect wrapper around JobService.
53  *
54  * @hide
55  */
56 public class HealthConnectDailyService extends JobService {
57     public static final String EXTRA_USER_ID = "user_id";
58     public static final String EXTRA_JOB_NAME_KEY = "job_name";
59     private static final String TAG = "HealthConnectDailyService";
60     @Nullable private static volatile UserHandle sUserHandle;
61 
62     /**
63      * Routes the job to the right place based on the job name, after performing common checks.,
64      *
65      * <p>Please handle exceptions for each task within the task. Do not crash the job as it might
66      * result in failure of other tasks being triggered from the job.
67      */
68     @Override
onStartJob(JobParameters params)69     public boolean onStartJob(JobParameters params) {
70         int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue= */ DEFAULT_INT);
71         String jobName = params.getExtras().getString(EXTRA_JOB_NAME_KEY);
72         Context context = getApplicationContext();
73         if (userId == DEFAULT_INT || sUserHandle == null || userId != sUserHandle.getIdentifier()) {
74             // This job is no longer valid, the service for this user should have been stopped.
75             // Just ignore this request in case we still got the request.
76             return false;
77         }
78 
79         if (Objects.isNull(jobName)) {
80             return false;
81         }
82 
83         HealthConnectInjector healthConnectInjector = HealthConnectInjector.getInstance();
84         DailyCleanupJob dailyCleanupJob = healthConnectInjector.getDailyCleanupJob();
85         ExportImportSettingsStorage exportImportSettingsStorage =
86                 healthConnectInjector.getExportImportSettingsStorage();
87         ExportManager exportManager = healthConnectInjector.getExportManager();
88         PreferenceHelper preferenceHelper = healthConnectInjector.getPreferenceHelper();
89         MigrationStateManager migrationStateManager =
90                 healthConnectInjector.getMigrationStateManager();
91         UsageStatsCollector usageStatsCollector =
92                 healthConnectInjector.getUsageStatsCollector(
93                         HealthConnectContext.create(
94                                 context,
95                                 sUserHandle,
96                                 /* databaseDirName= */ null,
97                                 healthConnectInjector.getEnvironmentDataDirectory()));
98         DatabaseStatsCollector databaseStatsCollector =
99                 healthConnectInjector.getDatabaseStatsCollector();
100         EcosystemStatsCollector ecosystemStatsCollector =
101                 new EcosystemStatsCollector(
102                         healthConnectInjector.getReadAccessLogsHelper(),
103                         healthConnectInjector.getChangeLogsHelper());
104         HealthConnectThreadScheduler threadScheduler = healthConnectInjector.getThreadScheduler();
105 
106         // This service executes each incoming job on a Handler running on the application's
107         // main thread. This means that we must offload the execution logic to background executor.
108         switch (jobName) {
109             case HC_DAILY_JOB:
110                 threadScheduler.scheduleInternalTask(
111                         () -> {
112                             HealthConnectDailyJobs.execute(
113                                     usageStatsCollector,
114                                     databaseStatsCollector,
115                                     dailyCleanupJob,
116                                     ecosystemStatsCollector,
117                                     healthConnectInjector.getHealthFitnessStatsLog());
118                             jobFinished(params, false);
119                         });
120                 return true;
121             case MIGRATION_COMPLETE_JOB_NAME:
122                 threadScheduler.scheduleInternalTask(
123                         () -> {
124                             MigrationStateChangeJob.executeMigrationCompletionJob(
125                                     context, preferenceHelper, migrationStateManager);
126                             jobFinished(params, false);
127                         });
128                 return true;
129             case MIGRATION_PAUSE_JOB_NAME:
130                 threadScheduler.scheduleInternalTask(
131                         () -> {
132                             MigrationStateChangeJob.executeMigrationPauseJob(
133                                     context, preferenceHelper, migrationStateManager);
134                             jobFinished(params, false);
135                         });
136                 return true;
137             case PERIODIC_EXPORT_JOB_NAME:
138                 threadScheduler.scheduleInternalTask(
139                         () -> {
140                             boolean isExportSuccessful =
141                                     ExportImportJobs.executePeriodicExportJob(
142                                             context,
143                                             Objects.requireNonNull(sUserHandle),
144                                             params.getExtras(),
145                                             exportManager,
146                                             exportImportSettingsStorage);
147                             // If the export is not successful, reschedule the job.
148                             jobFinished(params, !isExportSuccessful);
149                             // TODO(b/374702524) distinguish between a new job and a retry.
150                             // Call exportImportSettingsStorage.resetExportRepeatErrorOnRetryCount()
151                             // for new jobs. Like that we can filter out repeat errors for each of
152                             // the regular (weekly, daily etc) exports.
153                         });
154                 return true;
155             default:
156                 Slog.w(TAG, "Job name " + jobName + " is not supported.");
157                 break;
158         }
159         return false;
160     }
161 
162     /** Called when job needs to be stopped. Don't do anything here and let the job be killed. */
163     @Override
onStopJob(JobParameters params)164     public boolean onStopJob(JobParameters params) {
165         return false;
166     }
167 
168     /** Start periodically scheduling this service for {@code userId}. */
schedule(JobScheduler jobScheduler, UserHandle userHandle, JobInfo jobInfo)169     public static void schedule(JobScheduler jobScheduler, UserHandle userHandle, JobInfo jobInfo) {
170         Objects.requireNonNull(jobScheduler);
171         sUserHandle = userHandle;
172 
173         int result = jobScheduler.schedule(jobInfo);
174         if (result != JobScheduler.RESULT_SUCCESS) {
175             Slog.e(
176                     TAG,
177                     "Failed to schedule the job: "
178                             + jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY));
179         } else if (Constants.DEBUG) {
180             Slog.d(
181                     TAG,
182                     "Scheduled a job successfully: "
183                             + jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY));
184         }
185     }
186 }
187