Add OTP editing/viewing (WIP), some more translation

This commit is contained in:
MrLetsplay 2023-07-01 21:26:53 +02:00
parent 8a06d5eb4d
commit 3c90ab9d15
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
14 changed files with 391 additions and 140 deletions

View File

@ -43,7 +43,7 @@ dependencies {
androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation "androidx.biometric:biometric:1.1.0" implementation "androidx.biometric:biometric:1.1.0"
implementation 'com.cringe_studios:CringeAuthenticatorLibrary:1.2' implementation 'com.cringe_studios:CringeAuthenticatorLibrary:1.4'
implementation 'com.google.mlkit:barcode-scanning:17.1.0' implementation 'com.google.mlkit:barcode-scanning:17.1.0'
implementation 'com.google.code.gson:gson:2.8.9' implementation 'com.google.code.gson:gson:2.8.9'

View File

@ -41,7 +41,7 @@ public class IntroActivity extends AppCompatActivity {
binding.videoView.setOnCompletionListener(mp -> openMainActivity()); binding.videoView.setOnCompletionListener(mp -> openMainActivity());
binding.videoView.setOnErrorListener((MediaPlayer mp, int what, int extra) -> { binding.videoView.setOnErrorListener((MediaPlayer mp, int what, int extra) -> {
Toast.makeText(this, "Failed to play video", Toast.LENGTH_LONG).show(); Toast.makeText(this, R.string.intro_video_failed, Toast.LENGTH_LONG).show();
openMainActivity(); openMainActivity();
return true; return true;
}); });

View File

@ -3,14 +3,13 @@ package com.cringe_studios.cringe_authenticator;
import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG; import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG;
import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL; import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL;
import android.content.res.Configuration;
import android.os.Bundle; import android.os.Bundle;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.WindowManager;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter; import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.Toast; import android.widget.Toast;
@ -26,20 +25,18 @@ import androidx.fragment.app.Fragment;
import com.cringe_studios.cringe_authenticator.databinding.ActivityMainBinding; import com.cringe_studios.cringe_authenticator.databinding.ActivityMainBinding;
import com.cringe_studios.cringe_authenticator.databinding.DialogInputCodeChoiceBinding; import com.cringe_studios.cringe_authenticator.databinding.DialogInputCodeChoiceBinding;
import com.cringe_studios.cringe_authenticator.databinding.DialogInputCodeHotpBinding;
import com.cringe_studios.cringe_authenticator.databinding.DialogInputCodeTotpBinding;
import com.cringe_studios.cringe_authenticator.fragment.GroupFragment; import com.cringe_studios.cringe_authenticator.fragment.GroupFragment;
import com.cringe_studios.cringe_authenticator.fragment.HomeFragment; import com.cringe_studios.cringe_authenticator.fragment.HomeFragment;
import com.cringe_studios.cringe_authenticator.fragment.MenuFragment; import com.cringe_studios.cringe_authenticator.fragment.MenuFragment;
import com.cringe_studios.cringe_authenticator.fragment.NamedFragment; import com.cringe_studios.cringe_authenticator.fragment.NamedFragment;
import com.cringe_studios.cringe_authenticator.fragment.SettingsFragment; import com.cringe_studios.cringe_authenticator.fragment.SettingsFragment;
import com.cringe_studios.cringe_authenticator.scanner.QRScannerContract; import com.cringe_studios.cringe_authenticator.scanner.QRScannerContract;
import com.cringe_studios.cringe_authenticator.util.DialogCallback; import com.cringe_studios.cringe_authenticator.util.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.NavigationUtil; import com.cringe_studios.cringe_authenticator.util.NavigationUtil;
import com.cringe_studios.cringe_authenticator.util.SettingsUtil; import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
import com.cringe_studios.cringe_authenticator_library.OTPAlgorithm;
import com.cringe_studios.cringe_authenticator_library.OTPType; import com.cringe_studios.cringe_authenticator_library.OTPType;
import java.util.Locale;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
public class MainActivity extends AppCompatActivity { public class MainActivity extends AppCompatActivity {
@ -58,7 +55,9 @@ public class MainActivity extends AppCompatActivity {
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE); setLocale("de");
//getWindow().setFlags(WindowManager.LayoutParams.FLAG_SECURE, WindowManager.LayoutParams.FLAG_SECURE);
Integer themeID = SettingsUtil.THEMES.get(SettingsUtil.getTheme(this)); Integer themeID = SettingsUtil.THEMES.get(SettingsUtil.getTheme(this));
if(themeID != null) { if(themeID != null) {
@ -85,8 +84,8 @@ public class MainActivity extends AppCompatActivity {
if(!recentlyUnlocked && SettingsUtil.isBiometricLock(this) && supportsBiometricAuth) { if(!recentlyUnlocked && SettingsUtil.isBiometricLock(this) && supportsBiometricAuth) {
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder() BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle("Cringe Authenticator") .setTitle(getString(R.string.app_name))
.setSubtitle("Unlock the authenticator") .setSubtitle(getString(R.string.biometric_lock_subtitle))
.setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) .setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)
.build(); .build();
@ -99,7 +98,7 @@ public class MainActivity extends AppCompatActivity {
if(obj == null) return; // Cancelled if(obj == null) return; // Cancelled
if(!obj.isSuccess()) { if(!obj.isSuccess()) {
Toast.makeText(this, "Failed to scan code: " + obj.getErrorMessage(), Toast.LENGTH_LONG).show(); Toast.makeText(this, getString(R.string.qr_scanner_failed, obj.getErrorMessage()), Toast.LENGTH_LONG).show();
return; return;
} }
@ -111,6 +110,14 @@ public class MainActivity extends AppCompatActivity {
}); });
} }
private void setLocale(String lang) {
Locale locale = new Locale(lang);
Locale.setDefault(locale);
Configuration config = new Configuration();
config.locale = locale;
getResources().updateConfiguration(config, getResources().getDisplayMetrics());
}
private void launchApp() { private void launchApp() {
unlocked = true; unlocked = true;
@ -188,9 +195,9 @@ public class MainActivity extends AppCompatActivity {
options[1] = OTPType.HOTP.getFriendlyName() + " (HOTP)"; options[1] = OTPType.HOTP.getFriendlyName() + " (HOTP)";
AlertDialog dialog = new AlertDialog.Builder(this) AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("Select Code Type") .setTitle(R.string.create_totp_title)
.setView(binding.getRoot()) .setView(binding.getRoot())
.setNegativeButton("Cancel", (view, which) -> {}) .setNegativeButton(R.string.cancel, (view, which) -> {})
.create(); .create();
binding.codeTypes.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, options)); binding.codeTypes.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, options));
@ -211,105 +218,31 @@ public class MainActivity extends AppCompatActivity {
} }
private void showTOTPDialog() { private void showTOTPDialog() {
DialogInputCodeTotpBinding binding = DialogInputCodeTotpBinding.inflate(getLayoutInflater()); DialogUtil.showTOTPDialog(getLayoutInflater(), null, data -> {
binding.inputAlgorithm.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, OTPAlgorithm.values()));
binding.inputDigits.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new Integer[]{6, 7, 8, 9, 10, 11, 12}));
showCodeDialog(binding.getRoot(), () -> {
Fragment fragment = NavigationUtil.getCurrentFragment(this); Fragment fragment = NavigationUtil.getCurrentFragment(this);
if(!(fragment instanceof GroupFragment)) return true; if(!(fragment instanceof GroupFragment)) return;
try { ((GroupFragment) fragment).addOTP(data);
String name = binding.inputName.getText().toString(); }, () -> inputCode(), false);
String secret = binding.inputSecret.getText().toString();
OTPAlgorithm algorithm = (OTPAlgorithm) binding.inputAlgorithm.getSelectedItem();
int digits = (int) binding.inputDigits.getSelectedItem();
int period = Integer.parseInt(binding.inputPeriod.getText().toString());
boolean checksum = binding.inputChecksum.isChecked();
OTPData data = new OTPData(name, OTPType.TOTP, secret, algorithm, digits, period, 0, checksum);
String errorMessage = data.validate();
if(errorMessage != null) {
showErrorDialog(errorMessage);
return false;
}
((GroupFragment) fragment).addOTP(data);
return true;
}catch(NumberFormatException e) {
showErrorDialog("Invalid number entered");
return false;
}
});
} }
private void showHOTPDialog() { private void showHOTPDialog() {
DialogInputCodeHotpBinding binding = DialogInputCodeHotpBinding.inflate(getLayoutInflater()); DialogUtil.showHOTPDialog(getLayoutInflater(), null, data -> {
binding.inputAlgorithm.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, OTPAlgorithm.values()));
binding.inputDigits.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, new Integer[]{6, 7, 8, 9, 10, 11, 12}));
showCodeDialog(binding.getRoot(), () -> {
Fragment fragment = NavigationUtil.getCurrentFragment(this); Fragment fragment = NavigationUtil.getCurrentFragment(this);
if(!(fragment instanceof GroupFragment)) return true; if(!(fragment instanceof GroupFragment)) return;
try { ((GroupFragment) fragment).addOTP(data);
String name = binding.inputName.getText().toString(); }, () -> inputCode(), false);
String secret = binding.inputSecret.getText().toString();
OTPAlgorithm algorithm = (OTPAlgorithm) binding.inputAlgorithm.getSelectedItem();
int digits = (int) binding.inputDigits.getSelectedItem();
int counter = Integer.parseInt(binding.inputCounter.getText().toString());
boolean checksum = binding.inputChecksum.isChecked();
OTPData data = new OTPData(name, OTPType.TOTP, secret, algorithm, digits, 0, counter, checksum);
String errorMessage = data.validate();
if(errorMessage != null) {
showErrorDialog(errorMessage);
return false;
}
((GroupFragment) fragment).addOTP(data);
return true;
}catch(NumberFormatException e) {
showErrorDialog("Invalid number entered");
return false;
}
});
}
private void showCodeDialog(View view, DialogCallback ok) {
AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("Input Code")
.setView(view)
.setPositiveButton("Ok", (btnView, which) -> {})
.setNegativeButton("Cancel", (btnView, which) -> {})
.create();
dialog.setOnShowListener(d -> {
Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
okButton.setOnClickListener(v -> {
if(ok.callback()) dialog.dismiss();
});
});
dialog.show();
}
private void showErrorDialog(String errorMessage) {
new AlertDialog.Builder(this)
.setTitle("Failed to add code")
.setMessage(errorMessage)
.setPositiveButton("Ok", (dialog, which) -> {})
.show();
} }
public void addGroup(MenuItem item) { public void addGroup(MenuItem item) {
EditText t = new EditText(this); EditText t = new EditText(this);
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle("New Group") .setTitle(R.string.action_new_group)
.setView(t) .setView(t)
.setPositiveButton("Add", (view, which) -> { .setPositiveButton(R.string.add, (view, which) -> {
if(t.getText().length() == 0) { if(t.getText().length() == 0) {
showErrorDialog("You need to input a name"); DialogUtil.showErrorDialog(this, getString(R.string.new_group_missing_title));
return; return;
} }
@ -318,7 +251,7 @@ public class MainActivity extends AppCompatActivity {
((MenuFragment) frag).addGroup(t.getText().toString()); ((MenuFragment) frag).addGroup(t.getText().toString());
} }
}) })
.setNegativeButton("Cancel", (view, which) -> {}) .setNegativeButton(R.string.cancel, (view, which) -> {})
.show(); .show();
} }

