diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml index 3c7772a..208123b 100644 --- a/.idea/codeStyles/Project.xml +++ b/.idea/codeStyles/Project.xml @@ -3,18 +3,9 @@ - diff --git a/.idea/misc.xml b/.idea/misc.xml index 4700283..d5d35ec 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index 797acea..0000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index ce37383..ae00e97 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,4 +96,7 @@ dependencies { //implementation "com.google.dagger:hilt-android-testing:$hilt_ver" implementation "androidx.hilt:hilt-common:1.0.0-alpha03" implementation "androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03" + + //Bio-metric + implementation 'androidx.biometric:biometric:1.2.0-alpha03' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 637e37e..d7ce326 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,7 +3,8 @@ package="dev.spikeysanju.expensetracker"> - + = context.createDataStore(fileName) +} diff --git a/app/src/main/java/dev/spikeysanju/expensetracker/data/local/datastore/SettingsDataStore.kt b/app/src/main/java/dev/spikeysanju/expensetracker/data/local/datastore/SettingsDataStore.kt new file mode 100644 index 0000000..88fdb3e --- /dev/null +++ b/app/src/main/java/dev/spikeysanju/expensetracker/data/local/datastore/SettingsDataStore.kt @@ -0,0 +1,42 @@ +package dev.spikeysanju.expensetracker.data.local.datastore + +import android.content.Context +import androidx.datastore.preferences.core.booleanPreferencesKey +import androidx.datastore.preferences.core.edit +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import javax.inject.Singleton + + +class SettingsDataStore(context: Context) : + PrefsDataStore( + context, + PREF_FILE_SETTINGS + ), + SettingsImpl { + + // used to get the data from datastore + override val biometric: Flow + get() = dataStore.data.map { preferences -> + val biometric = preferences[BIOMETRIC_KEY] ?: false + biometric + } + + // used to save the bio-metric preference to datastore + override suspend fun saveToDataStore(isBiometricEnabled: Boolean) { + dataStore.edit { preferences -> + preferences[BIOMETRIC_KEY] = isBiometricEnabled + } + } + + companion object { + private const val PREF_FILE_SETTINGS = "settings_preference" + private val BIOMETRIC_KEY = booleanPreferencesKey("biometric_mode") + } +} + +@Singleton +interface SettingsImpl { + val biometric: Flow + suspend fun saveToDataStore(isBiometricEnabled: Boolean) +} diff --git a/app/src/main/java/dev/spikeysanju/expensetracker/data/local/datastore/UIModeDataStore.kt b/app/src/main/java/dev/spikeysanju/expensetracker/data/local/datastore/UIModeDataStore.kt index 33b1fe5..ba0f28b 100644 --- a/app/src/main/java/dev/spikeysanju/expensetracker/data/local/datastore/UIModeDataStore.kt +++ b/app/src/main/java/dev/spikeysanju/expensetracker/data/local/datastore/UIModeDataStore.kt @@ -1,18 +1,12 @@ package dev.spikeysanju.expensetracker.data.local.datastore import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit -import androidx.datastore.preferences.createDataStore import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map import javax.inject.Singleton -abstract class PrefsDataStore(context: Context, fileName: String) { - internal val dataStore: DataStore = context.createDataStore(fileName) -} class UIModeDataStore(context: Context) : PrefsDataStore( diff --git a/app/src/main/java/dev/spikeysanju/expensetracker/model/Transaction.kt b/app/src/main/java/dev/spikeysanju/expensetracker/model/Transaction.kt index 51e824b..abed210 100644 --- a/app/src/main/java/dev/spikeysanju/expensetracker/model/Transaction.kt +++ b/app/src/main/java/dev/spikeysanju/expensetracker/model/Transaction.kt @@ -26,7 +26,7 @@ data class Transaction( System.currentTimeMillis(), @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "id") - var id: Int = 0, + var id: Int = 0 ) : Serializable { val createdAtDateFormat: String get() = DateFormat.getDateTimeInstance() diff --git a/app/src/main/java/dev/spikeysanju/expensetracker/view/auth/AuthFragment.kt b/app/src/main/java/dev/spikeysanju/expensetracker/view/auth/AuthFragment.kt new file mode 100644 index 0000000..fbad2be --- /dev/null +++ b/app/src/main/java/dev/spikeysanju/expensetracker/view/auth/AuthFragment.kt @@ -0,0 +1,69 @@ +package dev.spikeysanju.expensetracker.view.auth + + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import dagger.hilt.android.AndroidEntryPoint +import dev.spikeysanju.expensetracker.R +import dev.spikeysanju.expensetracker.databinding.FragmentAuthBinding +import dev.spikeysanju.expensetracker.view.base.BaseFragment +import kotlinx.coroutines.flow.first +import kotlin.system.exitProcess + +@AndroidEntryPoint +class AuthFragment : BaseFragment() { + override val viewModel: AuthViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + + lifecycleScope.launchWhenStarted { + if(viewModel.bioMetricPreference.first()) + authenticate() + } + } + + override fun getViewBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentAuthBinding.inflate(inflater, container, false) + + + private fun authenticate(){ + val executor = ContextCompat.getMainExecutor(requireContext()) + val biometricPrompt = BiometricPrompt(this, executor, + object : BiometricPrompt.AuthenticationCallback() { + override fun onAuthenticationSucceeded( + result: BiometricPrompt.AuthenticationResult) { + super.onAuthenticationSucceeded(result) + findNavController().navigate(R.id.action_authFragment_to_dashboardFragment) + } + + override fun onAuthenticationFailed() { + super.onAuthenticationFailed() + //todo: show message wait for a second; static err code + exitProcess(-1) + } + + override fun onAuthenticationError(errorCode: Int, + errString: CharSequence) { + super.onAuthenticationError(errorCode, errString) + //todo: show message wait for a second; static err code + exitProcess(-2) + } + }) + + val promptInfo = BiometricPrompt.PromptInfo.Builder() + .setTitle("Please complete bio-metric login to use Expenso") + .setNegativeButtonText("Exit") + .build() + biometricPrompt.authenticate(promptInfo) + } + +} diff --git a/app/src/main/java/dev/spikeysanju/expensetracker/view/auth/AuthViewModel.kt b/app/src/main/java/dev/spikeysanju/expensetracker/view/auth/AuthViewModel.kt new file mode 100644 index 0000000..083742d --- /dev/null +++ b/app/src/main/java/dev/spikeysanju/expensetracker/view/auth/AuthViewModel.kt @@ -0,0 +1,24 @@ +package dev.spikeysanju.expensetracker.view.auth + + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.spikeysanju.expensetracker.data.local.datastore.SettingsDataStore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class AuthViewModel @Inject constructor( + application: Application +) : + AndroidViewModel(application) { + + // init datastore + private val settingsDataStore = SettingsDataStore(application) + + // get bio-metric preference + val bioMetricPreference = settingsDataStore.biometric +} diff --git a/app/src/main/java/dev/spikeysanju/expensetracker/view/dashboard/DashboardFragment.kt b/app/src/main/java/dev/spikeysanju/expensetracker/view/dashboard/DashboardFragment.kt index bd91702..24c0324 100644 --- a/app/src/main/java/dev/spikeysanju/expensetracker/view/dashboard/DashboardFragment.kt +++ b/app/src/main/java/dev/spikeysanju/expensetracker/view/dashboard/DashboardFragment.kt @@ -95,7 +95,7 @@ class DashboardFragment : override fun onMove( recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder ): Boolean { return true } @@ -280,6 +280,10 @@ class DashboardFragment : findNavController().navigate(R.id.action_dashboardFragment_to_aboutFragment) true } + R.id.action_settings -> { + findNavController().navigate(R.id.action_dashboardFragment_to_settingsFragment) + true + } else -> super.onOptionsItemSelected(item) } } diff --git a/app/src/main/java/dev/spikeysanju/expensetracker/view/main/MainActivity.kt b/app/src/main/java/dev/spikeysanju/expensetracker/view/main/MainActivity.kt index 842ed40..d639603 100644 --- a/app/src/main/java/dev/spikeysanju/expensetracker/view/main/MainActivity.kt +++ b/app/src/main/java/dev/spikeysanju/expensetracker/view/main/MainActivity.kt @@ -1,8 +1,12 @@ package dev.spikeysanju.expensetracker.view.main import android.os.Bundle +import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity +import androidx.biometric.BiometricPrompt +import androidx.core.content.ContextCompat +import androidx.lifecycle.lifecycleScope import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import androidx.navigation.ui.AppBarConfiguration @@ -14,6 +18,10 @@ import dev.spikeysanju.expensetracker.databinding.ActivityMainBinding import dev.spikeysanju.expensetracker.repo.TransactionRepo import dev.spikeysanju.expensetracker.utils.viewModelFactory import dev.spikeysanju.expensetracker.view.main.viewmodel.TransactionViewModel +import dev.spikeysanju.expensetracker.view.settings.SettingsViewModel +import kotlinx.coroutines.flow.first +import java.util.concurrent.Executor +import kotlin.system.exitProcess @AndroidEntryPoint class MainActivity : AppCompatActivity() { @@ -79,4 +87,5 @@ class MainActivity : AppCompatActivity() { navHostFragment.navController.navigateUp() return super.onSupportNavigateUp() } + } diff --git a/app/src/main/java/dev/spikeysanju/expensetracker/view/settings/SettingsFragment.kt b/app/src/main/java/dev/spikeysanju/expensetracker/view/settings/SettingsFragment.kt new file mode 100644 index 0000000..561b73c --- /dev/null +++ b/app/src/main/java/dev/spikeysanju/expensetracker/view/settings/SettingsFragment.kt @@ -0,0 +1,38 @@ +package dev.spikeysanju.expensetracker.view.settings + + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.lifecycleScope +import dagger.hilt.android.AndroidEntryPoint +import dev.spikeysanju.expensetracker.databinding.FragmentSettingsBinding +import dev.spikeysanju.expensetracker.view.base.BaseFragment +import kotlinx.coroutines.flow.first + +@AndroidEntryPoint +class SettingsFragment : BaseFragment() { + override val viewModel: SettingsViewModel by viewModels() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + // Set the item state + lifecycleScope.launchWhenStarted { + binding.biometric.isChecked = viewModel.bioMetricPreference.first() + } + + initViews() + } + + private fun initViews() = with(binding) { + biometric.setOnCheckedChangeListener { _, biometricEnabled -> + viewModel.setBioMetricLock(biometricEnabled) + } + } + + override fun getViewBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentSettingsBinding.inflate(inflater, container, false) +} diff --git a/app/src/main/java/dev/spikeysanju/expensetracker/view/settings/SettingsViewModel.kt b/app/src/main/java/dev/spikeysanju/expensetracker/view/settings/SettingsViewModel.kt new file mode 100644 index 0000000..55d7c04 --- /dev/null +++ b/app/src/main/java/dev/spikeysanju/expensetracker/view/settings/SettingsViewModel.kt @@ -0,0 +1,31 @@ +package dev.spikeysanju.expensetracker.view.settings + + +import android.app.Application +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dev.spikeysanju.expensetracker.data.local.datastore.SettingsDataStore +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class SettingsViewModel @Inject constructor( + application: Application +) : + AndroidViewModel(application) { + + // init datastore + private val settingsDataStore = SettingsDataStore(application) + + // get bio-metric preference + val bioMetricPreference = settingsDataStore.biometric + + fun setBioMetricLock(bioMetricLock: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + settingsDataStore.saveToDataStore(bioMetricLock) + } + } + +} diff --git a/app/src/main/res/layout/fragment_auth.xml b/app/src/main/res/layout/fragment_auth.xml new file mode 100644 index 0000000..8e206f5 --- /dev/null +++ b/app/src/main/res/layout/fragment_auth.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_settings.xml b/app/src/main/res/layout/fragment_settings.xml new file mode 100644 index 0000000..6aa1733 --- /dev/null +++ b/app/src/main/res/layout/fragment_settings.xml @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/menu_ui.xml b/app/src/main/res/menu/menu_ui.xml index 21b79d4..542cbb7 100644 --- a/app/src/main/res/menu/menu_ui.xml +++ b/app/src/main/res/menu/menu_ui.xml @@ -20,5 +20,10 @@ android:enabled="true" android:title="@string/text_about" app:showAsAction="never" /> + diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 6926ae6..8b6945b 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -1,94 +1,121 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7725c30..3163ab0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -37,5 +37,7 @@ \%s \nAmount: %s, \nTransaction-Type: %s, \nTag: %s, \nDate: %s, \nNote: %s, \nCreatedAt: %s \n\nVisit: https://github.com/Spikeysanju/Expenso No Transaction Yet! After your first transaction you will be able to view it here + Settings + Bio-metric Security diff --git a/build.gradle b/build.gradle index b64ea26..bcee468 100644 --- a/build.gradle +++ b/build.gradle @@ -1,12 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = "1.4.21" + ext.kotlin_version = "1.3.72" repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.2.0-beta04' + classpath "com.android.tools.build:gradle:4.1.2" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "androidx.navigation:navigation-safe-args-gradle-plugin:2.3.3" classpath 'com.google.dagger:hilt-android-gradle-plugin:2.30.1-alpha' diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ae8dbe4..104492c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https://services.gradle.org/distributions/gradle-6.7.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip