Add ability to select entries when importing

Move onScroll to seperate class to avoid duplicate code

Move importing logic back to the PreferencesFragment

Add minor changes

Add ImportEntry to properly track checked states

Minor layout changes
This commit is contained in:
Michael Schättgen 2019-04-20 01:25:04 +02:00 committed by Alexander Bakker
parent 24a93ecc9f
commit 21fd8fdd8d
16 changed files with 569 additions and 114 deletions

View file

@ -0,0 +1,49 @@
package com.beemdevelopment.aegis.helpers;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.view.View;
import android.view.animation.AccelerateInterpolator;
import android.view.animation.DecelerateInterpolator;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import com.getbase.floatingactionbutton.FloatingActionsMenu;
public class FabScrollHelper {
private View _fabMenu;
private boolean _isAnimating;
public FabScrollHelper(View floatingActionsMenu) {
_fabMenu = floatingActionsMenu;
}
public void onScroll(int dx, int dy) {
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
_isAnimating = true;
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
_fabMenu.animate()
.translationY(_fabMenu.getHeight() + fabBottomMargin)
.setInterpolator(new AccelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
_fabMenu.setVisibility(View.INVISIBLE);
super.onAnimationEnd(animation);
}
}).start();
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
_fabMenu.setVisibility(View.VISIBLE);
_fabMenu.animate()
.translationY(0)
.setInterpolator(new DecelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
super.onAnimationEnd(animation);
}
}).start();
}
}
}

View file

@ -30,6 +30,7 @@ import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
import com.beemdevelopment.aegis.db.DatabaseManager;
import com.beemdevelopment.aegis.db.DatabaseManagerException;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.helpers.PermissionHelper;
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
@ -79,7 +80,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
private FloatingActionsMenu _fabMenu;
private EntryListView _entryListView;
private boolean _isAnimating;
private FabScrollHelper _fabScrollHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -118,6 +119,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
_fabMenu.collapse();
startScanActivity();
});
_fabScrollHelper = new FabScrollHelper(_fabMenu);
}
@Override
@ -622,33 +625,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
@Override
public void onScroll(int dx, int dy) {
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
_isAnimating = true;
CoordinatorLayout.LayoutParams lp = (CoordinatorLayout.LayoutParams) _fabMenu.getLayoutParams();
int fabBottomMargin = lp.bottomMargin;
_fabMenu.animate()
.translationY(_fabMenu.getHeight() + fabBottomMargin)
.setInterpolator(new AccelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
_fabMenu.setVisibility(View.INVISIBLE);
super.onAnimationEnd(animation);
}
}).start();
} else if (dy < 0 && _fabMenu.getVisibility() != View.VISIBLE && !_isAnimating) {
_fabMenu.setVisibility(View.VISIBLE);
_fabMenu.animate()
.translationY(0)
.setInterpolator(new DecelerateInterpolator(2))
.setListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
_isAnimating = false;
super.onAnimationEnd(animation);
}
}).start();
}
_fabScrollHelper.onScroll(dx, dy);
}
}

View file

