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