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