@ -2,15 +2,11 @@ package com.beemdevelopment.aegis.ui;
import android.Manifest;
import android.app.Activity;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.net.Uri;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
@ -42,7 +38,6 @@ import com.beemdevelopment.aegis.importers.DatabaseImporterException;
import com.beemdevelopment.aegis.importers.DatabaseImporterResult;
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
import com.beemdevelopment.aegis.util.ByteInputStream;
import com.google.android.material.snackbar.Snackbar;
import com.takisoft.preferencex.PreferenceFragmentCompat;
import com.topjohnwu.superuser.Shell;
@ -52,7 +47,6 @@ import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
@ -67,6 +61,7 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
private static final int CODE_IMPORT_DECRYPT = 1;
private static final int CODE_SLOTS = 2;
private static final int CODE_GROUPS = 3;
private static final int CODE_SELECT_ENTRIES = 4;
// permission request codes
private static final int CODE_PERM_IMPORT = 0;
@ -382,6 +377,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
break;
case CODE_GROUPS:
onGroupManagerResult(resultCode, data);
case CODE_SELECT_ENTRIES:
onSelectEntriesResult(resultCode, data);
break;
}
}
@ -524,44 +521,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
List<DatabaseEntry> entries = result.getEntries();
List<DatabaseImporterEntryException> errors = result.getErrors();
for (DatabaseEntry entry : entries) {
// temporary: randomize the UUID of duplicate entries and add them anyway
if (_db.getEntryByUUID(entry.getUUID()) != null) {
entry.resetUUID();
}
_db.addEntry(entry);
}
if (!saveDatabase()) {
return;
}
_result.putExtra("needsRecreate", true);
Snackbar bar = Snackbar.make(getView(), String.format(Locale.getDefault(), getString(R.string.imported_entries_count), entries.size(), errors.size()), Snackbar.LENGTH_LONG);
if (errors.size() > 0) {
bar.setAction(R.string.details, v -> {
List<String> messages = new ArrayList<>();
for (DatabaseImporterEntryException e : errors) {
messages.add(e.getMessage());
}
String message = TextUtils.join("\n\n", messages);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.import_error_title)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(android.R.string.copy, (dialog, which) -> {
ClipboardManager clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", message);
clipboard.setPrimaryClip(clip);
Toast.makeText(getActivity(), getString(R.string.errors_copied), Toast.LENGTH_SHORT).show();
})
.create());
});
}
bar.show();
Intent intent = new Intent(getActivity(), SelectEntriesActivity.class);
intent.putExtra("entries", (ArrayList<DatabaseEntry>) entries);
intent.putExtra("errors", (ArrayList<DatabaseImporterEntryException>) errors);
startActivityForResult(intent, CODE_SELECT_ENTRIES);
}
private void onExport() {
@ -628,6 +591,31 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
}
}
private void onSelectEntriesResult(int resultCode, Intent data) {
if (resultCode != Activity.RESULT_OK) {
return;
}
List<DatabaseEntry> entries = (ArrayList<DatabaseEntry>) data.getSerializableExtra("entries");
for (DatabaseEntry entry : entries) {
// temporary: randomize the UUID of duplicate entries and add them anyway
if (_db.getEntryByUUID(entry.getUUID()) != null) {
entry.resetUUID();
}
_db.addEntry(entry);
}
if (!saveDatabase()) {
return;
}
String toastMessage = getResources().getString(R.string.imported_entries_count, entries.size());
Toast.makeText(getContext(), toastMessage, Toast.LENGTH_SHORT).show();
_result.putExtra("needsRecreate", true);
}
private boolean saveDatabase() {
try {
_db.save();

View file

@ -0,0 +1,138 @@
package com.beemdevelopment.aegis.ui;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporterEntryException;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import com.beemdevelopment.aegis.ui.views.ImportEntriesAdapter;
import com.getbase.floatingactionbutton.FloatingActionButton;
import java.util.ArrayList;
import java.util.List;
public class SelectEntriesActivity extends AegisActivity {
private ImportEntriesAdapter _adapter;
private FabScrollHelper _fabScrollHelper;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_select_entries);
ActionBar bar = getSupportActionBar();
bar.setHomeAsUpIndicator(R.drawable.ic_close);
bar.setDisplayHomeAsUpEnabled(true);
_adapter = new ImportEntriesAdapter();
RecyclerView entriesView = findViewById(R.id.list_entries);
entriesView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
onScroll(dx, dy);
}
});
LinearLayoutManager layoutManager = new LinearLayoutManager(this);
entriesView.setLayoutManager(layoutManager);
entriesView.setAdapter(_adapter);
entriesView.setNestedScrollingEnabled(false);
Intent intent = getIntent();
List<DatabaseEntry> entries = (ArrayList<DatabaseEntry>) intent.getSerializableExtra("entries");
List<DatabaseImporterEntryException> errors = (ArrayList<DatabaseImporterEntryException>) intent.getSerializableExtra("errors");
for (DatabaseEntry entry : entries) {
ImportEntry importEntry = new ImportEntry(entry);
_adapter.addEntry(importEntry);
}
if (errors.size() > 0) {
showErrorDialog(errors);
}
FloatingActionButton fabMenu = findViewById(R.id.fab);
fabMenu.setOnClickListener(v -> returnSelectedEntries());
_fabScrollHelper = new FabScrollHelper(fabMenu);
}
private void showErrorDialog(List<DatabaseImporterEntryException> errors) {
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.import_error_title)
.setMessage(getString(R.string.import_error_dialog, errors.size()))
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(getString(R.string.details), (dialog, which) -> showDetailedErrorDialog(errors))
.create());
}
private void showDetailedErrorDialog(List<DatabaseImporterEntryException> errors) {
List<String> messages = new ArrayList<>();
for (DatabaseImporterEntryException e : errors) {
messages.add(e.getMessage());
}
String message = TextUtils.join("\n\n", messages);
Dialogs.showSecureDialog(new AlertDialog.Builder(this)
.setTitle(R.string.import_error_title)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.setNeutralButton(android.R.string.copy, (dialog2, which2) -> {
ClipboardManager clipboard = (ClipboardManager) this.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text/plain", message);
clipboard.setPrimaryClip(clip);
Toast.makeText(this, getString(R.string.errors_copied), Toast.LENGTH_SHORT).show();
})
.create());
}
private void returnSelectedEntries() {
List<DatabaseEntry> entries = _adapter.getSelectedEntries();
Intent intent = new Intent();
intent.putExtra("entries", (ArrayList<DatabaseEntry>) entries);
setResult(RESULT_OK, intent);
finish();
}
public void onScroll(int dx, int dy) {
_fabScrollHelper.onScroll(dx, dy);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_select_entries, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
break;
case R.id.toggle_checkboxes:
_adapter.toggleCheckboxes();
break;
default:
return super.onOptionsItemSelected(item);
}
return true;
}
}

