From 0f1d0cf1063e0558414bacc7a5ecd49ad4e65181 Mon Sep 17 00:00:00 2001 From: M66B Date: Mon, 14 Jan 2019 10:29:47 +0000 Subject: [PATCH] Added setup navigation menu --- .../java/eu/faircode/email/ActivitySetup.java | 529 +++++++++++++++++- .../java/eu/faircode/email/ActivityView.java | 15 +- .../java/eu/faircode/email/FragmentSetup.java | 432 +------------- .../main/java/eu/faircode/email/Helper.java | 6 + .../res/drawable/baseline_unarchive_24.xml | 10 + app/src/main/res/layout/activity_setup.xml | 22 +- app/src/main/res/layout/fragment_setup.xml | 12 +- app/src/main/res/menu/menu_setup.xml | 18 - app/src/main/res/values/strings.xml | 1 + 9 files changed, 566 insertions(+), 479 deletions(-) create mode 100644 app/src/main/res/drawable/baseline_unarchive_24.xml delete mode 100644 app/src/main/res/menu/menu_setup.xml diff --git a/app/src/main/java/eu/faircode/email/ActivitySetup.java b/app/src/main/java/eu/faircode/email/ActivitySetup.java index b6800f4e7e..81f9f7a7f8 100644 --- a/app/src/main/java/eu/faircode/email/ActivitySetup.java +++ b/app/src/main/java/eu/faircode/email/ActivitySetup.java @@ -20,14 +20,53 @@ package eu.faircode.email; */ import android.content.BroadcastReceiver; +import android.content.ContentResolver; import android.content.Context; +import android.content.DialogInterface; import android.content.Intent; import android.content.IntentFilter; +import android.content.SharedPreferences; +import android.content.pm.PackageManager; +import android.content.res.AssetFileDescriptor; +import android.content.res.Configuration; +import android.net.Uri; import android.os.Bundle; +import android.preference.PreferenceManager; +import android.text.TextUtils; +import android.view.LayoutInflater; import android.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.ListView; +import com.google.android.material.snackbar.Snackbar; +import com.google.android.material.textfield.TextInputLayout; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.security.SecureRandom; +import java.security.spec.KeySpec; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.List; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; + +import androidx.appcompat.app.ActionBarDrawerToggle; +import androidx.drawerlayout.widget.DrawerLayout; import androidx.fragment.app.FragmentManager; import androidx.fragment.app.FragmentTransaction; import androidx.lifecycle.Lifecycle; @@ -35,8 +74,16 @@ import androidx.lifecycle.Observer; import androidx.localbroadcastmanager.content.LocalBroadcastManager; public class ActivitySetup extends ActivityBilling implements FragmentManager.OnBackStackChangedListener { + private View view; + private DrawerLayout drawerLayout; + private ListView drawerList; + private ActionBarDrawerToggle drawerToggle; + private boolean hasAccount; + private static final int KEY_ITERATIONS = 65536; + private static final int KEY_LENGTH = 256; + static final int REQUEST_PERMISSION = 1; static final int REQUEST_CHOOSE_ACCOUNT = 2; @@ -53,10 +100,85 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_setup); + + view = LayoutInflater.from(this).inflate(R.layout.activity_setup, null); + setContentView(view); getSupportActionBar().setDisplayHomeAsUpEnabled(true); + drawerLayout = findViewById(R.id.drawer_layout); + drawerLayout.setScrimColor(Helper.resolveColor(this, R.attr.colorDrawerScrim)); + + drawerToggle = new ActionBarDrawerToggle(this, drawerLayout, R.string.app_name, R.string.app_name) { + public void onDrawerClosed(View view) { + super.onDrawerClosed(view); + getSupportActionBar().setTitle(getString(R.string.app_name)); + } + + public void onDrawerOpened(View drawerView) { + super.onDrawerOpened(drawerView); + getSupportActionBar().setTitle(getString(R.string.app_name)); + } + }; + drawerLayout.addDrawerListener(drawerToggle); + + drawerList = findViewById(R.id.drawer_list); + drawerList.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView parent, View view, int position, long id) { + DrawerItem item = (DrawerItem) parent.getAdapter().getItem(position); + switch (item.getId()) { + case R.string.title_setup_help: + onMenuHelp(); + break; + case R.string.title_setup_export: + onMenuExport(); + break; + case R.string.title_setup_import: + onMenuImport(); + break; + case R.string.menu_legend: + onMenuLegend(); + break; + case R.string.menu_faq: + onMenuFAQ(); + break; + case R.string.menu_privacy: + onMenuPrivacy(); + break; + case R.string.menu_about: + onMenuAbout(); + break; + } + + drawerLayout.closeDrawer(drawerList); + } + }); + + PackageManager pm = getPackageManager(); + DrawerAdapter drawerArray = new DrawerAdapter(this); + + if (getIntentHelp().resolveActivity(pm) != null) + drawerArray.add(new DrawerItem(this, R.layout.item_drawer, R.drawable.baseline_settings_applications_24, R.string.title_setup_help)); + + drawerArray.add(new DrawerItem(R.layout.item_drawer_separator)); + + if (getIntentExport().resolveActivity(pm) != null) + drawerArray.add(new DrawerItem(this, R.layout.item_drawer, R.drawable.baseline_archive_24, R.string.title_setup_export)); + if (getIntentImport().resolveActivity(pm) != null) + drawerArray.add(new DrawerItem(this, R.layout.item_drawer, R.drawable.baseline_unarchive_24, R.string.title_setup_import)); + + drawerArray.add(new DrawerItem(R.layout.item_drawer_separator)); + + drawerArray.add(new DrawerItem(this, R.layout.item_drawer, R.drawable.baseline_help_24, R.string.menu_legend)); + if (Helper.getIntentFAQ().resolveActivity(getPackageManager()) != null) + drawerArray.add(new DrawerItem(this, R.layout.item_drawer, R.drawable.baseline_question_answer_24, R.string.menu_faq)); + if (Helper.getIntentPrivacy().resolveActivity(getPackageManager()) != null) + drawerArray.add(new DrawerItem(this, R.layout.item_drawer, R.drawable.baseline_account_box_24, R.string.menu_privacy)); + drawerArray.add(new DrawerItem(this, R.layout.item_drawer, R.drawable.baseline_info_24, R.string.menu_about)); + + drawerList.setAdapter(drawerArray); + getSupportFragmentManager().addOnBackStackChangedListener(this); if (getSupportFragmentManager().getFragments().size() == 0) { @@ -65,6 +187,9 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On fragmentTransaction.commit(); } + if (savedInstanceState != null) + drawerToggle.setDrawerIndicatorEnabled(savedInstanceState.getBoolean("toggle")); + DB.getInstance(this).account().liveAccounts(true).observe(this, new Observer>() { @Override public void onChanged(List accounts) { @@ -73,6 +198,18 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On }); } + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean("toggle", drawerToggle.isDrawerIndicatorEnabled()); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + drawerToggle.syncState(); + } + @Override protected void onResume() { super.onResume(); @@ -90,8 +227,39 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On lbm.unregisterReceiver(receiver); } + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + drawerToggle.onConfigurationChanged(newConfig); + } + + @Override + public void onBackPressed() { + if (drawerLayout.isDrawerOpen(drawerList)) + drawerLayout.closeDrawer(drawerList); + else + super.onBackPressed(); + } + + @Override + public void onBackStackChanged() { + int count = getSupportFragmentManager().getBackStackEntryCount(); + if (count == 0) { + if (hasAccount) + startActivity(new Intent(this, ActivityView.class)); + finish(); + } else { + if (drawerLayout.isDrawerOpen(drawerList)) + drawerLayout.closeDrawer(drawerList); + drawerToggle.setDrawerIndicatorEnabled(count == 1); + } + } + @Override public boolean onOptionsItemSelected(MenuItem item) { + if (drawerToggle.onOptionsItemSelected(item)) + return true; + switch (item.getItemId()) { case android.R.id.home: if (getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED)) @@ -102,14 +270,363 @@ public class ActivitySetup extends ActivityBilling implements FragmentManager.On } @Override - public void onBackStackChanged() { - if (getSupportFragmentManager().getBackStackEntryCount() == 0) { - if (hasAccount) - startActivity(new Intent(this, ActivityView.class)); - finish(); + public void onActivityResult(final int requestCode, int resultCode, final Intent data) { + if (requestCode == ActivitySetup.REQUEST_EXPORT || requestCode == ActivitySetup.REQUEST_IMPORT) { + if (resultCode == RESULT_OK && data != null) + fileSelected(requestCode == ActivitySetup.REQUEST_EXPORT, data); } } + private void fileSelected(final boolean export, final Intent data) { + View dview = LayoutInflater.from(this).inflate(R.layout.dialog_password, null); + final TextInputLayout etPassword1 = dview.findViewById(R.id.tilPassword1); + final TextInputLayout etPassword2 = dview.findViewById(R.id.tilPassword2); + + new DialogBuilderLifecycle(this, this) + .setView(dview) + .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + + String password1 = etPassword1.getEditText().getText().toString(); + String password2 = etPassword2.getEditText().getText().toString(); + + if (TextUtils.isEmpty(password1)) + Snackbar.make(view, R.string.title_setup_password_missing, Snackbar.LENGTH_LONG).show(); + else { + if (password1.equals(password2)) { + if (export) + handleExport(data, password1); + else + handleImport(data, password1); + } else + Snackbar.make(view, R.string.title_setup_password_different, Snackbar.LENGTH_LONG).show(); + } + } + }) + .show(); + } + + private void onMenuHelp() { + startActivity(getIntentHelp()); + } + + private void onMenuExport() { + if (Helper.isPro(this)) + try { + startActivityForResult(Helper.getChooser(this, getIntentExport()), ActivitySetup.REQUEST_EXPORT); + } catch (Throwable ex) { + Helper.unexpectedError(this, this, ex); + } + else { + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro"); + fragmentTransaction.commit(); + } + } + + private void onMenuImport() { + try { + startActivityForResult(Helper.getChooser(this, getIntentImport()), ActivitySetup.REQUEST_IMPORT); + } catch (Throwable ex) { + Helper.unexpectedError(this, this, ex); + } + } + + private void onMenuLegend() { + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + fragmentTransaction.replace(R.id.content_frame, new FragmentLegend()).addToBackStack("legend"); + fragmentTransaction.commit(); + } + + private void onMenuFAQ() { + Helper.view(this, this, Helper.getIntentFAQ()); + } + + private void onMenuPrivacy() { + Helper.view(this, this, Helper.getIntentPrivacy()); + } + + private void onMenuAbout() { + FragmentTransaction fragmentTransaction = getSupportFragmentManager().beginTransaction(); + fragmentTransaction.replace(R.id.content_frame, new FragmentAbout()).addToBackStack("about"); + fragmentTransaction.commit(); + } + + private Intent getIntentHelp() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://github.com/M66B/open-source-email/blob/master/SETUP.md#setup-help")); + return intent; + } + + 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, String password) { + Bundle args = new Bundle(); + args.putParcelable("uri", data.getData()); + args.putString("password", password); + + new SimpleTask() { + @Override + protected Void onExecute(Context context, Bundle args) throws Throwable { + Uri uri = args.getParcelable("uri"); + String password = args.getString("password"); + + if ("file".equals(uri.getScheme())) { + Log.w("Export uri=" + uri); + throw new IllegalArgumentException(context.getString(R.string.title_no_stream)); + } + + OutputStream out = null; + try { + Log.i("Writing URI=" + uri); + + byte[] salt = new byte[16]; + SecureRandom random = new SecureRandom(); + random.nextBytes(salt); + + // https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, KEY_ITERATIONS, KEY_LENGTH); + SecretKey secret = keyFactory.generateSecret(keySpec); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, secret); + + OutputStream raw = context.getContentResolver().openOutputStream(uri); + raw.write(salt); + raw.write(cipher.getIV()); + out = new CipherOutputStream(raw, cipher); + + 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 (!"pro".equals(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("Exported data"); + } finally { + if (out != null) + out.close(); + } + + return null; + } + + @Override + protected void onExecuted(Bundle args, Void data) { + Snackbar.make(view, R.string.title_setup_exported, Snackbar.LENGTH_LONG).show(); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + if (ex instanceof IllegalArgumentException) + Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show(); + else + Helper.unexpectedError(ActivitySetup.this, ActivitySetup.this, ex); + } + }.execute(this, args, "setup:export"); + } + + private void handleImport(Intent data, String password) { + Bundle args = new Bundle(); + args.putParcelable("uri", data.getData()); + args.putString("password", password); + + new SimpleTask() { + @Override + protected Void onExecute(Context context, Bundle args) throws Throwable { + Uri uri = args.getParcelable("uri"); + String password = args.getString("password"); + + if ("file".equals(uri.getScheme())) { + Log.w("Import uri=" + uri); + throw new IllegalArgumentException(context.getString(R.string.title_no_stream)); + } + + InputStream in = null; + try { + Log.i("Reading URI=" + uri); + ContentResolver resolver = context.getContentResolver(); + AssetFileDescriptor descriptor = resolver.openTypedAssetFileDescriptor(uri, "*/*", null); + InputStream raw = descriptor.createInputStream(); + + byte[] salt = new byte[16]; + byte[] prefix = new byte[16]; + if (raw.read(salt) != salt.length) + throw new IOException("length"); + if (raw.read(prefix) != prefix.length) + throw new IOException("length"); + + SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, KEY_ITERATIONS, KEY_LENGTH); + SecretKey secret = keyFactory.generateSecret(keySpec); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + IvParameterSpec iv = new IvParameterSpec(prefix); + cipher.init(Cipher.DECRYPT_MODE, secret, iv); + + in = new CipherInputStream(raw, cipher); + + BufferedReader reader = new BufferedReader(new InputStreamReader(in)); + StringBuilder response = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) + response.append(line); + Log.i("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.created = new Date().getTime(); + account.id = db.account().insertAccount(account); + Log.i("Imported account=" + account.name); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) + if (account.notify) + account.createNotificationChannel(context); + + 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("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("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("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"); + if (!"pro".equals(key)) { + Object value = jsetting.get("value"); + if (value instanceof Boolean) + editor.putBoolean(key, (Boolean) value); + else if (value instanceof Integer) + editor.putInt(key, (Integer) value); + else if (value instanceof Long) + editor.putLong(key, (Long) value); + else if (value instanceof String) + editor.putString(key, (String) value); + else + throw new IllegalArgumentException("Unknown settings type key=" + key); + Log.i("Imported setting=" + key); + } + } + editor.apply(); + + db.setTransactionSuccessful(); + } finally { + db.endTransaction(); + } + + Log.i("Imported data"); + ServiceSynchronize.reload(context, "import"); + } finally { + if (in != null) + in.close(); + } + + return null; + } + + @Override + protected void onExecuted(Bundle args, Void data) { + Snackbar.make(view, R.string.title_setup_imported, Snackbar.LENGTH_LONG).show(); + } + + @Override + protected void onException(Bundle args, Throwable ex) { + if (ex.getCause() instanceof BadPaddingException) + Snackbar.make(view, R.string.title_setup_password_invalid, Snackbar.LENGTH_LONG).show(); + else if (ex instanceof IllegalArgumentException) + Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show(); + else + Helper.unexpectedError(ActivitySetup.this, ActivitySetup.this, ex); + } + }.execute(this, args, "setup:import"); + } + BroadcastReceiver receiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { diff --git a/app/src/main/java/eu/faircode/email/ActivityView.java b/app/src/main/java/eu/faircode/email/ActivityView.java index 7fd5656306..ccf5888364 100644 --- a/app/src/main/java/eu/faircode/email/ActivityView.java +++ b/app/src/main/java/eu/faircode/email/ActivityView.java @@ -148,7 +148,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB setContentView(view); getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setHomeButtonEnabled(true); drawerLayout = findViewById(R.id.drawer_layout); drawerLayout.setScrimColor(Helper.resolveColor(this, R.attr.colorDrawerScrim)); @@ -304,7 +303,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_list_24, R.string.menu_operations)); drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_help_24, R.string.menu_legend)); - if (getIntentFAQ().resolveActivity(getPackageManager()) != null) + if (Helper.getIntentFAQ().resolveActivity(getPackageManager()) != null) drawerArray.add(new DrawerItem(ActivityView.this, R.layout.item_drawer, R.drawable.baseline_question_answer_24, R.string.menu_faq)); Intent pro = getIntentPro(); @@ -786,12 +785,6 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB sm.setDynamicShortcuts(shortcuts); } - private Intent getIntentFAQ() { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse("https://github.com/M66B/open-source-email/blob/master/FAQ.md")); - return intent; - } - private Intent getIntentRate() { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("market://details?id=" + BuildConfig.APPLICATION_ID)); if (intent.resolveActivity(getPackageManager()) == null) @@ -884,7 +877,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB } private void onMenuFAQ() { - Helper.view(this, this, getIntentFAQ()); + Helper.view(this, this, Helper.getIntentFAQ()); } private void onMenuPro() { @@ -904,7 +897,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB } private void onMenuRate() { - Intent faq = getIntentFAQ(); + Intent faq = Helper.getIntentFAQ(); if (faq.resolveActivity(getPackageManager()) == null) Helper.view(this, this, getIntentRate()); else { @@ -913,7 +906,7 @@ public class ActivityView extends ActivityBilling implements FragmentManager.OnB .setPositiveButton(R.string.title_yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - Helper.view(ActivityView.this, ActivityView.this, getIntentFAQ()); + Helper.view(ActivityView.this, ActivityView.this, Helper.getIntentFAQ()); } }) .setNegativeButton(R.string.title_no, new DialogInterface.OnClickListener() { diff --git a/app/src/main/java/eu/faircode/email/FragmentSetup.java b/app/src/main/java/eu/faircode/email/FragmentSetup.java index 386896ee65..8c1bba29db 100644 --- a/app/src/main/java/eu/faircode/email/FragmentSetup.java +++ b/app/src/main/java/eu/faircode/email/FragmentSetup.java @@ -28,13 +28,11 @@ import android.accounts.AuthenticatorException; import android.accounts.OperationCanceledException; import android.annotation.TargetApi; import android.content.ComponentName; -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; @@ -51,16 +49,12 @@ import android.text.TextWatcher; import android.text.method.LinkMovementMethod; import android.util.Patterns; 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; import android.widget.CheckBox; import android.widget.CompoundButton; import android.widget.EditText; -import android.widget.ImageButton; import android.widget.TextView; import android.widget.ToggleButton; @@ -69,30 +63,12 @@ import com.google.android.material.textfield.TextInputLayout; import com.sun.mail.imap.IMAPFolder; import com.sun.mail.imap.IMAPStore; -import org.json.JSONArray; -import org.json.JSONObject; - -import java.io.BufferedReader; import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.security.SecureRandom; -import java.security.spec.KeySpec; -import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Properties; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.CipherInputStream; -import javax.crypto.CipherOutputStream; -import javax.crypto.SecretKey; -import javax.crypto.SecretKeyFactory; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; import javax.mail.Folder; import javax.mail.Session; import javax.mail.Transport; @@ -111,8 +87,6 @@ import static android.app.Activity.RESULT_OK; public class FragmentSetup extends FragmentEx { private ViewGroup view; - private ImageButton ibHelp; - private EditText etName; private EditText etEmail; private Button btnAuthorize; @@ -148,9 +122,6 @@ public class FragmentSetup extends FragmentEx { private int auth_type = Helper.AUTH_TYPE_PASSWORD; - private static final int KEY_ITERATIONS = 65536; - private static final int KEY_LENGTH = 256; - private static final String[] permissions = new String[]{ Manifest.permission.READ_CONTACTS }; @@ -159,15 +130,12 @@ public class FragmentSetup extends FragmentEx { @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 = (ViewGroup) inflater.inflate(R.layout.fragment_setup, container, false); // Get controls - ibHelp = view.findViewById(R.id.ibHelp); - etName = view.findViewById(R.id.etName); btnAuthorize = view.findViewById(R.id.btnAuthorize); etEmail = view.findViewById(R.id.etEmail); @@ -200,13 +168,6 @@ public class FragmentSetup extends FragmentEx { // Wire controls - ibHelp.setOnClickListener(new View.OnClickListener() { - @Override - public void onClick(View v) { - startActivity(getIntentHelp()); - } - }); - btnAuthorize.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -605,7 +566,6 @@ public class FragmentSetup extends FragmentEx { }); // Initialize - ibHelp.setVisibility(View.GONE); grpQuickError.setVisibility(View.GONE); tvInstructions.setVisibility(View.GONE); tvInstructions.setMovementMethod(LinkMovementMethod.getInstance()); @@ -674,9 +634,6 @@ public class FragmentSetup extends FragmentEx { public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - PackageManager pm = getContext().getPackageManager(); - ibHelp.setVisibility(getIntentHelp().resolveActivity(pm) == null ? View.GONE : View.VISIBLE); - final DB db = DB.getInstance(getContext()); db.account().liveAccounts(true).observe(getViewLifecycleOwner(), new Observer>() { @@ -757,48 +714,6 @@ 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_legend: - onMenuLegend(); - return true; - - case R.id.menu_export: - onMenuExport(); - return true; - - case R.id.menu_import: - onMenuImport(); - return true; - - case R.id.menu_privacy: - onMenuPrivacy(); - return true; - - case R.id.menu_about: - onMenuAbout(); - return true; - - default: - return super.onOptionsItemSelected(item); - } - } - @Override public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { if (requestCode == ActivitySetup.REQUEST_PERMISSION) @@ -839,45 +754,12 @@ public class FragmentSetup extends FragmentEx { @Override public void onActivityResult(final int requestCode, int resultCode, final Intent data) { - if (requestCode == ActivitySetup.REQUEST_EXPORT || requestCode == ActivitySetup.REQUEST_IMPORT) { - if (resultCode == RESULT_OK && data != null) - fileSelected(requestCode == ActivitySetup.REQUEST_EXPORT, data); - } else if (requestCode == ActivitySetup.REQUEST_CHOOSE_ACCOUNT) { + if (requestCode == ActivitySetup.REQUEST_CHOOSE_ACCOUNT) { if (resultCode == RESULT_OK && data != null) accountSelected(data); } } - private void fileSelected(final boolean export, final Intent data) { - View dview = LayoutInflater.from(getContext()).inflate(R.layout.dialog_password, null); - final TextInputLayout etPassword1 = dview.findViewById(R.id.tilPassword1); - final TextInputLayout etPassword2 = dview.findViewById(R.id.tilPassword2); - - new DialogBuilderLifecycle(getContext(), getViewLifecycleOwner()) - .setView(dview) - .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - - String password1 = etPassword1.getEditText().getText().toString(); - String password2 = etPassword2.getEditText().getText().toString(); - - if (TextUtils.isEmpty(password1)) - Snackbar.make(view, R.string.title_setup_password_missing, Snackbar.LENGTH_LONG).show(); - else { - if (password1.equals(password2)) { - if (export) - handleExport(data, password1); - else - handleImport(data, password1); - } else - Snackbar.make(view, R.string.title_setup_password_different, Snackbar.LENGTH_LONG).show(); - } - } - }) - .show(); - } - private void accountSelected(Intent data) { String name = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME); String type = data.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE); @@ -937,66 +819,6 @@ public class FragmentSetup extends FragmentEx { } } - private void onMenuPrivacy() { - Helper.view(getContext(), getViewLifecycleOwner(), Helper.getIntentPrivacy()); - } - - private void onMenuLegend() { - FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); - fragmentTransaction.replace(R.id.content_frame, new FragmentLegend()).addToBackStack("legend"); - fragmentTransaction.commit(); - } - - private void onMenuExport() { - if (Helper.isPro(getContext())) - try { - startActivityForResult(Helper.getChooser(getContext(), getIntentExport()), ActivitySetup.REQUEST_EXPORT); - } catch (Throwable ex) { - Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); - } - else { - FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); - fragmentTransaction.replace(R.id.content_frame, new FragmentPro()).addToBackStack("pro"); - fragmentTransaction.commit(); - } - } - - private void onMenuImport() { - try { - startActivityForResult(Helper.getChooser(getContext(), getIntentImport()), ActivitySetup.REQUEST_IMPORT); - } catch (Throwable ex) { - Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); - } - } - - private void onMenuAbout() { - FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); - fragmentTransaction.replace(R.id.content_frame, new FragmentAbout()).addToBackStack("about"); - fragmentTransaction.commit(); - } - - private Intent getIntentHelp() { - Intent intent = new Intent(Intent.ACTION_VIEW); - intent.setData(Uri.parse("https://github.com/M66B/open-source-email/blob/master/SETUP.md#setup-help")); - return intent; - } - - 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 static Intent getIntentNotifications(Context context) { return new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS) .putExtra("app_package", context.getPackageName()) @@ -1004,258 +826,6 @@ public class FragmentSetup extends FragmentEx { .putExtra(Settings.EXTRA_APP_PACKAGE, context.getPackageName()); } - private void handleExport(Intent data, String password) { - Bundle args = new Bundle(); - args.putParcelable("uri", data.getData()); - args.putString("password", password); - - new SimpleTask() { - @Override - protected Void onExecute(Context context, Bundle args) throws Throwable { - Uri uri = args.getParcelable("uri"); - String password = args.getString("password"); - - if ("file".equals(uri.getScheme())) { - Log.w("Export uri=" + uri); - throw new IllegalArgumentException(context.getString(R.string.title_no_stream)); - } - - OutputStream out = null; - try { - Log.i("Writing URI=" + uri); - - byte[] salt = new byte[16]; - SecureRandom random = new SecureRandom(); - random.nextBytes(salt); - - // https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#Cipher - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, KEY_ITERATIONS, KEY_LENGTH); - SecretKey secret = keyFactory.generateSecret(keySpec); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - cipher.init(Cipher.ENCRYPT_MODE, secret); - - OutputStream raw = getContext().getContentResolver().openOutputStream(uri); - raw.write(salt); - raw.write(cipher.getIV()); - out = new CipherOutputStream(raw, cipher); - - 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 (!"pro".equals(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("Exported data"); - } finally { - if (out != null) - out.close(); - } - - return null; - } - - @Override - protected void onExecuted(Bundle args, Void data) { - Snackbar.make(view, R.string.title_setup_exported, Snackbar.LENGTH_LONG).show(); - } - - @Override - protected void onException(Bundle args, Throwable ex) { - if (ex instanceof IllegalArgumentException) - Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show(); - else - Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); - } - }.execute(this, args, "setup:export"); - } - - private void handleImport(Intent data, String password) { - Bundle args = new Bundle(); - args.putParcelable("uri", data.getData()); - args.putString("password", password); - - new SimpleTask() { - @Override - protected Void onExecute(Context context, Bundle args) throws Throwable { - Uri uri = args.getParcelable("uri"); - String password = args.getString("password"); - - if ("file".equals(uri.getScheme())) { - Log.w("Import uri=" + uri); - throw new IllegalArgumentException(context.getString(R.string.title_no_stream)); - } - - InputStream in = null; - try { - Log.i("Reading URI=" + uri); - ContentResolver resolver = getContext().getContentResolver(); - AssetFileDescriptor descriptor = resolver.openTypedAssetFileDescriptor(uri, "*/*", null); - InputStream raw = descriptor.createInputStream(); - - byte[] salt = new byte[16]; - byte[] prefix = new byte[16]; - if (raw.read(salt) != salt.length) - throw new IOException("length"); - if (raw.read(prefix) != prefix.length) - throw new IOException("length"); - - SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); - KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, KEY_ITERATIONS, KEY_LENGTH); - SecretKey secret = keyFactory.generateSecret(keySpec); - Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); - IvParameterSpec iv = new IvParameterSpec(prefix); - cipher.init(Cipher.DECRYPT_MODE, secret, iv); - - in = new CipherInputStream(raw, cipher); - - BufferedReader reader = new BufferedReader(new InputStreamReader(in)); - StringBuilder response = new StringBuilder(); - String line; - while ((line = reader.readLine()) != null) - response.append(line); - Log.i("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.created = new Date().getTime(); - account.id = db.account().insertAccount(account); - Log.i("Imported account=" + account.name); - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) - if (account.notify) - account.createNotificationChannel(context); - - 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("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("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("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"); - if (!"pro".equals(key)) { - Object value = jsetting.get("value"); - if (value instanceof Boolean) - editor.putBoolean(key, (Boolean) value); - else if (value instanceof Integer) - editor.putInt(key, (Integer) value); - else if (value instanceof Long) - editor.putLong(key, (Long) value); - else if (value instanceof String) - editor.putString(key, (String) value); - else - throw new IllegalArgumentException("Unknown settings type key=" + key); - Log.i("Imported setting=" + key); - } - } - editor.apply(); - - db.setTransactionSuccessful(); - } finally { - db.endTransaction(); - } - - Log.i("Imported data"); - ServiceSynchronize.reload(context, "import"); - } finally { - if (in != null) - in.close(); - } - - return null; - } - - @Override - protected void onExecuted(Bundle args, Void data) { - Snackbar.make(view, R.string.title_setup_imported, Snackbar.LENGTH_LONG).show(); - } - - @Override - protected void onException(Bundle args, Throwable ex) { - if (ex.getCause() instanceof BadPaddingException) - Snackbar.make(view, R.string.title_setup_password_invalid, Snackbar.LENGTH_LONG).show(); - else if (ex instanceof IllegalArgumentException) - Snackbar.make(view, ex.getMessage(), Snackbar.LENGTH_LONG).show(); - else - Helper.unexpectedError(getContext(), getViewLifecycleOwner(), ex); - } - }.execute(this, args, "setup:import"); - } - private void selectAccount() { Log.i("Select account"); startActivityForResult( diff --git a/app/src/main/java/eu/faircode/email/Helper.java b/app/src/main/java/eu/faircode/email/Helper.java index 6290e778a0..008faf78a9 100644 --- a/app/src/main/java/eu/faircode/email/Helper.java +++ b/app/src/main/java/eu/faircode/email/Helper.java @@ -160,6 +160,12 @@ public class Helper { return Intent.createChooser(intent, context.getString(R.string.title_select_app)); } + static Intent getIntentFAQ() { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setData(Uri.parse("https://github.com/M66B/open-source-email/blob/master/FAQ.md")); + return intent; + } + static Intent getIntentPrivacy() { Intent intent = new Intent(Intent.ACTION_VIEW); intent.setData(Uri.parse("https://github.com/M66B/open-source-email/blob/master/PRIVACY.md#fairemail")); diff --git a/app/src/main/res/drawable/baseline_unarchive_24.xml b/app/src/main/res/drawable/baseline_unarchive_24.xml new file mode 100644 index 0000000000..635073ff98 --- /dev/null +++ b/app/src/main/res/drawable/baseline_unarchive_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml index e53281b9ed..2f16046eca 100644 --- a/app/src/main/res/layout/activity_setup.xml +++ b/app/src/main/res/layout/activity_setup.xml @@ -1,6 +1,22 @@ - \ No newline at end of file + tools:context=".ActivitySetup"> + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_setup.xml b/app/src/main/res/layout/fragment_setup.xml index d819ab8ad4..8d0fbf3fe3 100644 --- a/app/src/main/res/layout/fragment_setup.xml +++ b/app/src/main/res/layout/fragment_setup.xml @@ -6,20 +6,12 @@ android:layout_height="match_parent" android:layout_margin="6dp" android:orientation="vertical" - tools:context=".ActivityView"> + tools:context=".ActivitySetup"> - - + app:layout_constraintTop_toTopOf="parent" />