Display some help text in the importer selection dialog

This commit is contained in:
Alexander Bakker 2020-10-25 13:35:38 +01:00
parent b21b6dc750
commit 69196ddbcc
6 changed files with 148 additions and 70 deletions

View file

@ -3,6 +3,9 @@ package com.beemdevelopment.aegis.importers;
import android.content.Context;
import android.content.pm.PackageManager;
import androidx.annotation.StringRes;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.util.UUIDMap;
import com.beemdevelopment.aegis.vault.VaultEntry;
import com.topjohnwu.superuser.io.SuFile;
@ -13,40 +16,29 @@ import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public abstract class DatabaseImporter {
private Context _context;
private static Map<String, Class<? extends DatabaseImporter>> _importers;
private static Map<String, Class<? extends DatabaseImporter>> _appImporters;
private static List<Definition> _importers;
static {
// note: keep these lists sorted alphabetically
_importers = new LinkedHashMap<>();
_importers.put("Aegis", AegisImporter.class);
_importers.put("Authenticator Plus", AuthenticatorPlusImporter.class);
_importers.put("Authy", AuthyImporter.class);
_importers.put("andOTP", AndOtpImporter.class);
_importers.put("FreeOTP", FreeOtpImporter.class);
_importers.put("FreeOTP+", FreeOtpPlusImporter.class);
_importers.put("Google Authenticator", GoogleAuthImporter.class);
_importers.put("Microsoft Authenticator", MicrosoftAuthImporter.class);
_importers.put("Plain text", GoogleAuthUriImporter.class);
_importers.put("Steam", SteamImporter.class);
_importers.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
_importers.put("WinAuth", WinAuthImporter.class);
_appImporters = new LinkedHashMap<>();
_appImporters.put("Authy", AuthyImporter.class);
_appImporters.put("FreeOTP", FreeOtpImporter.class);
_appImporters.put("FreeOTP+", FreeOtpPlusImporter.class);
_appImporters.put("Google Authenticator", GoogleAuthImporter.class);
_appImporters.put("Microsoft Authenticator", MicrosoftAuthImporter.class);
_appImporters.put("Steam", SteamImporter.class);
_appImporters.put("TOTP Authenticator", TotpAuthenticatorImporter.class);
_importers = new ArrayList<>();
_importers.add(new Definition("Aegis", AegisImporter.class, R.string.importer_help_aegis, false));
_importers.add(new Definition("Authenticator Plus", AuthenticatorPlusImporter.class, R.string.importer_help_authenticator_plus, false));
_importers.add(new Definition("Authy", AuthyImporter.class, R.string.importer_help_authy, true));
_importers.add(new Definition("andOTP", AndOtpImporter.class, R.string.importer_help_andotp, false));
_importers.add(new Definition("FreeOTP", FreeOtpImporter.class, R.string.importer_help_freeotp, true));
_importers.add(new Definition("FreeOTP+", FreeOtpPlusImporter.class, R.string.importer_help_freeotp_plus, true));
_importers.add(new Definition("Google Authenticator", GoogleAuthImporter.class, R.string.importer_help_google_authenticator, true));
_importers.add(new Definition("Microsoft Authenticator", MicrosoftAuthImporter.class, R.string.importer_help_microsoft_authenticator, true));
_importers.add(new Definition("Plain text", GoogleAuthUriImporter.class, R.string.importer_help_plain_text, false));
_importers.add(new Definition("Steam", SteamImporter.class, R.string.importer_help_steam, true));
_importers.add(new Definition("TOTP Authenticator", TotpAuthenticatorImporter.class, R.string.importer_help_totp_authenticator, true));
_importers.add(new Definition("WinAuth", WinAuthImporter.class, R.string.importer_help_winauth, false));
}
public DatabaseImporter(Context context) {
@ -88,12 +80,42 @@ public abstract class DatabaseImporter {
}
}
public static Map<String, Class<? extends DatabaseImporter>> getImporters() {
return Collections.unmodifiableMap(_importers);
public static List<Definition> getImporters(boolean isDirect) {
if (isDirect) {
return Collections.unmodifiableList(_importers.stream().filter(Definition::supportsDirect).collect(Collectors.toList()));
}
return Collections.unmodifiableList(_importers);
}
public static Map<String, Class<? extends DatabaseImporter>> getAppImporters() {
return Collections.unmodifiableMap(_appImporters);
public static class Definition {
private final String _name;
private final Class<? extends DatabaseImporter> _type;
private final @StringRes int _help;
private final boolean _supportsDirect;
public Definition(String name, Class<? extends DatabaseImporter> type, @StringRes int help, boolean supportsDirect) {
_name = name;
_type = type;
_help = help;
_supportsDirect = supportsDirect;
}
public String getName() {
return _name;
}
public Class<? extends DatabaseImporter> getType() {
return _type;
}
public @StringRes int getHelp() {
return _help;
}
public boolean supportsDirect() {
return _supportsDirect;
}
}
public static abstract class State {

View file

@ -15,10 +15,12 @@ import android.text.method.PasswordTransformationMethod;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.NumberPicker;
import android.widget.ProgressBar;
import android.widget.TextView;
@ -31,6 +33,7 @@ import com.beemdevelopment.aegis.Preferences;
import com.beemdevelopment.aegis.R;
import com.beemdevelopment.aegis.helpers.EditTextHelper;
import com.beemdevelopment.aegis.helpers.PasswordStrengthHelper;
import com.beemdevelopment.aegis.importers.DatabaseImporter;
import com.beemdevelopment.aegis.ui.tasks.KeyDerivationTask;
import com.beemdevelopment.aegis.vault.slots.PasswordSlot;
import com.beemdevelopment.aegis.vault.slots.Slot;
@ -39,6 +42,7 @@ import com.google.android.material.textfield.TextInputLayout;
import com.nulabinc.zxcvbn.Strength;
import com.nulabinc.zxcvbn.Zxcvbn;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
@ -383,6 +387,37 @@ public class Dialogs {
.create());
}
public static void showImportersDialog(Context context, boolean isDirect, ImporterListener listener) {
List<DatabaseImporter.Definition> importers = DatabaseImporter.getImporters(isDirect);
String[] names = importers.stream().map(DatabaseImporter.Definition::getName).toArray(String[]::new);
View view = LayoutInflater.from(context).inflate(R.layout.dialog_importers, null);
TextView helpText = view.findViewById(R.id.text_importer_help);
setImporterHelpText(helpText, importers.get(0), isDirect);
ListView listView = view.findViewById(R.id.list_importers);
listView.setAdapter(new ArrayAdapter<>(context, R.layout.card_importer, names));
listView.setItemChecked(0, true);
listView.setOnItemClickListener((parent, view1, position, id) -> {
setImporterHelpText(helpText, importers.get(position), isDirect);
});
Dialogs.showSecureDialog(new AlertDialog.Builder(context)
.setTitle(R.string.choose_application)
.setView(view)
.setPositiveButton(android.R.string.ok, (dialog1, which) -> {
listener.onImporterSelectionResult(importers.get(listView.getCheckedItemPosition()));
})
.create());
}
private static void setImporterHelpText(TextView view, DatabaseImporter.Definition definition, boolean isDirect) {
if (isDirect) {
view.setText(view.getResources().getString(R.string.importer_help_direct, definition.getName()));
} else {
view.setText(definition.getHelp());
}
}
public interface CheckboxInputListener {
void onCheckboxInputResult(boolean checkbox);
}
@ -399,4 +434,8 @@ public class Dialogs {
void onSlotResult(Slot slot, Cipher cipher);
void onException(Exception e);
}
public interface ImporterListener {
void onImporterSelectionResult(DatabaseImporter.Definition definition);
}
}

View file

@ -57,8 +57,6 @@ import java.io.OutputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import javax.crypto.Cipher;
@ -175,7 +173,13 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
importPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
startImport();
Dialogs.showImportersDialog(getContext(), false, definition -> {
_importerType = definition.getType();
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
startActivityForResult(intent, CODE_IMPORT);
});
return true;
}
});
@ -184,7 +188,10 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
importAppPreference.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
onImportApp();
Dialogs.showImportersDialog(getContext(), true, definition -> {
DatabaseImporter importer = DatabaseImporter.create(getContext(), definition.getType());
importApp(importer);
});
return true;
}
});
@ -540,42 +547,6 @@ public class PreferencesFragment extends PreferenceFragmentCompat {
getActivity().setResult(Activity.RESULT_OK, _result);
}
private void startImport() {
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getImporters();
String[] names = importers.keySet().toArray(new String[0]);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_application)
.setSingleChoiceItems(names, 0, null)
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
_importerType = importers.get(names[i]);
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("*/*");
startActivityForResult(intent, CODE_IMPORT);
}
})
.create());
}
private void onImportApp() {
Map<String, Class<? extends DatabaseImporter>> importers = DatabaseImporter.getAppImporters();
String[] names = importers.keySet().toArray(new String[0]);
Dialogs.showSecureDialog(new AlertDialog.Builder(getActivity())
.setTitle(R.string.choose_application)
.setSingleChoiceItems(names, 0, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
int i = ((AlertDialog) dialog).getListView().getCheckedItemPosition();
Class<? extends DatabaseImporter> importerType = Objects.requireNonNull(importers.get(names[i]));
DatabaseImporter importer = DatabaseImporter.create(getContext(), importerType);
importApp(importer);
})
.create());
}
private void importApp(DatabaseImporter importer) {
// obtain the global root shell and close it immediately after we're done
// TODO: find a way to use SuFileInputStream with Shell.newInstance()

View file

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:checkMark="?android:attr/listChoiceIndicatorSingle">
</CheckedTextView>

View file

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingStart="25dp"
android:paddingBottom="10dp"
android:paddingEnd="25dp"
android:paddingTop="10dp">
<ListView
android:id="@+id/list_importers"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="10dp"
android:choiceMode="singleChoice"
android:divider="@null" />
<TextView
android:id="@+id/text_importer_help"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp" />
</LinearLayout>

View file

@ -329,4 +329,19 @@
<string name="panic_trigger_ignore_toast">Aegis received panic trigger but setting is disabled, ignoring</string>
<string name="pref_panic_trigger_title">Delete vault on panic trigger</string>
<string name="pref_panic_trigger_summary">Delete vault when a panic trigger is received from Ripple</string>
<string name="importer_help_aegis">Supply an Aegis export/backup file.</string>
<string name="importer_help_authenticator_plus">Supply an Authenticator Plus export file obtained through <b>Settings -> Backup &amp; Restore -> Export as Text and HTML</b>.</string>
<string name="importer_help_authy">Supply a copy of <b>/data/data/com.authy.authy/shared_prefs/com.authy.storage.tokens.authenticator.xml</b>, located in the internal storage directory of Authy.</string>
<string name="importer_help_andotp">Supply an andOTP export/backup file.</string>
<string name="importer_help_freeotp">Supply a copy of <b>/data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml</b>, located in the internal storage directory of FreeOTP.</string>
<string name="importer_help_freeotp_plus">Supply a FreeOTP+ export file.</string>
<string name="importer_help_google_authenticator">Supply a copy of <b>/data/data/com.google.android.apps.authenticator2/databases/databases</b>, located in the internal storage directory of Google Authenticator.</string>
<string name="importer_help_microsoft_authenticator">Supply a copy of <b>/data/data/com.azure.authenticator/databases/PhoneFactor</b>, located in the internal storage directory of Microsoft Authenticator.</string>
<string name="importer_help_plain_text">Supply a plain text file with a Google Authenticator URI on each line.</string>
<string name="importer_help_steam">Supply a copy of <b>/data/data/com.valvesoftware.android.steam.community/files/Steamguard-*.json</b>, located in the internal storage directory of Steam.</string>
<string name="importer_help_totp_authenticator">Supply a TOTP Authenticator export file.</string>
<string name="importer_help_winauth">Supply a WinAuth export file.</string>
<string name="importer_help_direct">Import entries directly from %s. This requires the app to be installed on this device and for root access to be granted to Aegis.</string>
</resources>