diff --git a/app/build.gradle b/app/build.gradle index b2820092..5e050a47 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -170,6 +170,9 @@ dependencies { // Crash screen implementation 'com.melegy.redscreenofdeath:red-screen-of-death:0.1.2' + implementation 'net.lingala.zip4j:zip4j:2.7.0' + implementation 'org.apache.commons:commons-io:1.3.2' + /**** Builds and flavors ****/ // debugImplementation because LeakCanary should only run in debug builds. diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/BackupSettings.java b/app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/BackupSettings.java new file mode 100644 index 00000000..a6fc1508 --- /dev/null +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/asynctasks/BackupSettings.java @@ -0,0 +1,191 @@ +package ml.docilealligator.infinityforreddit.asynctasks; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Handler; + +import androidx.documentfile.provider.DocumentFile; + +import net.lingala.zip4j.ZipFile; +import net.lingala.zip4j.model.ZipParameters; +import net.lingala.zip4j.model.enums.EncryptionMethod; + +import org.apache.commons.io.FileUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.Executor; + +import ml.docilealligator.infinityforreddit.BuildConfig; +import ml.docilealligator.infinityforreddit.R; +import ml.docilealligator.infinityforreddit.utils.CustomThemeSharedPreferencesUtils; +import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; + +public class BackupSettings { + public static void backupSettings(Context context, Executor executor, Handler handler, + ContentResolver contentResolver, Uri destinationDirUri, + SharedPreferences defaultSharedPreferences, + SharedPreferences lightThemeSharedPreferences, + SharedPreferences darkThemeSharedPreferences, + SharedPreferences amoledThemeSharedPreferences, + SharedPreferences sortTypeSharedPreferences, + SharedPreferences postLayoutSharedPreferences, + SharedPreferences postFeedScrolledPositionSharedPreferences, + SharedPreferences mainActivityTabsSharedPreferences, + SharedPreferences nsfwAndSpoilerSharedPreferencs, + SharedPreferences bottomAppBarSharedPreferences, + SharedPreferences postHistorySharedPreferences, + BackupSettingsListener backupSettingsListener) { + executor.execute(() -> { + boolean res = saveSharedPreferencesToFile(context, defaultSharedPreferences, + SharedPreferencesUtils.DEFAULT_PREFERENCES_FILE); + boolean res1 = saveSharedPreferencesToFile(context, lightThemeSharedPreferences, + CustomThemeSharedPreferencesUtils.LIGHT_THEME_SHARED_PREFERENCES_FILE); + boolean res2 = saveSharedPreferencesToFile(context, darkThemeSharedPreferences, + CustomThemeSharedPreferencesUtils.DARK_THEME_SHARED_PREFERENCES_FILE); + boolean res3 = saveSharedPreferencesToFile(context, amoledThemeSharedPreferences, + CustomThemeSharedPreferencesUtils.AMOLED_THEME_SHARED_PREFERENCES_FILE); + boolean res4 = saveSharedPreferencesToFile(context, sortTypeSharedPreferences, + SharedPreferencesUtils.SORT_TYPE_SHARED_PREFERENCES_FILE); + boolean res5 = saveSharedPreferencesToFile(context, postLayoutSharedPreferences, + SharedPreferencesUtils.POST_LAYOUT_SHARED_PREFERENCES_FILE); + boolean res6 = saveSharedPreferencesToFile(context, postFeedScrolledPositionSharedPreferences, + SharedPreferencesUtils.FRONT_PAGE_SCROLLED_POSITION_SHARED_PREFERENCES_FILE); + boolean res7 = saveSharedPreferencesToFile(context, mainActivityTabsSharedPreferences, + SharedPreferencesUtils.MAIN_PAGE_TABS_SHARED_PREFERENCES_FILE); + boolean res8 = saveSharedPreferencesToFile(context, nsfwAndSpoilerSharedPreferencs, + SharedPreferencesUtils.NSFW_AND_SPOILER_SHARED_PREFERENCES_FILE); + boolean res9 = saveSharedPreferencesToFile(context, bottomAppBarSharedPreferences, + SharedPreferencesUtils.BOTTOM_APP_BAR_SHARED_PREFERENCES_FILE); + boolean res10 = saveSharedPreferencesToFile(context, postHistorySharedPreferences, + SharedPreferencesUtils.POST_HISTORY_SHARED_PREFERENCES_FILE); + + boolean zipRes = zipAndMoveToDestinationDir(context, contentResolver, destinationDirUri); + + try { + FileUtils.deleteDirectory(new File(context.getExternalCacheDir() + "/Backup/")); + } catch (IOException e) { + e.printStackTrace(); + } + + handler.post(() -> { + boolean finalResult = res && res1 && res2 && res3 && res4 && res5 && res6 && res7 && res8 && res9 && res10 && zipRes; + if (finalResult) { + backupSettingsListener.success(); + } else { + if (!zipRes) { + backupSettingsListener.failed(context.getText(R.string.create_zip_in_destination_directory_failed).toString()); + } else { + backupSettingsListener.failed(context.getText(R.string.backup_some_settings_failed).toString()); + } + } + }); + }); + } + + private static boolean saveSharedPreferencesToFile(Context context, SharedPreferences sharedPreferences, + String fileName) { + + boolean result = false; + ObjectOutputStream output = null; + String backupDir = context.getExternalCacheDir() + "/Backup/" + BuildConfig.VERSION_NAME; + if (!new File(backupDir).exists()) { + new File(backupDir).mkdirs(); + } else { + File backupDirFile = new File(backupDir); + backupDirFile.delete(); + backupDirFile.mkdirs(); + } + + try { + output = new ObjectOutputStream(new FileOutputStream(new File(backupDir + "/" + fileName + ".txt"))); + output.writeObject(sharedPreferences.getAll()); + + result = true; + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (output != null) { + output.flush(); + } + } catch (IOException ex) { + ex.printStackTrace(); + } + } + return result; + } + + private static boolean zipAndMoveToDestinationDir(Context context, ContentResolver contentResolver, Uri destinationDirUri) { + OutputStream outputStream = null; + boolean result = false; + try { + String time = new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date(System.currentTimeMillis())); + String fileName = "Infinity_For_Reddit_Settings_Backup_v" + BuildConfig.VERSION_NAME + "-" + time + ".zip"; + String filePath = context.getExternalCacheDir() + "/Backup/" + fileName; + ZipFile zip = new ZipFile(filePath, "123321".toCharArray()); + ZipParameters zipParameters = new ZipParameters(); + zipParameters.setEncryptFiles(true); + zipParameters.setEncryptionMethod(EncryptionMethod.AES); + zip.addFolder(new File(context.getExternalCacheDir() + "/Backup/" + BuildConfig.VERSION_NAME + "/"), zipParameters); + + DocumentFile dir = DocumentFile.fromTreeUri(context, destinationDirUri); + if (dir == null) { + return false; + } + DocumentFile checkForDuplicate = dir.findFile(fileName); + if (checkForDuplicate != null) { + checkForDuplicate.delete(); + } + DocumentFile destinationFile = dir.createFile("application/zip", fileName); + if (destinationFile == null) { + return false; + } + + outputStream = contentResolver.openOutputStream(destinationFile.getUri()); + if (outputStream == null) { + return false; + } + + byte[] fileReader = new byte[1024]; + + FileInputStream inputStream = new FileInputStream(filePath); + while (true) { + int read = inputStream.read(fileReader); + + if (read == -1) { + break; + } + + outputStream.write(fileReader, 0, read); + } + result = true; + } catch (IOException e) { + e.printStackTrace(); + + } finally { + if (outputStream != null) { + try { + outputStream.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + return result; + } + + public interface BackupSettingsListener { + void success(); + void failed(String errorMessage); + } +} diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/settings/AdvancedPreferenceFragment.java b/app/src/main/java/ml/docilealligator/infinityforreddit/settings/AdvancedPreferenceFragment.java index cbb6669f..0708edd7 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/settings/AdvancedPreferenceFragment.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/settings/AdvancedPreferenceFragment.java @@ -2,12 +2,15 @@ package ml.docilealligator.infinityforreddit.settings; import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.content.SharedPreferences; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.widget.Toast; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AppCompatActivity; import androidx.fragment.app.Fragment; import androidx.preference.Preference; @@ -25,6 +28,7 @@ import javax.inject.Named; import ml.docilealligator.infinityforreddit.Infinity; import ml.docilealligator.infinityforreddit.R; import ml.docilealligator.infinityforreddit.RedditDataRoomDatabase; +import ml.docilealligator.infinityforreddit.asynctasks.BackupSettings; import ml.docilealligator.infinityforreddit.asynctasks.DeleteAllPostLayouts; import ml.docilealligator.infinityforreddit.asynctasks.DeleteAllReadPosts; import ml.docilealligator.infinityforreddit.asynctasks.DeleteAllSortTypes; @@ -34,11 +38,15 @@ import ml.docilealligator.infinityforreddit.asynctasks.DeleteAllUsers; import ml.docilealligator.infinityforreddit.events.RecreateActivityEvent; import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils; +import static android.app.Activity.RESULT_OK; +import static android.content.Intent.ACTION_OPEN_DOCUMENT_TREE; + /** * A simple {@link Fragment} subclass. */ public class AdvancedPreferenceFragment extends PreferenceFragmentCompat { + private static final int SELECT_BACKUP_SETTINGS_DIRECTORY_REQUEST_CODE = 1; @Inject RedditDataRoomDatabase mRedditDataRoomDatabase; @Inject @@ -69,6 +77,12 @@ public class AdvancedPreferenceFragment extends PreferenceFragmentCompat { @Named("nsfw_and_spoiler") SharedPreferences nsfwAndBlurringSharedPreferences; @Inject + @Named("bottom_app_bar") + SharedPreferences bottomAppBarSharedPreferences; + @Inject + @Named("post_history") + SharedPreferences postHistorySharedPreferences; + @Inject Executor executor; private Activity activity; @@ -87,6 +101,8 @@ public class AdvancedPreferenceFragment extends PreferenceFragmentCompat { Preference deleteReadPostsPreference = findPreference(SharedPreferencesUtils.DELETE_READ_POSTS_IN_DATABASE); Preference deleteAllLegacySettingsPreference = findPreference(SharedPreferencesUtils.DELETE_ALL_LEGACY_SETTINGS); Preference resetAllSettingsPreference = findPreference(SharedPreferencesUtils.RESET_ALL_SETTINGS); + Preference backupSettingsPreference = findPreference(SharedPreferencesUtils.BACKUP_SETTINGS); + Preference restoreSettingsPreference = findPreference(SharedPreferencesUtils.RESTORE_SETTINGS); if (deleteSubredditsPreference != null) { deleteSubredditsPreference.setOnPreferenceClickListener(preference -> { @@ -252,6 +268,67 @@ public class AdvancedPreferenceFragment extends PreferenceFragmentCompat { return true; }); } + + if (backupSettingsPreference != null) { + backupSettingsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + Intent intent = new Intent(ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + startActivityForResult(intent, SELECT_BACKUP_SETTINGS_DIRECTORY_REQUEST_CODE); + + /*BackupSettings.backupSettings(activity, executor, new Handler(), activity.getContentResolver(), null, + mSharedPreferences, lightThemeSharedPreferences, darkThemeSharedPreferences, + amoledThemeSharedPreferences, mSortTypeSharedPreferences, mPostLayoutSharedPreferences, + postFeedScrolledPositionSharedPreferences, mainActivityTabsSharedPreferences, + nsfwAndBlurringSharedPreferences, bottomAppBarSharedPreferences, postHistorySharedPreferences, + new BackupSettings.BackupSettingsListener() { + @Override + public void success() { + + } + + @Override + public void failed() { + + } + });*/ + return true; + } + }); + } + + if (restoreSettingsPreference != null) { + restoreSettingsPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + return true; + } + }); + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + if (requestCode == SELECT_BACKUP_SETTINGS_DIRECTORY_REQUEST_CODE && resultCode == RESULT_OK) { + Uri uri = data.getData(); + BackupSettings.backupSettings(activity, executor, new Handler(), activity.getContentResolver(), uri, + mSharedPreferences, lightThemeSharedPreferences, darkThemeSharedPreferences, + amoledThemeSharedPreferences, mSortTypeSharedPreferences, mPostLayoutSharedPreferences, + postFeedScrolledPositionSharedPreferences, mainActivityTabsSharedPreferences, + nsfwAndBlurringSharedPreferences, bottomAppBarSharedPreferences, postHistorySharedPreferences, + new BackupSettings.BackupSettingsListener() { + @Override + public void success() { + Toast.makeText(activity, R.string.backup_settings_success, Toast.LENGTH_LONG).show(); + } + + @Override + public void failed(String errorMessage) { + Toast.makeText(activity, errorMessage, Toast.LENGTH_LONG).show(); + } + }); + } } @Override diff --git a/app/src/main/java/ml/docilealligator/infinityforreddit/utils/SharedPreferencesUtils.java b/app/src/main/java/ml/docilealligator/infinityforreddit/utils/SharedPreferencesUtils.java index 5670b82f..d1942a34 100644 --- a/app/src/main/java/ml/docilealligator/infinityforreddit/utils/SharedPreferencesUtils.java +++ b/app/src/main/java/ml/docilealligator/infinityforreddit/utils/SharedPreferencesUtils.java @@ -169,7 +169,10 @@ public class SharedPreferencesUtils { public static final String HIDE_SUBREDDIT_AND_USER_PREFIX = "hide_subreddit_and_user_prefix"; public static final String HIDE_THE_NUMBER_OF_VOTES = "hide_the_number_of_votes"; public static final String HIDE_THE_NUMBER_OF_COMMENTS = "hide_the_number_of_comments"; + public static final String BACKUP_SETTINGS = "backup_settings"; + public static final String RESTORE_SETTINGS = "restore_settings"; + public static final String DEFAULT_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit_preferences"; public static final String MAIN_PAGE_TABS_SHARED_PREFERENCES_FILE = "ml.docilealligator.infinityforreddit.main_page_tabs"; public static final String MAIN_PAGE_TAB_COUNT = "_main_page_tab_count"; public static final String MAIN_PAGE_SHOW_TAB_NAMES = "_main_page_show_tab_names"; diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 93adbf0b..795a6a0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -541,6 +541,8 @@ Hide the Number of Votes Hide the Number of Comments Show Avatar on the Left in the Navigation Drawer + Backup Settings + Restore Settings Cannot get the link @@ -1037,4 +1039,8 @@ Start by joining a subreddit! + Successfully exported settings to the destination directory + Could not create backup zip in the destination directory + Could not backup some settings but others were successfully exported to the destination directory + diff --git a/app/src/main/res/xml/advanced_preferences.xml b/app/src/main/res/xml/advanced_preferences.xml index 6992530d..68b74260 100644 --- a/app/src/main/res/xml/advanced_preferences.xml +++ b/app/src/main/res/xml/advanced_preferences.xml @@ -38,6 +38,14 @@ app:key="reset_all_settings" app:title="@string/settings_reset_all_settings_title" /> + + + +