From b38a7354376497d859f52878f7db4fec5724fadf Mon Sep 17 00:00:00 2001 From: M66B Date: Fri, 14 Sep 2018 11:33:22 +0000 Subject: [PATCH] Export/import settings Fixes #45 --- README.md | 1 + .../java/eu/faircode/email/ActivitySetup.java | 3 + .../java/eu/faircode/email/DaoAccount.java | 3 + .../java/eu/faircode/email/DaoAnswer.java | 3 + .../java/eu/faircode/email/DaoIdentity.java | 3 + .../java/eu/faircode/email/EntityAccount.java | 39 +++ .../java/eu/faircode/email/EntityAnswer.java | 16 + .../java/eu/faircode/email/EntityFolder.java | 23 ++ .../eu/faircode/email/EntityIdentity.java | 38 +++ .../java/eu/faircode/email/FragmentSetup.java | 286 +++++++++++++++++- app/src/main/res/menu/menu_setup.xml | 9 + app/src/main/res/values/strings.xml | 4 + 12 files changed, 427 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/menu/menu_setup.xml diff --git a/README.md b/README.md index f3fc007f..156ca430 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ Pro features * Sort on time, unread or starred * Progressive search (first local, then server) * Preview sender/subject in new messages status bar notification +* Export settings Simple ------ diff --git a/app/src/main/java/eu/faircode/email/ActivitySetup.java b/app/src/main/java/eu/faircode/email/ActivitySetup.java index f9830588..60ef505b 100644 --- a/app/src/main/java/eu/faircode/email/ActivitySetup.java +++ b/app/src/main/java/eu/faircode/email/ActivitySetup.java @@ -40,6 +40,9 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On static final int REQUEST_PERMISSION = 1; static final int REQUEST_CHOOSE_ACCOUNT = 2; + static final int REQUEST_EXPORT = 3; + static final int REQUEST_IMPORT = 4; + static final String ACTION_EDIT_ACCOUNT = BuildConfig.APPLICATION_ID + ".EDIT_ACCOUNT"; static final String ACTION_EDIT_IDENTITY = BuildConfig.APPLICATION_ID + ".EDIT_IDENTITY"; diff --git a/app/src/main/java/eu/faircode/email/DaoAccount.java b/app/src/main/java/eu/faircode/email/DaoAccount.java index 616bd395..ebbff044 100644 --- a/app/src/main/java/eu/faircode/email/DaoAccount.java +++ b/app/src/main/java/eu/faircode/email/DaoAccount.java @@ -29,6 +29,9 @@ import androidx.room.Update; @Dao public interface DaoAccount { + @Query("SELECT * FROM account") + List getAccounts(); + @Query("SELECT * FROM account WHERE synchronize = :synchronize") List getAccounts(boolean synchronize); diff --git a/app/src/main/java/eu/faircode/email/DaoAnswer.java b/app/src/main/java/eu/faircode/email/DaoAnswer.java index 420205df..a64dcafd 100644 --- a/app/src/main/java/eu/faircode/email/DaoAnswer.java +++ b/app/src/main/java/eu/faircode/email/DaoAnswer.java @@ -29,6 +29,9 @@ import androidx.room.Update; @Dao public interface DaoAnswer { + @Query("SELECT * FROM answer") + List getAnswers(); + @Query("SELECT * FROM answer WHERE id = :id") EntityAnswer getAnswer(long id); diff --git a/app/src/main/java/eu/faircode/email/DaoIdentity.java b/app/src/main/java/eu/faircode/email/DaoIdentity.java index 556ce944..a6200836 100644 --- a/app/src/main/java/eu/faircode/email/DaoIdentity.java +++ b/app/src/main/java/eu/faircode/email/DaoIdentity.java @@ -42,6 +42,9 @@ public interface DaoIdentity { @Query("SELECT * FROM identity") List getIdentities(); + @Query("SELECT * FROM identity WHERE account = :account") + List getIdentities(long account); + @Query("SELECT * FROM identity WHERE id = :id") EntityIdentity getIdentity(long id); diff --git a/app/src/main/java/eu/faircode/email/EntityAccount.java b/app/src/main/java/eu/faircode/email/EntityAccount.java index c3b26bb0..c905d6ad 100644 --- a/app/src/main/java/eu/faircode/email/EntityAccount.java +++ b/app/src/main/java/eu/faircode/email/EntityAccount.java @@ -19,6 +19,9 @@ package eu.faircode.email; Copyright 2018 by Marcel Bokhorst (M66B) */ +import org.json.JSONException; +import org.json.JSONObject; + import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.PrimaryKey; @@ -77,6 +80,42 @@ public class EntityAccount { return false; } + public JSONObject toJSON() throws JSONException { + JSONObject json = new JSONObject(); + json.put("name", name); + json.put("signature", signature); + json.put("host", host); + json.put("port", port); + json.put("user", user); + json.put("password", password); + json.put("auth_type", auth_type); + json.put("primary", primary); + json.put("synchronize", synchronize); + if (color != null) + json.put("color", color); + json.put("poll_interval", poll_interval); + return json; + } + + public static EntityAccount fromJSON(JSONObject json) throws JSONException { + EntityAccount account = new EntityAccount(); + if (json.has("name")) + account.name = json.getString("name"); + if (json.has("signature")) + account.signature = json.getString("signature"); + account.host = json.getString("host"); + account.port = json.getInt("port"); + account.user = json.getString("user"); + account.password = json.getString("password"); + account.auth_type = json.getInt("auth_type"); + account.primary = json.getBoolean("primary"); + account.synchronize = json.getBoolean("synchronize"); + if (json.has("color")) + account.color = json.getInt("color"); + account.poll_interval = json.getInt("poll_interval"); + return account; + } + @Override public String toString() { return name + (primary ? " ★" : ""); diff --git a/app/src/main/java/eu/faircode/email/EntityAnswer.java b/app/src/main/java/eu/faircode/email/EntityAnswer.java index ca55e53e..be1575cc 100644 --- a/app/src/main/java/eu/faircode/email/EntityAnswer.java +++ b/app/src/main/java/eu/faircode/email/EntityAnswer.java @@ -19,6 +19,9 @@ package eu.faircode.email; Copyright 2018 by Marcel Bokhorst (M66B) */ +import org.json.JSONException; +import org.json.JSONObject; + import java.io.Serializable; import androidx.annotation.NonNull; @@ -44,6 +47,19 @@ public class EntityAnswer implements Serializable { @NonNull public String text; + public JSONObject toJSON() throws JSONException { + JSONObject json = new JSONObject(); + json.put("name", name); + json.put("text", text); + return json; + } + + public static EntityAnswer fromJSON(JSONObject json) throws JSONException { + EntityAnswer answer = new EntityAnswer(); + answer.name = json.getString("name"); + answer.text = json.getString("text"); + return answer; + } @Override public boolean equals(Object obj) { diff --git a/app/src/main/java/eu/faircode/email/EntityFolder.java b/app/src/main/java/eu/faircode/email/EntityFolder.java index 80680b27..a1e1d378 100644 --- a/app/src/main/java/eu/faircode/email/EntityFolder.java +++ b/app/src/main/java/eu/faircode/email/EntityFolder.java @@ -22,6 +22,9 @@ package eu.faircode.email; import android.os.Parcel; import android.os.Parcelable; +import org.json.JSONException; +import org.json.JSONObject; + import java.util.Arrays; import java.util.List; @@ -203,4 +206,24 @@ public class EntityFolder implements Parcelable { return new EntityFolder[size]; } }; + + public JSONObject toJSON() throws JSONException { + JSONObject json = new JSONObject(); + json.put("name", name); + json.put("type", type); + json.put("unified", unified); + json.put("synchronize", synchronize); + json.put("after", after); + return json; + } + + public static EntityFolder fromJSON(JSONObject json) throws JSONException { + EntityFolder folder = new EntityFolder(); + folder.name = json.getString("name"); + folder.type = json.getString("type"); + folder.unified = json.getBoolean("unified"); + folder.synchronize = json.getBoolean("synchronize"); + folder.after = json.getInt("after"); + return folder; + } } diff --git a/app/src/main/java/eu/faircode/email/EntityIdentity.java b/app/src/main/java/eu/faircode/email/EntityIdentity.java index 2d6caa91..1edc3a69 100644 --- a/app/src/main/java/eu/faircode/email/EntityIdentity.java +++ b/app/src/main/java/eu/faircode/email/EntityIdentity.java @@ -19,6 +19,9 @@ package eu.faircode.email; Copyright 2018 by Marcel Bokhorst (M66B) */ +import org.json.JSONException; +import org.json.JSONObject; + import androidx.annotation.NonNull; import androidx.room.Entity; import androidx.room.ForeignKey; @@ -69,6 +72,41 @@ public class EntityIdentity { public String state; public String error; + public JSONObject toJSON() throws JSONException { + JSONObject json = new JSONObject(); + json.put("name", name); + json.put("email", email); + json.put("replyto", replyto); + json.put("host", host); + json.put("port", port); + json.put("starttls", starttls); + json.put("user", user); + json.put("password", password); + json.put("auth_type", auth_type); + json.put("primary", primary); + json.put("synchronize", synchronize); + json.put("store_sent", store_sent); + return json; + } + + public static EntityIdentity fromJSON(JSONObject json) throws JSONException { + EntityIdentity identity = new EntityIdentity(); + identity.name = json.getString("name"); + identity.email = json.getString("email"); + if (json.has("replyto")) + identity.replyto = json.getString("replyto"); + identity.host = json.getString("host"); + identity.port = json.getInt("port"); + identity.starttls = json.getBoolean("starttls"); + identity.user = json.getString("user"); + identity.password = json.getString("password"); + identity.auth_type = json.getInt("auth_type"); + identity.primary = json.getBoolean("primary"); + identity.synchronize = json.getBoolean("synchronize"); + identity.store_sent = json.getBoolean("store_sent"); + return identity; + } + @Override public boolean equals(Object obj) { if (obj instanceof EntityIdentity) { diff --git a/app/src/main/java/eu/faircode/email/FragmentSetup.java b/app/src/main/java/eu/faircode/email/FragmentSetup.java index ad8b2aef..494f93b6 100644 --- a/app/src/main/java/eu/faircode/email/FragmentSetup.java +++ b/app/src/main/java/eu/faircode/email/FragmentSetup.java @@ -21,11 +21,13 @@ package eu.faircode.email; import android.Manifest; import android.annotation.TargetApi; +import android.content.ContentResolver; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; import android.graphics.drawable.Drawable; import android.net.ConnectivityManager; import android.net.Uri; @@ -36,6 +38,9 @@ import android.preference.PreferenceManager; import android.provider.Settings; import android.util.Log; import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.widget.Button; @@ -44,6 +49,18 @@ import android.widget.TextView; import android.widget.Toast; import android.widget.ToggleButton; +import com.google.android.material.snackbar.Snackbar; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; import java.util.List; import androidx.annotation.NonNull; @@ -53,7 +70,11 @@ import androidx.core.content.ContextCompat; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.Observer; +import static android.app.Activity.RESULT_OK; + public class FragmentSetup extends FragmentEx { + private ViewGroup view; + private Button btnAccount; private TextView tvAccountDone; @@ -78,14 +99,21 @@ public class FragmentSetup extends FragmentEx { Manifest.permission.READ_CONTACTS }; + static final List EXPORT_SETTINGS = Arrays.asList( + "compress", + "avatars", + "theme" + ); + @Override @Nullable public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { setSubtitle(R.string.title_setup); + setHasOptionsMenu(true); check = getResources().getDrawable(R.drawable.baseline_check_24, getContext().getTheme()); - View view = inflater.inflate(R.layout.fragment_setup, container, false); + view = (ViewGroup) inflater.inflate(R.layout.fragment_setup, container, false); // Get controls btnAccount = view.findViewById(R.id.btnAccount); @@ -294,6 +322,43 @@ public class FragmentSetup extends FragmentEx { } } + @Override + public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { + inflater.inflate(R.menu.menu_setup, menu); + super.onCreateOptionsMenu(menu, inflater); + } + + @Override + public void onPrepareOptionsMenu(Menu menu) { + PackageManager pm = getContext().getPackageManager(); + menu.findItem(R.id.menu_export).setEnabled(getIntentExport().resolveActivity(pm) != null); + menu.findItem(R.id.menu_import).setEnabled(getIntentImport().resolveActivity(pm) != null); + super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.menu_export: + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); + if (prefs.getBoolean("pro", false)) + startActivityForResult(getIntentExport(), ActivitySetup.REQUEST_EXPORT); + else { + FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); + fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro"); + fragmentTransaction.commit(); + } + return true; + + case R.id.menu_import: + startActivityForResult(getIntentImport(), ActivitySetup.REQUEST_IMPORT); + return true; + + default: + return super.onOptionsItemSelected(item); + } + } + @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { boolean has = (grantResults.length > 0); @@ -307,4 +372,223 @@ public class FragmentSetup extends FragmentEx { tvPermissionsDone.setText(has ? R.string.title_setup_done : R.string.title_setup_to_do); tvPermissionsDone.setCompoundDrawablesWithIntrinsicBounds(has ? check : null, null, null, null); } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + Log.i(Helper.TAG, "Request=" + requestCode + " result=" + resultCode + " data=" + data); + + if (requestCode == ActivitySetup.REQUEST_EXPORT) { + if (resultCode == RESULT_OK && data != null) + handleExport(data); + + } else if (requestCode == ActivitySetup.REQUEST_IMPORT) { + if (resultCode == RESULT_OK && data != null) + handleImport(data); + } + } + + private static Intent getIntentExport() { + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + intent.putExtra(Intent.EXTRA_TITLE, "fairemail_backup_" + + new SimpleDateFormat("yyyyMMdd").format(new Date().getTime()) + ".json"); + return intent; + } + + private static Intent getIntentImport() { + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("*/*"); + return intent; + } + + private void handleExport(Intent data) { + Bundle args = new Bundle(); + args.putParcelable("uri", data.getData()); + + new SimpleTask() { + @Override + protected Void onLoad(Context context, Bundle args) throws Throwable { + Uri uri = args.getParcelable("uri"); + + OutputStream out = null; + try { + Log.i(Helper.TAG, "Writing URI=" + uri); + out = getContext().getContentResolver().openOutputStream(uri); + + DB db = DB.getInstance(context); + + // Accounts + JSONArray jaccounts = new JSONArray(); + for (EntityAccount account : db.account().getAccounts()) { + // Account + JSONObject jaccount = account.toJSON(); + + // Identities + JSONArray jidentities = new JSONArray(); + for (EntityIdentity identity : db.identity().getIdentities(account.id)) + jidentities.put(identity.toJSON()); + jaccount.put("identities", jidentities); + + // Folders + JSONArray jfolders = new JSONArray(); + for (EntityFolder folder : db.folder().getFolders(account.id)) + jfolders.put(folder.toJSON()); + jaccount.put("folders", jfolders); + + jaccounts.put(jaccount); + } + + // Answers + JSONArray janswers = new JSONArray(); + for (EntityAnswer answer : db.answer().getAnswers()) + janswers.put(answer.toJSON()); + + // Settings + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + JSONArray jsettings = new JSONArray(); + for (String key : prefs.getAll().keySet()) + if (EXPORT_SETTINGS.contains(key)) { + JSONObject jsetting = new JSONObject(); + jsetting.put("key", key); + jsetting.put("value", prefs.getAll().get(key)); + jsettings.put(jsetting); + } + + JSONObject jexport = new JSONObject(); + jexport.put("accounts", jaccounts); + jexport.put("answers", janswers); + jexport.put("settings", jsettings); + + out.write(jexport.toString(2).getBytes()); + + Log.i(Helper.TAG, "Exported data"); + } finally { + if (out != null) + out.close(); + } + + return null; + } + + @Override + protected void onLoaded(Bundle args, Void data) { + Snackbar.make(view, R.string.title_setup_exported, Snackbar.LENGTH_LONG).show(); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show(); + } + }.load(this, args); + } + + private void handleImport(Intent data) { + Bundle args = new Bundle(); + args.putParcelable("uri", data.getData()); + + new SimpleTask() { + @Override + protected Void onLoad(Context context, Bundle args) throws Throwable { + Uri uri = args.getParcelable("uri"); + + InputStream in = null; + try { + Log.i(Helper.TAG, "Reading URI=" + uri); + ContentResolver resolver = getContext().getContentResolver(); + AssetFileDescriptor descriptor = resolver.openTypedAssetFileDescriptor(uri, "*/*", null); + in = descriptor.createInputStream(); + + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) + response.append(line); + Log.i(Helper.TAG, "Importing " + resolver.toString()); + + JSONObject jimport = new JSONObject(response.toString()); + + DB db = DB.getInstance(context); + try { + db.beginTransaction(); + + JSONArray jaccounts = jimport.getJSONArray("accounts"); + for (int a = 0; a < jaccounts.length(); a++) { + JSONObject jaccount = (JSONObject) jaccounts.get(a); + EntityAccount account = EntityAccount.fromJSON(jaccount); + account.store_sent = false; + account.id = db.account().insertAccount(account); + Log.i(Helper.TAG, "Imported account=" + account.name); + + JSONArray jidentities = (JSONArray) jaccount.get("identities"); + for (int i = 0; i < jidentities.length(); i++) { + JSONObject jidentity = (JSONObject) jidentities.get(i); + EntityIdentity identity = EntityIdentity.fromJSON(jidentity); + identity.account = account.id; + identity.id = db.identity().insertIdentity(identity); + Log.i(Helper.TAG, "Imported identity=" + identity.email); + } + + JSONArray jfolders = (JSONArray) jaccount.get("folders"); + for (int f = 0; f < jfolders.length(); f++) { + JSONObject jfolder = (JSONObject) jfolders.get(f); + EntityFolder folder = EntityFolder.fromJSON(jfolder); + folder.account = account.id; + folder.id = db.folder().insertFolder(folder); + Log.i(Helper.TAG, "Imported folder=" + folder.name); + } + } + + JSONArray janswers = jimport.getJSONArray("answers"); + for (int a = 0; a < janswers.length(); a++) { + JSONObject janswer = (JSONObject) janswers.get(a); + EntityAnswer answer = EntityAnswer.fromJSON(janswer); + answer.id = db.answer().insertAnswer(answer); + Log.i(Helper.TAG, "Imported answer=" + answer.name); + } + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + SharedPreferences.Editor editor = prefs.edit(); + JSONArray jsettings = jimport.getJSONArray("settings"); + for (int s = 0; s < jsettings.length(); s++) { + JSONObject jsetting = (JSONObject) jsettings.get(s); + String key = jsetting.getString("key"); + Object value = jsetting.get("value"); + if (value instanceof Boolean) + editor.putBoolean(key, (Boolean) value); + else if (value instanceof String) + editor.putString(key, (String) value); + else + throw new IllegalArgumentException("Unknown settings type key=" + key); + Log.i(Helper.TAG, "Imported setting=" + key); + } + editor.apply(); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + Log.i(Helper.TAG, "Imported data"); + } finally { + if (in != null) + in.close(); + } + + return null; + } + + @Override + protected void onLoaded(Bundle args, Void data) { + Snackbar.make(view, R.string.title_setup_imported, Snackbar.LENGTH_LONG).show(); + ServiceSynchronize.reload(getContext(), "import"); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + Toast.makeText(getContext(), ex.toString(), Toast.LENGTH_LONG).show(); + } + }.load(this, args); + } } diff --git a/app/src/main/res/menu/menu_setup.xml b/app/src/main/res/menu/menu_setup.xml new file mode 100644 index 00000000..48d25a08 --- /dev/null +++ b/app/src/main/res/menu/menu_setup.xml @@ -0,0 +1,9 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 03ebb3c5..f68378b0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -56,6 +56,10 @@ Edit folder Setup + Export settings + Import settings + Settings exported + Settings imported Manage accounts To receive email Manage identities