• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.CountDownLatch;
40 import java.util.concurrent.Executors;
41 import java.util.concurrent.TimeUnit;
42 import java.util.concurrent.TimeoutException;
43 
44 /**
45  * An intermediary to help with the transfer of HealthConnect data during device-to-device transfer.
46  */
47 public class HealthConnectBackupAgent extends BackupAgent {
48     private static final String TAG = "HealthConnectBackupAgent";
49     private static final String BACKUP_DATA_DIR_NAME = "backup_data";
50     private static final boolean DEBUG = false;
51 
52     private HealthConnectManager mHealthConnectManager;
53 
54     @Override
onCreate()55     public void onCreate() {
56         if (DEBUG) {
57             Slog.v(TAG, "onCreate()");
58         }
59 
60         mHealthConnectManager = getHealthConnectService();
61     }
62 
63     @Override
onFullBackup(FullBackupDataOutput data)64     public void onFullBackup(FullBackupDataOutput data) throws IOException {
65         Map<String, ParcelFileDescriptor> pfdsByFileName = new ArrayMap<>();
66         Set<String> backupFileNames =
67                 mHealthConnectManager.getAllBackupFileNames(
68                         (data.getTransportFlags() & FLAG_DEVICE_TO_DEVICE_TRANSFER) != 0);
69         File backupDataDir = getBackupDataDir();
70         backupFileNames.forEach(
71                 (fileName) -> {
72                     File file = new File(backupDataDir, fileName);
73                     try {
74                         file.createNewFile();
75                         pfdsByFileName.put(
76                                 fileName,
77                                 ParcelFileDescriptor.open(
78                                         file, ParcelFileDescriptor.MODE_WRITE_ONLY));
79                     } catch (IOException e) {
80                         Slog.e(TAG, "Unable to backup " + fileName, e);
81                     }
82                 });
83 
84         mHealthConnectManager.getAllDataForBackup(pfdsByFileName);
85 
86         File[] backupFiles = backupDataDir.listFiles(file -> !file.isDirectory());
87         for (var file : backupFiles) {
88             backupFile(file, data);
89         }
90 
91         deleteBackupFiles();
92     }
93 
94     @Override
onRestoreFinished()95     public void onRestoreFinished() {
96         Slog.v(TAG, "Staging all of HC data");
97         Map<String, ParcelFileDescriptor> pfdsByFileName = new ArrayMap<>();
98         File[] filesToTransfer = getBackupDataDir().listFiles();
99 
100         // We work with a flat dir structure where all files to be transferred are sitting in this
101         // dir itself.
102         for (var file : filesToTransfer) {
103             try {
104                 pfdsByFileName.put(file.getName(), ParcelFileDescriptor.open(file, MODE_READ_ONLY));
105             } catch (Exception e) {
106                 // this should never happen as we are reading files from our own dir on the disk.
107                 Slog.e(TAG, "Unable to open restored file from disk.", e);
108             }
109         }
110 
111         CountDownLatch latch = new CountDownLatch(1);
112         mHealthConnectManager.stageAllHealthConnectRemoteData(
113                 pfdsByFileName,
114                 Executors.newSingleThreadExecutor(),
115                 new OutcomeReceiver<>() {
116                     @Override
117                     public void onResult(Void result) {
118                         Slog.i(TAG, "Backup data successfully staged. Deleting all files.");
119                         deleteBackupFiles();
120                         latch.countDown();
121                     }
122 
123                     @Override
124                     public void onError(@NonNull StageRemoteDataException err) {
125                         for (var fileNameToException : err.getExceptionsByFileNames().entrySet()) {
126                             Slog.w(
127                                     TAG,
128                                     "Failed staging Backup file: "
129                                             + fileNameToException.getKey()
130                                             + " with error: "
131                                             + fileNameToException.getValue());
132                         }
133                         deleteBackupFiles();
134                         latch.countDown();
135                     }
136                 });
137 
138         try {
139             boolean callbackCalled = latch.await(10, TimeUnit.SECONDS);
140             if (!callbackCalled) {
141                 throw new TimeoutException();
142             }
143         } catch (InterruptedException | TimeoutException e) {
144             Slog.e(TAG, "Exception while waiting for callback, Files might not be deleted", e);
145         }
146 
147         // close the FDs
148         for (var pfdToFileName : pfdsByFileName.entrySet()) {
149             try {
150                 pfdToFileName.getValue().close();
151             } catch (IOException e) {
152                 Slog.e(TAG, "Unable to close restored file from disk.", e);
153             }
154         }
155     }
156 
157     @Override
onBackup( ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState)158     public void onBackup(
159             ParcelFileDescriptor oldState, BackupDataOutput data, ParcelFileDescriptor newState) {
160         // we don't do incremental backup / restore.
161     }
162 
163     @Override
onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState)164     public void onRestore(BackupDataInput data, int appVersionCode, ParcelFileDescriptor newState) {
165         // we don't do incremental backup / restore.
166     }
167 
168     @VisibleForTesting
getBackupDataDir()169     File getBackupDataDir() {
170         File backupDataDir = new File(this.getFilesDir(), BACKUP_DATA_DIR_NAME);
171         backupDataDir.mkdirs();
172         return backupDataDir;
173     }
174 
175     @VisibleForTesting
getHealthConnectService()176     HealthConnectManager getHealthConnectService() {
177         return this.getSystemService(HealthConnectManager.class);
178     }
179 
180     @VisibleForTesting
deleteBackupFiles()181     void deleteBackupFiles() {
182         Slog.i(TAG, "Deleting all files.");
183         File[] filesToTransfer = getBackupDataDir().listFiles();
184         for (var file : filesToTransfer) {
185             file.delete();
186         }
187     }
188 
189     @VisibleForTesting
backupFile(File file, FullBackupDataOutput data)190     void backupFile(File file, FullBackupDataOutput data) {
191         fullBackupFile(file, data);
192     }
193 
194     @Override
onDestroy()195     public void onDestroy() {
196         super.onDestroy();
197         Slog.i(TAG, "onDestroy.");
198     }
199 }
200