View file

@ -0,0 +1,37 @@
package com.beemdevelopment.aegis.ui.models;
import com.beemdevelopment.aegis.db.DatabaseEntry;
public class ImportEntry {
private DatabaseEntry _entry;
private boolean _isChecked = true;
private Listener _listener;
public ImportEntry(DatabaseEntry entry) {
_entry = entry;
}
public void setOnCheckedChangedListener(Listener listener) {
_listener = listener;
}
public DatabaseEntry getDatabaseEntry() {
return _entry;
}
public boolean isChecked() {
return _isChecked;
}
public void setIsChecked(boolean isChecked) {
_isChecked = isChecked;
if (_listener != null) {
_listener.onCheckedChanged(_isChecked);
}
}
public interface Listener {
void onCheckedChanged(boolean value);
}
}

View file

@ -0,0 +1,97 @@
package com.beemdevelopment.aegis.ui.views;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import java.util.ArrayList;
import java.util.List;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
public class ImportEntriesAdapter extends RecyclerView.Adapter<ImportEntryHolder> {
private List<ImportEntry> _entries;
public ImportEntriesAdapter() {
_entries = new ArrayList<>();
}
public void addEntry(ImportEntry entry) {
_entries.add(entry);
int position = getItemCount() - 1;
if (position == 0) {
notifyDataSetChanged();
} else {
notifyItemInserted(position);
}
}
@NonNull
@Override
public ImportEntryHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_import_entry, parent, false);
return new ImportEntryHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ImportEntryHolder holder, int position) {
ImportEntry entry = _entries.get(position);
entry.setOnCheckedChangedListener(holder);
holder.setData(entry);
}
@Override
public void onViewRecycled(@NonNull ImportEntryHolder holder) {
holder.getEntry().setOnCheckedChangedListener(null);
}
@Override
public int getItemCount() {
return _entries.size();
}
public List<DatabaseEntry> getSelectedEntries() {
List<DatabaseEntry> entries = new ArrayList<>();
for (ImportEntry entry : getCheckedEntries()) {
entries.add(entry.getDatabaseEntry());
}
return entries;
}
private List<ImportEntry> getCheckedEntries() {
List<ImportEntry> entries = new ArrayList<>();
for (ImportEntry entry : _entries) {
if (entry.isChecked()) {
entries.add(entry);
}
}
return entries;
}
public void toggleCheckboxes() {
int checkedEntries = getCheckedEntries().size();
if (checkedEntries == 0 || checkedEntries != _entries.size()) {
setCheckboxStates(true);
} else {
setCheckboxStates(false);
}
}
private void setCheckboxStates(boolean checked) {
for (ImportEntry entry: _entries) {
if (entry.isChecked() != checked) {
entry.setIsChecked(checked);
}
}
}
}

View file

@ -0,0 +1,48 @@
package com.beemdevelopment.aegis.ui.views;
import android.content.Context;
import android.view.View;
import android.widget.CheckBox;
import android.widget.TextView;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.db.DatabaseEntry;
import com.beemdevelopment.aegis.ui.models.ImportEntry;
import androidx.recyclerview.widget.RecyclerView;
public class ImportEntryHolder extends RecyclerView.ViewHolder implements ImportEntry.Listener {
private TextView _issuer;
private TextView _accountName;
private CheckBox _checkbox;
private ImportEntry _entry;
public ImportEntryHolder(final View view) {
super(view);
_issuer = view.findViewById(R.id.profile_issuer);
_accountName = view.findViewById(R.id.profile_account_name);
_checkbox = view.findViewById(R.id.checkbox_import_entry);
view.setOnClickListener(v -> _entry.setIsChecked(!_entry.isChecked()));
}
public void setData(ImportEntry entry) {
_entry = entry;
Context context = itemView.getContext();
DatabaseEntry dbEntry = entry.getDatabaseEntry();
_issuer.setText(!dbEntry.getIssuer().isEmpty() ? dbEntry.getIssuer() : context.getString(R.string.unknown_issuer));
_accountName.setText(!dbEntry.getName().isEmpty() ? dbEntry.getName() : context.getString(R.string.unknown_account_name));
_checkbox.setChecked(entry.isChecked());
}
public ImportEntry getEntry() {
return _entry;
}
@Override
public void onCheckedChanged(boolean value) {
_checkbox.setChecked(value);
}
}