mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-04-24 15:56:07 +00:00
Merge pull request #72 from michaelschattgen/feature-selectentries
Add ability to select entries when importing
This commit is contained in:
commit
33b4b4b7ab
16 changed files with 569 additions and 114 deletions
|
@ -3,58 +3,57 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
package="com.beemdevelopment.aegis">
|
package="com.beemdevelopment.aegis">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.CAMERA"/>
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
|
||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="com.beemdevelopment.aegis.AegisApplication"
|
android:name=".AegisApplication"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@mipmap/${iconName}"
|
android:icon="@mipmap/${iconName}"
|
||||||
android:label="Aegis"
|
android:label="Aegis"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
tools:replace="android:theme"
|
|
||||||
android:theme="@style/AppTheme.NoActionBar"
|
android:theme="@style/AppTheme.NoActionBar"
|
||||||
tools:ignore="GoogleAppIndexingWarning">
|
tools:ignore="GoogleAppIndexingWarning"
|
||||||
|
tools:replace="android:theme">
|
||||||
<activity
|
<activity
|
||||||
android:name="com.beemdevelopment.aegis.ui.MainActivity"
|
android:name=".ui.SelectEntriesActivity"
|
||||||
|
android:label="Select entries"
|
||||||
|
android:theme="@style/AppTheme.TransparentActionBar"></activity>
|
||||||
|
<activity
|
||||||
|
android:name=".ui.MainActivity"
|
||||||
android:label="${title}"
|
android:label="${title}"
|
||||||
android:launchMode="singleTask">
|
android:launchMode="singleTask">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<activity
|
<activity
|
||||||
android:name="com.beemdevelopment.aegis.ui.ScannerActivity"
|
android:name=".ui.ScannerActivity"
|
||||||
|
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||||
android:label="Scan a QR code"
|
android:label="Scan a QR code"
|
||||||
android:theme="@style/AppTheme.Fullscreen"
|
|
||||||
android:screenOrientation="portrait"
|
android:screenOrientation="portrait"
|
||||||
android:configChanges="keyboardHidden|orientation|screenSize">
|
android:theme="@style/AppTheme.Fullscreen" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.beemdevelopment.aegis.ui.EditEntryActivity"
|
android:name=".ui.EditEntryActivity"
|
||||||
android:label="Edit profile"
|
android:label="Edit profile"
|
||||||
android:theme="@style/AppTheme.NoActionBar">
|
android:theme="@style/AppTheme.NoActionBar" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.beemdevelopment.aegis.ui.IntroActivity"
|
android:name=".ui.IntroActivity"
|
||||||
android:theme="@style/Theme.Intro">
|
android:theme="@style/Theme.Intro" />
|
||||||
</activity>
|
<activity android:name=".ui.AuthActivity" />
|
||||||
<activity android:name="com.beemdevelopment.aegis.ui.AuthActivity">
|
<activity android:name=".ui.PreferencesActivity" />
|
||||||
</activity>
|
|
||||||
<activity android:name="com.beemdevelopment.aegis.ui.PreferencesActivity">
|
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.beemdevelopment.aegis.ui.SlotManagerActivity"
|
android:name=".ui.SlotManagerActivity"
|
||||||
android:label="Manage key slots"
|
android:label="Manage key slots"
|
||||||
android:theme="@style/AppTheme.TransparentActionBar">
|
android:theme="@style/AppTheme.TransparentActionBar" />
|
||||||
</activity>
|
|
||||||
<activity
|
<activity
|
||||||
android:name="com.beemdevelopment.aegis.ui.GroupManagerActivity"
|
android:name=".ui.GroupManagerActivity"
|
||||||
android:label="Manage groups"
|
android:label="Manage groups"
|
||||||
android:theme="@style/AppTheme.TransparentActionBar">
|
android:theme="@style/AppTheme.TransparentActionBar" />
|
||||||
</activity>
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
|
||||||
|
</manifest>
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,6 +30,7 @@ import com.beemdevelopment.aegis.db.DatabaseEntry;
|
||||||
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
|
import com.beemdevelopment.aegis.db.DatabaseFileCredentials;
|
||||||
import com.beemdevelopment.aegis.db.DatabaseManager;
|
import com.beemdevelopment.aegis.db.DatabaseManager;
|
||||||
import com.beemdevelopment.aegis.db.DatabaseManagerException;
|
import com.beemdevelopment.aegis.db.DatabaseManagerException;
|
||||||
|
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||||
import com.beemdevelopment.aegis.helpers.PermissionHelper;
|
import com.beemdevelopment.aegis.helpers.PermissionHelper;
|
||||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
import com.beemdevelopment.aegis.otp.GoogleAuthInfoException;
|
||||||
|
@ -79,7 +80,7 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
private FloatingActionsMenu _fabMenu;
|
private FloatingActionsMenu _fabMenu;
|
||||||
private EntryListView _entryListView;
|
private EntryListView _entryListView;
|
||||||
|
|
||||||
private boolean _isAnimating;
|
private FabScrollHelper _fabScrollHelper;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
@ -118,6 +119,8 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
_fabMenu.collapse();
|
_fabMenu.collapse();
|
||||||
startScanActivity();
|
startScanActivity();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
_fabScrollHelper = new FabScrollHelper(_fabMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -622,33 +625,6 @@ public class MainActivity extends AegisActivity implements EntryListView.Listene
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onScroll(int dx, int dy) {
|
public void onScroll(int dx, int dy) {
|
||||||
if (dy > 0 && _fabMenu.getVisibility() == View.VISIBLE && !_isAnimating) {
|
_fabScrollHelper.onScroll(dx, dy);
|
||||||
_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();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,15 +2,11 @@ package com.beemdevelopment.aegis.ui;
|
||||||
|
|
||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ClipData;
|
|
||||||
import android.content.ClipboardManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
import android.content.DialogInterface;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.media.MediaScannerConnection;
|
import android.media.MediaScannerConnection;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
@ -42,7 +38,6 @@ import com.beemdevelopment.aegis.importers.DatabaseImporterException;
|
||||||
import com.beemdevelopment.aegis.importers.DatabaseImporterResult;
|
import com.beemdevelopment.aegis.importers.DatabaseImporterResult;
|
||||||
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
|
import com.beemdevelopment.aegis.ui.preferences.SwitchPreference;
|
||||||
import com.beemdevelopment.aegis.util.ByteInputStream;
|
import com.beemdevelopment.aegis.util.ByteInputStream;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
import com.takisoft.preferencex.PreferenceFragmentCompat;
|
import com.takisoft.preferencex.PreferenceFragmentCompat;
|
||||||
import com.topjohnwu.superuser.Shell;
|
import com.topjohnwu.superuser.Shell;
|
||||||
|
|
||||||
|
@ -52,7 +47,6 @@ import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.atomic.AtomicReference;
|
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_IMPORT_DECRYPT = 1;
|
||||||
private static final int CODE_SLOTS = 2;
|
private static final int CODE_SLOTS = 2;
|
||||||
private static final int CODE_GROUPS = 3;
|
private static final int CODE_GROUPS = 3;
|
||||||
|
private static final int CODE_SELECT_ENTRIES = 4;
|
||||||
|
|
||||||
// permission request codes
|
// permission request codes
|
||||||
private static final int CODE_PERM_IMPORT = 0;
|
private static final int CODE_PERM_IMPORT = 0;
|
||||||
|
@ -382,6 +377,8 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||||
break;
|
break;
|
||||||
case CODE_GROUPS:
|
case CODE_GROUPS:
|
||||||
onGroupManagerResult(resultCode, data);
|
onGroupManagerResult(resultCode, data);
|
||||||
|
case CODE_SELECT_ENTRIES:
|
||||||
|
onSelectEntriesResult(resultCode, data);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -524,44 +521,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
|
||||||
List<DatabaseEntry> entries = result.getEntries();
|
List<DatabaseEntry> entries = result.getEntries();
|
||||||
List<DatabaseImporterEntryException> errors = result.getErrors();
|
List<DatabaseImporterEntryException> errors = result.getErrors();
|
||||||
|
|
||||||
for (DatabaseEntry entry : entries) {
|
Intent intent = new Intent(getActivity(), SelectEntriesActivity.class);
|
||||||
// temporary: randomize the UUID of duplicate entries and add them anyway
|
intent.putExtra("entries", (ArrayList<DatabaseEntry>) entries);
|
||||||
if (_db.getEntryByUUID(entry.getUUID()) != null) {
|
intent.putExtra("errors", (ArrayList<DatabaseImporterEntryException>) errors);
|
||||||
entry.resetUUID();
|
startActivityForResult(intent, CODE_SELECT_ENTRIES);
|
||||||
}
|
|
||||||
|
|
||||||
_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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onExport() {
|
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() {
|
private boolean saveDatabase() {
|
||||||
try {
|
try {
|
||||||
_db.save();
|
_db.save();
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,5 @@
|
||||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
<vector android:height="24dp" android:tint="#F7F7F7"
|
||||||
android:width="24dp"
|
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||||
android:height="24dp"
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
android:viewportWidth="24.0"
|
<path android:fillColor="#FF000000" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
||||||
android:viewportHeight="24.0">
|
|
||||||
<path
|
|
||||||
android:fillColor="#FF000000"
|
|
||||||
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
|
|
||||||
</vector>
|
</vector>
|
||||||
|
|
5
app/src/main/res/drawable/ic_done_all_black_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_done_all_black_24dp.xml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<vector android:height="24dp" android:tint="#F7F7F7"
|
||||||
|
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||||
|
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<path android:fillColor="#FF000000" android:pathData="M18,7l-1.41,-1.41 -6.34,6.34 1.41,1.41L18,7zM22.24,5.59L11.66,16.17 7.48,12l-1.41,1.41L11.66,19l12,-12 -1.42,-1.41zM0.41,13.41L6,19l1.41,-1.41L1.83,12 0.41,13.41z"/>
|
||||||
|
</vector>
|
33
app/src/main/res/layout/activity_select_entries.xml
Normal file
33
app/src/main/res/layout/activity_select_entries.xml
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:fitsSystemWindows="true"
|
||||||
|
android:background="?attr/background"
|
||||||
|
android:id="@+id/importEntriesRootView"
|
||||||
|
tools:context="com.beemdevelopment.aegis.ui.SelectEntriesActivity">
|
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
|
android:id="@+id/list_entries"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:foreground="?android:attr/selectableItemBackground"
|
||||||
|
android:paddingBottom="60dp"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:scrollbars="vertical"/>
|
||||||
|
|
||||||
|
<com.getbase.floatingactionbutton.FloatingActionButton
|
||||||
|
android:id="@+id/fab"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="bottom|end"
|
||||||
|
android:layout_margin="@dimen/fab_margin"
|
||||||
|
app:fab_icon="@drawable/ic_check_black_24dp"
|
||||||
|
app:fab_colorNormal="@color/colorAccent"
|
||||||
|
app:fab_colorPressed="@color/colorAccent"
|
||||||
|
app:fab_labelStyle="@style/fab_label_style"
|
||||||
|
app:fab_labelsPosition="left">
|
||||||
|
</com.getbase.floatingactionbutton.FloatingActionButton>
|
||||||
|
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
76
app/src/main/res/layout/card_import_entry.xml
Normal file
76
app/src/main/res/layout/card_import_entry.xml
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:foreground="?android:attr/selectableItemBackground"
|
||||||
|
android:clickable="true"
|
||||||
|
android:focusable="true"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:background="?attr/cardBackground"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:layout_alignParentBottom="false"
|
||||||
|
android:layout_alignParentTop="false"
|
||||||
|
android:layout_alignWithParentIfMissing="false"
|
||||||
|
android:gravity="bottom"
|
||||||
|
android:id="@+id/relativeLayout"
|
||||||
|
android:layout_alignParentStart="false"
|
||||||
|
android:paddingTop="12dp"
|
||||||
|
android:paddingBottom="16dp"
|
||||||
|
android:paddingEnd="16dp"
|
||||||
|
android:paddingLeft="16dp"
|
||||||
|
android:paddingRight="16dp"
|
||||||
|
android:paddingStart="16dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/profile_issuer"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_marginTop="0dp"
|
||||||
|
android:fontFamily="sans-serif-light"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:textAppearance="?android:attr/textAppearanceMedium"
|
||||||
|
android:textColor="?attr/primaryText"
|
||||||
|
android:textSize="18sp"
|
||||||
|
android:textStyle="normal|bold"
|
||||||
|
tools:text="@string/issuer" />
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/profile_account_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/profile_issuer"
|
||||||
|
android:layout_marginTop="2dp"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/extra_info_text"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:text="AccountName" />
|
||||||
|
|
||||||
|
<CheckBox
|
||||||
|
android:id="@+id/checkbox_import_entry"
|
||||||
|
android:clickable="false"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginEnd="6dp" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
8
app/src/main/res/menu/menu_select_entries.xml
Normal file
8
app/src/main/res/menu/menu_select_entries.xml
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" >
|
||||||
|
<item android:id="@+id/toggle_checkboxes"
|
||||||
|
android:icon="@drawable/ic_done_all_black_24dp"
|
||||||
|
android:title="@string/toggle_checkboxes"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
</menu>
|
|
@ -109,7 +109,7 @@
|
||||||
<string name="file_not_found">Fout: Bestand niet gevonden</string>
|
<string name="file_not_found">Fout: Bestand niet gevonden</string>
|
||||||
<string name="reading_file_error">Er is een fout opgetreden tijdens het lezen van het bestand</string>
|
<string name="reading_file_error">Er is een fout opgetreden tijdens het lezen van het bestand</string>
|
||||||
<string name="root_error">Fout: Er kon geen root-access verkregen worden</string>
|
<string name="root_error">Fout: Er kon geen root-access verkregen worden</string>
|
||||||
<string name="imported_entries_count">%d items geïmporteerd. %d fouten.</string>
|
<string name="imported_entries_count">%d items geïmporteerd</string>
|
||||||
<string name="import_error_title">Er zijn fouten opgetreden tijdens het importeren</string>
|
<string name="import_error_title">Er zijn fouten opgetreden tijdens het importeren</string>
|
||||||
<string name="exporting_database_error">Er is een fout opgetreden tijdens het exporteren van de database</string>
|
<string name="exporting_database_error">Er is een fout opgetreden tijdens het exporteren van de database</string>
|
||||||
<string name="export_database_location">De database is geëxporteerd naar:</string>
|
<string name="export_database_location">De database is geëxporteerd naar:</string>
|
||||||
|
|
|
@ -108,7 +108,7 @@
|
||||||
<string name="file_not_found">Ошибка: файл не найден</string>
|
<string name="file_not_found">Ошибка: файл не найден</string>
|
||||||
<string name="reading_file_error">Произошла ошибка при попытке прочитать файл</string>
|
<string name="reading_file_error">Произошла ошибка при попытке прочитать файл</string>
|
||||||
<string name="root_error">Ошибка: невозможно получить root-доступ</string>
|
<string name="root_error">Ошибка: невозможно получить root-доступ</string>
|
||||||
<string name="imported_entries_count">Импортироаано %d записей. %d ошибки.</string>
|
<string name="imported_entries_count">Импортироаано %d записей</string>
|
||||||
<string name="exporting_database_error">Произошла ошибка при попытке экспорта базы данных</string>
|
<string name="exporting_database_error">Произошла ошибка при попытке экспорта базы данных</string>
|
||||||
<string name="export_database_location">База данных была экспортирована в:</string>
|
<string name="export_database_location">База данных была экспортирована в:</string>
|
||||||
<string name="export_warning">Это действие экспортирует базу данных из личного хранилища Aegis.</string>
|
<string name="export_warning">Это действие экспортирует базу данных из личного хранилища Aegis.</string>
|
||||||
|
|
|
@ -114,7 +114,8 @@
|
||||||
<string name="file_not_found">Error: File not found</string>
|
<string name="file_not_found">Error: File not found</string>
|
||||||
<string name="reading_file_error">An error occurred while trying to read the file</string>
|
<string name="reading_file_error">An error occurred while trying to read the file</string>
|
||||||
<string name="root_error">Error: unable to obtain root access</string>
|
<string name="root_error">Error: unable to obtain root access</string>
|
||||||
<string name="imported_entries_count">Imported %d entries. %d errors.</string>
|
<string name="imported_entries_count">Imported %d entries</string>
|
||||||
|
<string name="read_entries_count">Read %d entries. %d errors.</string>
|
||||||
<string name="import_error_title">One or more errors occurred during the import</string>
|
<string name="import_error_title">One or more errors occurred during the import</string>
|
||||||
<string name="exporting_database_error">An error occurred while trying to export the database</string>
|
<string name="exporting_database_error">An error occurred while trying to export the database</string>
|
||||||
<string name="export_database_location">The database has been exported to:</string>
|
<string name="export_database_location">The database has been exported to:</string>
|
||||||
|
@ -156,6 +157,10 @@
|
||||||
<string name="normal_viewmode_title">Normal</string>
|
<string name="normal_viewmode_title">Normal</string>
|
||||||
<string name="compact_mode_title">Compact</string>
|
<string name="compact_mode_title">Compact</string>
|
||||||
<string name="small_mode_title">Small</string>
|
<string name="small_mode_title">Small</string>
|
||||||
|
<string name="unknown_issuer">Unknown issuer</string>
|
||||||
|
<string name="unknown_account_name">Unknown account name</string>
|
||||||
|
<string name="import_error_dialog">Aegis could not import %d tokens. These tokens will be skipped. Press \'details\' to see more information about the errors.</string>
|
||||||
<string name="unable_to_read_qrcode">Unable to read and process QR code</string>
|
<string name="unable_to_read_qrcode">Unable to read and process QR code</string>
|
||||||
<string name="select_picture">Select picture</string>
|
<string name="select_picture">Select picture</string>
|
||||||
|
<string name="toggle_checkboxes">Toggle checkboxes</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
Loading…
Add table
Reference in a new issue