mirror of
https://codeberg.org/comaps/comaps
synced 2025-12-20 05:13:58 +00:00
[android] Backup bookmarks and tracks to a local folder
This commit adds backup of user data to a local folder on the device. Features: * Turn on/off regular backup * Choose new or existing folder for saving backup * Set how often backup runs * Set how many backups to keep * Create backup manually Signed-off-by: Mihail Mitrofanov <mk.mitrofanov@outlook.com>
This commit is contained in:
committed by
Konstantin Pastbin
parent
70c3f725f9
commit
df3850b86c
@@ -44,6 +44,7 @@ import androidx.fragment.app.FragmentManager;
|
|||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import app.organicmaps.api.Const;
|
import app.organicmaps.api.Const;
|
||||||
|
import app.organicmaps.backup.PeriodicBackupRunner;
|
||||||
import app.organicmaps.base.BaseMwmFragmentActivity;
|
import app.organicmaps.base.BaseMwmFragmentActivity;
|
||||||
import app.organicmaps.base.OnBackPressListener;
|
import app.organicmaps.base.OnBackPressListener;
|
||||||
import app.organicmaps.bookmarks.BookmarkCategoriesActivity;
|
import app.organicmaps.bookmarks.BookmarkCategoriesActivity;
|
||||||
@@ -139,6 +140,7 @@ import static app.organicmaps.leftbutton.LeftButtonsHolder.BUTTON_HELP_CODE;
|
|||||||
import static app.organicmaps.leftbutton.LeftButtonsHolder.BUTTON_RECORD_TRACK_CODE;
|
import static app.organicmaps.leftbutton.LeftButtonsHolder.BUTTON_RECORD_TRACK_CODE;
|
||||||
import static app.organicmaps.leftbutton.LeftButtonsHolder.BUTTON_SETTINGS_CODE;
|
import static app.organicmaps.leftbutton.LeftButtonsHolder.BUTTON_SETTINGS_CODE;
|
||||||
import static app.organicmaps.util.PowerManagment.POWER_MANAGEMENT_TAG;
|
import static app.organicmaps.util.PowerManagment.POWER_MANAGEMENT_TAG;
|
||||||
|
import static app.organicmaps.util.concurrency.UiThread.runLater;
|
||||||
|
|
||||||
public class MwmActivity extends BaseMwmFragmentActivity
|
public class MwmActivity extends BaseMwmFragmentActivity
|
||||||
implements PlacePageActivationListener,
|
implements PlacePageActivationListener,
|
||||||
@@ -253,6 +255,8 @@ public class MwmActivity extends BaseMwmFragmentActivity
|
|||||||
@NonNull
|
@NonNull
|
||||||
private DisplayManager mDisplayManager;
|
private DisplayManager mDisplayManager;
|
||||||
|
|
||||||
|
private PeriodicBackupRunner backupRunner;
|
||||||
|
|
||||||
ManageRouteBottomSheet mManageRouteBottomSheet;
|
ManageRouteBottomSheet mManageRouteBottomSheet;
|
||||||
|
|
||||||
private boolean mRemoveDisplayListener = true;
|
private boolean mRemoveDisplayListener = true;
|
||||||
@@ -607,6 +611,8 @@ public class MwmActivity extends BaseMwmFragmentActivity
|
|||||||
*/
|
*/
|
||||||
if (Map.isEngineCreated())
|
if (Map.isEngineCreated())
|
||||||
onRenderingInitializationFinished();
|
onRenderingInitializationFinished();
|
||||||
|
|
||||||
|
backupRunner = new PeriodicBackupRunner(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onSettingsResult(ActivityResult activityResult)
|
private void onSettingsResult(ActivityResult activityResult)
|
||||||
@@ -1352,6 +1358,11 @@ public class MwmActivity extends BaseMwmFragmentActivity
|
|||||||
final String backUrl = Framework.nativeGetParsedBackUrl();
|
final String backUrl = Framework.nativeGetParsedBackUrl();
|
||||||
if (!TextUtils.isEmpty(backUrl))
|
if (!TextUtils.isEmpty(backUrl))
|
||||||
Utils.openUri(this, Uri.parse(backUrl), null);
|
Utils.openUri(this, Uri.parse(backUrl), null);
|
||||||
|
|
||||||
|
if (backupRunner != null && !backupRunner.isAlreadyChecked() && backupRunner.isTimeToBackup())
|
||||||
|
{
|
||||||
|
backupRunner.doBackup();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@CallSuper
|
@CallSuper
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package app.organicmaps.backup;
|
||||||
|
|
||||||
|
import static app.organicmaps.settings.BackupSettingsFragment.MAX_BACKUPS_DEFAULT_COUNT;
|
||||||
|
import static app.organicmaps.settings.BackupSettingsFragment.MAX_BACKUPS_KEY;
|
||||||
|
import static app.organicmaps.util.StorageUtils.isFolderWritable;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.provider.DocumentsContract;
|
||||||
|
import android.text.SpannableStringBuilder;
|
||||||
|
import android.text.Spanned;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.style.AbsoluteSizeSpan;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import app.organicmaps.R;
|
||||||
|
import app.organicmaps.util.UiUtils;
|
||||||
|
import app.organicmaps.util.log.Logger;
|
||||||
|
|
||||||
|
public class BackupUtils
|
||||||
|
{
|
||||||
|
private static final String BACKUP_PREFIX = "backup_";
|
||||||
|
private static final String BACKUP_EXTENSION = ".kmz";
|
||||||
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd_HH-mm-ss").withLocale(Locale.US);
|
||||||
|
private static final String TAG = BackupUtils.class.getSimpleName();
|
||||||
|
|
||||||
|
public static CharSequence formatReadableFolderPath(Context context, @NonNull Uri uri)
|
||||||
|
{
|
||||||
|
String docId = DocumentsContract.getTreeDocumentId(uri);
|
||||||
|
String volumeId;
|
||||||
|
String subPath = "";
|
||||||
|
|
||||||
|
int colonIndex = docId.indexOf(':');
|
||||||
|
if (colonIndex >= 0)
|
||||||
|
{
|
||||||
|
volumeId = docId.substring(0, colonIndex);
|
||||||
|
subPath = docId.substring(colonIndex + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
volumeId = docId;
|
||||||
|
}
|
||||||
|
|
||||||
|
String volumeName;
|
||||||
|
if ("primary".equalsIgnoreCase(volumeId))
|
||||||
|
volumeName = context.getString(R.string.maps_storage_shared);
|
||||||
|
else
|
||||||
|
volumeName = context.getString(R.string.maps_storage_removable);
|
||||||
|
|
||||||
|
SpannableStringBuilder sb = new SpannableStringBuilder();
|
||||||
|
sb.append(volumeName + ": \n", new AbsoluteSizeSpan(UiUtils.dimen(context, R.dimen.text_size_body_3)), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
sb.append("/" + subPath, new AbsoluteSizeSpan(UiUtils.dimen(context, R.dimen.text_size_body_4)), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
return sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getMaxBackups(SharedPreferences prefs)
|
||||||
|
{
|
||||||
|
String rawValue = prefs.getString(MAX_BACKUPS_KEY, String.valueOf(MAX_BACKUPS_DEFAULT_COUNT));
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Integer.parseInt(rawValue);
|
||||||
|
} catch (NumberFormatException e)
|
||||||
|
{
|
||||||
|
Logger.e(TAG, "Failed to parse max backups count, raw value: " + rawValue + " set to default: " + MAX_BACKUPS_DEFAULT_COUNT, e);
|
||||||
|
prefs.edit()
|
||||||
|
.putString(MAX_BACKUPS_KEY, String.valueOf(MAX_BACKUPS_DEFAULT_COUNT))
|
||||||
|
.apply();
|
||||||
|
return MAX_BACKUPS_DEFAULT_COUNT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DocumentFile createUniqueBackupFolder(@NonNull DocumentFile parentDir, LocalDateTime backupTime)
|
||||||
|
{
|
||||||
|
String folderName = BACKUP_PREFIX + backupTime.format(DATE_FORMATTER);
|
||||||
|
return parentDir.createDirectory(folderName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getBackupName(LocalDateTime backupTime)
|
||||||
|
{
|
||||||
|
String formattedBackupTime = backupTime.format(DATE_FORMATTER);
|
||||||
|
return BACKUP_PREFIX + formattedBackupTime + BACKUP_EXTENSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DocumentFile[] getBackupFolders(DocumentFile parentDir)
|
||||||
|
{
|
||||||
|
List<DocumentFile> backupFolders = new ArrayList<>();
|
||||||
|
for (DocumentFile file : parentDir.listFiles())
|
||||||
|
{
|
||||||
|
if (file.isDirectory() && file.getName() != null && file.getName().startsWith(BACKUP_PREFIX))
|
||||||
|
backupFolders.add(file);
|
||||||
|
}
|
||||||
|
return backupFolders.toArray(new DocumentFile[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isBackupFolderAvailable(Context context, String storedFolderPath)
|
||||||
|
{
|
||||||
|
return !TextUtils.isEmpty(storedFolderPath) && isFolderWritable(context, storedFolderPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
package app.organicmaps.backup;
|
||||||
|
|
||||||
|
import static app.organicmaps.backup.BackupUtils.getBackupName;
|
||||||
|
import static app.organicmaps.backup.BackupUtils.getBackupFolders;
|
||||||
|
import static app.organicmaps.util.StorageUtils.copyFileToDocumentFile;
|
||||||
|
import static app.organicmaps.util.StorageUtils.deleteDirectoryRecursive;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import app.organicmaps.bookmarks.data.BookmarkCategory;
|
||||||
|
import app.organicmaps.bookmarks.data.BookmarkManager;
|
||||||
|
import app.organicmaps.bookmarks.data.BookmarkSharingResult;
|
||||||
|
import app.organicmaps.bookmarks.data.KmlFileType;
|
||||||
|
import app.organicmaps.util.concurrency.ThreadPool;
|
||||||
|
import app.organicmaps.util.concurrency.UiThread;
|
||||||
|
import app.organicmaps.util.log.Logger;
|
||||||
|
|
||||||
|
public class LocalBackupManager implements BookmarkManager.BookmarksSharingListener
|
||||||
|
{
|
||||||
|
public static final String TAG = LocalBackupManager.class.getSimpleName();
|
||||||
|
|
||||||
|
private final Activity activity;
|
||||||
|
private final String backupFolderPath;
|
||||||
|
private final int maxBackups;
|
||||||
|
private Listener listener;
|
||||||
|
|
||||||
|
public LocalBackupManager(@NonNull Activity activity, @NonNull String backupFolderPath, int maxBackups)
|
||||||
|
{
|
||||||
|
this.activity = activity;
|
||||||
|
this.backupFolderPath = backupFolderPath;
|
||||||
|
this.maxBackups = maxBackups;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void doBackup()
|
||||||
|
{
|
||||||
|
BookmarkManager.INSTANCE.addSharingListener(this);
|
||||||
|
|
||||||
|
prepareBookmarkCategoriesForSharing();
|
||||||
|
|
||||||
|
if (listener != null)
|
||||||
|
listener.onBackupStarted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setListener(@NonNull Listener listener)
|
||||||
|
{
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPreparedFileForSharing(@NonNull BookmarkSharingResult result)
|
||||||
|
{
|
||||||
|
BookmarkManager.INSTANCE.removeSharingListener(this);
|
||||||
|
|
||||||
|
ThreadPool.getWorker().execute(() -> {
|
||||||
|
ErrorCode errorCode = null;
|
||||||
|
switch (result.getCode())
|
||||||
|
{
|
||||||
|
case BookmarkSharingResult.SUCCESS ->
|
||||||
|
{
|
||||||
|
if (!saveBackup(result))
|
||||||
|
{
|
||||||
|
Logger.e(TAG, "Failed to save backup. See system log above");
|
||||||
|
errorCode = ErrorCode.FILE_ERROR;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.i(TAG, "Backup was created and saved successfully");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case BookmarkSharingResult.EMPTY_CATEGORY ->
|
||||||
|
{
|
||||||
|
errorCode = ErrorCode.EMPTY_CATEGORY;
|
||||||
|
Logger.e(TAG, "Failed to create backup. Category is empty");
|
||||||
|
}
|
||||||
|
case BookmarkSharingResult.ARCHIVE_ERROR ->
|
||||||
|
{
|
||||||
|
errorCode = ErrorCode.ARCHIVE_ERROR;
|
||||||
|
Logger.e(TAG, "Failed to create archive of bookmarks");
|
||||||
|
}
|
||||||
|
case BookmarkSharingResult.FILE_ERROR ->
|
||||||
|
{
|
||||||
|
errorCode = ErrorCode.FILE_ERROR;
|
||||||
|
Logger.e(TAG, "Failed create file for archive");
|
||||||
|
}
|
||||||
|
default ->
|
||||||
|
{
|
||||||
|
errorCode = ErrorCode.UNSUPPORTED;
|
||||||
|
Logger.e(TAG, "Failed to create backup. Unknown error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ErrorCode finalErrorCode = errorCode;
|
||||||
|
UiThread.run(() -> {
|
||||||
|
if (listener != null)
|
||||||
|
{
|
||||||
|
if (finalErrorCode == null)
|
||||||
|
listener.onBackupFinished();
|
||||||
|
else
|
||||||
|
listener.onBackupFailed(finalErrorCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean saveBackup(@NonNull BookmarkSharingResult result)
|
||||||
|
{
|
||||||
|
boolean isSuccess = false;
|
||||||
|
Uri folderUri = Uri.parse(backupFolderPath);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DocumentFile parentFolder = DocumentFile.fromTreeUri(activity, folderUri);
|
||||||
|
if (parentFolder != null && parentFolder.canWrite())
|
||||||
|
{
|
||||||
|
LocalDateTime now = LocalDateTime.now();
|
||||||
|
DocumentFile backupFolder = BackupUtils.createUniqueBackupFolder(parentFolder, now);
|
||||||
|
if (backupFolder != null)
|
||||||
|
{
|
||||||
|
String backupName = getBackupName(now);
|
||||||
|
DocumentFile backupFile = backupFolder.createFile(result.getMimeType(), backupName);
|
||||||
|
if (backupFile != null && copyFileToDocumentFile(activity, new File(result.getSharingPath()), backupFile))
|
||||||
|
{
|
||||||
|
Logger.i(TAG, "Backup saved to " + backupFile.getUri());
|
||||||
|
isSuccess = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.e(TAG, "Failed to create backup folder");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cleanOldBackups(parentFolder);
|
||||||
|
|
||||||
|
} catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.e(TAG, "Failed to save backup", e);
|
||||||
|
}
|
||||||
|
return isSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cleanOldBackups(DocumentFile parentDir)
|
||||||
|
{
|
||||||
|
DocumentFile[] backupFolders = getBackupFolders(parentDir);
|
||||||
|
if (backupFolders.length > maxBackups)
|
||||||
|
{
|
||||||
|
Arrays.sort(backupFolders, Comparator.comparing(DocumentFile::getName));
|
||||||
|
for (int i = 0; i < backupFolders.length - maxBackups; i++)
|
||||||
|
{
|
||||||
|
Logger.i(TAG, "Delete old backup " + backupFolders[i].getUri());
|
||||||
|
deleteDirectoryRecursive(backupFolders[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void prepareBookmarkCategoriesForSharing()
|
||||||
|
{
|
||||||
|
List<BookmarkCategory> categories = BookmarkManager.INSTANCE.getCategories();
|
||||||
|
long[] categoryIds = new long[categories.size()];
|
||||||
|
for (int i = 0; i < categories.size(); i++)
|
||||||
|
categoryIds[i] = categories.get(i).getId();
|
||||||
|
BookmarkManager.INSTANCE.prepareCategoriesForSharing(categoryIds, KmlFileType.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface Listener
|
||||||
|
{
|
||||||
|
void onBackupStarted();
|
||||||
|
|
||||||
|
void onBackupFinished();
|
||||||
|
|
||||||
|
void onBackupFailed(ErrorCode errorCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum ErrorCode
|
||||||
|
{
|
||||||
|
EMPTY_CATEGORY,
|
||||||
|
ARCHIVE_ERROR,
|
||||||
|
FILE_ERROR,
|
||||||
|
UNSUPPORTED,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
package app.organicmaps.backup;
|
||||||
|
|
||||||
|
import static app.organicmaps.backup.BackupUtils.getMaxBackups;
|
||||||
|
import static app.organicmaps.backup.BackupUtils.isBackupFolderAvailable;
|
||||||
|
import static app.organicmaps.settings.BackupSettingsFragment.BACKUP_FOLDER_PATH_KEY;
|
||||||
|
import static app.organicmaps.settings.BackupSettingsFragment.BACKUP_INTERVAL_KEY;
|
||||||
|
import static app.organicmaps.settings.BackupSettingsFragment.LAST_BACKUP_TIME_KEY;
|
||||||
|
import static app.organicmaps.util.StorageUtils.isFolderWritable;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import app.organicmaps.util.log.Logger;
|
||||||
|
|
||||||
|
public class PeriodicBackupRunner
|
||||||
|
{
|
||||||
|
private final Activity activity;
|
||||||
|
private static final String TAG = PeriodicBackupRunner.class.getSimpleName();
|
||||||
|
private final SharedPreferences prefs;
|
||||||
|
private boolean alreadyChecked = false;
|
||||||
|
|
||||||
|
public PeriodicBackupRunner(Activity activity)
|
||||||
|
{
|
||||||
|
this.activity = activity;
|
||||||
|
this.prefs = PreferenceManager.getDefaultSharedPreferences(activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isAlreadyChecked()
|
||||||
|
{
|
||||||
|
return alreadyChecked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isTimeToBackup()
|
||||||
|
{
|
||||||
|
long intervalMs = getBackupIntervalMs();
|
||||||
|
|
||||||
|
if (intervalMs <= 0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
long lastBackupTime = prefs.getLong(LAST_BACKUP_TIME_KEY, 0);
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
|
||||||
|
alreadyChecked = true;
|
||||||
|
|
||||||
|
return (now - lastBackupTime) >= intervalMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void doBackup()
|
||||||
|
{
|
||||||
|
String storedFolderPath = prefs.getString(BACKUP_FOLDER_PATH_KEY, null);
|
||||||
|
|
||||||
|
if (isBackupFolderAvailable(activity, storedFolderPath))
|
||||||
|
{
|
||||||
|
Logger.i(TAG, "Performing periodic backup");
|
||||||
|
performBackup(storedFolderPath, getMaxBackups(prefs));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.w(TAG, "Backup folder is not writable, passed path: " + storedFolderPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private long getBackupIntervalMs()
|
||||||
|
{
|
||||||
|
String defaultValue = "0";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return Long.parseLong(prefs.getString(BACKUP_INTERVAL_KEY, defaultValue));
|
||||||
|
} catch (NumberFormatException e)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performBackup(String backupFolderPath, int maxBackups)
|
||||||
|
{
|
||||||
|
LocalBackupManager backupManager = new LocalBackupManager(activity, backupFolderPath, maxBackups);
|
||||||
|
backupManager.setListener(new LocalBackupManager.Listener()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onBackupStarted()
|
||||||
|
{
|
||||||
|
Logger.i(TAG, "Periodic backup started");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackupFinished()
|
||||||
|
{
|
||||||
|
prefs.edit().putLong(LAST_BACKUP_TIME_KEY, System.currentTimeMillis()).apply();
|
||||||
|
Logger.i(TAG, "Periodic backup finished");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackupFailed(LocalBackupManager.ErrorCode errorCode)
|
||||||
|
{
|
||||||
|
Logger.e(TAG, "Periodic backup was failed with code: " + errorCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
backupManager.doBackup();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,384 @@
|
|||||||
|
package app.organicmaps.settings;
|
||||||
|
|
||||||
|
import static app.organicmaps.backup.BackupUtils.formatReadableFolderPath;
|
||||||
|
import static app.organicmaps.backup.BackupUtils.getMaxBackups;
|
||||||
|
import static app.organicmaps.backup.BackupUtils.isBackupFolderAvailable;
|
||||||
|
import static app.organicmaps.util.StorageUtils.isFolderWritable;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.preference.ListPreference;
|
||||||
|
import androidx.preference.Preference;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
import java.text.DateFormat;
|
||||||
|
|
||||||
|
import app.organicmaps.R;
|
||||||
|
import app.organicmaps.backup.LocalBackupManager;
|
||||||
|
import app.organicmaps.util.log.Logger;
|
||||||
|
|
||||||
|
|
||||||
|
public class BackupSettingsFragment
|
||||||
|
extends BaseXmlSettingsFragment
|
||||||
|
{
|
||||||
|
private ActivityResultLauncher<Intent> folderPickerLauncher;
|
||||||
|
|
||||||
|
private static final String TAG = LocalBackupManager.class.getSimpleName();
|
||||||
|
public static final String BACKUP_FOLDER_PATH_KEY = "backup_location";
|
||||||
|
public static final String LAST_BACKUP_TIME_KEY = "last_backup_time";
|
||||||
|
private static final String BACKUP_NOW_KEY = "backup_now";
|
||||||
|
public static final String BACKUP_INTERVAL_KEY = "backup_history_interval";
|
||||||
|
public static final String MAX_BACKUPS_KEY = "backup_history_count";
|
||||||
|
public static final int MAX_BACKUPS_DEFAULT_COUNT = 10;
|
||||||
|
public static final String DEFAULT_BACKUP_INTERVAL = "86400000"; // 24 hours in ms
|
||||||
|
|
||||||
|
private LocalBackupManager mBackupManager;
|
||||||
|
private SharedPreferences prefs;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected int getXmlResources()
|
||||||
|
{
|
||||||
|
return R.xml.prefs_backup;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@SuppressWarnings("NotNullFieldNotInitialized")
|
||||||
|
Preference backupLocationOption;
|
||||||
|
@NonNull
|
||||||
|
@SuppressWarnings("NotNullFieldNotInitialized")
|
||||||
|
ListPreference backupIntervalOption;
|
||||||
|
@NonNull
|
||||||
|
@SuppressWarnings("NotNullFieldNotInitialized")
|
||||||
|
Preference maxBackupsOption;
|
||||||
|
@NonNull
|
||||||
|
@SuppressWarnings("NotNullFieldNotInitialized")
|
||||||
|
Preference backupNowOption;
|
||||||
|
@NonNull
|
||||||
|
@SuppressWarnings("NotNullFieldNotInitialized")
|
||||||
|
Preference advancedCategory;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle savedInstanceState)
|
||||||
|
{
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
folderPickerLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {
|
||||||
|
boolean isSuccess = false;
|
||||||
|
|
||||||
|
String lastFolderPath = prefs.getString(BACKUP_FOLDER_PATH_KEY, null);
|
||||||
|
|
||||||
|
if (result.getResultCode() == Activity.RESULT_OK)
|
||||||
|
{
|
||||||
|
Intent data = result.getData();
|
||||||
|
Logger.i(TAG, "Folder selection result: " + data);
|
||||||
|
if (data == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Uri uri = data.getData();
|
||||||
|
if (uri != null)
|
||||||
|
{
|
||||||
|
takePersistableUriPermission(uri);
|
||||||
|
Logger.i(TAG, "Backup location changed to " + uri);
|
||||||
|
prefs.edit().putString(BACKUP_FOLDER_PATH_KEY, uri.toString()).apply();
|
||||||
|
setFormattedBackupPath(uri);
|
||||||
|
|
||||||
|
runBackup();
|
||||||
|
|
||||||
|
isSuccess = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.w(TAG, "Folder selection result is null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (result.getResultCode() == Activity.RESULT_CANCELED)
|
||||||
|
{
|
||||||
|
Logger.w(TAG, "User canceled folder selection");
|
||||||
|
if (TextUtils.isEmpty(lastFolderPath))
|
||||||
|
{
|
||||||
|
prefs.edit().putString(BACKUP_FOLDER_PATH_KEY, null).apply();
|
||||||
|
Logger.i(TAG, "Backup settings reset");
|
||||||
|
initBackupLocationOption();
|
||||||
|
}
|
||||||
|
else if (isFolderWritable(requireActivity(), lastFolderPath))
|
||||||
|
{
|
||||||
|
Logger.i(TAG, "Backup location not changed, using previous value " + lastFolderPath);
|
||||||
|
isSuccess = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.e(TAG, "Backup location not changed, but last folder is not writable: " + lastFolderPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resetLastBackupTime();
|
||||||
|
updateStatusSummaryOption();
|
||||||
|
|
||||||
|
Logger.i(TAG, "Folder selection result: " + isSuccess);
|
||||||
|
applyAdvancedSettings(isSuccess);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreatePreferences(Bundle savedInstanceState, String rootKey)
|
||||||
|
{
|
||||||
|
super.onCreatePreferences(savedInstanceState, rootKey);
|
||||||
|
|
||||||
|
prefs = PreferenceManager.getDefaultSharedPreferences(requireContext());
|
||||||
|
backupLocationOption = findPreference(BACKUP_FOLDER_PATH_KEY);
|
||||||
|
backupIntervalOption = findPreference(BACKUP_INTERVAL_KEY);
|
||||||
|
maxBackupsOption = findPreference(MAX_BACKUPS_KEY);
|
||||||
|
backupNowOption = findPreference(BACKUP_NOW_KEY);
|
||||||
|
|
||||||
|
initBackupLocationOption();
|
||||||
|
initBackupIntervalOption();
|
||||||
|
initMaxBackupsOption();
|
||||||
|
initBackupNowOption();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void initBackupLocationOption()
|
||||||
|
{
|
||||||
|
String storedFolderPath = prefs.getString(BACKUP_FOLDER_PATH_KEY, null);
|
||||||
|
boolean isEnabled = false;
|
||||||
|
if (!TextUtils.isEmpty(storedFolderPath))
|
||||||
|
{
|
||||||
|
if (isFolderWritable(requireContext(), storedFolderPath))
|
||||||
|
{
|
||||||
|
setFormattedBackupPath(Uri.parse(storedFolderPath));
|
||||||
|
isEnabled = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger.e(TAG, "Backup location is not available, path: " + storedFolderPath);
|
||||||
|
showBackupErrorAlertDialog(requireContext().getString(R.string.dialog_report_error_missing_folder));
|
||||||
|
backupLocationOption.setSummary(requireContext().getString(R.string.pref_backup_now_summary_folder_unavailable));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
backupLocationOption.setSummary(requireContext().getString(R.string.pref_backup_location_summary_initial));
|
||||||
|
}
|
||||||
|
|
||||||
|
applyAdvancedSettings(isEnabled);
|
||||||
|
|
||||||
|
backupLocationOption.setOnPreferenceClickListener(preference -> {
|
||||||
|
launchFolderPicker();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setFormattedBackupPath(@NonNull Uri uri)
|
||||||
|
{
|
||||||
|
backupLocationOption.setSummary(formatReadableFolderPath(requireContext(), uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initBackupIntervalOption()
|
||||||
|
{
|
||||||
|
String backupInterval = prefs.getString(BACKUP_INTERVAL_KEY, DEFAULT_BACKUP_INTERVAL);
|
||||||
|
|
||||||
|
CharSequence entry = getEntryForValue(backupIntervalOption, backupInterval);
|
||||||
|
if (entry != null)
|
||||||
|
backupIntervalOption.setSummary(entry);
|
||||||
|
|
||||||
|
backupIntervalOption.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
CharSequence newEntry = getEntryForValue(backupIntervalOption, newValue.toString());
|
||||||
|
Logger.i(TAG, "auto backup interval changed to " + newEntry);
|
||||||
|
if (newEntry != null)
|
||||||
|
backupIntervalOption.setSummary(newEntry);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initMaxBackupsOption()
|
||||||
|
{
|
||||||
|
maxBackupsOption.setSummary(String.valueOf(getMaxBackups(prefs)));
|
||||||
|
|
||||||
|
maxBackupsOption.setOnPreferenceChangeListener((preference, newValue) -> {
|
||||||
|
maxBackupsOption.setSummary(newValue.toString());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initBackupNowOption()
|
||||||
|
{
|
||||||
|
updateStatusSummaryOption();
|
||||||
|
backupNowOption.setOnPreferenceClickListener(preference -> {
|
||||||
|
runBackup();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStatusSummaryOption()
|
||||||
|
{
|
||||||
|
long lastBackupTime = prefs.getLong(LAST_BACKUP_TIME_KEY, 0L);
|
||||||
|
|
||||||
|
String summary;
|
||||||
|
if (lastBackupTime > 0)
|
||||||
|
{
|
||||||
|
String time = DateFormat.getDateTimeInstance(DateFormat.SHORT, DateFormat.SHORT).format(lastBackupTime);
|
||||||
|
summary = requireContext().getString(R.string.pref_backup_status_summary_success) + ": " + time;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
summary = requireContext().getString(R.string.pref_backup_now_summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
backupNowOption.setSummary(summary);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetLastBackupTime()
|
||||||
|
{
|
||||||
|
prefs.edit().remove(LAST_BACKUP_TIME_KEY).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyAdvancedSettings(boolean isBackupEnabled)
|
||||||
|
{
|
||||||
|
backupIntervalOption.setVisible(isBackupEnabled);
|
||||||
|
maxBackupsOption.setVisible(isBackupEnabled);
|
||||||
|
backupNowOption.setVisible(isBackupEnabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void runBackup()
|
||||||
|
{
|
||||||
|
String currentFolderPath = prefs.getString(BACKUP_FOLDER_PATH_KEY, null);
|
||||||
|
if (!TextUtils.isEmpty(currentFolderPath))
|
||||||
|
{
|
||||||
|
if (isFolderWritable(requireContext(), currentFolderPath))
|
||||||
|
{
|
||||||
|
mBackupManager = new LocalBackupManager(requireActivity(), currentFolderPath, getMaxBackups(prefs));
|
||||||
|
mBackupManager.setListener(new LocalBackupManager.Listener()
|
||||||
|
{
|
||||||
|
@Override
|
||||||
|
public void onBackupStarted()
|
||||||
|
{
|
||||||
|
Logger.i(TAG, "Manual backup started");
|
||||||
|
|
||||||
|
backupNowOption.setEnabled(false);
|
||||||
|
backupNowOption.setSummary(R.string.pref_backup_now_summary_progress);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackupFinished()
|
||||||
|
{
|
||||||
|
Logger.i(TAG, "Manual backup successful");
|
||||||
|
|
||||||
|
backupNowOption.setEnabled(true);
|
||||||
|
backupNowOption.setSummary(R.string.pref_backup_now_summary_ok);
|
||||||
|
|
||||||
|
prefs.edit().putLong(LAST_BACKUP_TIME_KEY, System.currentTimeMillis()).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackupFailed(LocalBackupManager.ErrorCode errorCode)
|
||||||
|
{
|
||||||
|
String errorMessage = switch (errorCode)
|
||||||
|
{
|
||||||
|
case EMPTY_CATEGORY -> requireContext().getString(R.string.pref_backup_now_summary_empty_lists);
|
||||||
|
default -> requireContext().getString(R.string.pref_backup_now_summary_failed);
|
||||||
|
};
|
||||||
|
|
||||||
|
Logger.e(TAG, "Manual backup was failed with code: " + errorCode);
|
||||||
|
|
||||||
|
backupNowOption.setEnabled(true);
|
||||||
|
backupNowOption.setSummary(errorMessage);
|
||||||
|
|
||||||
|
showBackupErrorAlertDialog(requireContext().getString(R.string.dialog_report_error_with_logs));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mBackupManager.doBackup();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
backupNowOption.setSummary(R.string.pref_backup_now_summary_folder_unavailable);
|
||||||
|
showBackupErrorAlertDialog(requireContext().getString(R.string.dialog_report_error_missing_folder));
|
||||||
|
Logger.e(TAG, "Manual backup error: folder " + currentFolderPath + " unavailable");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
backupNowOption.setSummary(R.string.pref_backup_now_summary_folder_unavailable);
|
||||||
|
Logger.e(TAG, "Manual backup error: no folder selected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void launchFolderPicker()
|
||||||
|
{
|
||||||
|
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||||
|
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||||
|
intent.putExtra("android.content.extra.SHOW_ADVANCED", true);
|
||||||
|
|
||||||
|
PackageManager packageManager = requireActivity().getPackageManager();
|
||||||
|
if (intent.resolveActivity(packageManager) != null)
|
||||||
|
folderPickerLauncher.launch(intent);
|
||||||
|
else
|
||||||
|
showNoFileManagerError();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showNoFileManagerError()
|
||||||
|
{
|
||||||
|
new MaterialAlertDialogBuilder(requireActivity())
|
||||||
|
.setMessage(R.string.error_no_file_manager_app)
|
||||||
|
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showBackupErrorAlertDialog(String message)
|
||||||
|
{
|
||||||
|
requireActivity().runOnUiThread(() -> {
|
||||||
|
new MaterialAlertDialogBuilder(requireActivity())
|
||||||
|
.setTitle(R.string.pref_backup_now_summary_failed)
|
||||||
|
.setMessage(message)
|
||||||
|
.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
|
||||||
|
.show();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void takePersistableUriPermission(Uri uri)
|
||||||
|
{
|
||||||
|
requireContext().getContentResolver().takePersistableUriPermission(
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static CharSequence getEntryForValue(@NonNull ListPreference listPref, @NonNull CharSequence value)
|
||||||
|
{
|
||||||
|
CharSequence[] entryValues = listPref.getEntryValues();
|
||||||
|
CharSequence[] entries = listPref.getEntries();
|
||||||
|
|
||||||
|
if (entryValues == null || entries == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
for (int i = 0; i < entryValues.length; i++)
|
||||||
|
{
|
||||||
|
if (entryValues[i].equals(value))
|
||||||
|
return entries[i];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -189,6 +189,10 @@ public class SettingsPrefsFragment extends BaseXmlSettingsFragment implements La
|
|||||||
LanguagesFragment langFragment = (LanguagesFragment)getSettingsActivity().stackFragment(LanguagesFragment.class, getString(R.string.change_map_locale), null);
|
LanguagesFragment langFragment = (LanguagesFragment)getSettingsActivity().stackFragment(LanguagesFragment.class, getString(R.string.change_map_locale), null);
|
||||||
langFragment.setListener(this);
|
langFragment.setListener(this);
|
||||||
}
|
}
|
||||||
|
else if (key.equals(getString(R.string.pref_backup)))
|
||||||
|
{
|
||||||
|
getSettingsActivity().stackFragment(BackupSettingsFragment.class, getString(R.string.pref_backup_title), null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return super.onPreferenceTreeClick(preference);
|
return super.onPreferenceTreeClick(preference);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package app.organicmaps.util;
|
package app.organicmaps.util;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
import android.content.ContentResolver;
|
import android.content.ContentResolver;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
@@ -10,10 +11,13 @@ import android.provider.DocumentsContract;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.content.FileProvider;
|
import androidx.core.content.FileProvider;
|
||||||
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
|
|
||||||
import app.organicmaps.BuildConfig;
|
import app.organicmaps.BuildConfig;
|
||||||
import app.organicmaps.util.log.Logger;
|
import app.organicmaps.util.log.Logger;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileInputStream;
|
||||||
import java.io.FileOutputStream;
|
import java.io.FileOutputStream;
|
||||||
import java.io.FilenameFilter;
|
import java.io.FilenameFilter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -323,4 +327,76 @@ public class StorageUtils
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean copyFileToDocumentFile(
|
||||||
|
@NonNull Activity activity,
|
||||||
|
@NonNull File sourceFile,
|
||||||
|
@NonNull DocumentFile targetFile
|
||||||
|
)
|
||||||
|
{
|
||||||
|
try (
|
||||||
|
InputStream in = new FileInputStream(sourceFile);
|
||||||
|
OutputStream out = activity.getContentResolver().openOutputStream(targetFile.getUri())
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (out == null)
|
||||||
|
{
|
||||||
|
Logger.e(TAG, "Failed to open output stream for " + targetFile.getUri());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
int length;
|
||||||
|
|
||||||
|
while ((length = in.read(buffer)) > 0)
|
||||||
|
out.write(buffer, 0, length);
|
||||||
|
|
||||||
|
out.flush();
|
||||||
|
return true;
|
||||||
|
} catch (IOException e)
|
||||||
|
{
|
||||||
|
Logger.e(TAG, "Failed to copy file from " + sourceFile.getAbsolutePath() + " to " + targetFile.getUri(), e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void deleteDirectoryRecursive(@NonNull DocumentFile dir)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
for (DocumentFile file : dir.listFiles())
|
||||||
|
{
|
||||||
|
if (file.isDirectory())
|
||||||
|
deleteDirectoryRecursive(file);
|
||||||
|
else
|
||||||
|
file.delete();
|
||||||
|
}
|
||||||
|
dir.delete();
|
||||||
|
} catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.e(TAG, "Failed to delete directory: " + dir.getUri(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isFolderWritable(Context context, String folderPath)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Uri folderUri = Uri.parse(folderPath);
|
||||||
|
DocumentFile folder = DocumentFile.fromTreeUri(context, folderUri);
|
||||||
|
if (folder != null && folder.canWrite())
|
||||||
|
{
|
||||||
|
DocumentFile tempFile = folder.createFile("application/octet-stream", "temp_file");
|
||||||
|
if (tempFile != null)
|
||||||
|
{
|
||||||
|
tempFile.delete();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e)
|
||||||
|
{
|
||||||
|
Logger.e(TAG, "Failed to check if folder is writable: " + folderPath, e);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -911,4 +911,29 @@
|
|||||||
<string name="pedestrian">Пешеход</string>
|
<string name="pedestrian">Пешеход</string>
|
||||||
<string name="clear">Очистить</string>
|
<string name="clear">Очистить</string>
|
||||||
<string name="route_type">Вид маршрута</string>
|
<string name="route_type">Вид маршрута</string>
|
||||||
|
<!-- Settings "Backup" category: "Backup" title -->
|
||||||
|
<string name="pref_backup_title">Резервное копирование меток и треков</string>
|
||||||
|
<string name="pref_backup_summary">Автоматически сохранять в папку на устройстве</string>
|
||||||
|
<string name="pref_backup_now_title">Создать резервную копию</string>
|
||||||
|
<string name="pref_backup_now_summary">Запустить резервное копирование вручную</string>
|
||||||
|
<string name="pref_backup_now_summary_progress">Идёт резервное копирование…</string>
|
||||||
|
<string name="pref_backup_now_summary_ok">Копирование успешно завершено</string>
|
||||||
|
<string name="pref_backup_now_summary_empty_lists">Нет данных для копирования</string>
|
||||||
|
<string name="pref_backup_now_summary_failed">Ошибка при копировании</string>
|
||||||
|
<string name="pref_backup_now_summary_folder_unavailable">Папка для копий недоступна</string>
|
||||||
|
<string name="pref_backup_status_summary_success">Последнее успешное копирование</string>
|
||||||
|
<string name="pref_backup_location_title">Папка для резервных копий</string>
|
||||||
|
<string name="pref_backup_location_summary_initial">Сначала выберите папку и дайте доступ</string>
|
||||||
|
<string name="pref_backup_history_title">Хранить количество копий</string>
|
||||||
|
<string name="pref_backup_interval_title">Автозапуск</string>
|
||||||
|
<string name="backup_interval_every_day">Каждый день</string>
|
||||||
|
<string name="backup_interval_every_week">Каждую неделю</string>
|
||||||
|
<string name="backup_interval_manual_only">Выключено (только вручную)</string>
|
||||||
|
<string name="dialog_report_error_missing_folder">Выбранная папка для резервного копирования недоступна или нет права записи в неё. Пожалуйста, выберите другую папку</string>
|
||||||
|
<string name="dialog_report_error_with_logs">Пожалуйста, отправьте нам отчет об ошибке:\n
|
||||||
|
- Включите \"Запись логов\" в настройках\n
|
||||||
|
- воспроизведите проблему\n
|
||||||
|
- на экране \"Справка\" нажмите кнопку \"Сообщить о проблеме\" и отправьте нам отчет по почте или в чат\n
|
||||||
|
- отключите логирование
|
||||||
|
</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
<string name="pref_keep_screen_on" translatable="false">KeepScreenOn</string>
|
<string name="pref_keep_screen_on" translatable="false">KeepScreenOn</string>
|
||||||
<string name="pref_show_on_lock_screen" translatable="false">ShowOnLockScreen</string>
|
<string name="pref_show_on_lock_screen" translatable="false">ShowOnLockScreen</string>
|
||||||
<string name="pref_map_locale" translatable="false">MapLanguage</string>
|
<string name="pref_map_locale" translatable="false">MapLanguage</string>
|
||||||
|
<string name="pref_backup" translatable="false">Backup</string>
|
||||||
<string name="pref_left_button" translatable="false">LeftButton</string>
|
<string name="pref_left_button" translatable="false">LeftButton</string>
|
||||||
|
|
||||||
<string name="notification_ticker_ltr" translatable="false">%1$s: %2$s</string>
|
<string name="notification_ticker_ltr" translatable="false">%1$s: %2$s</string>
|
||||||
|
|||||||
@@ -24,6 +24,28 @@
|
|||||||
<item>1</item>
|
<item>1</item>
|
||||||
</string-array>
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="backup_interval_entries">
|
||||||
|
<item>@string/backup_interval_every_day</item>
|
||||||
|
<item>@string/backup_interval_every_week</item>
|
||||||
|
<item>@string/backup_interval_manual_only</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="backup_interval_values">
|
||||||
|
<item>86400000</item> <!-- Every day -->
|
||||||
|
<item>604800000</item> <!-- Every week -->
|
||||||
|
<item>0</item> <!-- Manual only -->
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="backup_history_entries">
|
||||||
|
<item>3</item>
|
||||||
|
<item>10</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
|
<string-array name="backup_history_values">
|
||||||
|
<item>3</item>
|
||||||
|
<item>10</item>
|
||||||
|
</string-array>
|
||||||
|
|
||||||
<string-array name="map_style">
|
<string-array name="map_style">
|
||||||
<item>@string/off</item>
|
<item>@string/off</item>
|
||||||
<item>@string/on</item>
|
<item>@string/on</item>
|
||||||
|
|||||||
@@ -936,6 +936,32 @@
|
|||||||
<string name="codeberg">Codeberg</string>
|
<string name="codeberg">Codeberg</string>
|
||||||
<string name="pref_left_button_title">Left button setup</string>
|
<string name="pref_left_button_title">Left button setup</string>
|
||||||
<string name="pref_left_button_disable">Disable</string>
|
<string name="pref_left_button_disable">Disable</string>
|
||||||
|
|
||||||
|
<!-- Settings "Backup" category: "Backup" title -->
|
||||||
|
<string name="pref_backup_title">Bookmarks and tracks backup</string>
|
||||||
|
<string name="pref_backup_summary">Automatically backup to a folder on your device</string>
|
||||||
|
<string name="pref_backup_now_title">Backup now</string>
|
||||||
|
<string name="pref_backup_now_summary">Create a backup immediately</string>
|
||||||
|
<string name="pref_backup_now_summary_progress">Backup in progress…</string>
|
||||||
|
<string name="pref_backup_now_summary_ok">Backup completed successfully</string>
|
||||||
|
<string name="pref_backup_now_summary_empty_lists">Nothing to back up</string>
|
||||||
|
<string name="pref_backup_now_summary_failed">Backup failed</string>
|
||||||
|
<string name="pref_backup_now_summary_folder_unavailable">The backup folder is not available</string>
|
||||||
|
<string name="pref_backup_status_summary_success">Last successful backup</string>
|
||||||
|
<string name="pref_backup_location_title">Backup location</string>
|
||||||
|
<string name="pref_backup_location_summary_initial">Please select a folder first and grant permission</string>
|
||||||
|
<string name="pref_backup_history_title">Number of backups to keep</string>
|
||||||
|
<string name="pref_backup_interval_title">Automatic backup</string>
|
||||||
|
<string name="backup_interval_every_day">Daily</string>
|
||||||
|
<string name="backup_interval_every_week">Weekly</string>
|
||||||
|
<string name="backup_interval_manual_only">Off (manual only)</string>
|
||||||
|
<string name="dialog_report_error_missing_folder">The selected backup location is not available or writable. Select a different location, please.</string>
|
||||||
|
<string name="dialog_report_error_with_logs">Please send us an error report:\n
|
||||||
|
- \"Enable logging\" in the settings\n
|
||||||
|
- reproduce the problem\n
|
||||||
|
- in the \"Help/About\" screen press a \"Report a bug\" button and send it to us via email or chat\n
|
||||||
|
- disable logging
|
||||||
|
</string>
|
||||||
<string name="clear">Clear</string>
|
<string name="clear">Clear</string>
|
||||||
<string name="route_type">Route type</string>
|
<string name="route_type">Route type</string>
|
||||||
<string name="vehicle">Vehicle</string>
|
<string name="vehicle">Vehicle</string>
|
||||||
|
|||||||
24
android/app/src/main/res/xml/prefs_backup.xml
Normal file
24
android/app/src/main/res/xml/prefs_backup.xml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<Preference
|
||||||
|
android:key="backup_location"
|
||||||
|
android:summary="@string/pref_backup_location_summary_initial"
|
||||||
|
android:title="@string/pref_backup_location_title" />
|
||||||
|
<Preference
|
||||||
|
android:key="backup_now"
|
||||||
|
android:summary="@string/pref_backup_now_summary"
|
||||||
|
android:title="@string/pref_backup_now_title" />
|
||||||
|
<ListPreference
|
||||||
|
android:defaultValue="86400000"
|
||||||
|
android:entries="@array/backup_interval_entries"
|
||||||
|
android:entryValues="@array/backup_interval_values"
|
||||||
|
android:key="backup_history_interval"
|
||||||
|
android:title="@string/pref_backup_interval_title" />
|
||||||
|
<ListPreference
|
||||||
|
android:defaultValue="10"
|
||||||
|
android:entries="@array/backup_history_entries"
|
||||||
|
android:entryValues="@array/backup_history_values"
|
||||||
|
android:key="backup_history_count"
|
||||||
|
android:title="@string/pref_backup_history_title" />
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
@@ -112,6 +112,13 @@
|
|||||||
app:singleLineTitle="false"
|
app:singleLineTitle="false"
|
||||||
android:persistent="false"
|
android:persistent="false"
|
||||||
android:order="18"/>
|
android:order="18"/>
|
||||||
|
<Preference
|
||||||
|
android:key="@string/pref_backup"
|
||||||
|
android:title="@string/pref_backup_title"
|
||||||
|
android:summary="@string/pref_backup_summary"
|
||||||
|
app:singleLineTitle="false"
|
||||||
|
android:persistent="false"
|
||||||
|
android:order="19"/>
|
||||||
</androidx.preference.PreferenceCategory>
|
</androidx.preference.PreferenceCategory>
|
||||||
|
|
||||||
<androidx.preference.PreferenceCategory
|
<androidx.preference.PreferenceCategory
|
||||||
|
|||||||
Reference in New Issue
Block a user