Skip to content

Commit

Permalink
Merge pull request #125 from anuragkanojiya1/development
Browse files Browse the repository at this point in the history
Added Password Protection and biometric authentication by Encrypted Shared Preferences and biometrics in android
  • Loading branch information
CrazyMarvin authored Oct 18, 2024
2 parents 335e703 + 186c5a8 commit aa80a57
Show file tree
Hide file tree
Showing 10 changed files with 434 additions and 16 deletions.
5 changes: 3 additions & 2 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ android {
}

dependencies {

implementation("androidx.appcompat:appcompat:1.7.0")
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
Expand Down Expand Up @@ -108,7 +108,8 @@ dependencies {
implementation ("androidx.glance:glance-appwidget:1.0.0")
implementation ("androidx.glance:glance-material3:1.0.0")
implementation ("androidx.lifecycle:lifecycle-livedata-ktx:2.8.6")

implementation("androidx.security:security-crypto:1.1.0-alpha04")
implementation("androidx.biometric:biometric:1.4.0-alpha02")
}

kapt{
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
package rocks.poopjournal.fucksgiven

import android.app.LocaleConfig
import android.app.LocaleManager
import android.os.Build
import android.os.Build.VERSION.SDK_INT
import android.os.Bundle
import android.os.LocaleList
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.collectAsState
import androidx.core.os.LocaleListCompat
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.navigation.compose.rememberNavController
import dagger.hilt.android.AndroidEntryPoint
import rocks.poopjournal.fucksgiven.data.getPasswordProtectionEnabled
import rocks.poopjournal.fucksgiven.presentation.component.BiometricPromptManager
import rocks.poopjournal.fucksgiven.presentation.navigation.NavGraph
import rocks.poopjournal.fucksgiven.presentation.screens.PasswordPromptScreen
import rocks.poopjournal.fucksgiven.presentation.ui.theme.FucksGivenTheme
import rocks.poopjournal.fucksgiven.presentation.ui.utils.AppTheme
import rocks.poopjournal.fucksgiven.presentation.ui.utils.ThemeSetting
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : ComponentActivity() {
class MainActivity : AppCompatActivity() {
@Inject
lateinit var themeSetting: ThemeSetting

Expand All @@ -39,7 +41,15 @@ class MainActivity : ComponentActivity() {
AppTheme.DARK -> true
}
FucksGivenTheme(darkTheme = useDarkColors) {
NavGraph(navController = rememberNavController(), themeSetting = themeSetting, context = this)
var isAuthenticated by remember { mutableStateOf(false) }
val isPasswordProtectionEnabled = getPasswordProtectionEnabled(context = this)
if (isAuthenticated || !isPasswordProtectionEnabled) {
NavGraph(navController = rememberNavController(), themeSetting = themeSetting, context = this)
} else {
PasswordPromptScreen(context = this) {
isAuthenticated = true
}
}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package rocks.poopjournal.fucksgiven.data

import android.content.Context
import android.content.SharedPreferences
import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys

fun getEncryptedSharedPreferences(context: Context): SharedPreferences {
val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)

return EncryptedSharedPreferences.create(
"secure_prefs",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
}

fun savePassword(context: Context, password: String) {
val sharedPreferences = getEncryptedSharedPreferences(context)
val editor = sharedPreferences.edit()
editor.putString("user_password", password)
editor.apply()
}

fun getPassword(context: Context): String? {
val sharedPreferences = getEncryptedSharedPreferences(context)
return sharedPreferences.getString("user_password", null)
}

fun clearStoredPassword(context: Context) {
val sharedPreferences = getEncryptedSharedPreferences(context)
val editor = sharedPreferences.edit()
editor.remove("user_password")
editor.apply()
}

fun setPasswordProtectionEnabled(context: Context, enabled: Boolean) {
val sharedPreferences = getEncryptedSharedPreferences(context)
val editor = sharedPreferences.edit()
editor.putBoolean("password_protection_enabled", enabled)
editor.apply()
}

fun getPasswordProtectionEnabled(context: Context): Boolean {
val sharedPreferences = getEncryptedSharedPreferences(context)
return sharedPreferences.getBoolean("password_protection_enabled", false)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package rocks.poopjournal.fucksgiven.presentation.component

import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import androidx.biometric.BiometricManager
import androidx.biometric.BiometricManager.Authenticators.BIOMETRIC_STRONG
import androidx.biometric.BiometricManager.Authenticators.DEVICE_CREDENTIAL
import androidx.biometric.BiometricPrompt
import androidx.biometric.BiometricPrompt.PromptInfo
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.receiveAsFlow

class BiometricPromptManager(
private val activity: AppCompatActivity
) {
private val resultChannel = Channel<BiometricResult>()
val promptResults = resultChannel.receiveAsFlow()

fun showBiometricPrompt(
title: String,
description: String
) {
val manager = BiometricManager.from(activity)
val authenticators = if(Build.VERSION.SDK_INT >= 30) {
BIOMETRIC_STRONG or DEVICE_CREDENTIAL
} else BIOMETRIC_STRONG

val promptInfo = PromptInfo.Builder()
.setTitle(title)
.setDescription(description)
.setAllowedAuthenticators(authenticators)

if(Build.VERSION.SDK_INT < 30) {
promptInfo.setNegativeButtonText("Cancel")
}

when(manager.canAuthenticate(authenticators)) {
BiometricManager.BIOMETRIC_ERROR_HW_UNAVAILABLE -> {
resultChannel.trySend(BiometricResult.HardwareUnavailable)
return
}
BiometricManager.BIOMETRIC_ERROR_NO_HARDWARE -> {
resultChannel.trySend(BiometricResult.FeatureUnavailable)
return
}
BiometricManager.BIOMETRIC_ERROR_NONE_ENROLLED -> {
resultChannel.trySend(BiometricResult.AuthenticationNotSet)
return
}
else -> Unit
}

val prompt = BiometricPrompt(
activity,
object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
resultChannel.trySend(BiometricResult.AuthenticationError(errString.toString()))
}

override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
resultChannel.trySend(BiometricResult.AuthenticationSuccess)
}

override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
resultChannel.trySend(BiometricResult.AuthenticationFailed)
}
}
)
prompt.authenticate(promptInfo.build())
}

sealed interface BiometricResult {
data object HardwareUnavailable: BiometricResult
data object FeatureUnavailable: BiometricResult
data class AuthenticationError(val error: String): BiometricResult
data object AuthenticationFailed: BiometricResult
data object AuthenticationSuccess: BiometricResult
data object AuthenticationNotSet: BiometricResult
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ package rocks.poopjournal.fucksgiven.presentation.navigation
const val HOME_SCREEN = "HomeScreen"
const val SETTINGS_SCREEN = "SettingsScreen"
const val STATS_SCREEN = "StatsScreen"
const val ABOUT_SCREEN = "AboutScreen"
const val ABOUT_SCREEN = "AboutScreen"
const val PASSWORD_PROMPT_SCREEN = "PasswordPromptScreen"
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import androidx.navigation.compose.composable
import rocks.poopjournal.fucksgiven.presentation.screens.AboutScreen
import rocks.poopjournal.fucksgiven.presentation.viewmodel.HomeViewModel
import rocks.poopjournal.fucksgiven.presentation.screens.HomeScreen
import rocks.poopjournal.fucksgiven.presentation.screens.PasswordPromptScreen
import rocks.poopjournal.fucksgiven.presentation.screens.SettingScreen
import rocks.poopjournal.fucksgiven.presentation.screens.StatsScreen
import rocks.poopjournal.fucksgiven.presentation.ui.utils.ThemeSetting
Expand All @@ -19,7 +20,7 @@ import rocks.poopjournal.fucksgiven.presentation.viewmodel.StatsViewModel

@RequiresApi(Build.VERSION_CODES.P)
@Composable
fun NavGraph(navController: NavHostController,themeSetting: ThemeSetting,context: Context){
fun NavGraph(navController: NavHostController,themeSetting: ThemeSetting, context: Context){
val viewModel : HomeViewModel = hiltViewModel()
val statsViewModel : StatsViewModel = hiltViewModel()
val settingsViewModel : SettingsViewModel = hiltViewModel()
Expand All @@ -37,8 +38,12 @@ fun NavGraph(navController: NavHostController,themeSetting: ThemeSetting,context
}

composable(route = SETTINGS_SCREEN){
SettingScreen(navController = navController, viewModel = settingsViewModel)
SettingScreen(navController = navController, viewModel = settingsViewModel, context = context)
}
composable(route = PASSWORD_PROMPT_SCREEN){
PasswordPromptScreen(context, onAuthenticated = {
navController.navigate(HOME_SCREEN)
})
}

}
}
Loading

0 comments on commit aa80a57

Please sign in to comment.