• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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