Skip to content

Commit

Permalink
Add UI for app lock
Browse files Browse the repository at this point in the history
Signed-off-by: starry-shivam <[email protected]>
  • Loading branch information
starry-shivam committed Apr 13, 2024
1 parent 9ff9fbd commit 6668e57
Show file tree
Hide file tree
Showing 13 changed files with 241 additions and 19 deletions.
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@ dependencies {
implementation 'com.maxkeppeler.sheets-compose-dialogs:core:1.3.0'
implementation 'com.maxkeppeler.sheets-compose-dialogs:calendar:1.3.0'
implementation 'com.maxkeppeler.sheets-compose-dialogs:date-time:1.3.0'
// Taptarget compose.
implementation "com.pierfrancescosoffritti.taptargetcompose:core:1.1.0"
// Tap-target compose.
implementation "com.pierfrancescosoffritti.taptargetcompose:core:1.1.1"
// Lottie animations.
implementation "com.airbnb.android:lottie-compose:4.1.0"
// Bio-metric authentication.
Expand Down
54 changes: 41 additions & 13 deletions app/src/main/java/com/starry/greenstash/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,27 @@ import androidx.activity.compose.setContent
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricPrompt
import androidx.compose.animation.Crossfade
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.compose.rememberNavController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import com.starry.greenstash.ui.navigation.NavGraph
import com.starry.greenstash.ui.screens.other.AppLockedScreen
import com.starry.greenstash.ui.screens.settings.SettingsViewModel
import com.starry.greenstash.ui.screens.settings.ThemeMode
import com.starry.greenstash.ui.theme.GreenStashTheme
import com.starry.greenstash.utils.Utils
import com.starry.greenstash.utils.toToast
import dagger.hilt.android.AndroidEntryPoint
import java.util.concurrent.Executor

Expand Down Expand Up @@ -73,8 +79,10 @@ class MainActivity : AppCompatActivity() {
mainViewModel.refreshReminders()

val appLockStatus = settingsViewModel.getAppLockValue()
val showAppContents = mutableStateOf(false)

if (appLockStatus && !mainViewModel.appUnlocked) {
// check if app lock is enabled and user has not unlocked the app.
if (appLockStatus && !mainViewModel.isAppUnlocked()) {
executor = ContextCompat.getMainExecutor(this)
biometricPrompt = BiometricPrompt(this, executor,
object : BiometricPrompt.AuthenticationCallback() {
Expand All @@ -84,8 +92,8 @@ class MainActivity : AppCompatActivity() {
) {
super.onAuthenticationSucceeded(result)
// make app contents visible after successful authentication.
setAppContents()
mainViewModel.appUnlocked = true
showAppContents.value = true
mainViewModel.setAppUnlocked(true)
}

override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
Expand All @@ -98,11 +106,16 @@ class MainActivity : AppCompatActivity() {
*/
val biometricManager = BiometricManager.from(this@MainActivity)
if (biometricManager.canAuthenticate(Utils.getAuthenticators()) != BiometricManager.BIOMETRIC_SUCCESS) {
setAppContents()
mainViewModel.appUnlocked = true
// make app contents visible.
showAppContents.value = true
// disable app lock.
mainViewModel.setAppUnlocked(true)
// disable app lock in settings.
settingsViewModel.setAppLock(false)
// show error message.
getString(R.string.app_lock_unable_to_authenticate).toToast(this@MainActivity)
} else {
finish() // close the app.
showAppContents.value = false
}
}
})
Expand All @@ -113,14 +126,15 @@ class MainActivity : AppCompatActivity() {
.setAllowedAuthenticators(Utils.getAuthenticators())
.build()

biometricPrompt.authenticate(promptInfo)

} else {
setAppContents()
showAppContents.value = true
}

// set app contents based on the value of showAppContents.
setAppContents(showAppContents)
}

fun setAppContents() {
private fun setAppContents(showAppContents: State<Boolean>) {
setContent {
GreenStashTheme(settingsViewModel = settingsViewModel) {
val systemUiController = rememberSystemUiController()
Expand All @@ -138,9 +152,23 @@ class MainActivity : AppCompatActivity() {
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
val navController = rememberNavController()
val screen by mainViewModel.startDestination
NavGraph(navController = navController, screen)
Crossfade(
targetState = showAppContents,
label = "AppLockCrossFade",
animationSpec = tween(500)
) { showAppContents ->
// show app contents only if user has authenticated.
if (showAppContents.value) {
val navController = rememberNavController()
val screen by mainViewModel.startDestination
NavGraph(navController = navController, screen)
} else {
// show app locked screen if user has not authenticated.
AppLockedScreen(onAuthRequest = {
biometricPrompt.authenticate(promptInfo)
})
}
}
}
}
}
Expand Down
10 changes: 8 additions & 2 deletions app/src/main/java/com/starry/greenstash/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ class MainViewModel @Inject constructor(
private val reminderManager: ReminderManager
) : ViewModel() {
/**
* Storing app lock status to avoid asking for authentication
* Store app lock status to avoid asking for authentication
* when activity restarts like when changing app or device
* theme or when changing device orientation.
*/
var appUnlocked = false
private var _appUnlocked = false

private val _isLoading: MutableState<Boolean> = mutableStateOf(true)
val isLoading: State<Boolean> = _isLoading
Expand Down Expand Up @@ -82,4 +82,10 @@ class MainViewModel @Inject constructor(
reminderManager.checkAndScheduleReminders(goalDao.getAllGoals())
}
}

fun isAppUnlocked(): Boolean = _appUnlocked

fun setAppUnlocked(value: Boolean) {
_appUnlocked = value
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ private fun BackupScreenContent(
contentAlignment = Alignment.Center
) {
AsyncImage(
model = R.drawable.backup_logo,
model = R.drawable.backup_icon,
contentDescription = null,
modifier = Modifier.size(200.dp)
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
/**
* MIT License
*
* Copyright (c) [2022 - Present] Stɑrry Shivɑm
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/


package com.starry.greenstash.ui.screens.other

import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Fingerprint
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.FilledTonalButton
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.starry.greenstash.R
import com.starry.greenstash.ui.theme.greenstashFont
import kotlinx.coroutines.delay


@Composable
fun AppLockedScreen(onAuthRequest: () -> Unit) {
Column(
Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
LockIconCard() // Animated lock icon

Text(
text = stringResource(id = R.string.app_lock_screen_title),
style = MaterialTheme.typography.headlineSmall,
fontFamily = greenstashFont,
modifier = Modifier.padding(top = 16.dp, bottom = 4.dp)
)

Text(
text = stringResource(id = R.string.app_lock_screen_subtitle),
style = MaterialTheme.typography.bodySmall,
fontFamily = greenstashFont,
modifier = Modifier.padding(horizontal = 42.dp)
)

FilledTonalButton(
onClick = onAuthRequest,
modifier = Modifier.padding(top = 18.dp)
) {
Icon(
imageVector = Icons.Filled.Fingerprint,
contentDescription = stringResource(id = R.string.app_lock_button_icon_desc),
modifier = Modifier.size(ButtonDefaults.IconSize)
)
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
Text(
text = stringResource(id = R.string.app_lock_button_text),
fontFamily = greenstashFont,
)
}
}
}

@Composable
private fun LockIconCard() {
val isAnimated = remember { mutableStateOf(false) }
val animationSize by animateFloatAsState(
targetValue = if (isAnimated.value) 1f else 0.8f,
animationSpec = tween(durationMillis = 500), label = "animationSize"
)

LaunchedEffect(key1 = true) {
delay(300)
isAnimated.value = true
}

Card(
modifier = Modifier
.size(350.dp * animationSize)
.clip(CircleShape)
.animateContentSize(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp)
)
) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(24.dp),
contentAlignment = Alignment.Center
) {
AsyncImage(
model = R.drawable.app_lock_icon,
contentDescription = null,
modifier = Modifier.size(200.dp)
)
}
}

}


@Preview(showBackground = true)
@Composable
private fun AppLockedScreenPV() {
AppLockedScreen(onAuthRequest = {
// do nothing
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ 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.rememberScrollState
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
Expand Down Expand Up @@ -115,6 +117,7 @@ fun GoalCardStyle(navController: NavController) {
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState()),
) {
OutlinedCard(
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -472,7 +472,7 @@ private fun SecuritySettings(viewModel: SettingsViewModel) {
super.onAuthenticationSucceeded(result)
context.getString(R.string.auth_successful)
.toToast(context)
mainActivity.mainViewModel.appUnlocked = true
mainActivity.mainViewModel.setAppUnlocked(true)
viewModel.setAppLock(true)
}

Expand Down
Binary file added app/src/main/res/drawable/app_lock_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
File renamed without changes
7 changes: 7 additions & 0 deletions app/src/main/res/values-es/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
<string name="welcome_screen_text">¡Hola! Por favor selecciona tú moneda preferida para iniciar.</string>
<string name="welcome_screen_button">Comencemos</string>

<!-- App Lock Screen -->
<string name="app_lock_screen_title">GreenStash está bloqueado</string>
<string name="app_lock_screen_subtitle">Haz clic en el botón de abajo para autenticar.</string>
<string name="app_lock_button_text">Autorizar</string>
<string name="app_lock_button_icon_desc">Desbloquear Aplicación</string>
<string name="app_lock_unable_to_authenticate">El bloqueo de la aplicación está desactivado porque el dispositivo no puede autenticarse</string>

<!-- Home Screen -->
<string name="home_screen_header">Metas de Ahorro</string>
<string name="menu_button_desc">Open side menu</string>
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/values-tr/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
<string name="welcome_screen_text">Merhaba! Lütfen başlamak için tercih edilen para birimini seçin.</string>
<string name="welcome_screen_button">Başla</string>

<!-- App Lock Screen -->
<string name="app_lock_screen_title">GreenStash kilitli</string>
<string name="app_lock_screen_subtitle">Kimlik doğrulamak için aşağıdaki düğmeye tıklayın.</string>
<string name="app_lock_button_text">Yetkilendir</string>
<string name="app_lock_button_icon_desc">Uygulamayı Aç</string>
<string name="app_lock_unable_to_authenticate">Cihaz kimlik doğrulayamadığı için uygulama kilitli değil</string>

<!-- Home Screen -->
<string name="home_screen_header">Birikim Hedefleri</string>
<string name="menu_button_desc">Yan menüyü aç</string>
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/res/values-zh-rCN/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@
<string name="welcome_screen_text">您好!请选择您的货币。</string>
<string name="welcome_screen_button">开始</string>

<!-- App Lock Screen -->
<string name="app_lock_screen_title">GreenStash已锁定</string>
<string name="app_lock_screen_subtitle">点击下面的按钮进行身份验证。</string>
<string name="app_lock_button_text">授权</string>
<string name="app_lock_button_icon_desc">解锁应用</string>
<string name="app_lock_unable_to_authenticate">由于设备无法进行身份验证,应用程序锁定已禁用</string>

<!-- Home Screen -->
<string name="home_screen_header">省钱目标</string>
<string name="menu_button_desc">打开侧边菜单</string>
Expand Down
Loading

0 comments on commit 6668e57

Please sign in to comment.