Skip to content

Commit 03342c5

Browse files
committed
backup: add support for ADB backup
A new backup option, "ADB data", has been added. This option supersedes the "Internal data" option and the "External data" option. If the option is enabled, an AB formatted backup is taken instead of internal or external storage backup. The backup file takes the usual data<numeric> filename with .ab extension (and other extensions depending on the encryption method). Signed-off-by: Muntashir Al-Islam <muntashirakon@riseup.net>
1 parent 30ce0c1 commit 03342c5

17 files changed

Lines changed: 290 additions & 210 deletions

File tree

app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupFlags.java

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@
1818
import java.util.List;
1919
import java.util.Objects;
2020

21+
import io.github.muntashirakon.AppManager.BuildConfig;
2122
import io.github.muntashirakon.AppManager.R;
23+
import io.github.muntashirakon.AppManager.compat.ManifestCompat;
2224
import io.github.muntashirakon.AppManager.self.SelfPermissions;
2325
import io.github.muntashirakon.AppManager.settings.Prefs;
2426
import io.github.muntashirakon.AppManager.users.Users;
@@ -32,14 +34,14 @@ public final class BackupFlags {
3234
BACKUP_APK_FILES,
3335
BACKUP_INT_DATA,
3436
BACKUP_EXT_DATA,
37+
BACKUP_ADB_DATA,
3538
BACKUP_EXT_OBB_MEDIA,
3639
BACKUP_EXCLUDE_CACHE,
3740
BACKUP_EXTRAS,
3841
BACKUP_CACHE,
3942
BACKUP_MULTIPLE,
4043
BACKUP_RULES,
4144
BACKUP_NO_SIGNATURE_CHECK,
42-
BACKUP_ADB_DATA,
4345
})
4446
@Retention(RetentionPolicy.SOURCE)
4547
public @interface BackupFlag {
@@ -67,6 +69,7 @@ public final class BackupFlags {
6769
put(BACKUP_APK_FILES, new Pair<>(R.string.backup_apk_files, R.string.backup_apk_files_description));
6870
put(BACKUP_INT_DATA, new Pair<>(R.string.internal_data, R.string.backup_internal_data_description));
6971
put(BACKUP_EXT_DATA, new Pair<>(R.string.external_data, R.string.backup_external_data_description));
72+
put(BACKUP_ADB_DATA, new Pair<>(R.string.adb_data, R.string.adb_data_description));
7073
put(BACKUP_EXT_OBB_MEDIA, new Pair<>(R.string.backup_obb_media, R.string.backup_obb_media_description));
7174
put(BACKUP_CACHE, new Pair<>(R.string.backup_cache, R.string.backup_cache_description));
7275
put(BACKUP_EXTRAS, new Pair<>(R.string.backup_extras, R.string.backup_extras_description));
@@ -86,13 +89,17 @@ public static int getSupportedBackupFlags() {
8689
return flags;
8790
}
8891

92+
@NonNull
8993
public static List<Integer> getSupportedBackupFlagsAsArray() {
9094
List<Integer> backupFlags = new ArrayList<>();
9195
backupFlags.add(BACKUP_APK_FILES);
9296
if (SelfPermissions.canWriteToDataData()) {
9397
backupFlags.add(BACKUP_INT_DATA);
9498
}
9599
backupFlags.add(BACKUP_EXT_DATA);
100+
if (BuildConfig.DEBUG && SelfPermissions.checkSelfOrRemotePermission(ManifestCompat.permission.BACKUP)) {
101+
backupFlags.add(BACKUP_ADB_DATA);
102+
}
96103
backupFlags.add(BACKUP_EXT_OBB_MEDIA);
97104
backupFlags.add(BACKUP_CACHE);
98105
backupFlags.add(BACKUP_EXTRAS);
@@ -119,6 +126,9 @@ public static List<Integer> getBackupFlagsAsArray(@BackupFlag int flags) {
119126
if ((flags & BACKUP_EXT_DATA) != 0) {
120127
backupFlags.add(BACKUP_EXT_DATA);
121128
}
129+
if ((flags & BACKUP_ADB_DATA) != 0) {
130+
backupFlags.add(BACKUP_ADB_DATA);
131+
}
122132
if ((flags & BACKUP_EXT_OBB_MEDIA) != 0) {
123133
backupFlags.add(BACKUP_EXT_OBB_MEDIA);
124134
}
@@ -214,12 +224,16 @@ public boolean backupExternalData() {
214224
return (mFlags & BACKUP_EXT_DATA) != 0;
215225
}
216226

227+
public boolean backupAdbData() {
228+
return (mFlags & BACKUP_ADB_DATA) != 0;
229+
}
230+
217231
public boolean backupMediaObb() {
218232
return (mFlags & BACKUP_EXT_OBB_MEDIA) != 0;
219233
}
220234

221235
public boolean backupData() {
222-
return backupInternalData() || backupExternalData() || backupMediaObb();
236+
return backupInternalData() || backupExternalData() || backupAdbData() || backupMediaObb();
223237
}
224238

225239
public boolean backupRules() {
@@ -263,6 +277,10 @@ public CharSequence toLocalisedString(Context context) {
263277
sb.append(append ? "+" : "").append("Ext");
264278
append = true;
265279
}
280+
if (backupAdbData()) {
281+
sb.append(append ? "+" : "").append("ADB");
282+
append = true;
283+
}
266284
if (backupMediaObb()) {
267285
sb.append(append ? "+" : "").append("OBB");
268286
append = true;

app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupItems.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
package io.github.muntashirakon.AppManager.backup;
44

5-
import static io.github.muntashirakon.AppManager.backup.BackupManager.DATA_PREFIX;
65
import static io.github.muntashirakon.AppManager.backup.BackupManager.KEYSTORE_PREFIX;
76
import static io.github.muntashirakon.AppManager.backup.BackupManager.SOURCE_PREFIX;
87

@@ -356,14 +355,14 @@ public Path getInfoFile() throws IOException {
356355
} else return getBackupPath().findFile(MetadataManager.INFO_V5_FILE);
357356
}
358357

359-
public Path getMetadataV5File() throws IOException {
358+
public Path getMetadataV5File(boolean decryptIfRequired) throws IOException {
360359
if (mBackupMode) {
361360
// Needs to be encrypted in backup mode
362361
return getBackupPath().findOrCreateFile(MetadataManager.META_V5_FILE, null);
363362
} else {
364363
// Needs to be decrypted in restore mode
365364
Path file = getBackupPath().findFile(MetadataManager.META_V5_FILE + CryptoUtils.getExtension(mCryptoMode));
366-
return decrypt(new Path[]{file})[0];
365+
return decryptIfRequired ? decrypt(new Path[]{file})[0] : file;
367366
}
368367
}
369368

@@ -436,7 +435,7 @@ public Path[] getSourceFiles() {
436435
@NonNull
437436
public Path[] getDataFiles(int index) {
438437
String ext = CryptoUtils.getExtension(mCryptoMode);
439-
final String dataPrefix = DATA_PREFIX + index;
438+
final String dataPrefix = BackupUtils.getDataFilePrefix(index, null); // extension can be anything
440439
Path[] paths = getBackupPath().listFiles((dir, name) -> name.startsWith(dataPrefix) && name.endsWith(ext));
441440
return Paths.getSortedPaths(paths);
442441
}

app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupManager.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class BackupManager {
3434
public static final String DATA_PREFIX = "data";
3535
static final String KEYSTORE_PREFIX = "keystore";
3636
static final int KEYSTORE_PLACEHOLDER = -1000;
37+
static final String DATA_BACKUP_SPECIAL_PREFIX = "special:";
38+
static final String DATA_BACKUP_SPECIAL_ADB = DATA_BACKUP_SPECIAL_PREFIX + "adb";
3739

3840
public static final String CERT_PREFIX = "cert_";
3941
static final String MASTER_KEY = ".masterkey";

app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupOp.java

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
package io.github.muntashirakon.AppManager.backup;
44

55
import static io.github.muntashirakon.AppManager.backup.BackupManager.CERT_PREFIX;
6-
import static io.github.muntashirakon.AppManager.backup.BackupManager.DATA_PREFIX;
76
import static io.github.muntashirakon.AppManager.backup.BackupManager.KEYSTORE_PLACEHOLDER;
87
import static io.github.muntashirakon.AppManager.backup.BackupManager.KEYSTORE_PREFIX;
98
import static io.github.muntashirakon.AppManager.backup.BackupManager.MASTER_KEY;
@@ -22,6 +21,7 @@
2221
import android.content.pm.PermissionInfo;
2322
import android.graphics.Bitmap;
2423
import android.os.Build;
24+
import android.os.ParcelFileDescriptor;
2525
import android.os.RemoteException;
2626

2727
import androidx.annotation.NonNull;
@@ -47,6 +47,7 @@
4747
import io.github.muntashirakon.AppManager.apk.ApkSource;
4848
import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5;
4949
import io.github.muntashirakon.AppManager.compat.AppOpsManagerCompat;
50+
import io.github.muntashirakon.AppManager.compat.BackupCompat;
5051
import io.github.muntashirakon.AppManager.compat.DeviceIdleManagerCompat;
5152
import io.github.muntashirakon.AppManager.compat.ManifestCompat;
5253
import io.github.muntashirakon.AppManager.compat.NetworkPolicyManagerCompat;
@@ -76,6 +77,7 @@
7677
import io.github.muntashirakon.AppManager.utils.FreezeUtils;
7778
import io.github.muntashirakon.AppManager.utils.KeyStoreUtils;
7879
import io.github.muntashirakon.AppManager.utils.PackageUtils;
80+
import io.github.muntashirakon.AppManager.utils.ParcelFileDescriptorUtil;
7981
import io.github.muntashirakon.AppManager.utils.TarUtils;
8082
import io.github.muntashirakon.AppManager.utils.UIUtils;
8183
import io.github.muntashirakon.AppManager.utils.Utils;
@@ -244,8 +246,23 @@ public BackupMetadataV5 setupMetadataAndCrypto() throws CryptoException {
244246
metadata.versionName = mPackageInfo.versionName;
245247
metadata.versionCode = PackageInfoCompat.getLongVersionCode(mPackageInfo);
246248
metadata.apkName = new File(mApplicationInfo.sourceDir).getName();
247-
metadata.dataDirs = BackupUtils.getDataDirectories(mApplicationInfo, mBackupFlags.backupInternalData(),
248-
mBackupFlags.backupExternalData(), mBackupFlags.backupMediaObb());
249+
String[] dataDirs;
250+
if (mBackupFlags.backupAdbData()) {
251+
mBackupFlags.removeFlag(BackupFlags.BACKUP_INT_DATA);
252+
mBackupFlags.removeFlag(BackupFlags.BACKUP_EXT_DATA);
253+
List<String> defaultDirs = BackupUtils.getDataDirectories(mApplicationInfo, false,
254+
false, mBackupFlags.backupMediaObb());
255+
dataDirs = new String[defaultDirs.size() + 1];
256+
for (int i = 0; i < defaultDirs.size(); ++i) {
257+
dataDirs[i] = defaultDirs.get(i);
258+
}
259+
dataDirs[defaultDirs.size()] = BackupManager.DATA_BACKUP_SPECIAL_ADB;
260+
} else {
261+
// Non-ADB backup: default
262+
dataDirs = BackupUtils.getDataDirectories(mApplicationInfo, mBackupFlags.backupInternalData(),
263+
mBackupFlags.backupExternalData(), mBackupFlags.backupMediaObb()).toArray(new String[0]);
264+
}
265+
metadata.dataDirs = dataDirs;
249266
metadata.isSystem = (mApplicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
250267
metadata.isSplitApk = false;
251268
try (ApkFile apkFile = ApkSource.getApkSource(mApplicationInfo).resolve()) {
@@ -316,17 +333,15 @@ private void backupApkFiles() throws BackupException {
316333
}
317334

318335
private void backupData() throws BackupException {
319-
String sourceBackupFilePrefix;
320-
Path[] dataFiles;
321336
for (int i = 0; i < mMetadata.metadata.dataDirs.length; ++i) {
322-
sourceBackupFilePrefix = DATA_PREFIX + i + getExt(mMetadata.info.tarType);
323-
try {
324-
dataFiles = TarUtils.create(mMetadata.info.tarType, Paths.get(mMetadata.metadata.dataDirs[i]), mBackupItem.getUnencryptedBackupPath(),
325-
sourceBackupFilePrefix, null, null,
326-
BackupUtils.getExcludeDirs(!mBackupFlags.backupCache()), false)
327-
.toArray(new Path[0]);
328-
} catch (Throwable th) {
329-
throw new BackupException("Failed to backup data directory at " + mMetadata.metadata.dataDirs[i], th);
337+
Path[] dataFiles;
338+
String backupDataDir = mMetadata.metadata.dataDirs[i];
339+
if (backupDataDir.equals(BackupManager.DATA_BACKUP_SPECIAL_ADB)) {
340+
// ADB backup
341+
dataFiles = backupAdb(i);
342+
} else {
343+
// Regular directory backup
344+
dataFiles = backupDirectory(backupDataDir, i);
330345
}
331346
try {
332347
dataFiles = mBackupItem.encrypt(dataFiles);
@@ -339,6 +354,37 @@ private void backupData() throws BackupException {
339354
}
340355
}
341356

357+
@NonNull
358+
private Path[] backupDirectory(@NonNull String dir, int index) throws BackupException {
359+
String filePrefix = BackupUtils.getDataFilePrefix(index, getExt(mMetadata.info.tarType));
360+
try {
361+
return TarUtils.create(mMetadata.info.tarType, Paths.get(dir),
362+
mBackupItem.getUnencryptedBackupPath(),
363+
filePrefix, null, null,
364+
BackupUtils.getExcludeDirs(!mBackupFlags.backupCache()), false)
365+
.toArray(new Path[0]);
366+
} catch (Throwable th) {
367+
throw new BackupException("Failed to backup data directory at " + dir, th);
368+
}
369+
}
370+
371+
@NonNull
372+
private Path[] backupAdb(int index) throws BackupException {
373+
try {
374+
String filePrefix = BackupUtils.getDataFilePrefix(index, ".ab");
375+
Path abFile = mBackupItem.getUnencryptedBackupPath().createNewFile(filePrefix, null);
376+
try (OutputStream os = abFile.openOutputStream()) {
377+
ParcelFileDescriptor fd = ParcelFileDescriptorUtil.pipeTo(os);
378+
BackupCompat.adbBackup(mUserId, fd, false, false, false,
379+
false, false, false, false, true,
380+
new String[]{mPackageName});
381+
}
382+
return new Path[]{abFile};
383+
} catch (Throwable th) {
384+
throw new BackupException("Failed to backup ADB data.", th);
385+
}
386+
}
387+
342388
private void backupKeyStore() throws BackupException { // Called only when the app has an keystore item
343389
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
344390
// keystore v2 is not supported.

app/src/main/java/io/github/muntashirakon/AppManager/backup/BackupUtils.java

Lines changed: 7 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
import java.util.Objects;
2828
import java.util.regex.Pattern;
2929

30-
import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV2;
3130
import io.github.muntashirakon.AppManager.backup.struct.BackupMetadataV5;
3231
import io.github.muntashirakon.AppManager.db.entity.Backup;
3332
import io.github.muntashirakon.AppManager.db.utils.AppDb;
@@ -171,16 +170,6 @@ public static HashMap<String, Backup> getAllLatestBackupMetadataFromDb() {
171170
return backupMetadata;
172171
}
173172

174-
public static void putBackupToDbAndBroadcast(@NonNull Context context, @NonNull BackupMetadataV2 metadata) {
175-
if (Utils.isRoboUnitTest()) {
176-
return;
177-
}
178-
AppDb appDb = new AppDb();
179-
appDb.insert(Backup.fromBackupMetadata(metadata));
180-
appDb.updateApplication(context, metadata.packageName);
181-
BroadcastUtils.sendDbPackageAltered(context, new String[]{metadata.packageName});
182-
}
183-
184173
public static void putBackupToDbAndBroadcast(@NonNull Context context, @NonNull BackupMetadataV5 metadata) {
185174
if (Utils.isRoboUnitTest()) {
186175
return;
@@ -303,41 +292,12 @@ private static HashMap<String, List<BackupMetadataV5>> getAllMetadata() {
303292
return backupMetadata;
304293
}
305294

306-
@Nullable
307-
public static String getShortBackupName(@NonNull String backupFileName) {
308-
if (TextUtils.isDigitsOnly(backupFileName)) {
309-
// It's already a user handle
310-
return null;
311-
} else {
312-
int firstUnderscore = backupFileName.indexOf('_');
313-
if (firstUnderscore != -1) {
314-
// Found an underscore
315-
String userHandle = backupFileName.substring(0, firstUnderscore);
316-
if (TextUtils.isDigitsOnly(userHandle)) {
317-
// The new backup system
318-
return backupFileName.substring(firstUnderscore + 1);
319-
}
320-
}
321-
// Could be the old naming style
322-
throw new IllegalArgumentException("Invalid backup name " + backupFileName);
323-
}
324-
}
325-
326-
static int getUserHandleFromBackupName(@NonNull String backupFileName) {
327-
if (TextUtils.isDigitsOnly(backupFileName)) {
328-
return Integer.parseInt(backupFileName);
329-
} else {
330-
int firstUnderscore = backupFileName.indexOf('_');
331-
if (firstUnderscore != -1) {
332-
// Found an underscore
333-
String userHandle = backupFileName.substring(0, firstUnderscore);
334-
if (TextUtils.isDigitsOnly(userHandle)) {
335-
// The new backup system
336-
return Integer.parseInt(userHandle);
337-
}
338-
}
339-
throw new IllegalArgumentException("Invalid backup name " + backupFileName);
295+
@NonNull
296+
public static String getDataFilePrefix(int index, @Nullable String fullExtension) {
297+
if (fullExtension == null) {
298+
return BackupManager.DATA_PREFIX + index;
340299
}
300+
return BackupManager.DATA_PREFIX + index + fullExtension;
341301
}
342302

343303
@NonNull
@@ -355,7 +315,7 @@ static String[] getExcludeDirs(boolean includeCache, @Nullable String... others)
355315

356316
@SuppressLint("SdCardPath")
357317
@NonNull
358-
static String[] getDataDirectories(@NonNull ApplicationInfo applicationInfo, boolean loadInternal,
318+
static List<String> getDataDirectories(@NonNull ApplicationInfo applicationInfo, boolean loadInternal,
359319
boolean loadExternal, boolean loadMediaObb) {
360320
// Data directories *must* be readable and non-empty
361321
ArrayList<String> dataDirs = new ArrayList<>();
@@ -398,7 +358,7 @@ static String[] getDataDirectories(@NonNull ApplicationInfo applicationInfo, boo
398358
}
399359
}
400360
}
401-
return dataDirs.toArray(new String[0]);
361+
return dataDirs;
402362
}
403363

404364
/**

app/src/main/java/io/github/muntashirakon/AppManager/backup/MetadataManager.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public static BackupMetadataV5 readMetadata(@NonNull BackupItems.BackupItem back
8282
// Need to setup crypto in order to decrypt meta_v5.am.json
8383
setCrypto(backupItem, backupInfo);
8484
}
85-
Path metadataFile = v5AndUp ? backupItem.getMetadataV5File() : backupItem.getMetadataV2File();
85+
Path metadataFile = v5AndUp ? backupItem.getMetadataV5File(true) : backupItem.getMetadataV2File();
8686
String metadataString = metadataFile.getContentAsString();
8787
JSONObject jsonObject;
8888
if (TextUtils.isEmpty(metadataString)) {
@@ -137,14 +137,16 @@ private static Map<String, String> writeMetadataV2(@NonNull BackupMetadataV5 met
137137
@NonNull
138138
private static Map<String, String> writeMetadataV5(@NonNull BackupMetadataV5 metadata, @NonNull BackupItems.BackupItem backupFile) throws IOException {
139139
Map<String, String> filenameChecksumMap = new LinkedHashMap<>(2);
140-
Path metadataFile = backupFile.getMetadataV5File();
140+
Path metadataFile = backupFile.getMetadataV5File(true);
141141
try (OutputStream outputStream = metadataFile.openOutputStream()) {
142142
outputStream.write(metadata.metadata.serializeToJson().toString(4).getBytes());
143-
filenameChecksumMap.put(metadataFile.getName(), DigestUtils.getHexDigest(
144-
metadata.info.checksumAlgo, metadataFile));
145143
} catch (JSONException e) {
146144
throw new IOException(e.getMessage() + " for file " + metadataFile, e);
147145
}
146+
// Encrypt the metadata
147+
Path encryptedMetadataFile = backupFile.encrypt(new Path[]{metadataFile})[0];
148+
filenameChecksumMap.put(encryptedMetadataFile.getName(), DigestUtils.getHexDigest(
149+
metadata.info.checksumAlgo, encryptedMetadataFile));
148150
Path infoFile = backupFile.getInfoFile();
149151
try (OutputStream outputStream = infoFile.openOutputStream()) {
150152
outputStream.write(metadata.info.serializeToJson().toString(4).getBytes());

0 commit comments

Comments
 (0)