From aee4c51d79c87480fca777cfcb6b6d7acabe2165 Mon Sep 17 00:00:00 2001 From: Yuriy Liskov Date: Sat, 29 Nov 2025 07:39:16 +0200 Subject: [PATCH] backup upd --- .../settings/BackupSettingsPresenter.java | 7 +- .../common/misc/BackupAndRestoreHelper.java | 220 ++++++++++++++++++ .../misc/BackupAndRestoreHelperOld.java | 159 +++++++++++++ .../common/misc/BackupAndRestoreManager.java | 9 + smarttubetv/build.gradle | 4 +- 5 files changed, 394 insertions(+), 5 deletions(-) create mode 100644 common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreHelper.java create mode 100644 common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreHelperOld.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 d71bde44b..bdbd69736 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 @@ -117,8 +117,10 @@ public class BackupSettingsPresenter extends BasePresenter { BackupAndRestoreManager backupManager = new BackupAndRestoreManager(getContext()); + String backupPath = backupManager.getBackupPathRoot(); + options.add(UiOptionItem.from( - String.format("%s:\n%s", getContext().getString(R.string.app_backup), backupManager.getBackupPath()), + String.format("%s:\n%s", getContext().getString(R.string.app_backup), backupPath), option -> { AppDialogUtil.showConfirmationDialog(getContext(), getContext().getString(R.string.app_backup), () -> { mSidebarService.enableSection(MediaGroup.TYPE_SETTINGS, true); // prevent Settings lock @@ -127,9 +129,8 @@ public class BackupSettingsPresenter extends BasePresenter { }); })); - String backupPathCheck = backupManager.getBackupPathCheck(); options.add(UiOptionItem.from( - String.format("%s:\n%s", getContext().getString(R.string.app_restore), backupPathCheck != null ? backupPathCheck : ""), + String.format("%s:\n%s", getContext().getString(R.string.app_restore), backupPath), option -> { backupManager.getBackupNames(names -> showLocalRestoreDialog(backupManager, names)); })); 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 new file mode 100644 index 000000000..538960136 --- /dev/null +++ b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreHelper.java @@ -0,0 +1,220 @@ +package com.liskovsoft.smartyoutubetv2.common.misc; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Environment; +import android.provider.OpenableColumns; + +import androidx.core.content.FileProvider; + +import com.liskovsoft.smartyoutubetv2.common.misc.MotherActivity.OnResult; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import java.util.zip.ZipOutputStream; + +public class BackupAndRestoreHelper implements OnResult { + private static final int REQ_PICK_FILES = 1001; + private final Context mContext; + private Runnable mOnSuccess; + + public BackupAndRestoreHelper(Context context) { + mContext = context; + } + + public void exportAppMediaFolder() { + if (VERSION.SDK_INT < 30) { + return; + } + + File mediaDir = getExternalStorageDirectory(); + File dataDir = new File(mediaDir, "data"); + if (!dataDir.exists()) return; + + File zipFile = new File(mediaDir, "backup.zip"); + zipDirectory(dataDir, zipFile); + + Uri uri = FileProvider.getUriForFile( + mContext, + mContext.getPackageName() + ".update_provider", + zipFile + ); + + Intent intent = new Intent(Intent.ACTION_SEND); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_STREAM, uri); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + mContext.startActivity(Intent.createChooser(intent, "Send to Total Commander")); + } + + public void importAppMediaFolder(Runnable onSuccess) { + if (VERSION.SDK_INT < 30 || onSuccess == null) { + return; + } + + mOnSuccess = onSuccess; + + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("*/*"); + //intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + + ((MotherActivity) mContext).addOnResult(this); + + ((Activity) mContext).startActivityForResult(intent, REQ_PICK_FILES); + } + + @Override + public void onResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQ_PICK_FILES && resultCode == Activity.RESULT_OK) { + + if (data == null) return; + + File mediaDir = getExternalStorageDirectory(); + File dataDir = new File(mediaDir, "data"); + + if (!mediaDir.exists()) mediaDir.mkdirs(); + + Uri uri = data.getData(); + if (uri == null && data.getClipData() != null) { + uri = data.getClipData().getItemAt(0).getUri(); + } + if (uri == null) return; + + File zipFile = new File(mediaDir, "restore.zip"); + copyUriToFile(uri, zipFile); + + // Cleanup previous data + deleteRecursive(dataDir); + dataDir.mkdirs(); + + unzip(zipFile, mediaDir); + + zipFile.delete(); + + mOnSuccess.run(); + } + } + + private void copyUriToFile(Uri uri, File targetDir) { + try { + String fileName = getFileName(uri); + if (fileName == null) fileName = "imported_" + System.currentTimeMillis(); + + File outFile = new File(targetDir, fileName); + + 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) { + int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + cursor.moveToFirst(); + String name = cursor.getString(nameIndex); + cursor.close(); + return name; + } + return null; + } + + @SuppressWarnings("deprecation") + private File getExternalStorageDirectory() { + File result; + + if (VERSION.SDK_INT > 29) { + result = mContext.getExternalMediaDirs()[0]; + + if (!result.exists()) { + result.mkdirs(); + } + } else { + result = Environment.getExternalStorageDirectory(); + } + + return result; + } + + private void zipDirectory(File sourceDir, File zipFile) { + try { + ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFile)); + zipFileRecursive(zos, sourceDir, "data/"); + zos.close(); + } catch (Exception e) { e.printStackTrace(); } + } + + private void zipFileRecursive(ZipOutputStream zos, File file, String base) throws Exception { + if (file.isDirectory()) { + File[] children = file.listFiles(); + if (children != null) { + for (File child : children) { + zipFileRecursive(zos, child, base + child.getName() + "/"); + } + } + } else { + FileInputStream fis = new FileInputStream(file); + zos.putNextEntry(new ZipEntry(base.substring(5, base.length() -1))); // strip "data/" prefix and "/" at the end + byte[] buf = new byte[8192]; + int len; + while ((len = fis.read(buf)) > 0) zos.write(buf, 0, len); + fis.close(); + zos.closeEntry(); + } + } + + private void unzip(File zipFile, File targetRoot) { + try { + ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFile)); + ZipEntry entry; + byte[] buffer = new byte[8192]; + + while ((entry = zis.getNextEntry()) != null) { + File out = new File(targetRoot, entry.getName()); + if (entry.isDirectory()) { + out.mkdirs(); + } else { + out.getParentFile().mkdirs(); + FileOutputStream fos = new FileOutputStream(out); + int len; + while ((len = zis.read(buffer)) > 0) fos.write(buffer, 0, len); + fos.close(); + } + zis.closeEntry(); + } + zis.close(); + } catch (Exception e) { e.printStackTrace(); } + } + + private void deleteRecursive(File f) { + if (!f.exists()) return; + if (f.isDirectory()) { + File[] children = f.listFiles(); + if (children != null) { + for (File c : children) deleteRecursive(c); + } + } + f.delete(); + } +} diff --git a/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreHelperOld.java b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreHelperOld.java new file mode 100644 index 000000000..6e00183ec --- /dev/null +++ b/common/src/main/java/com/liskovsoft/smartyoutubetv2/common/misc/BackupAndRestoreHelperOld.java @@ -0,0 +1,159 @@ +package com.liskovsoft.smartyoutubetv2.common.misc; + +import android.app.Activity; +import android.content.ClipData; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Environment; +import android.provider.OpenableColumns; + +import androidx.core.content.FileProvider; + +import com.liskovsoft.smartyoutubetv2.common.misc.MotherActivity.OnResult; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; + +public class BackupAndRestoreHelperOld implements OnResult { + private static final int REQ_PICK_FILES = 1001; + private final Context mContext; + + public BackupAndRestoreHelperOld(Context context) { + mContext = context; + } + + public void exportAppMediaFolder(Context context) { + if (VERSION.SDK_INT < 30) { + return; + } + + File mediaDir = getExternalStorageDirectory(); + if (mediaDir == null || !mediaDir.exists()) return; + + // TODO: create zip file and send only one file zip + File[] files = mediaDir.listFiles(); + if (files == null || files.length == 0) return; + + ArrayList uris = new ArrayList<>(); + + for (File file : files) { + if (file.isFile()) { + Uri uri = FileProvider.getUriForFile( + context, + context.getPackageName() + ".update_provider", + file + ); + uris.add(uri); + } + } + + if (uris.isEmpty()) return; + + Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE); + intent.setType("*/*"); + intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + + context.startActivity( + Intent.createChooser(intent, "Send to Total Commander") + ); + } + + public void importAppMediaFolder(Context context) { + if (VERSION.SDK_INT < 30) { + return; + } + + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); + + ((MotherActivity) mContext).addOnResult(this); + + ((Activity) context).startActivityForResult(intent, REQ_PICK_FILES); + } + + @Override + public void onResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQ_PICK_FILES && resultCode == Activity.RESULT_OK) { + + if (data == null) return; + + File targetDir = getExternalStorageDirectory(); + if (!targetDir.exists()) targetDir.mkdirs(); + + // Несколько файлов (ClipData) + if (data.getClipData() != null) { + ClipData clip = data.getClipData(); + for (int i = 0; i < clip.getItemCount(); i++) { + Uri uri = clip.getItemAt(i).getUri(); + copyUriToFile(mContext, uri, targetDir); + } + } + // Один файл + else if (data.getData() != null) { + Uri uri = data.getData(); + copyUriToFile(mContext, uri, targetDir); + } + } + } + + private static void copyUriToFile(Context context, Uri uri, File targetDir) { + try { + String fileName = getFileName(context, uri); + if (fileName == null) fileName = "imported_" + System.currentTimeMillis(); + + File outFile = new File(targetDir, fileName); + + InputStream in = context.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 static String getFileName(Context context, Uri uri) { + Cursor cursor = context.getContentResolver().query(uri, null, null, null, null); + if (cursor != null) { + int nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME); + cursor.moveToFirst(); + String name = cursor.getString(nameIndex); + cursor.close(); + return name; + } + return null; + } + + @SuppressWarnings("deprecation") + private File getExternalStorageDirectory() { + File result; + + if (VERSION.SDK_INT > 29) { + result = mContext.getExternalMediaDirs()[0]; + + if (!result.exists()) { + result.mkdirs(); + } + } else { + result = Environment.getExternalStorageDirectory(); + } + + return result; + } +} 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 dbed73d05..a3b83a3cc 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 @@ -26,6 +26,7 @@ public class BackupAndRestoreManager implements MotherActivity.OnPermissions { private static final String SHARED_PREFS_SUBDIR = "shared_prefs"; private final List mDataDirs; private final List mBackupDirs; + private final BackupAndRestoreHelper mHelper; private Runnable mPendingHandler; public interface OnBackupNames { @@ -35,6 +36,8 @@ public class BackupAndRestoreManager implements MotherActivity.OnPermissions { public BackupAndRestoreManager(Context context) { mContext = context; + mHelper = new BackupAndRestoreHelper(context); + mDataDirs = new ArrayList<>(); mDataDirs.add(new File(mContext.getApplicationInfo().dataDir, SHARED_PREFS_SUBDIR)); @@ -148,6 +151,8 @@ public class BackupAndRestoreManager implements MotherActivity.OnPermissions { FileHelpers.delete(new File(destination, HiddenPrefs.SHARED_PREFERENCES_NAME + ".xml")); } } + + mHelper.exportAppMediaFolder(); } private void restoreData(String backupName) { @@ -266,6 +271,10 @@ public class BackupAndRestoreManager implements MotherActivity.OnPermissions { return currentBackup != null ? currentBackup.toString() : null; } + public String getBackupPathRoot() { + return String.format("%s/data", getExternalStorageDirectory()); + } + public String getBackupPathCheck() { File currentBackup = getBackupCheck(); diff --git a/smarttubetv/build.gradle b/smarttubetv/build.gradle index de49a0280..97cb6eeaf 100644 --- a/smarttubetv/build.gradle +++ b/smarttubetv/build.gradle @@ -50,8 +50,8 @@ android { applicationId "org.smarttube" minSdkVersion project.properties.minSdkVersion targetSdkVersion project.properties.targetSdkVersion - versionCode 2245 - versionName "30.55" + versionCode 2246 + versionName "30.56" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" buildConfigField "long", "TIMESTAMP", System.currentTimeMillis() + "L"