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