View File

@ -63,7 +63,11 @@ public class OTPData implements Serializable {
return counter; return counter;
} }
public String getPin() { public boolean hasChecksum() {
return checksum;
}
public String getPin() throws OTPException {
return getOTP().getPin(); return getOTP().getPin();
} }
@ -80,15 +84,18 @@ public class OTPData implements Serializable {
try { try {
getOTP(); getOTP();
return null; return null;
}catch(IllegalArgumentException | OTPException e) { }catch(RuntimeException e) {
return e.getMessage() != null ? e.getMessage() : e.toString(); return e.getMessage() != null ? e.getMessage() : e.toString();
} }
} }
private OTP getOTP() { private OTP getOTP() {
// TODO: checksum
if(otp != null) return otp; if(otp != null) return otp;
return otp = OTP.createNewOTP(type, secret, algorithm, digits, counter, period, checksum); try {
return otp = OTP.createNewOTP(type, secret, algorithm, digits, counter, period, checksum);
} catch (OTPException e) {
throw new RuntimeException(e.getMessage(), e);
}
} }
@NonNull @NonNull

View File

@ -9,13 +9,17 @@ import android.view.ViewGroup;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import com.cringe_studios.cringe_authenticator.OTPData; import com.cringe_studios.cringe_authenticator.OTPData;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.databinding.FragmentGroupBinding; import com.cringe_studios.cringe_authenticator.databinding.FragmentGroupBinding;
import com.cringe_studios.cringe_authenticator.otplist.OTPListAdapter; import com.cringe_studios.cringe_authenticator.otplist.OTPListAdapter;
import com.cringe_studios.cringe_authenticator.otplist.OTPListItem; import com.cringe_studios.cringe_authenticator.otplist.OTPListItem;
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.FabUtil; import com.cringe_studios.cringe_authenticator.util.FabUtil;
import com.cringe_studios.cringe_authenticator.util.SettingsUtil; import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
import com.cringe_studios.cringe_authenticator_library.OTPException;
import com.cringe_studios.cringe_authenticator_library.OTPType; import com.cringe_studios.cringe_authenticator_library.OTPType;
import java.util.List; import java.util.List;
@ -53,7 +57,8 @@ public class GroupFragment extends NamedFragment {
FabUtil.showFabs(requireActivity()); FabUtil.showFabs(requireActivity());
otpListAdapter = new OTPListAdapter(getContext()); otpListAdapter = new OTPListAdapter(requireContext(), data -> showOTPDialog(data));
binding.itemList.setAdapter(otpListAdapter); binding.itemList.setAdapter(otpListAdapter);
loadOTPs(); loadOTPs();
@ -69,6 +74,50 @@ public class GroupFragment extends NamedFragment {
return binding.getRoot(); return binding.getRoot();
} }
private void showOTPDialog(OTPData data) {
new AlertDialog.Builder(requireContext())
.setTitle(R.string.edit_otp_title)
.setItems(R.array.view_edit_delete, (dialog, which) -> {
switch(which) {
case 0:
DialogUtil.showViewCodeDialog(getLayoutInflater(), data, newData -> {
otpListAdapter.replace(data, newData);
saveOTPs();
}, () -> showOTPDialog(data));
break;
case 1:
DialogUtil.showEditCodeDialog(getLayoutInflater(), data, newData -> {
otpListAdapter.replace(data, newData);
saveOTPs();
}, () -> showOTPDialog(data));
break;
case 2: break;
}
})
.setNegativeButton(R.string.cancel, (dialog, which) -> {})
.show();
/*switch(data.getType()) {
case HOTP:
DialogUtil.showHOTPDialog(getLayoutInflater(), data, newData -> {
otpListAdapter.replace(data, newData);
saveOTPs();
});
break;
case TOTP:
DialogUtil.showTOTPDialog(getLayoutInflater(), data, newData -> {
otpListAdapter.replace(data, newData);
saveOTPs();
});
break;
}*/
}
private void saveOTPs() {
SettingsUtil.updateOTPs(requireContext(), groupName, otpListAdapter.getItems());
refreshCodes();
}
private void loadOTPs() { private void loadOTPs() {
List<OTPData> data = SettingsUtil.getOTPs(requireContext(), groupName); List<OTPData> data = SettingsUtil.getOTPs(requireContext(), groupName);
@ -86,7 +135,11 @@ public class GroupFragment extends NamedFragment {
for(int i = 0; i < binding.itemList.getChildCount(); i++) { for(int i = 0; i < binding.itemList.getChildCount(); i++) {
OTPListItem vh = (OTPListItem) binding.itemList.findViewHolderForAdapterPosition(i); OTPListItem vh = (OTPListItem) binding.itemList.findViewHolderForAdapterPosition(i);
if(vh == null) continue; if(vh == null) continue;
vh.getBinding().otpCode.setText(vh.getOTPData().getPin()); try {
vh.getBinding().otpCode.setText(vh.getOTPData().getPin());
} catch (OTPException e) {
DialogUtil.showErrorDialog(requireContext(), e.getMessage() == null ? "An error occurred while refreshing the code" : e.getMessage());
}
if(vh.getOTPData().getType() == OTPType.TOTP) { if(vh.getOTPData().getType() == OTPType.TOTP) {
long timeDiff = vh.getOTPData().getNextDueTime() - System.currentTimeMillis() / 1000; long timeDiff = vh.getOTPData().getNextDueTime() - System.currentTimeMillis() / 1000;

View File

@ -1,7 +1,6 @@
package com.cringe_studios.cringe_authenticator.fragment; package com.cringe_studios.cringe_authenticator.fragment;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -54,7 +53,6 @@ public class MenuFragment extends NamedFragment {
private void loadGroups() { private void loadGroups() {
List<String> items = SettingsUtil.getGroups(requireContext()); List<String> items = SettingsUtil.getGroups(requireContext());
Log.i("AMOGUS", "items: " + items);
for(String item : items) { for(String item : items) {
groupListAdapter.add(item); groupListAdapter.add(item);

View File

@ -11,6 +11,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.core.util.Consumer; import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.databinding.MenuItemBinding; import com.cringe_studios.cringe_authenticator.databinding.MenuItemBinding;
import java.util.ArrayList; import java.util.ArrayList;
@ -55,10 +56,10 @@ public class GroupListAdapter extends RecyclerView.Adapter<GroupListItem> {
holder.getBinding().button.setOnClickListener(view -> navigateToGroup.accept(group)); holder.getBinding().button.setOnClickListener(view -> navigateToGroup.accept(group));
holder.getBinding().button.setOnLongClickListener(view -> { holder.getBinding().button.setOnLongClickListener(view -> {
new AlertDialog.Builder(context) new AlertDialog.Builder(context)
.setTitle("Delete?") .setTitle(R.string.group_delete_title)
.setMessage("Delete this?") .setMessage(R.string.group_delete_message)
.setPositiveButton("Yes", (dialog, which) -> removeGroup.accept(group)) .setPositiveButton(R.string.yes, (dialog, which) -> removeGroup.accept(group))
.setNegativeButton("No", (dialog, which) -> {}) .setNegativeButton(R.string.no, (dialog, which) -> {})
.show(); .show();
// TODO: better method? // TODO: better method?
return true; return true;

View File

@ -9,10 +9,13 @@ import android.view.ViewGroup;
import android.widget.Toast; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.core.util.Consumer;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
import com.cringe_studios.cringe_authenticator.OTPData; import com.cringe_studios.cringe_authenticator.OTPData;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.databinding.OtpCodeBinding; import com.cringe_studios.cringe_authenticator.databinding.OtpCodeBinding;
import com.cringe_studios.cringe_authenticator_library.OTPException;
import com.cringe_studios.cringe_authenticator_library.OTPType; import com.cringe_studios.cringe_authenticator_library.OTPType;
import java.util.ArrayList; import java.util.ArrayList;
@ -26,10 +29,13 @@ public class OTPListAdapter extends RecyclerView.Adapter<OTPListItem> {
private Handler handler; private Handler handler;
public OTPListAdapter(Context context) { private Consumer<OTPData> showMenuCallback;
public OTPListAdapter(Context context, Consumer<OTPData> showMenuCallback) {
this.inflater = LayoutInflater.from(context); this.inflater = LayoutInflater.from(context);
this.items = new ArrayList<>(); this.items = new ArrayList<>();
this.handler = new Handler(Looper.getMainLooper()); this.handler = new Handler(Looper.getMainLooper());
this.showMenuCallback = showMenuCallback;
} }
@NonNull @NonNull
@ -52,11 +58,23 @@ public class OTPListAdapter extends RecyclerView.Adapter<OTPListItem> {
// Click delay for HOTP // Click delay for HOTP
view.setClickable(false); view.setClickable(false);
Toast.makeText(view.getContext(), "Generated new code", Toast.LENGTH_LONG).show(); Toast.makeText(view.getContext(), R.string.hotp_generated_new_code, Toast.LENGTH_SHORT).show();
data.incrementCounter(); data.incrementCounter();
holder.getBinding().otpCode.setText(data.getPin());
try {
holder.getBinding().otpCode.setText(data.getPin());
}catch(OTPException e) {
// TODO: show user an error message
return;
}
handler.postDelayed(() -> view.setClickable(true), 5000); handler.postDelayed(() -> view.setClickable(true), 5000);
}); });
holder.getBinding().getRoot().setOnLongClickListener(view -> {
showMenuCallback.accept(holder.getOTPData());
return true;
});
} }
@Override @Override
@ -64,11 +82,22 @@ public class OTPListAdapter extends RecyclerView.Adapter<OTPListItem> {
return items.size(); return items.size();
} }
public List<OTPData> getItems() {
return items;
}
public void add(OTPData data) { public void add(OTPData data) {
items.add(data); items.add(data);
notifyItemInserted(items.size() - 1); notifyItemInserted(items.size() - 1);
} }
public void replace(OTPData oldData, OTPData newData) {
int index = items.indexOf(oldData);
if(index == -1) return;
items.set(index, newData);
notifyItemChanged(index);
}
public void remove(OTPData data) { public void remove(OTPData data) {
int index = items.indexOf(data); int index = items.indexOf(data);
if(index == -1) return; if(index == -1) return;

View File

@ -9,6 +9,7 @@ import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatActivity;
import com.cringe_studios.cringe_authenticator.OTPData; import com.cringe_studios.cringe_authenticator.OTPData;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.util.OTPParser; import com.cringe_studios.cringe_authenticator.util.OTPParser;
public class URIHandlerActivity extends AppCompatActivity { public class URIHandlerActivity extends AppCompatActivity {
@ -30,9 +31,9 @@ public class URIHandlerActivity extends AppCompatActivity {
finish(); finish();
}catch(IllegalArgumentException e) { }catch(IllegalArgumentException e) {
AlertDialog dialog = new AlertDialog.Builder(this) AlertDialog dialog = new AlertDialog.Builder(this)
.setTitle("Failed to add code") .setTitle(R.string.uri_handler_failed_title)
.setMessage(e.getMessage()) .setMessage(e.getMessage())
.setPositiveButton("Ok", (d, which) -> finish()) .setPositiveButton(R.string.ok, (d, which) -> finish())
.create(); .create();
dialog.setOnDismissListener(d -> finish()); dialog.setOnDismissListener(d -> finish());

View File

@ -0,0 +1,175 @@
package com.cringe_studios.cringe_authenticator.util;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.core.util.Consumer;
import com.cringe_studios.cringe_authenticator.OTPData;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.databinding.DialogInputCodeHotpBinding;
import com.cringe_studios.cringe_authenticator.databinding.DialogInputCodeTotpBinding;
import com.cringe_studios.cringe_authenticator_library.OTPAlgorithm;
import com.cringe_studios.cringe_authenticator_library.OTPType;
import java.util.Arrays;
public class DialogUtil {
private static final Integer[] DIGITS = new Integer[]{6, 7, 8, 9, 10, 11, 12};
private static void showCodeDialog(Context context, View view, DialogCallback ok, Runnable back) {
AlertDialog dialog = new AlertDialog.Builder(context)
.setTitle(R.string.code_input_title)
.setView(view)
.setPositiveButton(R.string.ok, (btnView, which) -> {})
.setNeutralButton(R.string.back, (btnView, which) -> back.run())
.setNegativeButton(R.string.cancel, (btnView, which) -> {})
.create();
//dialog.getWindow().setBackgroundDrawableResource(R.drawable.button_themed); TODO: dialog style
dialog.setOnShowListener(d -> {
Button okButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
okButton.setOnClickListener(v -> {
if(ok.callback()) dialog.dismiss();
});
});
dialog.show();
}
public static void showErrorDialog(Context context, String errorMessage) {
new AlertDialog.Builder(context)
.setTitle(R.string.failed_title)
.setMessage(errorMessage)
.setPositiveButton(R.string.ok, (dialog, which) -> {})
.show();
}
public static void showTOTPDialog(LayoutInflater inflater, OTPData initialData, Consumer<OTPData> callback, Runnable back, boolean view) {
Context context = inflater.getContext();
DialogInputCodeTotpBinding binding = DialogInputCodeTotpBinding.inflate(inflater);
binding.inputAlgorithm.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, OTPAlgorithm.values()));
binding.inputAlgorithm.setEnabled(!view);
binding.inputDigits.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, DIGITS));
binding.inputDigits.setEnabled(!view);
binding.inputName.setEnabled(!view);
binding.inputSecret.setEnabled(!view);
binding.inputPeriod.setEnabled(!view);
binding.inputChecksum.setEnabled(!view);
if(initialData != null) {
binding.inputName.setText(initialData.getName());
binding.inputSecret.setText(initialData.getSecret());
binding.inputAlgorithm.setSelection(initialData.getAlgorithm().ordinal());
int index = Arrays.asList(DIGITS).indexOf(initialData.getDigits());
if(index != -1) binding.inputDigits.setSelection(index);
binding.inputPeriod.setText(String.valueOf(initialData.getPeriod()));
binding.inputChecksum.setChecked(initialData.hasChecksum());
}
showCodeDialog(context, binding.getRoot(), () -> {
try {
String name = binding.inputName.getText().toString();
String secret = binding.inputSecret.getText().toString();
OTPAlgorithm algorithm = (OTPAlgorithm) binding.inputAlgorithm.getSelectedItem();
int digits = (int) binding.inputDigits.getSelectedItem();
int period = Integer.parseInt(binding.inputPeriod.getText().toString());
boolean checksum = binding.inputChecksum.isChecked();
OTPData data = new OTPData(name, OTPType.TOTP, secret, algorithm, digits, period, 0, checksum);
String errorMessage = data.validate();
if(errorMessage != null) {
showErrorDialog(context, errorMessage);
return false;
}
callback.accept(data);
return true;
}catch(NumberFormatException e) {
showErrorDialog(context, context.getString(R.string.input_code_invalid_number));
return false;
}
}, back);
}
public static void showHOTPDialog(LayoutInflater inflater, OTPData initialData, Consumer<OTPData> callback, Runnable back, boolean view) {
Context context = inflater.getContext();
DialogInputCodeHotpBinding binding = DialogInputCodeHotpBinding.inflate(inflater);
binding.inputAlgorithm.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, OTPAlgorithm.values()));
binding.inputAlgorithm.setEnabled(!view);
binding.inputDigits.setAdapter(new ArrayAdapter<>(context, android.R.layout.simple_list_item_1, DIGITS));
binding.inputDigits.setEnabled(!view);
binding.inputName.setEnabled(!view);
binding.inputSecret.setEnabled(!view);
binding.inputCounter.setEnabled(!view);
binding.inputChecksum.setEnabled(!view);
if(initialData != null) {
binding.inputName.setText(initialData.getName());
binding.inputSecret.setText(initialData.getSecret());
binding.inputAlgorithm.setSelection(initialData.getAlgorithm().ordinal());
int index = Arrays.asList(DIGITS).indexOf(initialData.getDigits());
if(index != -1) binding.inputDigits.setSelection(index);
binding.inputCounter.setText(String.valueOf(initialData.getCounter()));
binding.inputChecksum.setChecked(initialData.hasChecksum());
}
showCodeDialog(context, binding.getRoot(), () -> {
try {
String name = binding.inputName.getText().toString();
String secret = binding.inputSecret.getText().toString();
OTPAlgorithm algorithm = (OTPAlgorithm) binding.inputAlgorithm.getSelectedItem();
int digits = (int) binding.inputDigits.getSelectedItem();
int counter = Integer.parseInt(binding.inputCounter.getText().toString());
boolean checksum = binding.inputChecksum.isChecked();
OTPData data = new OTPData(name, OTPType.TOTP, secret, algorithm, digits, 0, counter, checksum);
String errorMessage = data.validate();
if(errorMessage != null) {
showErrorDialog(context, errorMessage);
return false;
}
callback.accept(data);
return true;
}catch(NumberFormatException e) {
showErrorDialog(context, context.getString(R.string.input_code_invalid_number));
return false;
}
}, back);
}
public static void showViewCodeDialog(LayoutInflater inflater, @NonNull OTPData initialData, Consumer<OTPData> callback, Runnable back) {
switch(initialData.getType()) {
case HOTP: showHOTPDialog(inflater, initialData, callback, back, true); break;
case TOTP: showTOTPDialog(inflater, initialData, callback, back, true); break;
}
}
public static void showEditCodeDialog(LayoutInflater inflater, @NonNull OTPData initialData, Consumer<OTPData> callback, Runnable back) {
switch(initialData.getType()) {
case HOTP: showHOTPDialog(inflater, initialData, callback, back, false); break;
case TOTP: showTOTPDialog(inflater, initialData, callback, back, false); break;
}
}
}

View File

@ -76,6 +76,12 @@ public class SettingsUtil {
.apply(); .apply();
} }
public static void updateOTPs(Context ctx, String group, List<OTPData> otps) {
ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit()
.putString("group." + group, GSON.toJson(otps.toArray(new OTPData[0])))
.apply();
}
private static void deleteOTPs(Context ctx, String group) { private static void deleteOTPs(Context ctx, String group) {
ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit() ctx.getSharedPreferences(GROUPS_PREFS_NAME, Context.MODE_PRIVATE).edit()
.remove("group." + group) .remove("group." + group)

View File

@ -3,15 +3,15 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
tools:context="com.cringe_studios.cringe_authenticator.MainActivity"> tools:context="com.cringe_studios.cringe_authenticator.MainActivity">
<item <item
android:id="@+id/action_code" android:id="@+id/action_new_group"
android:orderInCategory="100" android:orderInCategory="100"
android:title="New Group" android:title="@string/action_new_group"
app:showAsAction="never" app:showAsAction="never"
android:onClick="addGroup" /> android:onClick="addGroup" />
<item <item
android:id="@+id/action_settings" android:id="@+id/action_settings"
android:orderInCategory="100" android:orderInCategory="100"
android:title="Settings" android:title="@string/action_settings"
app:showAsAction="never" app:showAsAction="never"
android:onClick="openSettings" /> android:onClick="openSettings" />
</menu> </menu>

View File

@ -4,16 +4,40 @@
<string name="action_settings">Einstellungen</string> <string name="action_settings">Einstellungen</string>
<string name="next">Weiter</string> <string name="next">Weiter</string>
<string name="previous">Zurück</string> <string name="previous">Zurück</string>
<string name="edit">Editieren</string> <string name="edit">Bearbeiten</string>
<string name="choose_language">Sprachauswahl</string> <string name="choose_language">Sprachauswahl</string>
<string name="cancel">Abbrechen</string> <string name="cancel">Abbrechen</string>
<string name="add">hinzufügen</string> <string name="add">Hinzufügen</string>
<string name="no">nein</string> <string name="no">Nein</string>
<string name="yes">ja</string> <string name="yes">Ja</string>
<string name="invalid_input">ungültige Eingabe</string> <string name="invalid_input">Ungültige Eingabe</string>
<string name="about">über</string> <string name="about">Über</string>
<string name="haptic_feedback">vibration</string> <string name="haptic_feedback">Vibration</string>
<string name="reset_app">App zurücksetzen</string> <string name="reset_app">App zurücksetzen</string>
<string name="import_export">importieren / exportieren</string> <string name="import_export">Importieren / Exportieren</string>
<string name="language">Sprache</string> <string name="language">Sprache</string>
<string name="biometric_lock_subtitle">Entsperre den Authentifikator</string>
<string name="create_totp_title">Wähle den Code-Typ</string>
<string name="action_new_group">Neue Gruppe</string>
<string name="new_group_missing_title">Du musst einen Namen eingeben</string>
<string name="qr_scanner_failed">Scannen des Codes fehlgeschlagen: %s</string>
<string name="intro_video_failed">Abspielen des Videos fehlgeschlagen</string>
<string name="edit_otp_title">OTP bearbeiten</string>
<string name="group_delete_title">Löschen?</string>
<string name="group_delete_message">Gruppe löschen?</string>
<string name="hotp_generated_new_code">Neuen Code generiert</string>
<string name="uri_handler_failed_title">Hinzufügen des Codes fehlgeschlagen</string>
<string name="code_input_title">Code eingeben</string>
<string name="failed_title">Aktion fehlgeschlagen</string>
<string name="input_code_invalid_number">Ungültige Zahl</string>
<string name="back">Zurück</string>
<string-array name="view_edit_delete">
<item>Anzeigen</item>
<item>Bearbeiten</item>
<item>Löschen</item>
</string-array>
<string-array name="rename_delete">
<item>Umbenennen</item>
<item>Löschen</item>
</string-array>
</resources> </resources>

View File

@ -44,16 +44,40 @@
vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim. vestibulum. Fusce dictum libero quis erat maximus, vitae volutpat diam dignissim.
</string> </string>
<string name="edit">Edit</string> <string name="edit">Edit</string>
<string name="choose_language">choose Language</string> <string name="choose_language">Choose Language</string>
<string name="cancel">cancel</string> <string name="cancel">Cancel</string>
<string name="add">add</string> <string name="add">Add</string>
<string name="ok" translatable="false">Ok</string> <string name="ok" translatable="false">OK</string>
<string name="no">no</string> <string name="no">No</string>
<string name="yes">yes</string> <string name="yes">Yes</string>
<string name="invalid_input">invalid input</string> <string name="invalid_input">Invalid Input</string>
<string name="about">about</string> <string name="about">About</string>
<string name="haptic_feedback">haptic feedback</string> <string name="haptic_feedback">Haptic Feedback</string>
<string name="reset_app">reset app</string> <string name="reset_app">Reset App</string>
<string name="import_export">import / export</string> <string name="import_export">Import / Export</string>
<string name="language">language</string> <string name="language">Language</string>
<string name="biometric_lock_subtitle">Unlock the authenticator</string>
<string name="create_totp_title">Select Code Type</string>
<string name="action_new_group">New Group</string>
<string name="new_group_missing_title">You need to input a name</string>
<string name="qr_scanner_failed">Failed to scan code: %s</string>
<string name="intro_video_failed">Failed to play video</string>
<string name="edit_otp_title">Edit OTP</string>
<string name="group_delete_title">Delete?</string>
<string name="group_delete_message">Delete this?</string>
<string name="hotp_generated_new_code">Generated new code</string>
<string name="uri_handler_failed_title">Failed to add code</string>
<string name="code_input_title">Input Code</string>
<string name="failed_title">Action failed</string>
<string name="input_code_invalid_number">Invalid number entered</string>
<string name="back">Back</string>
<string-array name="view_edit_delete">
<item>View</item>
<item>Edit</item>
<item>Delete</item>
</string-array>
<string-array name="rename_delete">
<item>Rename</item>
<item>Delete</item>
</string-array>
</resources> </resources>