Skip to content

Commit

Permalink
Implement proper "App Lock" mechanism (re-work the existing one)
Browse files Browse the repository at this point in the history
  • Loading branch information
Iliyan Germanov committed Nov 15, 2021
1 parent ec3347c commit 6140b95
Show file tree
Hide file tree
Showing 9 changed files with 169 additions and 166 deletions.
2 changes: 1 addition & 1 deletion app/src/main/java/com/ivy/wallet/Constants.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ object Constants {
const val URL_IVY_CONTRIBUTORS =
"https://github.com/ILIYANGERMANOV/ivy-wallet#contributors-see-graph"

const val USER_INACTIVE_TIME_LIMIT = 300 //Time in seconds
const val USER_INACTIVE_TIME_LIMIT = 1 //Time in seconds
}
6 changes: 6 additions & 0 deletions app/src/main/java/com/ivy/wallet/base/MVVMExt.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,18 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.withContext

fun <T> MutableLiveData<T>.asLiveData(): LiveData<T> {
return this
}

fun <T> MutableStateFlow<T>.asFlow(): StateFlow<T> {
return this
}

fun Fragment.args(putArgs: Bundle.() -> Unit): Fragment {
arguments = Bundle().apply { putArgs() }
return this
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ class SharedPrefs(appContext: Context) {
//-------------------------------- Bank Integrations temp ----------------------------------

//----------------------------- App Settings -----------------------------------------------
const val LOCK_APP = "lock_app"
const val APP_LOCK_ENABLED = "lock_app"
const val START_DATE_OF_MONTH = "start_date_of_month"
//----------------------------- App Settings -----------------------------------------------

Expand Down
124 changes: 50 additions & 74 deletions app/src/main/java/com/ivy/wallet/ui/IvyActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContract
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricPrompt
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.core.content.ContextCompat
import androidx.core.view.WindowCompat
import androidx.lifecycle.viewmodel.compose.viewModel
Expand Down Expand Up @@ -96,7 +96,9 @@ class IvyActivity : AppCompatActivity() {
private lateinit var openFileContract: ActivityResultLauncher<Unit>
private lateinit var onFileOpened: (fileUri: Uri) -> Unit

private var appLockedEnabled: Boolean = false

private val viewModel: IvyViewModel by viewModels()


@ExperimentalAnimationApi
@ExperimentalFoundationApi
Expand Down Expand Up @@ -190,9 +192,6 @@ class IvyActivity : AppCompatActivity() {
val viewModel: IvyViewModel = viewModel()
val isSystemInDarkTheme = isSystemInDarkTheme()

val appLockedEnabled by viewModel.appLockedEnabled.observeAsState(false)
val isUserInactive by ivyContext.isUserInactive

LaunchedEffect(isSystemInDarkTheme) {
viewModel.start(isSystemInDarkTheme, intent)
viewModel.initBilling(this@IvyActivity)
Expand All @@ -201,77 +200,50 @@ class IvyActivity : AppCompatActivity() {
IvyApp(
ivyContext = ivyContext,
) {
if (appLockedEnabled) {
//update this.appLockedEnabled here
// because only dependant Compose code on appLockedEnabled state will be updated
//when appLockedEnabled state is changed
this@IvyActivity.appLockedEnabled = true

ivyContext.navigateTo(
Screen.AppLock(
onShowOSBiometricsModal = {
authenticateWithOSBiometricsModal(
viewModel.handleBiometricAuthenticationResult(
onAuthSuccess = {
viewModel.unlockAuthenticated(intent)
}
)
)
},
onContinueWithoutAuthentication = {
viewModel.unlockAuthenticated(intent)
}
),
allowBackStackStore = false
)
}
val appLocked by viewModel.appLocked.collectAsState()

if (appLockedEnabled && isUserInactive) {
ivyContext.resetUserInActiveTimer()
ivyContext.navigateTo(
Screen.AppLock(
when (appLocked) {
null -> {
//display nothing
}
true -> {
AppLockedScreen(
onShowOSBiometricsModal = {
authenticateWithOSBiometricsModal(
viewModel.handleBiometricAuthenticationResult(
onAuthSuccess = {
//go back to previous screen
ivyContext.back()
}
)
biometricPromptCallback = viewModel.handleBiometricAuthResult()
)
},
onContinueWithoutAuthentication = {
ivyContext.back()
viewModel.unlockApp()
}
)
)
}

when (val screen = ivyContext.currentScreen) {

is Screen.Main -> MainScreen(screen = screen)
is Screen.Onboarding -> OnboardingScreen(screen = screen)
is Screen.EditTransaction -> EditTransactionScreen(screen = screen)
is Screen.ItemStatistic -> ItemStatisticScreen(screen = screen)
is Screen.PieChartStatistic -> PieChartStatisticScreen(screen = screen)
is Screen.Categories -> CategoriesScreen(screen = screen)
is Screen.Settings -> SettingsScreen(screen = screen)
is Screen.PlannedPayments -> PlannedPaymentsScreen(screen = screen)
is Screen.EditPlanned -> EditPlannedScreen(screen = screen)
is Screen.BalanceScreen -> BalanceScreen(screen = screen)
is Screen.Paywall -> PaywallScreen(
screen = screen,
activity = this@IvyActivity
)
is Screen.Test -> TestScreen(screen = screen)
is Screen.AnalyticsReport -> AnalyticsReport(screen = screen)
is Screen.Import -> ImportCSVScreen(screen = screen)
is Screen.ConnectBank -> ConnectBankScreen(screen = screen)
is Screen.Report -> ReportScreen(screen = screen)
is Screen.Budget -> BudgetScreen(screen = screen)
is Screen.WebView -> WebViewScreen(screen = screen)
is Screen.AppLock -> AppLockedScreen(screen = screen)
null -> {
}
false -> {
when (val screen = ivyContext.currentScreen) {
is Screen.Main -> MainScreen(screen = screen)
is Screen.Onboarding -> OnboardingScreen(screen = screen)
is Screen.EditTransaction -> EditTransactionScreen(screen = screen)
is Screen.ItemStatistic -> ItemStatisticScreen(screen = screen)
is Screen.PieChartStatistic -> PieChartStatisticScreen(screen = screen)
is Screen.Categories -> CategoriesScreen(screen = screen)
is Screen.Settings -> SettingsScreen(screen = screen)
is Screen.PlannedPayments -> PlannedPaymentsScreen(screen = screen)
is Screen.EditPlanned -> EditPlannedScreen(screen = screen)
is Screen.BalanceScreen -> BalanceScreen(screen = screen)
is Screen.Paywall -> PaywallScreen(
screen = screen,
activity = this@IvyActivity
)
is Screen.Test -> TestScreen(screen = screen)
is Screen.AnalyticsReport -> AnalyticsReport(screen = screen)
is Screen.Import -> ImportCSVScreen(screen = screen)
is Screen.ConnectBank -> ConnectBankScreen(screen = screen)
is Screen.Report -> ReportScreen(screen = screen)
is Screen.Budget -> BudgetScreen(screen = screen)
is Screen.WebView -> WebViewScreen(screen = screen)
null -> {
}
}
}
}
}
Expand All @@ -280,7 +252,7 @@ class IvyActivity : AppCompatActivity() {

override fun onWindowFocusChanged(hasFocus: Boolean) {
super.onWindowFocusChanged(hasFocus)
if (appLockedEnabled && !hasFocus) {
if (viewModel.isAppLockEnabled() && !hasFocus) {
window.setFlags(
WindowManager.LayoutParams.FLAG_SECURE,
WindowManager.LayoutParams.FLAG_SECURE
Expand All @@ -292,14 +264,14 @@ class IvyActivity : AppCompatActivity() {

override fun onResume() {
super.onResume()
if (appLockedEnabled)
ivyContext.checkUserInactiveTimeStatus()
if (viewModel.isAppLockEnabled())
viewModel.checkUserInactiveTimeStatus()
}

override fun onPause() {
super.onPause()
if (appLockedEnabled)
ivyContext.startUserInactiveTimeCounter()
if (viewModel.isAppLockEnabled())
viewModel.startUserInactiveTimeCounter()
}

private fun authenticateWithOSBiometricsModal(
Expand All @@ -326,8 +298,12 @@ class IvyActivity : AppCompatActivity() {
}

override fun onBackPressed() {
if (!ivyContext.onBackPressed()) {
if (viewModel.isAppLocked()) {
super.onBackPressed()
} else {
if (!ivyContext.onBackPressed()) {
super.onBackPressed()
}
}
}

Expand Down
39 changes: 0 additions & 39 deletions app/src/main/java/com/ivy/wallet/ui/IvyContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,19 @@ package com.ivy.wallet.ui

import android.net.Uri
import androidx.compose.foundation.lazy.LazyListState
import androidx.compose.runtime.State
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import com.ivy.wallet.BuildConfig
import com.ivy.wallet.Constants
import com.ivy.wallet.Constants.USER_INACTIVE_TIME_LIMIT
import com.ivy.wallet.persistence.SharedPrefs
import com.ivy.wallet.ui.main.MainTab
import com.ivy.wallet.ui.onboarding.model.TimePeriod
import com.ivy.wallet.ui.paywall.PaywallReason
import com.ivy.wallet.ui.theme.Theme
import kotlinx.coroutines.*
import java.time.LocalDate
import java.time.LocalTime
import java.util.*
import java.util.concurrent.atomic.AtomicLong

class IvyContext {
var currentScreen: Screen? by mutableStateOf(null)
Expand Down Expand Up @@ -220,39 +216,4 @@ class IvyContext {
fun switchTheme(theme: Theme) {
this.theme = theme
}

// UserInactivity ------------------------------------------------------------------------------
private val _isUserInactive = mutableStateOf(false)
val isUserInactive: State<Boolean> = _isUserInactive

private val userInactiveTime = AtomicLong(0)
private var userInactiveJob: Job? = null

fun resetUserInActiveTimer() {
_isUserInactive.value = (false)
userInactiveTime.set(0)
}

fun startUserInactiveTimeCounter() {
if (userInactiveJob != null && userInactiveJob!!.isActive) return

userInactiveJob = GlobalScope.launch(Dispatchers.IO) {
while (userInactiveTime.get() < USER_INACTIVE_TIME_LIMIT && userInactiveJob != null && !userInactiveJob?.isCancelled!!) {
delay(1000)
userInactiveTime.incrementAndGet()
}
if (!isUserInactive.value)
_isUserInactive.value = (true)
cancel()
}
}

fun checkUserInactiveTimeStatus() {
if (userInactiveTime.get() < USER_INACTIVE_TIME_LIMIT) {
if (userInactiveJob != null && !userInactiveJob?.isCancelled!!) {
userInactiveJob?.cancel()
resetUserInActiveTimer()
}
}
}
}
Loading

0 comments on commit 6140b95

Please sign in to comment.