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