Unlock UI

This commit is contained in:
MrLetsplay 2023-09-18 16:56:34 +02:00
parent 3981e525eb
commit 2733834df0
Signed by: mr
SSH Key Fingerprint: SHA256:92jBH80vpXyaZHjaIl47pjRq+Yt7XGTArqQg1V7hSqg
19 changed files with 371 additions and 110 deletions

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="SERIAL_NUMBER" />
<value value="R38N50464FV" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2023-09-18T13:23:48.152301762Z" />
</component>
</project>

View File

@ -36,8 +36,13 @@
android:theme="@style/Theme.CringeAuthenticator.None"
android:configChanges="orientation|screenSize">
</activity>
<activity android:name=".unlock.UnlockActivity"
android:exported="false"
android:theme="@style/Theme.CringeAuthenticator.None"
android:configChanges="orientation|screenSize">
</activity>
<activity android:name=".scanner.QRScannerActivity"
android:exported="true"
android:exported="false"
android:theme="@style/Theme.CringeAuthenticator.None"
android:configChanges="orientation|screenSize">
</activity>
@ -47,7 +52,7 @@
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="otpauth" />
<data android:scheme="otpauth"/>
<data android:scheme="otpauth-migration" />
</intent-filter>
</activity>

View File

@ -0,0 +1,37 @@
package com.cringe_studios.cringe_authenticator;
import android.os.Bundle;
import android.util.Log;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import com.cringe_studios.cringe_authenticator.unlock.UnlockContract;
public class BaseActivity extends AppCompatActivity {
private ActivityResultLauncher<Void> startUnlockActivity;
private Runnable unlockSuccess, unlockFailure;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
registerCallbacks();
}
private void registerCallbacks() {
startUnlockActivity = registerForActivityResult(new UnlockContract(), success -> {
if(success && unlockSuccess != null) unlockSuccess.run();
if(!success && unlockFailure != null) unlockFailure.run();
});
}
public void promptUnlock(Runnable success, Runnable failure) {
unlockSuccess = success;
unlockFailure = failure;
startUnlockActivity.launch(null);
}
}

View File

