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