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