Android 11+ local restore fix

This commit is contained in:
Yuriy Liskov
2025-12-05 15:45:10 +02:00
parent 6202da0339
commit a47d7e937f
6 changed files with 328 additions and 8 deletions

View File

@@ -54,6 +54,12 @@ public class BackupSettingsPresenter extends BasePresenter<Void> {
createAndShowDialog();
}
public void showLocalRestoreDialog() {
BackupAndRestoreManager backupManager = new BackupAndRestoreManager(getContext());
backupManager.getBackupNames(names -> showLocalRestoreDialog(backupManager, names));
}
private void createAndShowDialog() {
AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext());

View File

@@ -12,6 +12,7 @@ import android.widget.Toast;
import androidx.core.content.FileProvider;
import com.liskovsoft.smartyoutubetv2.common.app.presenters.settings.BackupSettingsPresenter;
import com.liskovsoft.smartyoutubetv2.common.misc.MotherActivity.OnResult;
import java.io.File;
@@ -91,7 +92,7 @@ public class BackupAndRestoreHelper implements OnResult {
if (uri == null) return;
File zipFile = new File(mediaDir, "restore.zip");
copyUriToFile(uri, zipFile);
copyUriToDir(uri, zipFile);
// Cleanup previous data
deleteRecursive(dataDir);
@@ -127,12 +128,14 @@ public class BackupAndRestoreHelper implements OnResult {
copyUriToFile(zipUri, tempZip);
// Unpack ZIP to data folder
unzip(tempZip, dataDir);
unzip(tempZip, mediaDir);
// Delete the temporary ZIP
tempZip.delete();
Toast.makeText(mContext, "Backup restored successfully", Toast.LENGTH_SHORT).show();
BackupSettingsPresenter.instance(mContext).showLocalRestoreDialog();
//Toast.makeText(mContext, "Backup restored successfully", Toast.LENGTH_SHORT).show();
// TODO: possibly launch restore dialog
} catch (Exception e) {
@@ -141,7 +144,7 @@ public class BackupAndRestoreHelper implements OnResult {
}
}
private void copyUriToFile(Uri uri, File targetDir) {
private void copyUriToDir(Uri uri, File targetDir) {
try {
String fileName = getFileName(uri);
if (fileName == null) fileName = "imported_" + System.currentTimeMillis();
@@ -165,6 +168,25 @@ public class BackupAndRestoreHelper implements OnResult {
}
}
private void copyUriToFile(Uri uri, File outFile) {
try {
InputStream in = mContext.getContentResolver().openInputStream(uri);
OutputStream out = new FileOutputStream(outFile);
byte[] buffer = new byte[8192];
int len;
while ((len = in.read(buffer)) != -1) {
out.write(buffer, 0, len);
}
in.close();
out.close();
} catch (Exception e) {
e.printStackTrace();
}
}
private String getFileName(Uri uri) {
Cursor cursor = mContext.getContentResolver().query(uri, null, null, null, null);
if (cursor != null) {
@@ -212,7 +234,7 @@ public class BackupAndRestoreHelper implements OnResult {
}
} else {
FileInputStream fis = new FileInputStream(file);
zos.putNextEntry(new ZipEntry(base.substring(5, base.length() -1))); // strip "data/" prefix and "/" at the end
zos.putNextEntry(new ZipEntry(base.substring(0, base.length() -1))); // strip "/" at the end to mark as file
byte[] buf = new byte[8192];
int len;
while ((len = fis.read(buf)) > 0) zos.write(buf, 0, len);

View File

@@ -27,6 +27,7 @@ public class BackupAndRestoreManager implements MotherActivity.OnPermissions {
private final List<File> mDataDirs;
private final List<File> mBackupDirs;
private final BackupAndRestoreHelper mHelper;
private final String[] mBackupPatterns;
private Runnable mPendingHandler;
public interface OnBackupNames {
@@ -41,6 +42,13 @@ public class BackupAndRestoreManager implements MotherActivity.OnPermissions {
mDataDirs = new ArrayList<>();
mDataDirs.add(new File(mContext.getApplicationInfo().dataDir, SHARED_PREFS_SUBDIR));
mBackupPatterns = new String[] {
"yt_service_prefs.xml",
"com.liskovsoft.appupdatechecker2.preferences.xml",
"com.liskovsoft.sharedutils.prefs.GlobalPreferences.xml",
"_preferences.xml" // before _ should be the app package name
};
mBackupDirs = new ArrayList<>();
initBackupDirs();

View File

@@ -243,6 +243,7 @@ public class GDriveBackupManager {
String[] altPackages = new String[] {
"org.smarttube.beta",
"org.smarttube.stable",
"org.smarttube.fdroid",
"com.liskovsoft.smarttubetv.beta",
"com.teamsmart.videomanager.tv"
};

View File

@@ -0,0 +1,283 @@
package com.liskovsoft.smartyoutubetv2.common.misc;
import android.annotation.SuppressLint;
import android.content.Context;
import android.net.Uri;
import android.os.Build;
import com.liskovsoft.googleapi.oauth2.impl.GoogleSignInService;
import com.liskovsoft.googleapi.service.DriveService;
import com.liskovsoft.sharedutils.helpers.FileHelpers;
import com.liskovsoft.sharedutils.helpers.Helpers;
import com.liskovsoft.sharedutils.helpers.MessageHelpers;
import com.liskovsoft.sharedutils.rx.RxHelper;
import com.liskovsoft.smartyoutubetv2.common.R;
import com.liskovsoft.smartyoutubetv2.common.app.presenters.GoogleSignInPresenter;
import com.liskovsoft.smartyoutubetv2.common.prefs.GeneralData;
import com.liskovsoft.smartyoutubetv2.common.utils.AppDialogUtil;
import com.liskovsoft.smartyoutubetv2.common.utils.Utils;
import java.io.File;
import java.util.Collection;
import io.reactivex.Observable;
import io.reactivex.android.schedulers.AndroidSchedulers;
import io.reactivex.disposables.Disposable;
import io.reactivex.functions.Consumer;
import io.reactivex.schedulers.Schedulers;
public class GDriveBackupManagerOld {
@SuppressLint("StaticFieldLeak")
private static GDriveBackupManagerOld sInstance;
private final Context mContext;
private static final String SHARED_PREFS_SUBDIR = "shared_prefs";
private static final String BACKUP_NAME = "backup.zip";
private final GoogleSignInService mSignInService;
private final String mDataDir;
private final String mBackupDir;
private final GeneralData mGeneralData;
private Disposable mBackupAction;
private Disposable mRestoreAction;
private final String[] mBackupNames;
private boolean mIsBlocking;
private GDriveBackupManagerOld(Context context) {
mContext = context;
mGeneralData = GeneralData.instance(context);
mDataDir = String.format("%s/%s", mContext.getApplicationInfo().dataDir, SHARED_PREFS_SUBDIR);
mBackupDir = String.format("SmartTubeBackup/%s", context.getPackageName());
mSignInService = GoogleSignInService.instance();
mBackupNames = new String[] {
"yt_service_prefs.xml",
"com.liskovsoft.appupdatechecker2.preferences.xml",
"com.liskovsoft.sharedutils.prefs.GlobalPreferences.xml",
"_preferences.xml" // before _ should be the app package name
};
}
public static GDriveBackupManagerOld instance(Context context) {
if (sInstance == null) {
sInstance = new GDriveBackupManagerOld(context);
}
return sInstance;
}
public static void unhold() {
sInstance = null;
}
public void backup() {
mIsBlocking = false;
backupInt();
}
public void backupBlocking() {
mIsBlocking = true;
backupInt();
}
private void backupInt() {
if (mIsBlocking && !mSignInService.isSigned()) {
return;
}
if (RxHelper.isAnyActionRunning(mBackupAction, mRestoreAction)) {
if (!mIsBlocking)
MessageHelpers.showMessage(mContext, R.string.wait_data_loading);
return;
}
if (mSignInService.isSigned()) {
startBackupConfirm();
} else {
logIn(this::startBackupConfirm);
}
}
public void restore() {
if (RxHelper.isAnyActionRunning(mBackupAction, mRestoreAction)) {
MessageHelpers.showMessage(mContext, R.string.wait_data_loading);
return;
}
if (mSignInService.isSigned()) {
startRestoreConfirm();
} else {
logIn(this::startRestoreConfirm);
}
}
private void startBackupConfirm() {
if (!mIsBlocking) {
AppDialogUtil.showConfirmationDialog(mContext, mContext.getString(R.string.app_backup), this::startBackup);
} else {
startBackup();
}
}
private void startBackup() {
String backupDir = getBackupDir();
startBackup2(backupDir, mDataDir);
}
private void startBackup(String backupDir, String dataDir) {
Collection<File> files = FileHelpers.listFileTree(new File(dataDir));
Consumer<File> backupConsumer = file -> {
if (file.isFile()) {
if (checkFileName(file.getName())) {
if (!mIsBlocking) MessageHelpers.showLongMessage(mContext, mContext.getString(R.string.app_backup) + "\n" + file.getName());
RxHelper.runBlocking(DriveService.uploadFile(file, Uri.parse(String.format("%s%s", backupDir, file.getAbsolutePath().replace(dataDir, "")))));
}
}
};
if (mIsBlocking) {
Observable.fromIterable(files)
.blockingSubscribe(backupConsumer);
} else {
mBackupAction = Observable.fromIterable(files)
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()) // run subscribe on separate thread
.subscribe(backupConsumer, error -> MessageHelpers.showLongMessage(mContext, error.getMessage()));
}
}
private void startBackup2(String backupDir, String dataDir) {
File source = new File(dataDir);
File zipFile = new File(mContext.getCacheDir(), BACKUP_NAME);
ZipHelper.zipFolder(source, zipFile, mBackupNames);
Observable<Void> uploadFile = DriveService.uploadFile(zipFile, Uri.parse(String.format("%s/%s", backupDir, BACKUP_NAME)));
if (mIsBlocking) {
RxHelper.runBlocking(uploadFile);
} else {
MessageHelpers.showLongMessage(mContext, mContext.getString(R.string.app_backup));
mBackupAction = uploadFile
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
unused -> {},
error -> MessageHelpers.showLongMessage(mContext, error.getMessage()),
() -> MessageHelpers.showMessage(mContext, R.string.msg_done)
);
}
}
private void startRestoreConfirm() {
AppDialogUtil.showConfirmationDialog(mContext, mContext.getString(R.string.app_restore), this::startRestore);
}
private void startRestore() {
startRestore2(getBackupDir(), mDataDir,
() -> startRestore2(getAltBackupDir(), mDataDir,
() -> startRestore(getBackupDir(), mDataDir,
() -> startRestore(getAltBackupDir(), mDataDir, null))));
}
private void startRestore(String backupDir, String dataDir, Runnable onError) {
mRestoreAction = DriveService.getList(Uri.parse(backupDir))
.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io()) // run subscribe on separate thread
.subscribe(names -> {
// remove old data
FileHelpers.delete(dataDir);
for (String name : names) {
if (checkFileName(name)) {
MessageHelpers.showLongMessage(mContext, mContext.getString(R.string.app_restore) + "\n" + name);
DriveService.getFile(Uri.parse(String.format("%s/%s", backupDir, name)))
.blockingSubscribe(inputStream -> FileHelpers.copy(inputStream, new File(dataDir, fixAltPackageName(name))));
}
}
Utils.restartTheApp(mContext);
}, error -> {
if (onError != null)
onError.run();
else MessageHelpers.showLongMessage(mContext, R.string.nothing_found);
});
}
private void startRestore2(String backupDir, String dataDir, Runnable onError) {
MessageHelpers.showLongMessage(mContext, mContext.getString(R.string.app_restore));
mRestoreAction = DriveService.getFile(Uri.parse(String.format("%s/%s", backupDir, BACKUP_NAME)))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(inputStream -> {
File zipFile = new File(mContext.getCacheDir(), BACKUP_NAME);
FileHelpers.copy(inputStream, zipFile);
File out = new File(dataDir);
// remove old data
FileHelpers.delete(out);
ZipHelper.unzipToFolder(zipFile, out);
fixFileNames(out);
Utils.restartTheApp(mContext);
}, error -> {
if (onError != null)
onError.run();
else MessageHelpers.showLongMessage(mContext, R.string.nothing_found);
}, () -> MessageHelpers.showMessage(mContext, R.string.msg_done));
}
private void logIn(Runnable onDone) {
GoogleSignInPresenter.instance(mContext).start(onDone);
}
private boolean checkFileName(String name) {
return Helpers.endsWithAny(name, mBackupNames);
}
private String fixAltPackageName(String name) {
String altPackageName = getAltPackageName();
return name.replace(altPackageName, mContext.getPackageName());
}
private String getAltPackageName() {
String[] altPackages = new String[] {
"org.smarttube.beta",
"org.smarttube.stable",
"org.smarttube.fdroid",
"com.liskovsoft.smarttubetv.beta",
"com.teamsmart.videomanager.tv"
};
return mContext.getPackageName().equals(altPackages[0]) ? altPackages[1] : altPackages[0];
}
private String getDeviceSuffix() {
return mGeneralData.isDeviceSpecificBackupEnabled() ? "_" + Build.MODEL.replace(" ", "_") : "";
}
private String getAltBackupDir() {
String backupDir = getBackupDir();
String altPackageName = getAltPackageName();
return backupDir.replace(mContext.getPackageName(), altPackageName);
}
public String getBackupDir() {
return mBackupDir + getDeviceSuffix();
}
/**
* Fix file names from other app versions
*/
private void fixFileNames(File dataDir) {
Collection<File> files = FileHelpers.listFileTree(dataDir);
String suffix = "_preferences.xml";
String targetName = mContext.getPackageName() + suffix;
for (File file : files) {
if (file.getName().endsWith(suffix) && !file.getName().endsWith(targetName)) {
FileHelpers.copy(file, new File(file.getParentFile(), targetName));
FileHelpers.delete(file);
}
}
}
}

View File

@@ -26,7 +26,7 @@ apply plugin: 'com.android.application'
// First letter of flavor name must be in Uppercase
if (new File("${projectDir}/google-services.json").exists() && (getGradle().getStartParameter().getTaskRequests().toString().contains("Stbeta") ||
getGradle().getStartParameter().getTaskRequests().toString().contains("Strtarmenia"))) {
getGradle().getStartParameter().getTaskRequests().toString().contains("Ststable"))) {
// Google Services Gradle plugin (transforms google-services.json file)
apply plugin: 'com.google.gms.google-services'
@@ -50,8 +50,8 @@ android {
applicationId "org.smarttube"
minSdkVersion project.properties.minSdkVersion
targetSdkVersion project.properties.targetSdkVersion
versionCode 2246
versionName "30.56"
versionCode 2247
versionName "30.57"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
buildConfigField "long", "TIMESTAMP", System.currentTimeMillis() + "L"