From a6e0f7122908e3b33d537fa16f122925443d8c40 Mon Sep 17 00:00:00 2001 From: Owen LeJeune Date: Tue, 23 Jan 2024 11:04:54 -0500 Subject: [PATCH] various changes --- app/build.gradle | 2 + .../tvtime/api/tmdb/api/BasePagingSource.kt | 2 - .../tvtime/preferences/AppPreferences.kt | 6 +++ .../tvtime/ui/screens/AccountScreen.kt | 27 ++++++++++- .../tvtime/ui/screens/MediaDetailScreen.kt | 2 +- .../tvtime/ui/screens/SettingsScreen.kt | 23 ++++++++- .../tvtime/ui/viewmodel/SettingsViewModel.kt | 12 +++++ .../tvtime/utils/BiometricUtils.kt | 47 +++++++++++++++++++ 8 files changed, 116 insertions(+), 5 deletions(-) create mode 100644 app/src/main/java/com/owenlejeune/tvtime/utils/BiometricUtils.kt diff --git a/app/build.gradle b/app/build.gradle index 4223423..401bce6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -93,6 +93,8 @@ dependencies { implementation "androidx.paging:paging-compose:$compose_paging" implementation "androidx.constraintlayout:constraintlayout-compose:$compose_constraint_layout" implementation "com.google.accompanist:accompanist-webview:$compose_accompanist" + implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05" + // material you def monet_compat = "0.4.1" diff --git a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/BasePagingSource.kt b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/BasePagingSource.kt index dbbbf38..3f23ddc 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/BasePagingSource.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/api/tmdb/api/BasePagingSource.kt @@ -1,7 +1,6 @@ package com.owenlejeune.tvtime.api.tmdb.api import android.content.Context -import android.widget.Toast import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel @@ -12,7 +11,6 @@ import androidx.paging.PagingData import androidx.paging.PagingSource import androidx.paging.PagingState import androidx.paging.cachedIn -import com.owenlejeune.tvtime.R import com.owenlejeune.tvtime.ui.viewmodel.ViewModelConstants import kotlinx.coroutines.flow.Flow import org.koin.core.component.KoinComponent diff --git a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt index 5cd1e2d..2f5eaa3 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/preferences/AppPreferences.kt @@ -40,6 +40,7 @@ class AppPreferences(context: Context) { private val FLOATING_BOTTOM_BAR = "floating_bottom_bar" private val RECENT_SEARCHES = "recent_searches" private val SHOW_GOT_QUOTES = "show_got_quotes" + private val USE_ACCOUNT_BIOMETRICS = "use_account_biometrics" } private val preferences: SharedPreferences = context.getSharedPreferences(PREF_FILE, Context.MODE_PRIVATE) @@ -81,6 +82,11 @@ class AppPreferences(context: Context) { get() = preferences.getInt(SELECTED_COLOR, selectedColorDefault) set(value) { preferences.put(SELECTED_COLOR, value) } + val accountBiometricsDefault: Boolean = false + var accountBiometrics: Boolean + get() = preferences.getBoolean(USE_ACCOUNT_BIOMETRICS, accountBiometricsDefault) + set(value) { preferences.put(USE_ACCOUNT_BIOMETRICS, value) } + /******* Session Tokens ********/ var authorizedSessionValues: SessionManager.AuthorizedSessionValues? get() = preferences.getString(AUTHORIZED_SESSION_VALUES, null)?.let { diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/AccountScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/AccountScreen.kt index ce8af5b..90791df 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/AccountScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/AccountScreen.kt @@ -13,10 +13,12 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.fragment.app.FragmentActivity import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.paging.compose.LazyPagingItems @@ -34,6 +36,7 @@ import com.owenlejeune.tvtime.api.tmdb.api.v4.model.RatedTv import com.owenlejeune.tvtime.extensions.getCalendarYear import com.owenlejeune.tvtime.extensions.lazyPagingItems import com.owenlejeune.tvtime.extensions.unlessEmpty +import com.owenlejeune.tvtime.preferences.AppPreferences import com.owenlejeune.tvtime.ui.components.AccountIcon import com.owenlejeune.tvtime.ui.components.BackButton import com.owenlejeune.tvtime.ui.components.MediaResultCard @@ -44,16 +47,38 @@ import com.owenlejeune.tvtime.ui.navigation.AppNavItem import com.owenlejeune.tvtime.ui.screens.tabs.AccountTabNavItem import com.owenlejeune.tvtime.ui.viewmodel.AccountViewModel import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel +import com.owenlejeune.tvtime.utils.BiometricUtils import com.owenlejeune.tvtime.utils.SessionManager import com.owenlejeune.tvtime.utils.TmdbUtils import com.owenlejeune.tvtime.utils.types.MediaViewType import kotlinx.coroutines.launch +import org.koin.java.KoinJavaComponent.get import java.util.Date import kotlin.reflect.KClass -@OptIn(ExperimentalMaterial3Api::class) @Composable fun AccountScreen( + appNavController: NavHostController, + doSignInPartTwo: Boolean = false, + appPreferences: AppPreferences = get(AppPreferences::class.java) +) { + var proceed by remember { mutableStateOf(!appPreferences.accountBiometrics) } + if (!proceed) { + val activity = LocalContext.current as FragmentActivity + BiometricUtils.showBiometricPrompt( + activity, + onFailed = { appNavController.popBackStack() }, + onError = { appNavController.popBackStack() }, + onSuccess = { proceed = true } + ) + } else { + AccountView(appNavController = appNavController, doSignInPartTwo = doSignInPartTwo) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AccountView( appNavController: NavHostController, doSignInPartTwo: Boolean = false ) { diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt index 3b23c2c..7a90306 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/MediaDetailScreen.kt @@ -1115,7 +1115,7 @@ private fun ReviewsCard( if (isExpanded) { reviews } else { - reviews.subList(0, 3) + reviews.subList(0, minOf(reviews.size, 3)) } } ?: emptyList() Column( diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SettingsScreen.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SettingsScreen.kt index 6334c70..c176d13 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/screens/SettingsScreen.kt @@ -60,6 +60,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView +import androidx.fragment.app.FragmentActivity import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import androidx.recyclerview.widget.ItemTouchHelper @@ -85,7 +86,9 @@ import com.owenlejeune.tvtime.ui.viewmodel.ApplicationViewModel import com.owenlejeune.tvtime.ui.viewmodel.SettingsViewModel import com.owenlejeune.tvtime.ui.views.HomeTabRecyclerAdapter import com.owenlejeune.tvtime.ui.views.ItemMoveCallback +import com.owenlejeune.tvtime.utils.BiometricUtils import com.owenlejeune.tvtime.utils.SessionManager +import com.owenlejeune.tvtime.utils.canShowBiometricsPrompt import kotlinx.coroutines.launch import org.koin.java.KoinJavaComponent.get @@ -237,7 +240,7 @@ fun DesignPreferences( settingsNavController: NavController, ) { val settingsViewModel = viewModel() - val activity = LocalContext.current as Activity + val activity = LocalContext.current as FragmentActivity Column( modifier = Modifier @@ -315,6 +318,24 @@ fun DesignPreferences( if (showWallpaperPicker.value) { WallpaperPicker(showPopup = showWallpaperPicker) } + + if (canShowBiometricsPrompt()) { + SwitchPreference( + titleText = "Biometrics", + subtitleText = "###", + checkState = settingsViewModel.useAccountBiometrics.collectAsState(), + onCheckedChange = { + settingsViewModel.toggleUseAccountBiometrics() + if (it) { + BiometricUtils.showBiometricPrompt( + activity, + onFailed = { settingsViewModel.setUseAccountBiometrics(false) }, + onError = { settingsViewModel.setUseAccountBiometrics(false) } + ) + } + } + ) + } } } diff --git a/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/SettingsViewModel.kt b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/SettingsViewModel.kt index de5b4cb..3aba8fb 100644 --- a/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/SettingsViewModel.kt +++ b/app/src/main/java/com/owenlejeune/tvtime/ui/viewmodel/SettingsViewModel.kt @@ -26,6 +26,7 @@ class SettingsViewModel: ViewModel() { private val _showBackdropGallery = MutableStateFlow(preferences.showBackdropGallery) private val _showNextMcuProduction = MutableStateFlow(preferences.showNextMcuProduction) private val _showGotQuotes = MutableStateFlow(preferences.showGotQuotes) + private val _useAccountBiometrics = MutableStateFlow(preferences.accountBiometrics) } val showSearchBar = _showSearchBar.asStateFlow() @@ -42,6 +43,7 @@ class SettingsViewModel: ViewModel() { val showBackdropGallery = _showBackdropGallery.asStateFlow() val showNextMcuProduction = _showNextMcuProduction.asStateFlow() val showGotQuotes = _showGotQuotes.asStateFlow() + val useAccountBiometrics = _useAccountBiometrics.asStateFlow() fun toggleShowSearchBar() { _showSearchBar.value = _showSearchBar.value.not() @@ -168,4 +170,14 @@ class SettingsViewModel: ViewModel() { preferences.showGotQuotes = value } + fun toggleUseAccountBiometrics() { + _useAccountBiometrics.value = _useAccountBiometrics.value.not() + preferences.accountBiometrics = _useAccountBiometrics.value + } + + fun setUseAccountBiometrics(value: Boolean) { + _useAccountBiometrics.value = value + preferences.accountBiometrics = value + } + } \ No newline at end of file diff --git a/app/src/main/java/com/owenlejeune/tvtime/utils/BiometricUtils.kt b/app/src/main/java/com/owenlejeune/tvtime/utils/BiometricUtils.kt new file mode 100644 index 0000000..fc4e172 --- /dev/null +++ b/app/src/main/java/com/owenlejeune/tvtime/utils/BiometricUtils.kt @@ -0,0 +1,47 @@ +package com.owenlejeune.tvtime.utils + +import androidx.biometric.BiometricManager +import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG +import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL +import androidx.biometric.BiometricPrompt +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import androidx.core.content.ContextCompat +import androidx.fragment.app.FragmentActivity + +object BiometricUtils { + + fun showBiometricPrompt(activity: FragmentActivity, onError: () -> Unit = {}, onFailed: () -> Unit = {}, onSuccess: () -> Unit = {}) { + val executor = ContextCompat.getMainExecutor(activity) + val biometricPrompt = BiometricPrompt(activity, executor, object: BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationError(errorCode: Int, errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + onError() + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + onFailed() + } + + override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + onSuccess() + } + }) + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle("Title") + .setSubtitle("Subtitle") + .setNegativeButtonText("Cancel") + .build() + biometricPrompt.authenticate(promptInfo) + } + +} + +@Composable +fun canShowBiometricsPrompt(): Boolean { + val context = LocalContext.current + val biometricManager = BiometricManager.from(context) + return biometricManager.canAuthenticate(BIOMETRIC_STRONG or DEVICE_CREDENTIAL) == BiometricManager.BIOMETRIC_SUCCESS +} \ No newline at end of file