Skip to content

Commit

Permalink
Use Storage Access Framework for exporting settings.
Browse files Browse the repository at this point in the history
  • Loading branch information
kahays committed Jan 13, 2022
1 parent 3335f85 commit 38d2f23
Show file tree
Hide file tree
Showing 4 changed files with 63 additions and 120 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,9 @@
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Resources;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.Environment;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
Expand All @@ -53,13 +50,11 @@
import org.jsoup.nodes.Document;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.Calendar;
import java.io.OutputStream;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
Expand Down Expand Up @@ -433,43 +428,21 @@ public boolean canLoadAvatars(){


/**
* Export the app's current preferences to a file in the app folder.
* <p>
* You need to ensure the user has granted write permissions before calling this!
* Export the app's current preferences to a user-picked location.
*
* @param settingsUri the file/location to export to
* @return false if the export failed
* @see #importSettings(Uri)
*/
public boolean exportSettings() {
public boolean exportSettings(@NonNull Uri settingsUri) {
Map settings = mPrefs.getAll();
Calendar date = Calendar.getInstance();
Gson gson = new Gson();
// serialise all SharedPreferences mappings to JSON
String settingsJson = gson.toJson(settings);

if (!Environment.getExternalStorageState().equalsIgnoreCase(Environment.MEDIA_MOUNTED)) {
Log.w(TAG, "exportSettings: external storage not mounted");
return false;
}
// TODO: 11/12/2017 the folder name should probably be a global constant, it's referenced elsewhere too e.g. custom themes location
// TODO: 17/12/2017 honestly we could probably do with a class that handles the Awful folder and all the storage state checking in one place
File awfulFolder = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/awful");
if (!awfulFolder.isDirectory() && !awfulFolder.mkdir()) {
Log.w(TAG, "exportSettings: failed to create missing awful folder!");
return false;
}

// build the filename using the current app version and today's date
String filename;
try {
PackageInfo pInfo = mContext.getPackageManager().getPackageInfo(mContext.getPackageName(), 0);
filename = String.format(Locale.US, "awful-%d-%d-%d-%d.settings",
pInfo.versionCode, date.get(Calendar.DATE), date.get(Calendar.MONTH) + 1, date.get(Calendar.YEAR));
} catch (NameNotFoundException e) {
Log.w(TAG, "exportSettings: can't get package name for app version code", e);
return false;
}

// save the JSON in binary format
Log.i(TAG, "exporting settings to file: " + filename);
try (FileOutputStream out = new FileOutputStream(new File(awfulFolder.getAbsolutePath(), filename))) {
Log.i(TAG, "exporting settings to uri: " + settingsUri.getLastPathSegment());
try (OutputStream out = getContext().getContentResolver().openOutputStream(settingsUri)) {
out.write(settingsJson.getBytes());
return true;
} catch (IOException e) {
Expand All @@ -484,9 +457,9 @@ public boolean exportSettings() {
*
* @param settingsUri the file to import
* @return false if importing failed completely
* @see #exportSettings()
* @see #exportSettings(Uri)
*/
boolean importSettings(@NonNull Uri settingsUri) {
public boolean importSettings(@NonNull Uri settingsUri) {
Log.i(TAG, "importing settings from file: " + settingsUri.getLastPathSegment());
BufferedReader br;
try {
Expand All @@ -502,8 +475,14 @@ boolean importSettings(@NonNull Uri settingsUri) {
}

// read settings JSON file and deserialise into the types SharedPreferences dumps as
Map<String, Object> settings = new Gson().fromJson(br, new TypeToken<Map<String, Object>>() {
}.getType());
Map<String, Object> settings;
try {
settings = new Gson().fromJson(br, new TypeToken<Map<String, Object>>() {
}.getType());
} catch (Exception e) {
e.printStackTrace();
return false;
}
SharedPreferences.Editor editor = mPrefs.edit();

// TODO: 15/12/2017 there's no checking here at all - need to handle any errors safely. What happens when a pref no longer exists, or has its type changed between versions?
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package com.ferg.awfulapp.preferences;

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
Expand All @@ -15,8 +14,6 @@
import android.os.Bundle;
import android.preference.PreferenceManager;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.Toolbar;
import android.view.MenuItem;
Expand All @@ -28,7 +25,6 @@
import com.ferg.awfulapp.constants.Constants;
import com.ferg.awfulapp.preferences.fragments.RootSettings;
import com.ferg.awfulapp.preferences.fragments.SettingsFragment;
import com.ferg.awfulapp.util.AwfulUtils;

import org.apache.commons.lang3.StringUtils;

Expand Down Expand Up @@ -62,7 +58,8 @@ public class SettingsActivity extends AwfulActivity implements AwfulPreferences.
private static final String ROOT_FRAGMENT_TAG = "rootfragtag";
private static final String SUBMENU_FRAGMENT_TAG = "subfragtag";
public static final int DIALOG_ABOUT = 1;
public static final int SETTINGS_FILE = 2;
public static final int SETTINGS_IMPORT = 2;
public static final int SETTINGS_EXPORT = 3;

public AwfulPreferences prefs;
private String currentThemeName;
Expand Down Expand Up @@ -272,33 +269,27 @@ public void onPreferenceChange(AwfulPreferences preferences, String key) {

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == Activity.RESULT_OK) {
if (requestCode == SETTINGS_FILE) {
if (AwfulUtils.isMarshmallow()) {
int permissionCheck = ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE);
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
this.importData = data;
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, Constants.AWFUL_PERMISSION_READ_EXTERNAL_STORAGE);
} else {
importFile(data);
}
} else {
importFile(data);
}
if (requestCode == SETTINGS_IMPORT) {
importFile(data);
} else if (requestCode == SETTINGS_EXPORT) {
exportSettings(data);
}
}
}

protected void importFile(Intent data) {
Uri settingsUri = data.getData();
if (settingsUri != null && AwfulPreferences.getInstance(this).importSettings(settingsUri)) {
Toast.makeText(this, "Import success!", Toast.LENGTH_SHORT).show();
this.finish();
} else {
Toast.makeText(this, "Unable to import settings file", Toast.LENGTH_SHORT).show();
}
boolean success = (settingsUri != null && AwfulPreferences.getInstance(this).importSettings(settingsUri));
Toast.makeText(this, (success ? "Import success!" : "Unable to import settings file"), Toast.LENGTH_SHORT).show();
}

private void exportSettings(Intent data) {
Uri settingsUri = data.getData();
boolean success = (settingsUri != null && AwfulPreferences.getInstance(this).exportSettings(settingsUri));
Toast.makeText(this, (success ? "Settings exported!" : "Failed to export"), Toast.LENGTH_SHORT).show();
}

@Override
protected Dialog onCreateDialog(int dialogId) {
Expand Down Expand Up @@ -329,21 +320,4 @@ protected Dialog onCreateDialog(int dialogId) {
return super.onCreateDialog(dialogId);
}
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case Constants.AWFUL_PERMISSION_READ_EXTERNAL_STORAGE: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
importFile(importData);
} else {
Toast.makeText(this, R.string.no_file_permission_settings_import, Toast.LENGTH_LONG).show();
}
break;
}
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
package com.ferg.awfulapp.preferences.fragments;

import android.Manifest;
import android.app.Activity;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.preference.Preference;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import android.widget.Toast;

import com.ferg.awfulapp.NavigationEvent;
import com.ferg.awfulapp.R;
import com.ferg.awfulapp.constants.Constants;
import com.ferg.awfulapp.dialog.Changelog;
import com.ferg.awfulapp.preferences.SettingsActivity;
import com.ferg.awfulapp.util.AwfulUtils;

import java.util.Calendar;
import java.util.Locale;

/**
* Created by baka kaba on 04/05/2015.
Expand Down Expand Up @@ -97,53 +98,43 @@ public boolean onPreferenceClick(Preference preference) {
private class ExportListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
if (AwfulUtils.isMarshmallow()) {
int permissionCheck = ContextCompat.checkSelfPermission(getActivity(), Manifest.permission.WRITE_EXTERNAL_STORAGE);
if (permissionCheck != PackageManager.PERMISSION_GRANTED) {
requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, Constants.AWFUL_PERMISSION_WRITE_EXTERNAL_STORAGE);
} else {
exportSettings();
}
} else {
exportSettings();
Activity context = getActivity();
PackageInfo pInfo;
try {
pInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
} catch (PackageManager.NameNotFoundException e) {
// super unlikely
e.printStackTrace();
return false;
}
Calendar date = Calendar.getInstance();

Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT)
.setType("*/*")
.addCategory(Intent.CATEGORY_OPENABLE)
.putExtra(Intent.EXTRA_TITLE, String.format(Locale.US, "awful-%d-%d-%d-%d.settings",
pInfo.versionCode, date.get(Calendar.DATE), date.get(Calendar.MONTH) + 1, date.get(Calendar.YEAR)));

getActivity().startActivityForResult(Intent.createChooser(intent, getString(R.string.export_settings_chooser_title)), SettingsActivity.SETTINGS_EXPORT);
return true;
}
}

private void exportSettings() {
String message = mPrefs.exportSettings() ? "Settings exported" : "Failed to export!";
Toast.makeText(getActivity(), message, Toast.LENGTH_LONG).show();
}

/**
* Listener for the 'Import settings' option
*/
private class ImportListener implements Preference.OnPreferenceClickListener {
@Override
public boolean onPreferenceClick(Preference preference) {
// ACTION_GET_CONTENT may return URIs for deleted content as well,
// which is super confusing. workarounds seem like more trouble
// than they're worth right now.
// see https://stackoverflow.com/questions/55122556
Intent intent = new Intent(Intent.ACTION_GET_CONTENT)
.setType("*/*")
.addCategory(Intent.CATEGORY_OPENABLE);
getActivity().startActivityForResult(Intent.createChooser(intent, getString(R.string.import_settings_chooser_title)), SettingsActivity.SETTINGS_FILE);
getActivity().startActivityForResult(Intent.createChooser(intent, getString(R.string.import_settings_chooser_title)), SettingsActivity.SETTINGS_IMPORT);
return true;
}
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case Constants.AWFUL_PERMISSION_WRITE_EXTERNAL_STORAGE: {
// If request is cancelled, the result arrays are empty.
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
exportSettings();
} else {
Toast.makeText(getActivity(), R.string.no_file_permission_settings_export, Toast.LENGTH_LONG).show();
}
break;
}
default:
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
}
}
3 changes: 1 addition & 2 deletions Awful.apk/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,6 @@
<string name="alert_ok">@android:string/ok</string>
<string name="alert_settings">Settings</string>
<string name="no_file_permission_attachment">Well, you didn\'t allow Awful to access files, so it can\'t add the attachment you just selected. Good job.</string>
<string name="no_file_permission_settings_import">You know, you didn\'t allow Awful to access files, so it can\'t import the settings file you just selected. Well done.</string>
<string name="no_file_permission_settings_export">You didn\'t allow Awful to access files, so it can\'t export the settings file. Congratulations.</string>
<string name="no_file_permission_download">Awful doesn\'t have permission to store the download. Welp, guess not then.</string>
<string name="no_file_permission_theme">Can\'t access custom theme because Awful lacks storage permissions. Reverting to default css.</string>
<string name="permission_rationale_external_storage">To show custom themes and layouts, Awful needs access to device storage</string>
Expand Down Expand Up @@ -333,6 +331,7 @@
<string name="prefs_misc">Device and navigation</string>
<string name="export_settings">Export settings</string>
<string name="export_settings_summary">Save a backup of your Awful choices</string>
<string name="export_settings_chooser_title">Select export file</string>
<string name="import_settings">Import settings</string>
<string name="import_settings_summary">Restore your backed up settings</string>
<string name="import_settings_chooser_title">Select settings file</string>
Expand Down

0 comments on commit 38d2f23

Please sign in to comment.