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