diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 00000000..e510c497 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +UnCrack \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 00000000..cf304975 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,18 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/other.xml b/.idea/other.xml new file mode 100644 index 00000000..720dd0e3 --- /dev/null +++ b/.idea/other.xml @@ -0,0 +1,384 @@ + + + + + + \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0342da91..31775f82 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -126,7 +126,7 @@ dependencies { implementation("androidx.hilt:hilt-navigation-compose:1.2.0") // Datastore - implementation("androidx.datastore:datastore-preferences:1.0.0") + implementation("androidx.datastore:datastore-preferences:1.1.2") // Lottie Animation implementation("com.airbnb.android:lottie-compose:5.0.3") @@ -163,4 +163,6 @@ dependencies { // BCrypt implementation("org.mindrot:jbcrypt:0.4") + + implementation("androidx.biometric:biometric:1.2.0-alpha05") } \ No newline at end of file diff --git a/app/release/app-release.aab b/app/release/app-release.aab new file mode 100644 index 00000000..c15cfeac Binary files /dev/null and b/app/release/app-release.aab differ diff --git a/app/src/main/java/com/aritradas/uncrack/MainActivity.kt b/app/src/main/java/com/aritradas/uncrack/MainActivity.kt index c32f7327..bd2eb114 100644 --- a/app/src/main/java/com/aritradas/uncrack/MainActivity.kt +++ b/app/src/main/java/com/aritradas/uncrack/MainActivity.kt @@ -14,9 +14,15 @@ import androidx.annotation.RequiresApi import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.core.view.WindowCompat +import androidx.fragment.app.FragmentActivity +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import com.aritradas.uncrack.navigation.Navigation import com.aritradas.uncrack.presentation.settings.SettingsViewModel +import com.aritradas.uncrack.sharedViewModel.SharedViewModel import com.aritradas.uncrack.ui.theme.UnCrackTheme +import com.aritradas.uncrack.util.AppBioMetricManager import com.aritradas.uncrack.util.NetworkConnectivityObserver import com.google.android.gms.tasks.Task import com.google.android.play.core.appupdate.AppUpdateManagerFactory @@ -25,11 +31,18 @@ import com.google.android.play.core.appupdate.AppUpdateOptions import com.google.android.play.core.install.model.AppUpdateType import com.google.android.play.core.install.model.UpdateAvailability import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch +import javax.inject.Inject @AndroidEntryPoint -class MainActivity : ComponentActivity() { +class MainActivity : FragmentActivity() { private val settingsViewModel: SettingsViewModel by viewModels() + private val viewModel: SharedViewModel by viewModels() + + @Inject + lateinit var appBioMetricManager: AppBioMetricManager private val activityResultLauncher = registerForActivityResult( ActivityResultContracts.StartIntentSenderForResult() @@ -75,6 +88,28 @@ class MainActivity : ComponentActivity() { Navigation(this, connectivityObserver) } } + + setObserver() + } + + private fun setObserver() { + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.initAuth.collect { value -> + if (value && viewModel.loading.value) { + viewModel.showBiometricPrompt(this@MainActivity) + } + } + } + } + + lifecycleScope.launch { + lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) { + viewModel.finishActivity.collect { value -> + if (value) finish() + } + } + } } private fun checkForAppUpdate() { diff --git a/app/src/main/java/com/aritradas/uncrack/components/SettingsItemGroup.kt b/app/src/main/java/com/aritradas/uncrack/components/SettingsItemGroup.kt index 9cf6bcc1..2bd42d56 100644 --- a/app/src/main/java/com/aritradas/uncrack/components/SettingsItemGroup.kt +++ b/app/src/main/java/com/aritradas/uncrack/components/SettingsItemGroup.kt @@ -12,6 +12,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.dp import com.aritradas.uncrack.ui.theme.BackgroundLight +import com.aritradas.uncrack.ui.theme.SurfaceVariantLight @Composable fun SettingsItemGroup( @@ -21,9 +22,9 @@ fun SettingsItemGroup( Column( modifier = modifier .widthIn(max = 500.dp) - .padding(start = 12.dp, end = 12.dp) - .clip(RoundedCornerShape(8.dp)) - .background(BackgroundLight), + .padding(horizontal = 12.dp) + .clip(RoundedCornerShape(10.dp)) + .background(SurfaceVariantLight), horizontalAlignment = Alignment.CenterHorizontally ) { columnScope() diff --git a/app/src/main/java/com/aritradas/uncrack/data/datastore/DataStoreUtil.kt b/app/src/main/java/com/aritradas/uncrack/data/datastore/DataStoreUtil.kt index ad42dc7c..691b085b 100644 --- a/app/src/main/java/com/aritradas/uncrack/data/datastore/DataStoreUtil.kt +++ b/app/src/main/java/com/aritradas/uncrack/data/datastore/DataStoreUtil.kt @@ -15,5 +15,6 @@ class DataStoreUtil @Inject constructor(context: Context) { private val Context.dataStore: DataStore by preferencesDataStore("settings") val IS_DARK_MODE_KEY = booleanPreferencesKey("dark_mode") val IS_SS_BLOCK_KEY = booleanPreferencesKey("ss_block") + val IS_BIOMETRIC_AUTH_SET_KEY = booleanPreferencesKey("biometric_auth") } } \ No newline at end of file diff --git a/app/src/main/java/com/aritradas/uncrack/di/AppModule.kt b/app/src/main/java/com/aritradas/uncrack/di/AppModule.kt index df01ca37..db62fbd8 100644 --- a/app/src/main/java/com/aritradas/uncrack/di/AppModule.kt +++ b/app/src/main/java/com/aritradas/uncrack/di/AppModule.kt @@ -27,6 +27,11 @@ import javax.inject.Singleton @InstallIn(SingletonComponent::class) object AppModule { + @Provides + fun provideContext(@ApplicationContext context: Context): Context { + return context + } + @Provides fun provideDataStoreUtil(@ApplicationContext context: Context): DataStoreUtil = DataStoreUtil(context) diff --git a/app/src/main/java/com/aritradas/uncrack/di/BioMetricUtil.kt b/app/src/main/java/com/aritradas/uncrack/di/BioMetricUtil.kt new file mode 100644 index 00000000..cf2f0f91 --- /dev/null +++ b/app/src/main/java/com/aritradas/uncrack/di/BioMetricUtil.kt @@ -0,0 +1,18 @@ +package com.aritradas.uncrack.di + +import android.content.Context +import com.aritradas.uncrack.util.AppBioMetricManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +class BioMetricUtil { + + @Provides + fun provideAppBioMetricManager(context: Context): AppBioMetricManager { + return AppBioMetricManager(context) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aritradas/uncrack/di/ViewModelModule.kt b/app/src/main/java/com/aritradas/uncrack/di/ViewModelModule.kt new file mode 100644 index 00000000..c4491818 --- /dev/null +++ b/app/src/main/java/com/aritradas/uncrack/di/ViewModelModule.kt @@ -0,0 +1,22 @@ +package com.aritradas.uncrack.di + +import com.aritradas.uncrack.data.datastore.DataStoreUtil +import com.aritradas.uncrack.sharedViewModel.SharedViewModel +import com.aritradas.uncrack.util.AppBioMetricManager +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.components.ViewModelComponent + +@Module +@InstallIn(ViewModelComponent::class) +class ViewModelModule { + + @Provides + fun provideMainViewModel( + bioMetricManager: AppBioMetricManager, + dataStoreUtil: DataStoreUtil, + ): SharedViewModel { + return SharedViewModel(bioMetricManager, dataStoreUtil) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aritradas/uncrack/navigation/Navigation.kt b/app/src/main/java/com/aritradas/uncrack/navigation/Navigation.kt index b061f3cd..8fbfe7b7 100644 --- a/app/src/main/java/com/aritradas/uncrack/navigation/Navigation.kt +++ b/app/src/main/java/com/aritradas/uncrack/navigation/Navigation.kt @@ -70,6 +70,7 @@ import com.aritradas.uncrack.ui.theme.FadeOut import com.aritradas.uncrack.ui.theme.OnPrimaryContainerLight import com.aritradas.uncrack.ui.theme.OnSurfaceVariantLight import com.aritradas.uncrack.ui.theme.PrimaryDark +import com.aritradas.uncrack.ui.theme.SurfaceVariantLight import com.aritradas.uncrack.util.BackPressHandler import com.aritradas.uncrack.util.ConnectivityObserver import kotlinx.collections.immutable.ImmutableList @@ -156,6 +157,7 @@ fun Navigation( composable(Screen.SignUpScreen.name) { SignupScreen( + navController, authViewModel, connectivityObserver, onSignUp = { @@ -271,7 +273,6 @@ fun Navigation( composable(route = Screen.SettingsScreen.name) { SettingsScreen( - activity, navController, settingsViewModel ) diff --git a/app/src/main/java/com/aritradas/uncrack/presentation/auth/login/LoginScreen.kt b/app/src/main/java/com/aritradas/uncrack/presentation/auth/login/LoginScreen.kt index 711bcb91..fcf94a86 100644 --- a/app/src/main/java/com/aritradas/uncrack/presentation/auth/login/LoginScreen.kt +++ b/app/src/main/java/com/aritradas/uncrack/presentation/auth/login/LoginScreen.kt @@ -1,6 +1,7 @@ package com.aritradas.uncrack.presentation.auth.login import android.widget.Toast +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -47,6 +48,7 @@ import com.aritradas.uncrack.components.UCButton import com.aritradas.uncrack.components.UCTextField import com.aritradas.uncrack.navigation.Screen import com.aritradas.uncrack.presentation.auth.AuthViewModel +import com.aritradas.uncrack.ui.theme.BackgroundLight import com.aritradas.uncrack.ui.theme.DMSansFontFamily import com.aritradas.uncrack.ui.theme.OnPrimaryContainerLight import com.aritradas.uncrack.ui.theme.PrimaryLight @@ -105,6 +107,7 @@ fun LoginScreen( Column( modifier = Modifier .fillMaxSize() + .background(BackgroundLight) .padding(paddingValues) .padding(16.dp) ) { @@ -202,10 +205,6 @@ fun LoginScreen( } } } - - if (isLoading) { - ProgressDialog {} - } } else -> { NoInternetScreen() diff --git a/app/src/main/java/com/aritradas/uncrack/presentation/auth/signup/SignupScreen.kt b/app/src/main/java/com/aritradas/uncrack/presentation/auth/signup/SignupScreen.kt index e0471321..a2d397de 100644 --- a/app/src/main/java/com/aritradas/uncrack/presentation/auth/signup/SignupScreen.kt +++ b/app/src/main/java/com/aritradas/uncrack/presentation/auth/signup/SignupScreen.kt @@ -1,6 +1,7 @@ package com.aritradas.uncrack.presentation.auth.signup import android.widget.Toast +import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -45,6 +46,7 @@ import com.aritradas.uncrack.components.UCButton import com.aritradas.uncrack.components.UCTextField import com.aritradas.uncrack.navigation.Screen import com.aritradas.uncrack.presentation.auth.AuthViewModel +import com.aritradas.uncrack.ui.theme.BackgroundLight import com.aritradas.uncrack.ui.theme.DMSansFontFamily import com.aritradas.uncrack.ui.theme.OnPrimaryContainerLight import com.aritradas.uncrack.ui.theme.PrimaryLight @@ -60,6 +62,7 @@ import kotlinx.coroutines.flow.collectLatest @Composable fun SignupScreen( + navController: NavController, authViewModel: AuthViewModel, connectivityObserver: ConnectivityObserver, modifier: Modifier = Modifier, @@ -106,9 +109,6 @@ fun SignupScreen( } } - if (isLoading) { - ProgressDialog {} - } when(networkStatus) { ConnectivityObserver.Status.Available -> { @@ -118,6 +118,7 @@ fun SignupScreen( Column( modifier = Modifier .fillMaxSize() + .background(BackgroundLight) .padding(paddingValues) .padding(16.dp) ) { @@ -229,7 +230,7 @@ fun SignupScreen( Text( modifier = Modifier.clickable { - + navController.navigate(Screen.LoginScreen.name) }, text = stringResource(id = R.string.login), style = medium16.copy(color = PrimaryLight) diff --git a/app/src/main/java/com/aritradas/uncrack/presentation/masterKey/confirmMasterKey/ConfirmMasterKeyScreen.kt b/app/src/main/java/com/aritradas/uncrack/presentation/masterKey/confirmMasterKey/ConfirmMasterKeyScreen.kt index 80233b1b..e58e5397 100644 --- a/app/src/main/java/com/aritradas/uncrack/presentation/masterKey/confirmMasterKey/ConfirmMasterKeyScreen.kt +++ b/app/src/main/java/com/aritradas/uncrack/presentation/masterKey/confirmMasterKey/ConfirmMasterKeyScreen.kt @@ -36,8 +36,10 @@ import com.aritradas.uncrack.components.UCButton import com.aritradas.uncrack.components.UCTextField import com.aritradas.uncrack.navigation.Screen import com.aritradas.uncrack.presentation.masterKey.KeyViewModel +import com.aritradas.uncrack.ui.theme.BackgroundLight import com.aritradas.uncrack.ui.theme.SurfaceVariantLight import com.aritradas.uncrack.ui.theme.bold30 +import kotlinx.coroutines.delay @Composable fun ConfirmMasterKeyScreen( @@ -45,16 +47,23 @@ fun ConfirmMasterKeyScreen( modifier: Modifier = Modifier, masterKeyViewModel: KeyViewModel = hiltViewModel() ) { - - val context = LocalContext.current var confirmMasterKey by remember { mutableStateOf("") } var passwordVisibility by remember { mutableStateOf(false) } + var isLoading by remember { mutableStateOf(false) } val savedMasterKey = masterKeyViewModel.keyModel.password LaunchedEffect(Unit) { masterKeyViewModel.getMasterKey() } + LaunchedEffect(isLoading) { + if (isLoading) { + delay(2000) + isLoading = false + navController.navigate(Screen.VaultScreen.name) + } + } + Scaffold( modifier.fillMaxSize() ) { paddingValues -> @@ -63,7 +72,7 @@ fun ConfirmMasterKeyScreen( modifier = Modifier .fillMaxSize() .padding(paddingValues) - .background(SurfaceVariantLight) + .background(BackgroundLight) .padding(16.dp) ) { @@ -98,10 +107,7 @@ fun ConfirmMasterKeyScreen( R.string.hide_password ) - - IconButton(onClick = - { passwordVisibility = passwordVisibility.not() } - ) { + IconButton(onClick = { passwordVisibility = passwordVisibility.not() }) { Icon( modifier = Modifier.size(24.dp), painter = image, @@ -117,8 +123,12 @@ fun ConfirmMasterKeyScreen( modifier = Modifier .fillMaxWidth(), text = stringResource(R.string.unlock_uncrack), + isLoading = isLoading, + loadingText = "Unlocking...", onClick = { - navController.navigate(Screen.VaultScreen.name) + if (savedMasterKey == confirmMasterKey) { + isLoading = true + } }, enabled = savedMasterKey == confirmMasterKey ) diff --git a/app/src/main/java/com/aritradas/uncrack/presentation/masterKey/createMasterKey/CreateMasterKeyScreen.kt b/app/src/main/java/com/aritradas/uncrack/presentation/masterKey/createMasterKey/CreateMasterKeyScreen.kt index 1d889a41..55b302f4 100644 --- a/app/src/main/java/com/aritradas/uncrack/presentation/masterKey/createMasterKey/CreateMasterKeyScreen.kt +++ b/app/src/main/java/com/aritradas/uncrack/presentation/masterKey/createMasterKey/CreateMasterKeyScreen.kt @@ -36,6 +36,7 @@ import com.aritradas.uncrack.components.UCTextField import com.aritradas.uncrack.domain.model.Key import com.aritradas.uncrack.navigation.Screen import com.aritradas.uncrack.presentation.masterKey.KeyViewModel +import com.aritradas.uncrack.ui.theme.BackgroundLight import com.aritradas.uncrack.ui.theme.OnPrimaryContainerLight import com.aritradas.uncrack.ui.theme.SurfaceTintLight import com.aritradas.uncrack.ui.theme.SurfaceVariantLight @@ -66,7 +67,7 @@ fun CreateMasterKeyScreen( modifier = Modifier .fillMaxSize() .padding(paddingValues) - .background(SurfaceVariantLight) + .background(BackgroundLight) .padding(16.dp) ) { Text( diff --git a/app/src/main/java/com/aritradas/uncrack/presentation/profile/ProfileScreen.kt b/app/src/main/java/com/aritradas/uncrack/presentation/profile/ProfileScreen.kt index f4168574..9f0d891b 100644 --- a/app/src/main/java/com/aritradas/uncrack/presentation/profile/ProfileScreen.kt +++ b/app/src/main/java/com/aritradas/uncrack/presentation/profile/ProfileScreen.kt @@ -6,10 +6,13 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.systemBars import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -30,8 +33,8 @@ import com.aritradas.uncrack.navigation.Screen import com.aritradas.uncrack.sharedViewModel.UserViewModel import com.aritradas.uncrack.ui.theme.BackgroundLight import com.aritradas.uncrack.ui.theme.OnSurfaceLight +import com.aritradas.uncrack.ui.theme.SurfaceLight import com.aritradas.uncrack.ui.theme.SurfaceTintLight -import com.aritradas.uncrack.ui.theme.SurfaceVariantLight import com.aritradas.uncrack.ui.theme.medium22 import com.aritradas.uncrack.ui.theme.normal14 import com.aritradas.uncrack.util.Constants @@ -45,19 +48,20 @@ fun ProfileScreen( val context = LocalContext.current val userData by userViewModel.state.collectAsState() + val paddingValues = WindowInsets.systemBars.asPaddingValues() Column( modifier = modifier .fillMaxSize() - .background(SurfaceVariantLight), + .padding(top = paddingValues.calculateTopPadding() + 10.dp) + .background(BackgroundLight), horizontalAlignment = Alignment.CenterHorizontally ) { Row( modifier = Modifier .fillMaxWidth() - .background(BackgroundLight) - .padding(16.dp), + .background(BackgroundLight), verticalAlignment = Alignment.CenterVertically ) { @@ -69,10 +73,9 @@ fun ProfileScreen( ) { ProfileContainer( userViewModel = userViewModel, - modifier = Modifier.padding(top = 20.dp) ) - Spacer(modifier = Modifier.height(22.dp)) + Spacer(modifier = Modifier.height(20.dp)) Text( text = userData.name, @@ -94,12 +97,7 @@ fun ProfileScreen( HorizontalDivider( thickness = 2.dp, - color = SurfaceVariantLight - ) - - HorizontalDivider( - thickness = 2.dp, - color = SurfaceVariantLight + color = SurfaceLight ) UCSettingsCard( @@ -126,7 +124,7 @@ fun ProfileScreen( HorizontalDivider( thickness = 2.dp, - color = SurfaceVariantLight + color = SurfaceLight ) UCSettingsCard( diff --git a/app/src/main/java/com/aritradas/uncrack/presentation/settings/BiometricAuthListener.kt b/app/src/main/java/com/aritradas/uncrack/presentation/settings/BiometricAuthListener.kt new file mode 100644 index 00000000..cc69d822 --- /dev/null +++ b/app/src/main/java/com/aritradas/uncrack/presentation/settings/BiometricAuthListener.kt @@ -0,0 +1,7 @@ +package com.aritradas.uncrack.presentation.settings + +interface BiometricAuthListener { + fun onBiometricAuthSuccess() + fun onUserCancelled() + fun onErrorOccurred() +} \ No newline at end of file diff --git a/app/src/main/java/com/aritradas/uncrack/presentation/settings/SettingsScreen.kt b/app/src/main/java/com/aritradas/uncrack/presentation/settings/SettingsScreen.kt index f43b93c1..7009ee9e 100644 --- a/app/src/main/java/com/aritradas/uncrack/presentation/settings/SettingsScreen.kt +++ b/app/src/main/java/com/aritradas/uncrack/presentation/settings/SettingsScreen.kt @@ -1,6 +1,5 @@ package com.aritradas.uncrack.presentation.settings -import android.app.Activity import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -19,6 +18,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.mutableStateOf @@ -26,10 +26,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavHostController +import com.aritradas.uncrack.MainActivity import com.aritradas.uncrack.R import com.aritradas.uncrack.components.SettingsItemGroup import com.aritradas.uncrack.components.ThemeDialog @@ -37,25 +39,27 @@ import com.aritradas.uncrack.components.UCSettingsCard import com.aritradas.uncrack.components.UCSwitchCard import com.aritradas.uncrack.components.UCTopAppBar import com.aritradas.uncrack.navigation.Screen +import com.aritradas.uncrack.ui.theme.BackgroundLight import com.aritradas.uncrack.ui.theme.OnPrimaryContainerLight import com.aritradas.uncrack.ui.theme.OnSurfaceVariantLight -import com.aritradas.uncrack.ui.theme.SurfaceVariantLight -import com.aritradas.uncrack.ui.theme.bold20 +import com.aritradas.uncrack.ui.theme.SurfaceLight import com.aritradas.uncrack.ui.theme.medium14 import com.aritradas.uncrack.ui.theme.normal16 +import com.aritradas.uncrack.ui.theme.semiBold18 @OptIn(ExperimentalMaterial3Api::class) @Composable fun SettingsScreen( - activity: Activity, navController: NavHostController, settingsViewModel: SettingsViewModel, modifier: Modifier = Modifier ) { + val context = LocalContext.current val isScreenshotEnabled by settingsViewModel.isScreenshotEnabled.observeAsState(false) val onLogOutComplete by settingsViewModel.onLogOutComplete.observeAsState(false) val onDeleteAccountComplete by settingsViewModel.onDeleteAccountComplete.observeAsState(false) + val biometricAuthState by settingsViewModel.biometricAuthState.collectAsState() var openThemeDialog by remember { mutableStateOf(false) } var openLogoutDialog by remember { mutableStateOf(false) } var openDeleteAccountDialog by remember { mutableStateOf(false) } @@ -76,7 +80,6 @@ fun SettingsScreen( Icon( painter = painterResource(id = R.drawable.logout), contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer ) }, title = { @@ -135,7 +138,6 @@ fun SettingsScreen( Icon( painter = painterResource(id = R.drawable.delete_icon), contentDescription = null, - tint = MaterialTheme.colorScheme.onPrimaryContainer ) }, title = { @@ -194,7 +196,7 @@ fun SettingsScreen( UCTopAppBar( modifier = Modifier.fillMaxWidth(), title = "Settings", - colors = TopAppBarDefaults.topAppBarColors(SurfaceVariantLight), + colors = TopAppBarDefaults.topAppBarColors(BackgroundLight), onBackPress = { navController.popBackStack() } ) } @@ -204,7 +206,7 @@ fun SettingsScreen( modifier = modifier .fillMaxSize() .padding(paddingValues) - .background(SurfaceVariantLight), + .background(BackgroundLight) ) { Text( @@ -212,7 +214,7 @@ fun SettingsScreen( .fillMaxWidth() .padding(start = 16.dp, end = 18.dp, top = 18.dp), text = stringResource(id = R.string.security), - style = bold20.copy(color = OnPrimaryContainerLight) + style = semiBold18.copy(color = OnPrimaryContainerLight) ) Spacer(modifier = Modifier.height(14.dp)) @@ -225,20 +227,22 @@ fun SettingsScreen( } ) -// HorizontalDivider( -// thickness = 2.dp, -// color = SurfaceVariantLight -// ) -// -// UCSwitchCard( -// itemName = stringResource(R.string.unlock_with_biometric), -// isChecked = false, -// onChecked = {} -// ) + HorizontalDivider( + thickness = 2.dp, + color = SurfaceLight + ) + + UCSwitchCard( + itemName = stringResource(R.string.unlock_with_biometric), + isChecked = biometricAuthState, + onChecked = { + settingsViewModel.showBiometricPrompt(context as MainActivity) + } + ) HorizontalDivider( thickness = 2.dp, - color = SurfaceVariantLight + color = SurfaceLight ) UCSwitchCard( @@ -278,7 +282,7 @@ fun SettingsScreen( .fillMaxWidth() .padding(start = 16.dp, end = 18.dp, top = 18.dp), text = stringResource(R.string.danger_zone), - style = bold20.copy(color = OnPrimaryContainerLight) + style = semiBold18.copy(color = OnPrimaryContainerLight) ) Spacer(modifier = Modifier.height(14.dp)) diff --git a/app/src/main/java/com/aritradas/uncrack/presentation/settings/SettingsViewModel.kt b/app/src/main/java/com/aritradas/uncrack/presentation/settings/SettingsViewModel.kt index 53e59082..57077882 100644 --- a/app/src/main/java/com/aritradas/uncrack/presentation/settings/SettingsViewModel.kt +++ b/app/src/main/java/com/aritradas/uncrack/presentation/settings/SettingsViewModel.kt @@ -1,22 +1,34 @@ package com.aritradas.uncrack.presentation.settings +import androidx.datastore.preferences.core.edit import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.aritradas.uncrack.MainActivity +import com.aritradas.uncrack.data.datastore.DataStoreUtil import com.aritradas.uncrack.domain.repository.AccountRepository import com.aritradas.uncrack.domain.repository.KeyRepository +import com.aritradas.uncrack.util.AppBioMetricManager import com.aritradas.uncrack.util.runIO import com.google.firebase.Firebase import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.auth import com.google.firebase.firestore.FirebaseFirestore import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel class SettingsViewModel @Inject constructor( private val keyRepository: KeyRepository, - private val accountRepository: AccountRepository + private val accountRepository: AccountRepository, + private val appBioMetricManager: AppBioMetricManager, + dataStoreUtil: DataStoreUtil ): ViewModel() { private val auth = Firebase.auth @@ -26,6 +38,41 @@ class SettingsViewModel @Inject constructor( val isScreenshotEnabled: LiveData get() = _isScreenshotEnabled private val user = auth.currentUser private val userDB = FirebaseFirestore.getInstance() + private val dataStore = dataStoreUtil.dataStore + private val _biometricAuthState = MutableStateFlow(false) + val biometricAuthState: StateFlow = _biometricAuthState + + init { + viewModelScope.launch(Dispatchers.IO) { + dataStore.data.map { preferences -> + preferences[DataStoreUtil.IS_BIOMETRIC_AUTH_SET_KEY] ?: false + }.collect { + _biometricAuthState.value = it + } + } + } + + fun showBiometricPrompt(activity: MainActivity) { + appBioMetricManager.initBiometricPrompt( + activity = activity, + listener = object : BiometricAuthListener { + override fun onBiometricAuthSuccess() { + viewModelScope.launch { + dataStore.edit { preferences -> + preferences[DataStoreUtil.IS_BIOMETRIC_AUTH_SET_KEY] = + !_biometricAuthState.value + } + } + } + + override fun onUserCancelled() { + } + + override fun onErrorOccurred() { + } + } + ) + } fun setScreenshotEnabled(enabled: Boolean) { _isScreenshotEnabled.value = enabled diff --git a/app/src/main/java/com/aritradas/uncrack/presentation/tools/ToolsScreen.kt b/app/src/main/java/com/aritradas/uncrack/presentation/tools/ToolsScreen.kt index a21b23b3..1a04c4eb 100644 --- a/app/src/main/java/com/aritradas/uncrack/presentation/tools/ToolsScreen.kt +++ b/app/src/main/java/com/aritradas/uncrack/presentation/tools/ToolsScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.unit.dp import androidx.navigation.NavController import com.aritradas.uncrack.R import com.aritradas.uncrack.navigation.Screen +import com.aritradas.uncrack.ui.theme.BackgroundLight import com.aritradas.uncrack.ui.theme.OnPrimaryContainerLight import com.aritradas.uncrack.ui.theme.SurfaceVariantLight import com.aritradas.uncrack.ui.theme.medium18 @@ -48,17 +49,10 @@ fun ToolsScreen( modifier = Modifier .fillMaxSize() .padding(paddingValues) - .background(SurfaceVariantLight) + .background(BackgroundLight) .padding(16.dp), ) { - Text( - text = stringResource(R.string.tools), - style = medium28.copy(OnPrimaryContainerLight) - ) - - Spacer(modifier = Modifier.height(20.dp)) - Row( modifier = modifier .fillMaxWidth() @@ -66,7 +60,7 @@ fun ToolsScreen( .clickable { navController.navigate(Screen.PasswordGeneratorScreen.name) } - .background(Color.White) + .background(SurfaceVariantLight) .shadow( elevation = 5.dp, spotColor = Color(0x0D666666), @@ -111,7 +105,7 @@ fun ToolsScreen( .clickable { navController.navigate(Screen.PasswordHealthScreen.name) } - .background(Color.White) + .background(SurfaceVariantLight) .shadow( elevation = 5.dp, spotColor = Color(0x0D666666), diff --git a/app/src/main/java/com/aritradas/uncrack/presentation/vault/VaultScreen.kt b/app/src/main/java/com/aritradas/uncrack/presentation/vault/VaultScreen.kt index 0ae3568e..d505c9ce 100644 --- a/app/src/main/java/com/aritradas/uncrack/presentation/vault/VaultScreen.kt +++ b/app/src/main/java/com/aritradas/uncrack/presentation/vault/VaultScreen.kt @@ -39,11 +39,13 @@ import com.aritradas.uncrack.components.TypewriterText import com.aritradas.uncrack.components.VaultCard import com.aritradas.uncrack.sharedViewModel.UserViewModel import com.aritradas.uncrack.presentation.vault.viewmodel.VaultViewModel +import com.aritradas.uncrack.ui.theme.BackgroundLight import com.aritradas.uncrack.ui.theme.OnSurfaceVariantLight import com.aritradas.uncrack.ui.theme.PrimaryContainerLight import com.aritradas.uncrack.ui.theme.SurfaceVariantLight import com.aritradas.uncrack.ui.theme.medium24 import com.aritradas.uncrack.ui.theme.normal16 +import com.aritradas.uncrack.util.BackPressHandler @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -58,6 +60,8 @@ fun VaultScreen( var searchQuery by rememberSaveable { mutableStateOf("") } val user by userViewModel.state.collectAsState() + BackPressHandler() + LaunchedEffect(Unit) { vaultViewModel.getAccounts() userViewModel.getCurrentUser() @@ -77,19 +81,25 @@ fun VaultScreen( } ) { paddingValues -> Column( - modifier = modifier + modifier = Modifier .fillMaxSize() .padding(paddingValues) - .background(SurfaceVariantLight) - .padding(16.dp) + .background(BackgroundLight) + .then(modifier), + verticalArrangement = Arrangement.Top ) { Text( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), text = "Hello, ${user.name}", style = medium24.copy(Color.Black) ) SearchBar( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 8.dp), query = searchQuery, onQueryChange = { searchQuery = it @@ -111,7 +121,6 @@ fun VaultScreen( "Linkedin" )) } - }, colors = SearchBarDefaults.colors( containerColor = PrimaryContainerLight @@ -135,7 +144,8 @@ fun VaultScreen( LazyColumn( modifier = Modifier - .fillMaxSize(), + .fillMaxSize() + .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(12.dp) ) { if (accounts.isNotEmpty()) { @@ -150,7 +160,6 @@ fun VaultScreen( } else { item { EmptyState( - modifier = Modifier.padding(top = 100.dp), stateTitle = "Hey ${user.name}, \n currently there are no passwords saved", image = R.drawable.vault_empty_state ) diff --git a/app/src/main/java/com/aritradas/uncrack/sharedViewModel/SharedViewModel.kt b/app/src/main/java/com/aritradas/uncrack/sharedViewModel/SharedViewModel.kt new file mode 100644 index 00000000..e9c8c788 --- /dev/null +++ b/app/src/main/java/com/aritradas/uncrack/sharedViewModel/SharedViewModel.kt @@ -0,0 +1,77 @@ +package com.aritradas.uncrack.sharedViewModel + +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import com.aritradas.uncrack.MainActivity +import com.aritradas.uncrack.data.datastore.DataStoreUtil +import com.aritradas.uncrack.presentation.settings.BiometricAuthListener +import com.aritradas.uncrack.util.AppBioMetricManager +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SharedViewModel @Inject constructor( + private val appBioMetricManager: AppBioMetricManager, + dataStoreUtil: DataStoreUtil, +) : ViewModel() { + + private val dataStore = dataStoreUtil.dataStore + + private val _loading = MutableStateFlow(true) + val loading: StateFlow = _loading.asStateFlow() + + private val _initAuth = MutableStateFlow(false) + val initAuth: StateFlow = _initAuth.asStateFlow() + + private val _finishActivity = MutableStateFlow(false) + val finishActivity: StateFlow = _finishActivity.asStateFlow() + + init { + viewModelScope.launch(Dispatchers.IO) { + dataStore.data.map { preferences -> + preferences[DataStoreUtil.IS_BIOMETRIC_AUTH_SET_KEY] ?: false + }.collect { biometricAuthState -> + if (biometricAuthState && appBioMetricManager.canAuthenticate()) { + _initAuth.emit(true) + } else { + delay(1_000L) + _loading.emit(false) + } + } + } + } + + fun showBiometricPrompt(mainActivity: MainActivity) { + appBioMetricManager.initBiometricPrompt( + activity = mainActivity, + listener = object : BiometricAuthListener { + override fun onBiometricAuthSuccess() { + viewModelScope.launch { + _loading.emit(false) + } + } + + override fun onUserCancelled() { + finishActivity() + } + + override fun onErrorOccurred() { + finishActivity() + } + } + ) + } + + private fun finishActivity() { + viewModelScope.launch { + _finishActivity.emit(true) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aritradas/uncrack/util/AppBioMetricManager.kt b/app/src/main/java/com/aritradas/uncrack/util/AppBioMetricManager.kt new file mode 100644 index 00000000..ec1d9c9b --- /dev/null +++ b/app/src/main/java/com/aritradas/uncrack/util/AppBioMetricManager.kt @@ -0,0 +1,64 @@ +package com.aritradas.uncrack.util + +import android.content.Context +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricPrompt +import com.aritradas.uncrack.MainActivity +import com.aritradas.uncrack.presentation.settings.BiometricAuthListener +import javax.inject.Inject + +class AppBioMetricManager @Inject constructor(appContext: Context) { + + private var biometricPrompt: BiometricPrompt? = null + private val biometricManager = BiometricManager.from(appContext) + + fun canAuthenticate(): Boolean { + return when (biometricManager.canAuthenticate(BiometricManager.Authenticators.BIOMETRIC_STRONG)) { + BiometricManager.BIOMETRIC_SUCCESS -> { + true + } + + BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> { + false + } + + else -> { + false + } + } + } + + fun initBiometricPrompt(activity: MainActivity, listener: BiometricAuthListener) { + biometricPrompt = BiometricPrompt( + activity, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + val cancelled = errorCode in arrayListOf( + BiometricPrompt.ERROR_CANCELED, + BiometricPrompt.ERROR_USER_CANCELED, + BiometricPrompt.ERROR_NEGATIVE_BUTTON, + ) + if (cancelled) { + listener.onUserCancelled() + } else { + listener.onErrorOccurred() + } + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + listener.onBiometricAuthSuccess() + } + }, + ) + + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setAllowedAuthenticators(BiometricManager.Authenticators.BIOMETRIC_STRONG) + .setTitle("Unlock UnCrack") + .setSubtitle("Confirm biometric to get logged in") + .setNegativeButtonText("Cancel") + .build() + biometricPrompt?.authenticate(promptInfo) + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/profile_image_1.xml b/app/src/main/res/drawable/profile_image_1.xml new file mode 100644 index 00000000..a638bf4b --- /dev/null +++ b/app/src/main/res/drawable/profile_image_1.xml @@ -0,0 +1,34 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/profile_image_2.xml b/app/src/main/res/drawable/profile_image_2.xml new file mode 100644 index 00000000..68175242 --- /dev/null +++ b/app/src/main/res/drawable/profile_image_2.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/profile_image_3.xml b/app/src/main/res/drawable/profile_image_3.xml new file mode 100644 index 00000000..d1e9b6db --- /dev/null +++ b/app/src/main/res/drawable/profile_image_3.xml @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/profile_image_4.xml b/app/src/main/res/drawable/profile_image_4.xml new file mode 100644 index 00000000..22ac685c --- /dev/null +++ b/app/src/main/res/drawable/profile_image_4.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/uncrack_release.jks b/uncrack_release.jks index 377c2584..967bd6b1 100644 Binary files a/uncrack_release.jks and b/uncrack_release.jks differ