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.exportimport; 18 19 import static android.health.connect.exportimport.ImportStatus.DATA_IMPORT_ERROR_NONE; 20 import static android.health.connect.exportimport.ImportStatus.DATA_IMPORT_ERROR_UNKNOWN; 21 import static android.health.connect.exportimport.ImportStatus.DATA_IMPORT_ERROR_VERSION_MISMATCH; 22 import static android.health.connect.exportimport.ImportStatus.DATA_IMPORT_ERROR_WRONG_FILE; 23 24 import static java.util.Objects.requireNonNull; 25 26 import android.annotation.NonNull; 27 import android.content.Context; 28 import android.database.sqlite.SQLiteDatabase; 29 import android.database.sqlite.SQLiteException; 30 import android.net.Uri; 31 import android.os.UserHandle; 32 import android.util.Slog; 33 34 import com.android.internal.annotations.VisibleForTesting; 35 import com.android.server.healthconnect.storage.ExportImportSettingsStorage; 36 import com.android.server.healthconnect.storage.HealthConnectDatabase; 37 import com.android.server.healthconnect.storage.TransactionManager; 38 39 import java.io.File; 40 import java.io.FileNotFoundException; 41 42 /** 43 * Manages import related tasks. 44 * 45 * @hide 46 */ 47 public final class ImportManager { 48 49 @VisibleForTesting static final String IMPORT_DATABASE_DIR_NAME = "export_import"; 50 51 @VisibleForTesting static final String IMPORT_DATABASE_FILE_NAME = "health_connect_import.db"; 52 53 private static final String TAG = "HealthConnectImportManager"; 54 55 private final Context mContext; 56 private final DatabaseMerger mDatabaseMerger; 57 58 @SuppressWarnings("NullAway") // TODO(b/317029272): fix this suppression ImportManager(@onNull Context context)59 public ImportManager(@NonNull Context context) { 60 requireNonNull(context); 61 mContext = context; 62 mDatabaseMerger = new DatabaseMerger(context); 63 } 64 65 /** Reads and merges the backup data from a local file. */ runImport(UserHandle userHandle, Uri file)66 public synchronized void runImport(UserHandle userHandle, Uri file) { 67 Slog.i(TAG, "Import started."); 68 ExportImportSettingsStorage.setImportOngoing(true); 69 DatabaseContext dbContext = 70 DatabaseContext.create(mContext, IMPORT_DATABASE_DIR_NAME, userHandle); 71 File importDbFile = dbContext.getDatabasePath(IMPORT_DATABASE_FILE_NAME); 72 73 try { 74 try { 75 Compressor.decompress(new File(file.getPath()), importDbFile); 76 Slog.i(TAG, "Import file unzipped: " + importDbFile.getAbsolutePath()); 77 } catch (Exception e) { 78 Slog.e( 79 TAG, 80 "Failed to get copy to destination: " + importDbFile.getAbsolutePath(), 81 e); 82 ExportImportSettingsStorage.setLastImportError(DATA_IMPORT_ERROR_UNKNOWN); 83 return; 84 } 85 86 try { 87 if (canMerge(importDbFile)) { 88 HealthConnectDatabase stagedDatabase = 89 new HealthConnectDatabase(dbContext, IMPORT_DATABASE_FILE_NAME); 90 mDatabaseMerger.merge(stagedDatabase); 91 } 92 } catch (SQLiteException e) { 93 Slog.i(TAG, "Import failed, not a database: " + e); 94 ExportImportSettingsStorage.setLastImportError(DATA_IMPORT_ERROR_WRONG_FILE); 95 return; 96 } catch (IllegalStateException e) { 97 Slog.i(TAG, "Import failed: " + e); 98 ExportImportSettingsStorage.setLastImportError(DATA_IMPORT_ERROR_VERSION_MISMATCH); 99 return; 100 } catch (Exception e) { 101 Slog.i(TAG, "Import failed: " + e); 102 ExportImportSettingsStorage.setLastImportError(DATA_IMPORT_ERROR_UNKNOWN); 103 return; 104 } 105 106 ExportImportSettingsStorage.setLastImportError(DATA_IMPORT_ERROR_NONE); 107 Slog.i(TAG, "Import completed"); 108 } finally { 109 // Delete the staged db as we are done merging. 110 Slog.i(TAG, "Deleting staged db after merging"); 111 SQLiteDatabase.deleteDatabase(importDbFile); 112 113 ExportImportSettingsStorage.setImportOngoing(false); 114 } 115 } 116 canMerge(File importDbFile)117 private boolean canMerge(File importDbFile) 118 throws FileNotFoundException, IllegalStateException, SQLiteException { 119 int currentDbVersion = TransactionManager.getInitialisedInstance().getDatabaseVersion(); 120 if (importDbFile.exists()) { 121 try (SQLiteDatabase importDb = 122 SQLiteDatabase.openDatabase( 123 importDbFile, new SQLiteDatabase.OpenParams.Builder().build())) { 124 int stagedDbVersion = importDb.getVersion(); 125 Slog.i( 126 TAG, 127 "merging staged data, current version = " 128 + currentDbVersion 129 + ", staged version = " 130 + stagedDbVersion); 131 if (currentDbVersion < stagedDbVersion) { 132 throw new IllegalStateException("Module needs upgrade for merging to version."); 133 } 134 } 135 } else { 136 throw new FileNotFoundException("No database file found to merge."); 137 } 138 139 Slog.i(TAG, "File can be merged."); 140 return true; 141 } 142 } 143