From a47d7e937f0d840367844d68ddf1fca6bfcf9d01 Mon Sep 17 00:00:00 2001 From: Yuriy Liskov Date: Fri, 5 Dec 2025 15:45:10 +0200 Subject: [PATCH] Android 11+ local restore fix --- .../settings/BackupSettingsPresenter.java | 6 + .../common/misc/BackupAndRestoreHelper.java | 32 +- .../common/misc/BackupAndRestoreManager.java | 8 + .../common/misc/GDriveBackupManager.java | 1 + .../common/misc/GDriveBackupManagerOld.java | 283 ++++++++++++++++++ smarttubetv/build.gradle | 6 +- 6 files changed, 328 insertions(+), 8 deletions(-) create mode 100644 common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/GDriveBackupManagerOld.java diff --git a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/BackupSettingsPresenter.java b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/BackupSettingsPresenter.java index bdbd69736..218ec321b 100644 --- a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/BackupSettingsPresenter.java +++ b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/app/presenters/settings/BackupSettingsPresenter.java @@ -54,6 +54,12 @@ public class BackupSettingsPresenter extends BasePresenter { createAndShowDialog(); } + public void showLocalRestoreDialog() { + BackupAndRestoreManager backupManager = new BackupAndRestoreManager(getContext()); + + backupManager.getBackupNames(names -> showLocalRestoreDialog(backupManager, names)); + } + private void createAndShowDialog() { AppDialogPresenter settingsPresenter = AppDialogPresenter.instance(getContext()); diff --git a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreHelper.java b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreHelper.java index 628d10d6a..7e850f396 100644 --- a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreHelper.java +++ b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreHelper.java @@ -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); diff --git a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreManager.java b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreManager.java index a3b83a3cc..5630ae869 100644 --- a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreManager.java +++ b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreManager.java @@ -27,6 +27,7 @@ public class BackupAndRestoreManager implements MotherActivity.OnPermissions { private final List mDataDirs; private final List 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(); diff --git a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/GDriveBackupManager.java b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/GDriveBackupManager.java index baf2b7864..3e185a321 100644 --- a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/GDriveBackupManager.java +++ b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/GDriveBackupManager.java @@ -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" }; diff --git a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/GDriveBackupManagerOld.java b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/GDriveBackupManagerOld.java new file mode 100644 index 000000000..9662392d8 --- /dev/null +++ b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/GDriveBackupManagerOld.java @@ -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 files = FileHelpers.listFileTree(new File(dataDir)); + + Consumer 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 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 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); + } + } + } +} diff --git a/smarttubetv/build.gradle b/smarttubetv/build.gradle index 3697711cf..4e4dd81d2 100644 --- a/smarttubetv/build.gradle +++ b/smarttubetv/build.gradle @@ -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"