Merge pull request #400 from FineFindus/feat/import-export
feat: implement import/export of settings
This commit is contained in:
@@ -3,6 +3,11 @@ package org.joinmastodon.android.fragments.settings;
|
||||
import android.app.Activity;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
@@ -11,6 +16,13 @@ import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonParser;
|
||||
import com.google.gson.ToNumberPolicy;
|
||||
|
||||
import org.joinmastodon.android.BuildConfig;
|
||||
import org.joinmastodon.android.GlobalUserPreferences;
|
||||
import org.joinmastodon.android.MastodonApp;
|
||||
@@ -21,27 +33,26 @@ import org.joinmastodon.android.api.session.AccountSessionManager;
|
||||
import org.joinmastodon.android.fragments.HasAccountID;
|
||||
import org.joinmastodon.android.model.viewmodel.CheckableListItem;
|
||||
import org.joinmastodon.android.model.viewmodel.ListItem;
|
||||
import org.joinmastodon.android.ui.M3AlertDialogBuilder;
|
||||
import org.joinmastodon.android.ui.Snackbar;
|
||||
import org.joinmastodon.android.ui.utils.UiUtils;
|
||||
import org.joinmastodon.android.updater.GithubSelfUpdater;
|
||||
import org.joinmastodon.android.utils.FileProvider;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.StringReader;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.file.Files;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.time.format.FormatStyle;
|
||||
import java.util.ArrayList;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import me.grishka.appkit.imageloader.ImageCache;
|
||||
@@ -51,6 +62,7 @@ import me.grishka.appkit.utils.V;
|
||||
|
||||
public class SettingsAboutAppFragment extends BaseSettingsFragment<Void> implements HasAccountID{
|
||||
private static final String TAG="SettingsAboutAppFragment";
|
||||
private static final int IMPORT_RESULT=314;
|
||||
private ListItem<Void> mediaCacheItem, copyCrashLogItem;
|
||||
private CheckableListItem<Void> enablePreReleasesItem;
|
||||
private AccountSession session;
|
||||
@@ -58,7 +70,7 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void> impleme
|
||||
private File crashLogFile=new File(MastodonApp.context.getFilesDir(), "crash.log");
|
||||
|
||||
// MOSHIDON
|
||||
private ListItem<Void> clearRecentEmojisItem;
|
||||
private ListItem<Void> clearRecentEmojisItem, exportItem, importItem;
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -73,6 +85,8 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void> impleme
|
||||
new ListItem<>(R.string.mo_settings_contribute, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.mo_repo_url))),
|
||||
new ListItem<>(R.string.settings_tos, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), "https://"+session.domain+"/terms")),
|
||||
new ListItem<>(R.string.settings_privacy_policy, 0, R.drawable.ic_fluent_open_24_regular, i->UiUtils.launchWebBrowser(getActivity(), getString(R.string.privacy_policy_url)), 0, true),
|
||||
exportItem=new ListItem<>(R.string.export_settings_title, R.string.export_settings_summary, R.drawable.ic_fluent_arrow_export_24_filled, this::onExportClick),
|
||||
importItem=new ListItem<>(R.string.import_settings_title, R.string.import_settings_summary, R.drawable.ic_fluent_arrow_import_24_filled, this::onImportClick, 0, true),
|
||||
clearRecentEmojisItem=new ListItem<>(R.string.mo_clear_recent_emoji, 0, this::onClearRecentEmojisClick),
|
||||
mediaCacheItem=new ListItem<>(R.string.settings_clear_cache, 0, this::onClearMediaCacheClick),
|
||||
new ListItem<>(getString(R.string.sk_settings_clear_timeline_cache), session.domain, this::onClearTimelineCacheClick),
|
||||
@@ -146,6 +160,149 @@ public class SettingsAboutAppFragment extends BaseSettingsFragment<Void> impleme
|
||||
Toast.makeText(getContext(), R.string.mo_recent_emoji_cleared, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
private void onExportClick(ListItem<?> item){
|
||||
Gson gson = new Gson();
|
||||
JsonObject jsonObject = new JsonObject();
|
||||
jsonObject.addProperty("versionName", BuildConfig.VERSION_NAME);
|
||||
jsonObject.addProperty("versionCode", BuildConfig.VERSION_CODE);
|
||||
|
||||
// GlobalUserPreferences
|
||||
//TODO: remove prefs that should not be exported
|
||||
JsonElement je = gson.toJsonTree(GlobalUserPreferences.getPrefs().getAll());
|
||||
jsonObject.add("GlobalUserPreferences", je);
|
||||
|
||||
// add account local prefs
|
||||
for(AccountSession accountSession: AccountSessionManager.getInstance().getLoggedInAccounts()) {
|
||||
Map<String, ?> prefs = accountSession.getRawLocalPreferences().getAll();
|
||||
//TODO: remove prefs that should not be exported
|
||||
JsonElement accountPrefs = gson.toJsonTree(prefs);
|
||||
jsonObject.add(accountSession.self.id, accountPrefs);
|
||||
}
|
||||
|
||||
try {
|
||||
File file = new File(getContext().getCacheDir(), "moshidon-exported-settings.json");
|
||||
FileWriter writer = new FileWriter(file);
|
||||
writer.write(jsonObject.toString());
|
||||
writer.flush();
|
||||
writer.close();
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_SEND);
|
||||
intent.setType("application/json");
|
||||
Uri outputUri = FileProvider.getUriForFile(getContext(), getContext().getPackageName() + ".fileprovider", file);
|
||||
intent.putExtra(Intent.EXTRA_STREAM, outputUri);
|
||||
startActivity(Intent.createChooser(intent, getContext().getString(R.string.export_settings_share)));
|
||||
} catch (IOException e) {
|
||||
Toast.makeText(getContext(), getContext().getString(R.string.export_settings_fail), Toast.LENGTH_SHORT).show();
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void onImportClick(ListItem<?> item){
|
||||
new M3AlertDialogBuilder(getContext())
|
||||
.setTitle(R.string.import_settings_confirm)
|
||||
.setIcon(R.drawable.ic_fluent_warning_24_regular)
|
||||
.setMessage(R.string.import_settings_confirm_body)
|
||||
.setPositiveButton(R.string.ok, (dialogInterface, i) -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType("application/json");
|
||||
startActivityForResult(intent, IMPORT_RESULT);
|
||||
})
|
||||
.setNegativeButton(R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data){
|
||||
if(requestCode==IMPORT_RESULT && resultCode==Activity.RESULT_OK){
|
||||
Uri uri=data.getData();
|
||||
if(uri==null){
|
||||
return;
|
||||
}
|
||||
try{
|
||||
InputStream inputStream=getContext().getContentResolver().openInputStream(uri);
|
||||
if(inputStream==null)
|
||||
return;
|
||||
BufferedReader reader=new BufferedReader(new InputStreamReader(inputStream));
|
||||
StringBuilder stringBuilder=new StringBuilder();
|
||||
String line;
|
||||
while((line=reader.readLine())!=null){
|
||||
stringBuilder.append(line);
|
||||
}
|
||||
inputStream.close();
|
||||
String jsonString=stringBuilder.toString();
|
||||
|
||||
Gson gson=new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create();
|
||||
JsonObject jsonObject=JsonParser.parseString(jsonString).getAsJsonObject();
|
||||
|
||||
//check if json has required attributes
|
||||
if(!(jsonObject.has("versionName") && jsonObject.has("versionCode") && jsonObject.has("GlobalUserPreferences"))){
|
||||
Toast.makeText(getContext(), getContext().getString(R.string.import_settings_failed), Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
String versionName=jsonObject.get("versionName").getAsString();
|
||||
int versionCode=jsonObject.get("versionCode").getAsInt();
|
||||
Log.i(TAG, "onActivityResult: Reading exported settings ("+versionName+" "+versionCode+")");
|
||||
|
||||
// retrieve GlobalUserPreferences
|
||||
Map<String, ?> jsonGlobalPrefs=gson.fromJson(jsonObject.getAsJsonObject("GlobalUserPreferences"), Map.class);
|
||||
SharedPreferences.Editor globalPrefsEditor=GlobalUserPreferences.getPrefs().edit();
|
||||
for(String key : jsonGlobalPrefs.keySet()){
|
||||
Object value=jsonGlobalPrefs.get(key);
|
||||
if(value==null)
|
||||
continue;
|
||||
savePrefValue(globalPrefsEditor, key, value);
|
||||
}
|
||||
|
||||
// retrieve LocalPreferences for all logged in accounts
|
||||
//TODO: maybe show a dialog for which accounts to import?
|
||||
for(AccountSession accountSession : AccountSessionManager.getInstance().getLoggedInAccounts()){
|
||||
if(!jsonObject.has(accountSession.self.id))
|
||||
continue;
|
||||
Map<String, ?> prefs=gson.fromJson(jsonObject.getAsJsonObject(accountSession.self.id), Map.class);
|
||||
|
||||
SharedPreferences.Editor prefEditor=accountSession.getRawLocalPreferences().edit();
|
||||
for(String key : prefs.keySet()){
|
||||
Object value=prefs.get(key);
|
||||
if(value==null)
|
||||
continue;
|
||||
savePrefValue(prefEditor, key, value);
|
||||
}
|
||||
}
|
||||
|
||||
// restart app to apply new preferences
|
||||
// https://stackoverflow.com/a/46848226
|
||||
PackageManager packageManager=getContext().getPackageManager();
|
||||
Intent intent=packageManager.getLaunchIntentForPackage(getContext().getPackageName());
|
||||
ComponentName componentName=intent.getComponent();
|
||||
Intent mainIntent=Intent.makeRestartActivityTask(componentName);
|
||||
// Required for API 34 and later
|
||||
// Ref: https://developer.android.com/about/versions/14/behavior-changes-14#safer-intents
|
||||
mainIntent.setPackage(getContext().getPackageName());
|
||||
getContext().startActivity(mainIntent);
|
||||
Runtime.getRuntime().exit(0);
|
||||
}catch(IOException e){
|
||||
Log.w(TAG, e);
|
||||
Toast.makeText(getContext(), getContext().getString(R.string.import_settings_failed), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void savePrefValue(SharedPreferences.Editor editor, String key, Object value) {
|
||||
if(value.getClass().equals(Boolean.class))
|
||||
editor.putBoolean(key, (Boolean) value);
|
||||
// gson parses all numbers either long (for int) or double (the rest)
|
||||
else if(value.getClass().equals(Long.class))
|
||||
editor.putInt(key, ((Long) value).intValue());
|
||||
else if(value.getClass().equals(Double.class))
|
||||
editor.putFloat(key, ((Double) value).floatValue());
|
||||
else
|
||||
editor.putString(key, String.valueOf(value));
|
||||
//explicitly immediately since the app will restarted soon after
|
||||
// and it may not have the time to write the values in the background
|
||||
editor.commit();
|
||||
}
|
||||
|
||||
private void updateMediaCacheItem(){
|
||||
long size=ImageCache.getInstance(getActivity()).getDiskCache().size();
|
||||
mediaCacheItem.subtitle=UiUtils.formatFileSize(getActivity(), size, false);
|
||||
|
||||
Reference in New Issue
Block a user