• 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.migration;
18 
19 import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_ALLOWED;
20 import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_COMPLETE;
21 import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_IDLE;
22 import static android.health.connect.HealthConnectDataState.MIGRATION_STATE_IN_PROGRESS;
23 
24 import static com.android.server.healthconnect.HealthConnectDailyJobs.HC_DAILY_JOB;
25 import static com.android.server.healthconnect.HealthConnectDailyService.EXTRA_JOB_NAME_KEY;
26 import static com.android.server.healthconnect.HealthConnectDailyService.EXTRA_USER_ID;
27 import static com.android.server.healthconnect.migration.MigrationConstants.CURRENT_STATE_START_TIME_KEY;
28 import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_COMPLETE_JOB_NAME;
29 import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_PAUSE_JOB_NAME;
30 import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_STATE_CHANGE_NAMESPACE;
31 
32 import android.annotation.NonNull;
33 import android.app.job.JobInfo;
34 import android.app.job.JobScheduler;
35 import android.content.ComponentName;
36 import android.content.Context;
37 import android.os.PersistableBundle;
38 
39 import com.android.server.healthconnect.HealthConnectDailyService;
40 import com.android.server.healthconnect.HealthConnectDeviceConfigManager;
41 import com.android.server.healthconnect.storage.datatypehelpers.PreferenceHelper;
42 
43 import java.time.Instant;
44 import java.util.List;
45 import java.util.Objects;
46 
47 /**
48  * A state-change jobs scheduler and executor. Schedules migration completion job to run daily, and
49  * migration pause job to run every 4 hours
50  *
51  * @hide
52  */
53 public final class MigrationStateChangeJob {
54     static final int MIN_JOB_ID = MigrationStateChangeJob.class.hashCode();
55     private static final HealthConnectDeviceConfigManager sHealthConnectDeviceConfigManager =
56             HealthConnectDeviceConfigManager.getInitialisedInstance();
57 
scheduleMigrationCompletionJob(Context context, int userId)58     public static void scheduleMigrationCompletionJob(Context context, int userId) {
59         if (!HealthConnectDeviceConfigManager.getInitialisedInstance()
60                 .isCompleteStateChangeJobEnabled()) {
61             return;
62         }
63         ComponentName componentName = new ComponentName(context, HealthConnectDailyService.class);
64         final PersistableBundle extras = new PersistableBundle();
65         extras.putInt(EXTRA_USER_ID, userId);
66         extras.putString(EXTRA_JOB_NAME_KEY, MIGRATION_COMPLETE_JOB_NAME);
67         JobInfo.Builder builder =
68                 new JobInfo.Builder(MIN_JOB_ID + userId, componentName)
69                         .setPeriodic(
70                                 sHealthConnectDeviceConfigManager
71                                         .getMigrationCompletionJobRunInterval())
72                         .setExtras(extras);
73 
74         HealthConnectDailyService.schedule(
75                 Objects.requireNonNull(context.getSystemService(JobScheduler.class))
76                         .forNamespace(MIGRATION_STATE_CHANGE_NAMESPACE),
77                 userId,
78                 builder.build());
79     }
80 
scheduleMigrationPauseJob(Context context, int userId)81     public static void scheduleMigrationPauseJob(Context context, int userId) {
82         if (!HealthConnectDeviceConfigManager.getInitialisedInstance()
83                 .isPauseStateChangeJobEnabled()) {
84             return;
85         }
86         ComponentName componentName = new ComponentName(context, HealthConnectDailyService.class);
87         final PersistableBundle extras = new PersistableBundle();
88         extras.putInt(EXTRA_USER_ID, userId);
89         extras.putString(EXTRA_JOB_NAME_KEY, MIGRATION_PAUSE_JOB_NAME);
90         JobInfo.Builder builder =
91                 new JobInfo.Builder(MIN_JOB_ID + userId, componentName)
92                         .setPeriodic(
93                                 sHealthConnectDeviceConfigManager.getMigrationPauseJobRunInterval())
94                         .setExtras(extras);
95         HealthConnectDailyService.schedule(
96                 Objects.requireNonNull(context.getSystemService(JobScheduler.class))
97                         .forNamespace(MIGRATION_STATE_CHANGE_NAMESPACE),
98                 userId,
99                 builder.build());
100     }
101 
102     /** Execute migration completion job */
executeMigrationCompletionJob(@onNull Context context)103     public static void executeMigrationCompletionJob(@NonNull Context context) {
104         if (!HealthConnectDeviceConfigManager.getInitialisedInstance()
105                 .isCompleteStateChangeJobEnabled()) {
106             return;
107         }
108         if (MigrationStateManager.getInitialisedInstance().getMigrationState()
109                 == MIGRATION_STATE_COMPLETE) {
110             return;
111         }
112         PreferenceHelper preferenceHelper = PreferenceHelper.getInstance();
113 
114         String currentStateStartTime = preferenceHelper.getPreference(CURRENT_STATE_START_TIME_KEY);
115 
116         // This is a fallback but should never happen.
117         if (Objects.isNull(currentStateStartTime)) {
118             preferenceHelper.insertOrReplacePreference(
119                     CURRENT_STATE_START_TIME_KEY, Instant.now().toString());
120             return;
121         }
122         Instant executionTime =
123                 Instant.parse(currentStateStartTime)
124                         .plusMillis(
125                                 MigrationStateManager.getInitialisedInstance().getMigrationState()
126                                                 == MIGRATION_STATE_IDLE
127                                         ? sHealthConnectDeviceConfigManager
128                                                 .getIdleStateTimeoutPeriod()
129                                                 .toMillis()
130                                         : sHealthConnectDeviceConfigManager
131                                                 .getNonIdleStateTimeoutPeriod()
132                                                 .toMillis())
133                         .minusMillis(sHealthConnectDeviceConfigManager.getExecutionTimeBuffer());
134 
135         if (MigrationStateManager.getInitialisedInstance().getMigrationState()
136                         == MIGRATION_STATE_ALLOWED
137                 || MigrationStateManager.getInitialisedInstance().getMigrationState()
138                         == MIGRATION_STATE_IN_PROGRESS) {
139             String allowedStateTimeout =
140                     MigrationStateManager.getInitialisedInstance().getAllowedStateTimeout();
141             if (!Objects.isNull(allowedStateTimeout)) {
142                 Instant parsedAllowedStateTimeout =
143                         Instant.parse(allowedStateTimeout)
144                                 .minusMillis(
145                                         sHealthConnectDeviceConfigManager.getExecutionTimeBuffer());
146                 executionTime =
147                         executionTime.isAfter(parsedAllowedStateTimeout)
148                                 ? parsedAllowedStateTimeout
149                                 : executionTime;
150             }
151         }
152 
153         if (Instant.now().isAfter(executionTime)) {
154             // TODO (b/278728774) fix race condition
155             MigrationStateManager.getInitialisedInstance()
156                     .updateMigrationState(context, MIGRATION_STATE_COMPLETE, true);
157         }
158     }
159 
160     /** Execute migration pausing job. */
executeMigrationPauseJob(@onNull Context context)161     public static void executeMigrationPauseJob(@NonNull Context context) {
162         if (!HealthConnectDeviceConfigManager.getInitialisedInstance()
163                 .isPauseStateChangeJobEnabled()) {
164             return;
165         }
166         if (MigrationStateManager.getInitialisedInstance().getMigrationState()
167                 != MIGRATION_STATE_IN_PROGRESS) {
168             return;
169         }
170         PreferenceHelper preferenceHelper = PreferenceHelper.getInstance();
171         String currentStateStartTime = preferenceHelper.getPreference(CURRENT_STATE_START_TIME_KEY);
172         // This is a fallback but should never happen.
173         if (Objects.isNull(currentStateStartTime)) {
174             preferenceHelper.insertOrReplacePreference(
175                     CURRENT_STATE_START_TIME_KEY, Instant.now().toString());
176             return;
177         }
178 
179         Instant executionTime =
180                 Instant.parse(currentStateStartTime)
181                         .plusMillis(
182                                 sHealthConnectDeviceConfigManager
183                                         .getInProgressStateTimeoutPeriod()
184                                         .toMillis())
185                         .minusMillis(sHealthConnectDeviceConfigManager.getExecutionTimeBuffer());
186 
187         if (Instant.now().isAfter(executionTime)) {
188             // If we move to ALLOWED from IN_PROGRESS, then we have reached the IN_PROGRESS_TIMEOUT
189             MigrationStateManager.getInitialisedInstance()
190                     .updateMigrationState(
191                             context, MIGRATION_STATE_ALLOWED, /* timeoutReached= */ true);
192         }
193     }
194 
existsAStateChangeJob(@onNull Context context, @NonNull String jobName)195     public static boolean existsAStateChangeJob(@NonNull Context context, @NonNull String jobName) {
196         JobScheduler jobScheduler =
197                 Objects.requireNonNull(context.getSystemService(JobScheduler.class))
198                         .forNamespace(MIGRATION_STATE_CHANGE_NAMESPACE);
199         List<JobInfo> allJobs = jobScheduler.getAllPendingJobs();
200         for (JobInfo job : allJobs) {
201             if (jobName.equals(job.getExtras().getString(EXTRA_JOB_NAME_KEY))) {
202                 return true;
203             }
204         }
205         return false;
206     }
207 
208     /** Cancels old migration jobs that are persisted and were never canceled. */
209     // TODO(b/276415134): Code clean-up
cleanupOldPersistentMigrationJobs(@onNull Context context)210     static void cleanupOldPersistentMigrationJobs(@NonNull Context context) {
211         JobScheduler jobScheduler = context.getSystemService(JobScheduler.class);
212         Objects.requireNonNull(jobScheduler);
213 
214         List<JobInfo> allJobs = jobScheduler.getAllPendingJobs();
215         for (JobInfo job : allJobs) {
216             if (job.isPersisted()
217                     && job.getService()
218                             .equals(new ComponentName(context, HealthConnectDailyService.class))
219                     && !Objects.equals(
220                             job.getExtras().getString(EXTRA_JOB_NAME_KEY), HC_DAILY_JOB)) {
221                 jobScheduler.cancel(job.getId());
222             }
223         }
224     }
225 
cancelAllJobs(@onNull Context context)226     public static void cancelAllJobs(@NonNull Context context) {
227         JobScheduler jobScheduler =
228                 Objects.requireNonNull(context.getSystemService(JobScheduler.class))
229                         .forNamespace(MIGRATION_STATE_CHANGE_NAMESPACE);
230         jobScheduler.getAllPendingJobs().forEach(jobInfo -> jobScheduler.cancel(jobInfo.getId()));
231     }
232 }
233