1 /* 2 * Copyright (C) 2024 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.storage; 18 19 import static android.health.connect.Constants.DEFAULT_INT; 20 import static android.health.connect.exportimport.ScheduledExportStatus.DATA_EXPORT_ERROR_UNSPECIFIED; 21 22 import static com.android.healthfitness.flags.Flags.exportImportFastFollow; 23 24 import android.annotation.Nullable; 25 import android.content.ContentProviderClient; 26 import android.content.Context; 27 import android.database.Cursor; 28 import android.health.connect.exportimport.ImportStatus; 29 import android.health.connect.exportimport.ScheduledExportSettings; 30 import android.health.connect.exportimport.ScheduledExportStatus; 31 import android.health.connect.exportimport.ScheduledExportStatus.DataExportError; 32 import android.net.Uri; 33 import android.os.RemoteException; 34 import android.provider.DocumentsContract; 35 import android.provider.OpenableColumns; 36 import android.util.Slog; 37 38 import com.android.server.healthconnect.storage.datatypehelpers.PreferenceHelper; 39 40 import java.time.Instant; 41 42 /** 43 * Stores the settings for the scheduled export service, including settings that are exposed to the 44 * UI via ScheduledExportStatus and values used exclusively by the system server for logic and 45 * logging. 46 * 47 * @hide 48 */ 49 public final class ExportImportSettingsStorage { 50 // Scheduled Export Settings 51 public static final String EXPORT_URI_PREFERENCE_KEY = "export_uri_key"; 52 public static final String EXPORT_PERIOD_PREFERENCE_KEY = "export_period_key"; 53 54 // Scheduled Export State 55 private static final String LAST_SUCCESSFUL_EXPORT_PREFERENCE_KEY = 56 "last_successful_export_key"; 57 private static final String LAST_FAILED_EXPORT_PREFERENCE_KEY = "last_failed_export_key"; 58 private static final String LAST_EXPORT_ERROR_PREFERENCE_KEY = "last_export_error_key"; 59 private static final String LAST_SUCCESSFUL_EXPORT_URI_PREFERENCE_KEY = 60 "last_successful_export_uri_key"; 61 private static final String NEXT_EXPORT_SEQUENTIAL_NUMBER_PREFERENCE_KEY = 62 "next_export_sequential_number_key"; 63 64 // Import State 65 private static final String IMPORT_STATE_PREFERENCE_KEY = "import_state_key"; 66 67 private static final String EXPORT_REPEAT_ERROR_ON_RETRY_COUNT_KEY = 68 "repeat_error_on_retry_count"; 69 70 private static final String TAG = "HealthConnectExportImport"; 71 72 private final PreferenceHelper mPreferenceHelper; 73 ExportImportSettingsStorage(PreferenceHelper preferenceHelper)74 public ExportImportSettingsStorage(PreferenceHelper preferenceHelper) { 75 mPreferenceHelper = preferenceHelper; 76 } 77 78 /** 79 * Configures the settings for the scheduled export of Health Connect data. 80 * 81 * @param settings Settings to use for the scheduled export. Use null to clear the settings. 82 */ configure(@ullable ScheduledExportSettings settings)83 public void configure(@Nullable ScheduledExportSettings settings) { 84 if (settings != null) { 85 configureNonNull(settings); 86 } else { 87 clear(); 88 } 89 } 90 91 /** Configures the settings for the scheduled export of Health Connect data. */ configureNonNull(ScheduledExportSettings settings)92 private void configureNonNull(ScheduledExportSettings settings) { 93 if (settings.getUri() != null) { 94 Uri uri = settings.getUri(); 95 mPreferenceHelper.insertOrReplacePreference(EXPORT_URI_PREFERENCE_KEY, uri.toString()); 96 String lastExportError = 97 mPreferenceHelper.getPreference(LAST_EXPORT_ERROR_PREFERENCE_KEY); 98 if (lastExportError != null) { 99 mPreferenceHelper.removeKey(LAST_EXPORT_ERROR_PREFERENCE_KEY); 100 } 101 if (exportImportFastFollow()) { 102 String previousExportSequentialNumber = 103 mPreferenceHelper.getPreference( 104 NEXT_EXPORT_SEQUENTIAL_NUMBER_PREFERENCE_KEY); 105 if (previousExportSequentialNumber == null) { 106 mPreferenceHelper.insertOrReplacePreference( 107 NEXT_EXPORT_SEQUENTIAL_NUMBER_PREFERENCE_KEY, String.valueOf(1)); 108 } else { 109 int nextSequentialNumber = Integer.parseInt(previousExportSequentialNumber) + 1; 110 mPreferenceHelper.insertOrReplacePreference( 111 NEXT_EXPORT_SEQUENTIAL_NUMBER_PREFERENCE_KEY, 112 String.valueOf(nextSequentialNumber)); 113 } 114 } 115 } 116 117 if (settings.getPeriodInDays() != DEFAULT_INT) { 118 String periodInDays = String.valueOf(settings.getPeriodInDays()); 119 mPreferenceHelper.insertOrReplacePreference(EXPORT_PERIOD_PREFERENCE_KEY, periodInDays); 120 } 121 } 122 123 /** Clears the settings for the scheduled export of Health Connect data. */ clear()124 private void clear() { 125 mPreferenceHelper.removeKey(EXPORT_URI_PREFERENCE_KEY); 126 mPreferenceHelper.removeKey(EXPORT_PERIOD_PREFERENCE_KEY); 127 } 128 129 /** Gets scheduled export URI for exporting Health Connect data. */ getUri()130 public Uri getUri() { 131 String result = mPreferenceHelper.getPreference(EXPORT_URI_PREFERENCE_KEY); 132 if (result == null) throw new IllegalArgumentException("Export URI cannot be null."); 133 return Uri.parse(result); 134 } 135 136 /** Get the uri of the last successful export. */ getLastSuccessfulExportUri()137 public @Nullable Uri getLastSuccessfulExportUri() { 138 String result = mPreferenceHelper.getPreference(LAST_SUCCESSFUL_EXPORT_URI_PREFERENCE_KEY); 139 return result == null ? null : Uri.parse(result); 140 } 141 142 /** Get the time of the last successful export. */ getLastSuccessfulExportTime()143 public @Nullable Instant getLastSuccessfulExportTime() { 144 String result = mPreferenceHelper.getPreference(LAST_SUCCESSFUL_EXPORT_PREFERENCE_KEY); 145 return result == null ? null : Instant.ofEpochMilli(Long.parseLong(result)); 146 } 147 148 /** Gets scheduled export period for exporting Health Connect data. */ getScheduledExportPeriodInDays()149 public int getScheduledExportPeriodInDays() { 150 String result = mPreferenceHelper.getPreference(EXPORT_PERIOD_PREFERENCE_KEY); 151 152 if (result == null) return 0; 153 return Integer.parseInt(result); 154 } 155 156 /** Set the last successful export time for the currently configured export. */ setLastSuccessfulExport(Instant instant, Uri uri)157 public void setLastSuccessfulExport(Instant instant, Uri uri) { 158 mPreferenceHelper.insertOrReplacePreference( 159 LAST_SUCCESSFUL_EXPORT_PREFERENCE_KEY, String.valueOf(instant.toEpochMilli())); 160 mPreferenceHelper.removeKey(LAST_EXPORT_ERROR_PREFERENCE_KEY); 161 mPreferenceHelper.insertOrReplacePreference( 162 LAST_SUCCESSFUL_EXPORT_URI_PREFERENCE_KEY, uri.toString()); 163 } 164 165 /** Convenience method for getting the last recorded Export error. */ getLastExportError()166 public @DataExportError int getLastExportError() { 167 String lastError = mPreferenceHelper.getPreference(LAST_EXPORT_ERROR_PREFERENCE_KEY); 168 if (lastError != null) { 169 return Integer.parseInt(lastError); 170 } 171 return DATA_EXPORT_ERROR_UNSPECIFIED; 172 } 173 174 /** Set errors and time during the last failed export attempt. */ setLastExportError( @cheduledExportStatus.DataExportError int error, Instant instant)175 public void setLastExportError( 176 @ScheduledExportStatus.DataExportError int error, Instant instant) { 177 mPreferenceHelper.insertOrReplacePreference( 178 LAST_EXPORT_ERROR_PREFERENCE_KEY, String.valueOf(error)); 179 mPreferenceHelper.insertOrReplacePreference( 180 LAST_FAILED_EXPORT_PREFERENCE_KEY, String.valueOf(instant.toEpochMilli())); 181 } 182 183 /** Get the status of the currently scheduled export. */ getScheduledExportStatus(Context context)184 public ScheduledExportStatus getScheduledExportStatus(Context context) { 185 String lastExportTime = 186 mPreferenceHelper.getPreference(LAST_SUCCESSFUL_EXPORT_PREFERENCE_KEY); 187 String lastFailedExportTime = 188 mPreferenceHelper.getPreference(LAST_FAILED_EXPORT_PREFERENCE_KEY); 189 String lastExportError = mPreferenceHelper.getPreference(LAST_EXPORT_ERROR_PREFERENCE_KEY); 190 String periodInDays = mPreferenceHelper.getPreference(EXPORT_PERIOD_PREFERENCE_KEY); 191 String nextExportSequentialNumber = 192 exportImportFastFollow() 193 ? mPreferenceHelper.getPreference( 194 NEXT_EXPORT_SEQUENTIAL_NUMBER_PREFERENCE_KEY) 195 : String.valueOf(0); 196 197 String lastExportFileName = null; 198 String lastExportAppName = null; 199 String nextExportFileName = null; 200 String nextExportAppName = null; 201 202 String nextExportUriString = mPreferenceHelper.getPreference(EXPORT_URI_PREFERENCE_KEY); 203 if (nextExportUriString != null) { 204 Uri uri = Uri.parse(nextExportUriString); 205 nextExportAppName = getExportAppName(context, uri); 206 nextExportFileName = getExportFileName(context, uri); 207 } 208 209 String lastSuccessfulExportUriString = 210 mPreferenceHelper.getPreference(LAST_SUCCESSFUL_EXPORT_URI_PREFERENCE_KEY); 211 if (lastSuccessfulExportUriString != null) { 212 Uri uri = Uri.parse(lastSuccessfulExportUriString); 213 lastExportAppName = getExportAppName(context, uri); 214 lastExportFileName = getExportFileName(context, uri); 215 } 216 217 return new ScheduledExportStatus.Builder() 218 .setLastSuccessfulExportTime( 219 lastExportTime == null 220 ? null 221 : Instant.ofEpochMilli(Long.parseLong(lastExportTime))) 222 .setLastFailedExportTime( 223 lastFailedExportTime == null 224 ? null 225 : Instant.ofEpochMilli(Long.parseLong(lastFailedExportTime))) 226 .setDataExportError( 227 lastExportError == null 228 ? ScheduledExportStatus.DATA_EXPORT_ERROR_NONE 229 : Integer.parseInt(lastExportError)) 230 .setNextExportSequentialNumber( 231 nextExportSequentialNumber == null 232 ? 0 233 : Integer.parseInt(nextExportSequentialNumber)) 234 .setPeriodInDays(periodInDays == null ? 0 : Integer.parseInt(periodInDays)) 235 .setLastExportFileName(lastExportFileName) 236 .setLastExportAppName(lastExportAppName) 237 .setNextExportFileName(nextExportFileName) 238 .setNextExportAppName(nextExportAppName) 239 .build(); 240 } 241 242 /** 243 * Get the number of times that the current export has failed with the same error message during 244 * retries. 245 */ getExportRepeatErrorOnRetryCount()246 public int getExportRepeatErrorOnRetryCount() { 247 String repeatErrorOnRetry_count = 248 mPreferenceHelper.getPreference(EXPORT_REPEAT_ERROR_ON_RETRY_COUNT_KEY); 249 return (repeatErrorOnRetry_count == null) 250 ? 0 251 : Integer.parseInt(repeatErrorOnRetry_count); 252 } 253 254 /** 255 * Increase the number of times that the current export has failed with the same error message 256 * during retries. Should be called when an export fails during retries. 257 */ increaseExportRepeatErrorOnRetryCount()258 public void increaseExportRepeatErrorOnRetryCount() { 259 String repeatErrorOnRetry_count = 260 mPreferenceHelper.getPreference(EXPORT_REPEAT_ERROR_ON_RETRY_COUNT_KEY); 261 int count = 262 (repeatErrorOnRetry_count == null) 263 ? 0 264 : Integer.parseInt(repeatErrorOnRetry_count); 265 count++; 266 mPreferenceHelper.insertOrReplacePreference( 267 EXPORT_REPEAT_ERROR_ON_RETRY_COUNT_KEY, String.valueOf(count)); 268 } 269 270 /** 271 * Reset the count of times that the current export has failed with the same error message 272 * during retries. Should be called when an export succeeds, when the current error message does 273 * not match the previous error message, when export settings are changed by the user and when 274 * retries finish/a new regular export is scheduled. 275 */ resetExportRepeatErrorOnRetryCount()276 public void resetExportRepeatErrorOnRetryCount() { 277 mPreferenceHelper.removeKey(EXPORT_REPEAT_ERROR_ON_RETRY_COUNT_KEY); 278 } 279 280 /** Set the state of an import to started, success or an error of the last import attempt. */ setImportState(@mportStatus.DataImportState int state)281 public void setImportState(@ImportStatus.DataImportState int state) { 282 mPreferenceHelper.insertOrReplacePreference( 283 IMPORT_STATE_PREFERENCE_KEY, String.valueOf(state)); 284 } 285 286 /** Get the status of the last data import. */ getImportStatus()287 public ImportStatus getImportStatus() { 288 String lastImportState = mPreferenceHelper.getPreference(IMPORT_STATE_PREFERENCE_KEY); 289 290 return new ImportStatus( 291 lastImportState == null 292 ? ImportStatus.DATA_IMPORT_ERROR_NONE 293 : Integer.parseInt(lastImportState)); 294 } 295 296 /** Get the file name of either the last or the next export, depending on the passed uri. */ getExportFileName(Context context, Uri destinationUri)297 private static @Nullable String getExportFileName(Context context, Uri destinationUri) { 298 try (Cursor cursor = 299 context.getContentResolver().query(destinationUri, null, null, null, null)) { 300 if (cursor != null && cursor.moveToFirst()) { 301 return cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME)); 302 } 303 } catch (IllegalArgumentException exception) { 304 Slog.i(TAG, "Failed to get the file name", exception); 305 } 306 return null; 307 } 308 309 /** Get the app name of either the last or the next export, depending on the passed uri. */ getExportAppName(Context context, Uri destinationUri)310 private static @Nullable String getExportAppName(Context context, Uri destinationUri) { 311 try (ContentProviderClient contentProviderClient = 312 context.getContentResolver().acquireUnstableContentProviderClient(destinationUri)) { 313 if (contentProviderClient != null) { 314 Uri rootsUri = DocumentsContract.buildRootsUri(destinationUri.getAuthority()); 315 try (Cursor contentProviderCursor = 316 contentProviderClient.query(rootsUri, null, null, null, null)) { 317 if (contentProviderCursor != null && contentProviderCursor.moveToFirst()) { 318 String appName = 319 contentProviderCursor.getString( 320 contentProviderCursor.getColumnIndexOrThrow( 321 DocumentsContract.Root.COLUMN_TITLE)); 322 return appName; 323 } 324 } 325 } 326 } catch (RemoteException | SecurityException | IllegalArgumentException exception) { 327 Slog.e(TAG, "Failed to get the app name", exception); 328 } 329 return null; 330 } 331 } 332