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