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; 18 19 import static android.health.connect.Constants.DEFAULT_INT; 20 21 import static com.android.server.healthconnect.HealthConnectDailyJobs.HC_DAILY_JOB; 22 import static com.android.server.healthconnect.exportimport.ExportImportJobs.PERIODIC_EXPORT_JOB_NAME; 23 import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_COMPLETE_JOB_NAME; 24 import static com.android.server.healthconnect.migration.MigrationConstants.MIGRATION_PAUSE_JOB_NAME; 25 26 import android.annotation.Nullable; 27 import android.app.job.JobInfo; 28 import android.app.job.JobParameters; 29 import android.app.job.JobScheduler; 30 import android.app.job.JobService; 31 import android.content.Context; 32 import android.health.connect.Constants; 33 import android.os.UserHandle; 34 import android.util.Slog; 35 36 import com.android.server.healthconnect.exportimport.ExportImportJobs; 37 import com.android.server.healthconnect.exportimport.ExportManager; 38 import com.android.server.healthconnect.injector.HealthConnectInjector; 39 import com.android.server.healthconnect.logging.EcosystemStatsCollector; 40 import com.android.server.healthconnect.logging.UsageStatsCollector; 41 import com.android.server.healthconnect.migration.MigrationStateChangeJob; 42 import com.android.server.healthconnect.migration.MigrationStateManager; 43 import com.android.server.healthconnect.storage.DailyCleanupJob; 44 import com.android.server.healthconnect.storage.ExportImportSettingsStorage; 45 import com.android.server.healthconnect.storage.HealthConnectContext; 46 import com.android.server.healthconnect.storage.datatypehelpers.DatabaseStatsCollector; 47 import com.android.server.healthconnect.storage.datatypehelpers.PreferenceHelper; 48 49 import java.util.Objects; 50 51 /** 52 * Health Connect wrapper around JobService. 53 * 54 * @hide 55 */ 56 public class HealthConnectDailyService extends JobService { 57 public static final String EXTRA_USER_ID = "user_id"; 58 public static final String EXTRA_JOB_NAME_KEY = "job_name"; 59 private static final String TAG = "HealthConnectDailyService"; 60 @Nullable private static volatile UserHandle sUserHandle; 61 62 /** 63 * Routes the job to the right place based on the job name, after performing common checks., 64 * 65 * <p>Please handle exceptions for each task within the task. Do not crash the job as it might 66 * result in failure of other tasks being triggered from the job. 67 */ 68 @Override onStartJob(JobParameters params)69 public boolean onStartJob(JobParameters params) { 70 int userId = params.getExtras().getInt(EXTRA_USER_ID, /* defaultValue= */ DEFAULT_INT); 71 String jobName = params.getExtras().getString(EXTRA_JOB_NAME_KEY); 72 Context context = getApplicationContext(); 73 if (userId == DEFAULT_INT || sUserHandle == null || userId != sUserHandle.getIdentifier()) { 74 // This job is no longer valid, the service for this user should have been stopped. 75 // Just ignore this request in case we still got the request. 76 return false; 77 } 78 79 if (Objects.isNull(jobName)) { 80 return false; 81 } 82 83 HealthConnectInjector healthConnectInjector = HealthConnectInjector.getInstance(); 84 DailyCleanupJob dailyCleanupJob = healthConnectInjector.getDailyCleanupJob(); 85 ExportImportSettingsStorage exportImportSettingsStorage = 86 healthConnectInjector.getExportImportSettingsStorage(); 87 ExportManager exportManager = healthConnectInjector.getExportManager(); 88 PreferenceHelper preferenceHelper = healthConnectInjector.getPreferenceHelper(); 89 MigrationStateManager migrationStateManager = 90 healthConnectInjector.getMigrationStateManager(); 91 UsageStatsCollector usageStatsCollector = 92 healthConnectInjector.getUsageStatsCollector( 93 HealthConnectContext.create( 94 context, 95 sUserHandle, 96 /* databaseDirName= */ null, 97 healthConnectInjector.getEnvironmentDataDirectory())); 98 DatabaseStatsCollector databaseStatsCollector = 99 healthConnectInjector.getDatabaseStatsCollector(); 100 EcosystemStatsCollector ecosystemStatsCollector = 101 new EcosystemStatsCollector( 102 healthConnectInjector.getReadAccessLogsHelper(), 103 healthConnectInjector.getChangeLogsHelper()); 104 HealthConnectThreadScheduler threadScheduler = healthConnectInjector.getThreadScheduler(); 105 106 // This service executes each incoming job on a Handler running on the application's 107 // main thread. This means that we must offload the execution logic to background executor. 108 switch (jobName) { 109 case HC_DAILY_JOB: 110 threadScheduler.scheduleInternalTask( 111 () -> { 112 HealthConnectDailyJobs.execute( 113 usageStatsCollector, 114 databaseStatsCollector, 115 dailyCleanupJob, 116 ecosystemStatsCollector, 117 healthConnectInjector.getHealthFitnessStatsLog()); 118 jobFinished(params, false); 119 }); 120 return true; 121 case MIGRATION_COMPLETE_JOB_NAME: 122 threadScheduler.scheduleInternalTask( 123 () -> { 124 MigrationStateChangeJob.executeMigrationCompletionJob( 125 context, preferenceHelper, migrationStateManager); 126 jobFinished(params, false); 127 }); 128 return true; 129 case MIGRATION_PAUSE_JOB_NAME: 130 threadScheduler.scheduleInternalTask( 131 () -> { 132 MigrationStateChangeJob.executeMigrationPauseJob( 133 context, preferenceHelper, migrationStateManager); 134 jobFinished(params, false); 135 }); 136 return true; 137 case PERIODIC_EXPORT_JOB_NAME: 138 threadScheduler.scheduleInternalTask( 139 () -> { 140 boolean isExportSuccessful = 141 ExportImportJobs.executePeriodicExportJob( 142 context, 143 Objects.requireNonNull(sUserHandle), 144 params.getExtras(), 145 exportManager, 146 exportImportSettingsStorage); 147 // If the export is not successful, reschedule the job. 148 jobFinished(params, !isExportSuccessful); 149 // TODO(b/374702524) distinguish between a new job and a retry. 150 // Call exportImportSettingsStorage.resetExportRepeatErrorOnRetryCount() 151 // for new jobs. Like that we can filter out repeat errors for each of 152 // the regular (weekly, daily etc) exports. 153 }); 154 return true; 155 default: 156 Slog.w(TAG, "Job name " + jobName + " is not supported."); 157 break; 158 } 159 return false; 160 } 161 162 /** Called when job needs to be stopped. Don't do anything here and let the job be killed. */ 163 @Override onStopJob(JobParameters params)164 public boolean onStopJob(JobParameters params) { 165 return false; 166 } 167 168 /** Start periodically scheduling this service for {@code userId}. */ schedule(JobScheduler jobScheduler, UserHandle userHandle, JobInfo jobInfo)169 public static void schedule(JobScheduler jobScheduler, UserHandle userHandle, JobInfo jobInfo) { 170 Objects.requireNonNull(jobScheduler); 171 sUserHandle = userHandle; 172 173 int result = jobScheduler.schedule(jobInfo); 174 if (result != JobScheduler.RESULT_SUCCESS) { 175 Slog.e( 176 TAG, 177 "Failed to schedule the job: " 178 + jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY)); 179 } else if (Constants.DEBUG) { 180 Slog.d( 181 TAG, 182 "Scheduled a job successfully: " 183 + jobInfo.getExtras().getString(EXTRA_JOB_NAME_KEY)); 184 } 185 } 186 } 187