• 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.exportimport;
18 
19 import static com.android.healthfitness.flags.Flags.exportImportFastFollow;
20 import static com.android.healthfitness.flags.Flags.extendExportImportTelemetry;
21 
22 import android.app.job.JobInfo;
23 import android.app.job.JobScheduler;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.os.PersistableBundle;
27 import android.os.UserHandle;
28 import android.util.Slog;
29 
30 import com.android.internal.annotations.VisibleForTesting;
31 import com.android.server.healthconnect.HealthConnectDailyService;
32 import com.android.server.healthconnect.storage.ExportImportSettingsStorage;
33 
34 import java.time.Duration;
35 import java.util.Objects;
36 
37 /**
38  * Defines jobs related to Health Connect export and imports.
39  *
40  * @hide
41  */
42 public class ExportImportJobs {
43     private static final String TAG = "HealthConnectExportJobs";
44     private static final int MIN_JOB_ID = ExportImportJobs.class.hashCode();
45 
46     @VisibleForTesting static final String IS_FIRST_EXPORT = "is_first_export";
47     @VisibleForTesting static final String NAMESPACE = "HEALTH_CONNECT_IMPORT_EXPORT_JOBS";
48 
49     public static final String PERIODIC_EXPORT_JOB_NAME = "periodic_export_job";
50 
51     /**
52      * Checks if the rescheduling is needed and schedules the periodic export job if so.
53      *
54      * <p>Needed in particular for device restarts and user switches. The job is persisted in those
55      * cases and we want to avoid rescheduling, because otherwise, if a user restarts their phone
56      * often, the job may never complete.
57      */
schedulePeriodicJobIfNotScheduled( UserHandle userHandle, Context context, ExportImportSettingsStorage exportImportSettingsStorage, ExportManager exportManager)58     public static void schedulePeriodicJobIfNotScheduled(
59             UserHandle userHandle,
60             Context context,
61             ExportImportSettingsStorage exportImportSettingsStorage,
62             ExportManager exportManager) {
63         if (!exportImportFastFollow()
64                 || Objects.requireNonNull(context.getSystemService(JobScheduler.class))
65                         .forNamespace(NAMESPACE)
66                         .getAllPendingJobs()
67                         .isEmpty()) {
68             schedulePeriodicExportJob(
69                     userHandle, context, exportImportSettingsStorage, exportManager);
70         }
71     }
72 
73     /** Schedule the periodic export job. */
schedulePeriodicExportJob( UserHandle userHandle, Context context, ExportImportSettingsStorage exportImportSettingsStorage, ExportManager exportManager)74     public static void schedulePeriodicExportJob(
75             UserHandle userHandle,
76             Context context,
77             ExportImportSettingsStorage exportImportSettingsStorage,
78             ExportManager exportManager) {
79         int periodInDays = exportImportSettingsStorage.getScheduledExportPeriodInDays();
80 
81         if (extendExportImportTelemetry()) {
82             // A new export is set up, reset the retry count for logging
83             exportImportSettingsStorage.resetExportRepeatErrorOnRetryCount();
84         }
85 
86         if (exportImportFastFollow()) {
87             // We should always cancel the job as we are persisting the job now.
88             Objects.requireNonNull(context.getSystemService(JobScheduler.class))
89                     .forNamespace(NAMESPACE)
90                     .cancelAll();
91 
92             // TODO(b/364855153): Move to next condition once fast follow flag is enabled.
93             // If export is off we try to delete the local files, just in case it happened the
94             // rare case where those files weren't deleted after the last export.
95             if (periodInDays <= 0) {
96                 exportManager.deleteLocalExportFiles(userHandle);
97             }
98         }
99         // If period is 0 the user has turned export off, we should no longer schedule a new job
100         if (periodInDays <= 0) {
101             return;
102         }
103 
104         PersistableBundle extras = new PersistableBundle();
105         extras.putInt(HealthConnectDailyService.EXTRA_USER_ID, userHandle.getIdentifier());
106         extras.putString(HealthConnectDailyService.EXTRA_JOB_NAME_KEY, PERIODIC_EXPORT_JOB_NAME);
107 
108         long periodInMillis = Duration.ofDays(periodInDays).toMillis();
109         long flexInMillis = Duration.ofHours(6).toMillis();
110         if (periodInDays >= 7) {
111             // For weekly / monthly export, allow export to happen at least one day before.
112             flexInMillis = Duration.ofDays(1).toMillis();
113         }
114         if (exportImportSettingsStorage.getLastSuccessfulExportTime() == null
115                 || !exportImportSettingsStorage
116                         .getUri()
117                         .equals(exportImportSettingsStorage.getLastSuccessfulExportUri())) {
118 
119             // Shorten the period for the first export to any new URI.
120             // This will be changed back once the first export to any new location is done.
121             periodInMillis = Duration.ofHours(1).toMillis();
122             flexInMillis = periodInMillis;
123 
124             extras.putBoolean(IS_FIRST_EXPORT, true);
125         }
126         Slog.i(
127                 TAG,
128                 "Scheduling export job with period (millis) = "
129                         + periodInMillis
130                         + " flex = "
131                         + flexInMillis);
132 
133         ComponentName componentName = new ComponentName(context, HealthConnectDailyService.class);
134         JobInfo.Builder builder =
135                 new JobInfo.Builder(MIN_JOB_ID + userHandle.getIdentifier(), componentName)
136                         .setRequiresCharging(true)
137                         .setRequiresDeviceIdle(true)
138                         .setPeriodic(
139                                 periodInMillis,
140                                 // Flex interval.
141                                 // The flex period begins after (periodInMillis - flexInMillis)
142                                 // Flex is the max of the specified time, or 5% of periodInMillis.
143                                 flexInMillis)
144                         .setExtras(extras);
145         if (exportImportFastFollow()) {
146             // Persist the job to avoid rescheduling when restarting the device.
147             // Otherwise if the user repeatedly restarts their phone, an export may never happen.
148             builder = builder.setPersisted(true);
149         }
150 
151         HealthConnectDailyService.schedule(
152                 Objects.requireNonNull(context.getSystemService(JobScheduler.class))
153                         .forNamespace(NAMESPACE),
154                 userHandle,
155                 builder.build());
156     }
157 
158     /**
159      * Execute the periodic export job. It returns true if the export was successful (no need to
160      * reschedule the job). False otherwise.
161      */
162     // TODO(b/318484778): Use dependency injection instead of passing an instance to the method.
executePeriodicExportJob( Context context, UserHandle userHandle, PersistableBundle extras, ExportManager exportManager, ExportImportSettingsStorage exportImportSettingsStorage)163     public static boolean executePeriodicExportJob(
164             Context context,
165             UserHandle userHandle,
166             PersistableBundle extras,
167             ExportManager exportManager,
168             ExportImportSettingsStorage exportImportSettingsStorage) {
169         if (exportImportSettingsStorage.getScheduledExportPeriodInDays() <= 0) {
170             // If there is no need to run the export, it counts like a success regarding job
171             // reschedule.
172             return true;
173         }
174 
175         boolean exportSuccess = exportManager.runExport(userHandle);
176         boolean firstExport = extras.getBoolean(IS_FIRST_EXPORT, false);
177         if (exportSuccess && firstExport) {
178             schedulePeriodicExportJob(
179                     userHandle, context, exportImportSettingsStorage, exportManager);
180         }
181         return exportSuccess;
182 
183         // TODO(b/325599089): Do we need an additional periodic / one-off task to make sure a single
184         //  export completes? We need to test if JobScheduler will call the job again if jobFinished
185         //  is never called.
186 
187         // TODO(b/325599089): Consider if we need to do any checkpointing here in case the job
188         //  doesn't complete and we need to pick it up again.
189     }
190 }
191