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.health.connect.backuprestore;
18 
19 import static android.os.ParcelFileDescriptor.MODE_READ_ONLY;
20 
21 import android.annotation.NonNull;
22 import android.app.backup.BackupAgent;
23 import android.app.backup.BackupDataInput;
24 import android.app.backup.BackupDataOutput;
25 import android.app.backup.FullBackupDataOutput;
26 import android.health.connect.HealthConnectManager;
27 import android.health.connect.restore.StageRemoteDataException;
28 import android.os.OutcomeReceiver;
29 import android.os.ParcelFileDescriptor;
30 import android.util.ArrayMap;
31 import android.util.Slog;
32 
33 import com.android.internal.annotations.VisibleForTesting;
34 
35 import java.io.File;
36 import java.io.IOException;
37 import java.util.Map;
38 import java.util.Set;
39 import java.util.concurrent.Executors;
40 
41 /**
42  * An intermediary to help with the transfer of HealthConnect data during device-to-device transfer.
43  */
44 public class HealthConnectBackupAgent extends BackupAgent {
45     private static final String TAG = "HealthConnectBackupAgent";
46     private static final String BACKUP_DATA_DIR_NAME = "backup_data";
47     private static final boolean DEBUG = false;
48 
49     private HealthConnectManager mHealthConnectManager;
50 
51     @Override
onCreate()52     public void onCreate() {
53         if (DEBUG) {
54             Slog.v(TAG, "onCreate()");
55         }
56 
57         mHealthConnectManager = getHealthConnectService();
58     }
59 
60     @Override
onFullBackup(FullBackupDataOutput data)61     public void onFullBackup(FullBackupDataOutput data) throws IOException {
62         Map<String, ParcelFileDescriptor> pfdsByFileName = new ArrayMap<>();
63         Set<String> backupFileNames =
64                 mHealthConnectManager.getAllBackupFileNames(
65                         (data.getTransportFlags() & FLAG_DEVICE_TO_DEVICE_TRANSFER) != 0);
66         File backupDataDir = getBackupDataDir();
67         backupFileNames.forEach(
68                 (fileName) -> {
69                     File file = new File(backupDataDir, fileName);
70                     try {
71                         file.createNewFile();
72                         pfdsByFileName.put(
73                                 fileName,
74                                 ParcelFileDescriptor.open(
75                                         file, ParcelFileDescriptor.MODE_WRITE_ONLY));
76                     } catch (IOException e) {
77                         Slog.e(TAG, "Unable to backup " + fileName, e);
78                     }
79                 });
80 
81         mHealthConnectManager.getAllDataForBackup(pfdsByFileName);
82 
83         File[] backupFiles = backupDataDir.listFiles(file -> !file.isDirectory());
84         for (var file : backupFiles) {
85             backupFile(file, data);
86         }
87 
88         deleteBackupFiles();
89     }
90 
91     @Override
onRestoreFinished()92     public void onRestoreFinished() {
93         Slog.v(TAG, "Staging all of HC data");
94         Map<String, ParcelFileDescriptor> pfdsByFileName = new ArrayMap<>();
95         File[] filesToTransfer = getBackupDataDir().listFiles();
96 
97         // We work with a flat dir structure where all files to be transferred are sitting in this
98         // dir itself.
99         for (var file : filesToTransfer) {
100             try {
101                 pfdsByFileName.put(file.getName(), ParcelFileDescriptor.open(file, MODE_READ_ONLY));
102             } catch (Exception e) {
103                 // this should never happen as we are reading files from our own dir on the disk.
104                 Slog.e(TAG, "Unable to open restored file from disk.", e);
105             }
106         }
107 
108         mHealthConnectManager.stageAllHealthConnectRemoteData(
109                 pfdsByFileName,
110                 Executors.newSingleThreadExecutor(),
111                 new OutcomeReceiver<>() {
112                     @Override
113                     public void onResult(Void result) {
114                         Slog.i(TAG, "Backup data successfully staged. Deleting all files.");
115                         deleteBackupFiles();
116                     }
117 
118                     @Override
119                     public void onError(@NonNull StageRemoteDataException err) {
120                         for (var fileNameToException : err.getExceptionsByFileNames().entrySet()) {
121                             Slog.w(
122                                     TAG,
123                                     "Failed staging Backup file: "
124                                             + fileNameToException.getKey()
125                                             + " with error: "
126                                             + fileNameToException.getValue());
127                         }
128                         deleteBackupFiles();
129                     }
130                 });
131 
132         // close the FDs
133         for (var pfdToFileName : pfdsByFileName.entrySet()) {
134             try {
135                 pfdToFileName.getValue().close();
136             } catch (IOException e) {
137                 Slog.e(TAG, "Unable to close restored file from disk.", e);
138             }
139         }
140     }
141 
142     @Override
onBackup( ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)143     public void onBackup(
144             ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) {
145         // we don't do incremental backup / restore.
146     }
147 
148     @Override
onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)149     public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) {
150         // we don't do incremental backup / restore.
151     }
152 
153     @VisibleForTesting
getBackupDataDir()154     File getBackupDataDir() {
155         File backupDataDir = new File(this.getFilesDir(), BACKUP_DATA_DIR_NAME);
156         backupDataDir.mkdirs();
157         return backupDataDir;
158     }
159 
160     @VisibleForTesting
getHealthConnectService()161     HealthConnectManager getHealthConnectService() {
162         return this.getSystemService(HealthConnectManager.class);
163     }
164 
165     @VisibleForTesting
deleteBackupFiles()166     void deleteBackupFiles() {
167         Slog.i(TAG, "Deleting all files.");
168         File[] filesToTransfer = getBackupDataDir().listFiles();
169         for (var file : filesToTransfer) {
170             file.delete();
171         }
172     }
173 
174     @VisibleForTesting
backupFile(File file, FullBackupDataOutput data)175     void backupFile(File file, FullBackupDataOutput data) {
176         fullBackupFile(file, data);
177     }
178 }
179