@ -10,6 +10,7 @@ import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import com.cringe_studios.cringe_authenticator.databinding.ActivityIntroBinding;
import com.cringe_studios.cringe_authenticator.unlock.UnlockActivity;
import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
public class IntroActivity extends AppCompatActivity {
@ -50,7 +51,7 @@ public class IntroActivity extends AppCompatActivity {
}
public void openMainActivity() {
Intent m = new Intent(getApplicationContext(), MainActivity.class);
Intent m = new Intent(getApplicationContext(), UnlockActivity.class);
m.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(m);
finish();

View File

@ -1,12 +1,7 @@
package com.cringe_studios.cringe_authenticator;
import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG;
import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL;
import android.content.res.Configuration;
import android.os.Bundle;
import android.security.keystore.KeyProtection;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
@ -19,13 +14,8 @@ import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import com.cringe_studios.cringe_authenticator.crypto.Crypto;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
import com.cringe_studios.cringe_authenticator.databinding.ActivityMainBinding;
import com.cringe_studios.cringe_authenticator.databinding.DialogInputCodeChoiceBinding;
import com.cringe_studios.cringe_authenticator.fragment.AboutFragment;
@ -44,25 +34,9 @@ import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
import com.cringe_studios.cringe_authenticator_library.OTPType;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.util.Arrays;
import java.util.Locale;
import java.util.concurrent.Executor;
import javax.crypto.SecretKey;
public class MainActivity extends AppCompatActivity {
private static final long LOCK_TIMEOUT = 10000;
public class MainActivity extends BaseActivity {
private ActivityMainBinding binding;
@ -95,37 +69,7 @@ public class MainActivity extends AppCompatActivity {
setLocale(SettingsUtil.getLocale(this));
OTPDatabase.promptLoadDatabase(this, () -> {
launchApp();
}, () -> finishAffinity());
/*Executor executor = ContextCompat.getMainExecutor(this);
BiometricPrompt prompt = new BiometricPrompt(this, executor, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
finishAffinity();
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
launchApp();
}
});
boolean supportsBiometricAuth = BiometricManager.from(this).canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS;
boolean recentlyUnlocked = savedInstanceState != null && (System.currentTimeMillis() - savedInstanceState.getLong("pauseTime", 0L) < LOCK_TIMEOUT);
if(!recentlyUnlocked && SettingsUtil.isBiometricLock(this) && supportsBiometricAuth) {
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.app_name))
.setSubtitle(getString(R.string.biometric_lock_subtitle))
.setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)
.build();
prompt.authenticate(info);
}else {
launchApp();
}*/
OTPDatabase.promptLoadDatabase(this, this::launchApp, this::finishAffinity);
startQRCodeScan = registerForActivityResult(new QRScannerContract(), obj -> {
if(obj == null) return; // Cancelled
@ -160,7 +104,7 @@ public class MainActivity extends AppCompatActivity {
binding.fabMenu.setOnClickListener(view -> NavigationUtil.navigate(this, MenuFragment.class, null));
binding.fabScan.setOnClickListener(view -> scanCode());
binding.fabScanImage.setOnClickListener(view -> scanCode());
//binding.fabScanImage.setOnClickListener(view -> scanCode()); TODO: scan image
binding.fabInput.setOnClickListener(view -> inputCode());
Fragment fragment = NavigationUtil.getCurrentFragment(this);

View File

@ -93,11 +93,16 @@ public class GroupFragment extends NamedFragment {
break;
case 2:
DialogUtil.showChooseGroupDialog(requireContext(), group -> {
OTPDatabase.promptLoadDatabase(requireContext(), () -> {
OTPDatabase.getLoadedDatabase().addOTP(group, data);
// TODO: save
otpListAdapter.remove(data);
}, () -> DialogUtil.showErrorDialog(requireContext(), "Failed to add OTP"));
OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
try {
OTPDatabase.getLoadedDatabase().addOTP(group, data);
OTPDatabase.saveDatabase(requireContext(), SettingsUtil.getCryptoParameters(requireContext()));
otpListAdapter.remove(data);
saveOTPs();
} catch (OTPDatabaseException | CryptoException e) {
DialogUtil.showErrorDialog(requireContext(), e.toString());
}
}, null);
saveOTPs();
}, null);
break;
@ -119,33 +124,37 @@ public class GroupFragment extends NamedFragment {
}
private void saveOTPs() {
//SettingsUtil.updateOTPs(requireContext(), groupID, otpListAdapter.getItems());
if(OTPDatabase.getLoadedDatabase() == null) {
// TODO: prompt user
return;
}
OTPDatabase.getLoadedDatabase().updateOTPs(groupID, otpListAdapter.getItems());
try {
OTPDatabase.saveDatabase(requireContext(), SettingsUtil.getCryptoParameters(requireContext()));
} catch (OTPDatabaseException | CryptoException e) {
DialogUtil.showErrorDialog(requireContext(), e.toString());
}
refreshCodes();
OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
try {
OTPDatabase.getLoadedDatabase().updateOTPs(groupID, otpListAdapter.getItems());
OTPDatabase.saveDatabase(requireContext(), SettingsUtil.getCryptoParameters(requireContext()));
refreshCodes();
} catch (OTPDatabaseException | CryptoException e) {
DialogUtil.showErrorDialog(requireContext(), e.toString());
}
}, null);
}
private void loadOTPs() {
/*List<OTPData> data = SettingsUtil.getOTPs(requireContext(), groupID); TODO
OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
List<OTPData> data = OTPDatabase.getLoadedDatabase().getOTPs(groupID);
for(OTPData otp : data) {
otpListAdapter.add(otp);
}*/
for(OTPData otp : data) {
otpListAdapter.add(otp);
}
}, null);
}
public void addOTP(OTPData data) {
//SettingsUtil.addOTP(requireContext(), groupID, data); TODO
otpListAdapter.add(data);
OTPDatabase.promptLoadDatabase(requireActivity(), () -> {
try {
OTPDatabase.getLoadedDatabase().addOTP(groupID, data);
OTPDatabase.saveDatabase(requireContext(), SettingsUtil.getCryptoParameters(requireContext()));
otpListAdapter.add(data);
} catch (OTPDatabaseException | CryptoException e) {
DialogUtil.showErrorDialog(requireContext(), "Failed to save database: " + e);
}
}, null);
}
public void refreshCodes() {

View File

@ -71,6 +71,7 @@ public class SettingsFragment extends NamedFragment {
}
});
binding.settingsEnableEncryption.setChecked(SettingsUtil.isDatabaseEncrypted(requireContext()));
binding.settingsEnableEncryption.setOnCheckedChangeListener((view, checked) -> {
if(!OTPDatabase.isDatabaseLoaded()) {
// TODO: prompt user

View File

@ -3,6 +3,7 @@ package com.cringe_studios.cringe_authenticator.otplist;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -58,6 +59,7 @@ public class OTPListAdapter extends RecyclerView.Adapter<OTPListItem> {
holder.getBinding().progress.setVisibility(data.getType() == OTPType.TOTP ? View.VISIBLE : View.INVISIBLE);
holder.getBinding().getRoot().setOnClickListener(view -> {
Log.i("CLICKED", "CLICKED: " + view.isClickable());
if(data.getType() != OTPType.HOTP) return;
// Click delay for HOTP

View File

@ -0,0 +1,123 @@
package com.cringe_studios.cringe_authenticator.unlock;
import static androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG;
import static androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL;
import android.content.Intent;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
import androidx.biometric.BiometricManager;
import androidx.biometric.BiometricPrompt;
import androidx.core.content.ContextCompat;
import com.cringe_studios.cringe_authenticator.MainActivity;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.crypto.Crypto;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
import com.cringe_studios.cringe_authenticator.databinding.ActivityUnlockBinding;
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.OTPDatabase;
import com.cringe_studios.cringe_authenticator.util.OTPDatabaseException;
import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
import java.util.concurrent.Executor;
import javax.crypto.SecretKey;
public class UnlockActivity extends AppCompatActivity {
private static final long LOCK_TIMEOUT = 10000;
private ActivityUnlockBinding binding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ThemeUtil.loadTheme(this);
if(!SettingsUtil.isDatabaseEncrypted(this)) {
launchApp();
return;
}
binding = ActivityUnlockBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
if(SettingsUtil.isBiometricLock(this)) {
Executor executor = ContextCompat.getMainExecutor(this);
BiometricPrompt prompt = new BiometricPrompt(this, executor, new BiometricPrompt.AuthenticationCallback() {
@Override
public void onAuthenticationError(int errorCode, @NonNull CharSequence errString) {
//finishAffinity();
}
@Override
public void onAuthenticationSucceeded(@NonNull BiometricPrompt.AuthenticationResult result) {
launchApp();
}
});
boolean supportsBiometricAuth = BiometricManager.from(this).canAuthenticate(BIOMETRIC_STRONG | DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS;
boolean recentlyUnlocked = savedInstanceState != null && (System.currentTimeMillis() - savedInstanceState.getLong("pauseTime", 0L) < LOCK_TIMEOUT);
if(!recentlyUnlocked && SettingsUtil.isBiometricLock(this) && supportsBiometricAuth) {
BiometricPrompt.PromptInfo info = new BiometricPrompt.PromptInfo.Builder()
.setTitle(getString(R.string.app_name))
.setSubtitle(getString(R.string.biometric_lock_subtitle))
.setAllowedAuthenticators(BIOMETRIC_STRONG | DEVICE_CREDENTIAL)
.build();
//prompt.authenticate(info);
binding.unlockBiometrics.setOnClickListener(view -> {
//prompt.authenticate(info);
});
}else {
launchApp();
}
}
binding.unlockButton.setOnClickListener(view -> {
if(binding.unlockPassword.getText().length() == 0) {
DialogUtil.showErrorDialog(this, "You need to enter a password");
return;
}
String password = binding.unlockPassword.getText().toString();
try {
SecretKey key = Crypto.generateKey(SettingsUtil.getCryptoParameters(this), password);
OTPDatabase.loadDatabase(this, key);
launchApp();
}catch(CryptoException e) {
DialogUtil.showErrorDialog(this, "Failed to load database: Invalid password or database corrupted", this::failure);
} catch (OTPDatabaseException e) {
DialogUtil.showErrorDialog(this, "Failed to load database: " + e, this::failure);
}
});
}
private void launchApp() {
if(getIntent() != null && getIntent().hasExtra("contract")) {
setResult(RESULT_OK);
finish();
return;
}
Intent m = new Intent(getApplicationContext(), MainActivity.class);
m.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(m);
finish();
}
private void failure() {
setResult(RESULT_CANCELED);
finish();
}
}

View File

@ -0,0 +1,25 @@
package com.cringe_studios.cringe_authenticator.unlock;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.cringe_studios.cringe_authenticator.scanner.QRScannerActivity;
import com.cringe_studios.cringe_authenticator.scanner.ScannerResult;
public class UnlockContract extends ActivityResultContract<Void, Boolean> {
@NonNull
@Override
public Intent createIntent(@NonNull Context context, Void unused) {
return new Intent(context, UnlockActivity.class).putExtra("contract", true);
}
@Override
public Boolean parseResult(int i, @Nullable Intent intent) {
return i == Activity.RESULT_OK;
}
}

View File

@ -3,15 +3,18 @@ package com.cringe_studios.cringe_authenticator.urihandler;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.widget.Toast;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import com.cringe_studios.cringe_authenticator.BaseActivity;
import com.cringe_studios.cringe_authenticator.R;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
import com.cringe_studios.cringe_authenticator.model.OTPData;
import com.cringe_studios.cringe_authenticator.unlock.UnlockContract;
import com.cringe_studios.cringe_authenticator.util.DialogUtil;
import com.cringe_studios.cringe_authenticator.util.OTPDatabase;
import com.cringe_studios.cringe_authenticator.util.OTPDatabaseException;
@ -20,7 +23,7 @@ import com.cringe_studios.cringe_authenticator.util.SettingsUtil;
import com.cringe_studios.cringe_authenticator.util.StyledDialogBuilder;
import com.cringe_studios.cringe_authenticator.util.ThemeUtil;
public class URIHandlerActivity extends AppCompatActivity {
public class URIHandlerActivity extends BaseActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
@ -63,19 +66,19 @@ public class URIHandlerActivity extends AppCompatActivity {
}
private void importCodes(OTPData... data) {
DialogUtil.showChooseGroupDialog(this, group -> {
for(OTPData d : data) {
OTPDatabase.promptLoadDatabase(this, () -> {
OTPDatabase.promptLoadDatabase(this, () -> {
DialogUtil.showChooseGroupDialog(this, group -> {
for(OTPData d : data) {
OTPDatabase.getLoadedDatabase().addOTP(group, d);
try {
OTPDatabase.saveDatabase(this, SettingsUtil.getCryptoParameters(this));
} catch (OTPDatabaseException | CryptoException e) {
DialogUtil.showErrorDialog(this, e.toString());
}
}, () -> finishAffinity());
}
Toast.makeText(this, R.string.uri_handler_code_added, Toast.LENGTH_SHORT).show();
}, this::finishAndRemoveTask);
}
Toast.makeText(this, R.string.uri_handler_code_added, Toast.LENGTH_SHORT).show();
}, this::finishAndRemoveTask);
}, null);
}
}

View File

@ -9,18 +9,18 @@ import com.google.android.material.floatingactionbutton.FloatingActionButton;
public class FabUtil {
public static void showFabs(Activity activity) {
FloatingActionButton fabScan = activity.findViewById(R.id.fab_scan);
if(fabScan != null) {
fabScan.setVisibility(View.VISIBLE);
fabScan.setClickable(true);
fabScan.animate().translationX(-activity.getResources().getDimension(R.dimen.fab1_offset));
}
FloatingActionButton fabScanImage = activity.findViewById(R.id.fab_scan_image);
if(fabScanImage != null) {
fabScanImage.setVisibility(View.VISIBLE);
fabScanImage.setClickable(true);
fabScanImage.animate().translationX(-activity.getResources().getDimension(R.dimen.fab2_offset));
fabScanImage.animate().translationX(-activity.getResources().getDimension(R.dimen.fab1_offset));
}
FloatingActionButton fabScan = activity.findViewById(R.id.fab_scan);
if(fabScan != null) {
fabScan.setVisibility(View.VISIBLE);
fabScan.setClickable(true);
fabScan.animate().translationX(-activity.getResources().getDimension(R.dimen.fab2_offset));
}
FloatingActionButton fabInput = activity.findViewById(R.id.fab_input);

View File

@ -1,13 +1,18 @@
package com.cringe_studios.cringe_authenticator.util;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.util.Consumer;
import com.cringe_studios.cringe_authenticator.BaseActivity;
import com.cringe_studios.cringe_authenticator.crypto.Crypto;
import com.cringe_studios.cringe_authenticator.crypto.CryptoException;
import com.cringe_studios.cringe_authenticator.crypto.CryptoParameters;
import com.cringe_studios.cringe_authenticator.model.OTPData;
import com.cringe_studios.cringe_authenticator.unlock.UnlockContract;
import com.google.gson.JsonObject;
import com.google.gson.JsonSyntaxException;
@ -60,12 +65,16 @@ public class OTPDatabase {
otps.remove(groupId);
}
public static void promptLoadDatabase(Context ctx, Runnable success, Runnable failure) {
public static void promptLoadDatabase(Activity ctx, Runnable success, Runnable failure) {
if(isDatabaseLoaded()) {
success.run();
return;
}
if(!(ctx instanceof BaseActivity)) {
throw new RuntimeException("NOT BASEACTIVITY");
}
if(!SettingsUtil.isDatabaseEncrypted(ctx)) {
try {
loadDatabase(ctx, null);
@ -76,7 +85,7 @@ public class OTPDatabase {
return;
}
DialogUtil.showInputPasswordDialog(ctx, password -> {
/*DialogUtil.showInputPasswordDialog(ctx, password -> {
try {
SecretKey key = Crypto.generateKey(SettingsUtil.getCryptoParameters(ctx), password);
loadDatabase(ctx, key);
@ -86,7 +95,8 @@ public class OTPDatabase {
} catch (OTPDatabaseException e) {
DialogUtil.showErrorDialog(ctx, "Failed to load database: " + e, failure);
}
}, failure);
}, failure);*/
((BaseActivity) ctx).promptUnlock(success, failure);
}
public static OTPDatabase loadDatabase(Context context, SecretKey key) throws OTPDatabaseException, CryptoException {

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17.81,4.47C17.73,4.47 17.65,4.45 17.58,4.41C15.66,3.42 14,3 12,3C10.03,3 8.15,3.47 6.44,4.41C6.2,4.54 5.9,4.45 5.76,4.21C5.63,3.97 5.72,3.66 5.96,3.53C7.82,2.5 9.86,2 12,2C14.14,2 16,2.47 18.04,3.5C18.29,3.65 18.38,3.95 18.25,4.19C18.16,4.37 18,4.47 17.81,4.47M3.5,9.72C3.4,9.72 3.3,9.69 3.21,9.63C3,9.47 2.93,9.16 3.09,8.93C4.08,7.53 5.34,6.43 6.84,5.66C10,4.04 14,4.03 17.15,5.65C18.65,6.42 19.91,7.5 20.9,8.9C21.06,9.12 21,9.44 20.78,9.6C20.55,9.76 20.24,9.71 20.08,9.5C19.18,8.22 18.04,7.23 16.69,6.54C13.82,5.07 10.15,5.07 7.29,6.55C5.93,7.25 4.79,8.25 3.89,9.5C3.81,9.65 3.66,9.72 3.5,9.72M9.75,21.79C9.62,21.79 9.5,21.74 9.4,21.64C8.53,20.77 8.06,20.21 7.39,19C6.7,17.77 6.34,16.27 6.34,14.66C6.34,11.69 8.88,9.27 12,9.27C15.12,9.27 17.66,11.69 17.66,14.66A0.5,0.5 0,0 1,17.16 15.16A0.5,0.5 0,0 1,16.66 14.66C16.66,12.24 14.57,10.27 12,10.27C9.43,10.27 7.34,12.24 7.34,14.66C7.34,16.1 7.66,17.43 8.27,18.5C8.91,19.66 9.35,20.15 10.12,20.93C10.31,21.13 10.31,21.44 10.12,21.64C10,21.74 9.88,21.79 9.75,21.79M16.92,19.94C15.73,19.94 14.68,19.64 13.82,19.05C12.33,18.04 11.44,16.4 11.44,14.66A0.5,0.5 0,0 1,11.94 14.16A0.5,0.5 0,0 1,12.44 14.66C12.44,16.07 13.16,17.4 14.38,18.22C15.09,18.7 15.92,18.93 16.92,18.93C17.16,18.93 17.56,18.9 17.96,18.83C18.23,18.78 18.5,18.96 18.54,19.24C18.59,19.5 18.41,19.77 18.13,19.82C17.56,19.93 17.06,19.94 16.92,19.94M14.91,22C14.87,22 14.82,22 14.78,22C13.19,21.54 12.15,20.95 11.06,19.88C9.66,18.5 8.89,16.64 8.89,14.66C8.89,13.04 10.27,11.72 11.97,11.72C13.67,11.72 15.05,13.04 15.05,14.66C15.05,15.73 16,16.6 17.13,16.6C18.28,16.6 19.21,15.73 19.21,14.66C19.21,10.89 15.96,7.83 11.96,7.83C9.12,7.83 6.5,9.41 5.35,11.86C4.96,12.67 4.76,13.62 4.76,14.66C4.76,15.44 4.83,16.67 5.43,18.27C5.53,18.53 5.4,18.82 5.14,18.91C4.88,19 4.59,18.87 4.5,18.62C4,17.31 3.77,16 3.77,14.66C3.77,13.46 4,12.37 4.45,11.42C5.78,8.63 8.73,6.82 11.96,6.82C16.5,6.82 20.21,10.33 20.21,14.65C20.21,16.27 18.83,17.59 17.13,17.59C15.43,17.59 14.05,16.27 14.05,14.65C14.05,13.58 13.12,12.71 11.97,12.71C10.82,12.71 9.89,13.58 9.89,14.65C9.89,16.36 10.55,17.96 11.76,19.16C12.71,20.1 13.62,20.62 15.03,21C15.3,21.08 15.45,21.36 15.38,21.62C15.33,21.85 15.12,22 14.91,22Z"/>
</vector>

View File

@ -54,7 +54,7 @@
android:layout_marginBottom="16dp"
android:backgroundTint="?attr/colorTheme1"
android:visibility="gone"
app:srcCompat="@drawable/baseline_qr_code_scanner_24"
app:srcCompat="@drawable/baseline_settings_24"
app:tint="@color/white" />
<com.google.android.material.floatingactionbutton.FloatingActionButton

View File

@ -0,0 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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:padding="16dp">
<TextView
android:id="@+id/unlock_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Unlock Cringe Authenticator"
android:textSize="21sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/unlock_password_text"
android:layout_width="0dp"
android:layout_height="19dp"
android:layout_marginTop="32dp"
android:text="Password"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/unlock_title" />
<EditText
android:id="@+id/unlock_password"
android:layout_width="0dp"
android:layout_height="45dp"
android:layout_marginTop="10dp"
android:ems="10"
android:inputType="textPassword"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/unlock_password_text" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/unlock_button"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/button_themed"
android:text="Unlock"
android:textAllCaps="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/unlock_password" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/unlock_biometrics"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:background="@drawable/button_themed"
android:text="Unlock using biometrics"
android:textAllCaps="false"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/unlock_button" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -3,6 +3,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".fragment.HomeFragment">
<LinearLayout
@ -37,7 +38,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_weight="1"
android:src="@drawable/ic_hamburger" />
android:src="@drawable/ic_hamburger"
app:tint="?android:attr/textColorPrimary" />
<ImageView
android:id="@+id/about_cringe_studios"
@ -45,7 +47,8 @@
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:layout_weight="1"
android:src="@drawable/ic_hamburger" />
android:src="@drawable/ic_hamburger"
app:tint="?android:attr/textColorPrimary" />
</LinearLayout>
<TextView

View File

@ -21,5 +21,12 @@
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Space
android:layout_width="0dp"
android:layout_height="65dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/itemList" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>

View File

@ -12,7 +12,7 @@
android:padding="16dp">
<TextView
android:id="@+id/textview_first"
android:id="@+id/unlock_password_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="This is the home fragment"