diff --git a/ui/BankingAndroidApp/build.gradle b/ui/BankingAndroidApp/build.gradle index 9561bcb9..70c10faa 100644 --- a/ui/BankingAndroidApp/build.gradle +++ b/ui/BankingAndroidApp/build.gradle @@ -98,6 +98,7 @@ dependencies { implementation "net.dankito.text.extraction:pdfbox-android-text-extractor:$textExtractorVersion" implementation "com.github.clans:fab:$clansFloatingActionButtonVersion" + implementation 'info.hoang8f:android-segmented:1.0.6' implementation "com.otaliastudios:autocomplete:$autocompleteVersion" diff --git a/ui/BankingAndroidApp/src/main/AndroidManifest.xml b/ui/BankingAndroidApp/src/main/AndroidManifest.xml index 41002078..48056eed 100644 --- a/ui/BankingAndroidApp/src/main/AndroidManifest.xml +++ b/ui/BankingAndroidApp/src/main/AndroidManifest.xml @@ -15,18 +15,29 @@ android:theme="@style/AppTheme"> - + + + + + \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/ActivityExtensions.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/ActivityExtensions.kt new file mode 100644 index 00000000..ded62c06 --- /dev/null +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/ActivityExtensions.kt @@ -0,0 +1,20 @@ +package net.dankito.banking.ui.android.activities + +import android.app.Activity +import android.content.Intent +import android.util.DisplayMetrics + + +fun Activity.navigateToActivity(activityClass: Class) { + val intent = Intent(applicationContext, activityClass) + + startActivity(intent) +} + +val Activity.screenWidth: Int + get() { + val displayMetrics = DisplayMetrics() + windowManager.defaultDisplay.getMetrics(displayMetrics) + + return displayMetrics.widthPixels + } \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LandingActivity.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LandingActivity.kt new file mode 100644 index 00000000..7f7bdb2a --- /dev/null +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LandingActivity.kt @@ -0,0 +1,44 @@ +package net.dankito.banking.ui.android.activities + +import android.app.Activity +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import net.dankito.banking.ui.android.MainActivity +import net.dankito.banking.ui.android.authentication.AuthenticationService +import net.dankito.banking.ui.android.authentication.AuthenticationType +import net.dankito.banking.ui.android.di.BankingComponent +import javax.inject.Inject + + +open class LandingActivity : AppCompatActivity() { + + @Inject + protected lateinit var authenticationService: AuthenticationService + + + init { + BankingComponent.component.inject(this) + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val authenticationType = authenticationService.getAuthenticationType() + + if (authenticationType == AuthenticationType.None) { + launchActivity(MainActivity::class.java) + } + else { + launchActivity(LoginActivity::class.java) + } + } + + + protected open fun launchActivity(activityClass: Class) { + navigateToActivity(activityClass) + + finish() + } + +} \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LoginActivity.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LoginActivity.kt new file mode 100644 index 00000000..edfec304 --- /dev/null +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/activities/LoginActivity.kt @@ -0,0 +1,62 @@ +package net.dankito.banking.ui.android.activities + +import android.os.Bundle +import android.view.View +import kotlinx.android.synthetic.main.activity_login.* +import net.dankito.banking.ui.android.MainActivity +import net.dankito.banking.ui.android.R +import net.dankito.banking.ui.android.authentication.AuthenticationService +import net.dankito.banking.ui.android.authentication.AuthenticationType +import net.dankito.banking.ui.android.di.BankingComponent +import javax.inject.Inject + + +open class LoginActivity : BaseActivity() { + + @Inject + protected lateinit var authenticationService: AuthenticationService + + + init { + BankingComponent.component.inject(this) + } + + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + initUi() + } + + + protected open fun initUi() { + setContentView(R.layout.activity_login) + + val authenticationType = authenticationService.getAuthenticationType() + + if (authenticationType == AuthenticationType.Password) { + lytBiometricAuthentication.visibility = View.GONE + + btnLogin.setOnClickListener { checkEnteredPasswordAndLogIn() } + } + else { + lytPasswordAuthentication.visibility = View.GONE + + btnBiometricAuthentication.authenticationSuccessful = { biometricAuthenticationSuccessful() } + } + } + + + protected open fun checkEnteredPasswordAndLogIn() { + logIn() + } + + protected open fun biometricAuthenticationSuccessful() { + logIn() + } + + protected open fun logIn() { + navigateToActivity(MainActivity::class.java) + } + +} \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationService.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationService.kt new file mode 100644 index 00000000..b22ddab0 --- /dev/null +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationService.kt @@ -0,0 +1,32 @@ +package net.dankito.banking.ui.android.authentication + +import net.dankito.banking.util.ISerializer +import net.dankito.utils.multiplatform.File + + +open class AuthenticationService( + protected val dataFolder: File, + protected val serializer: ISerializer +) { + + open val isBiometricAuthenticationSupported: Boolean + get() = true + + open fun getAuthenticationType(): AuthenticationType { + return AuthenticationType.None + } + + + fun setAuthenticationMethodToBiometric() { + + } + + fun setAuthenticationMethodToPassword(newPassword: String) { + + } + + fun removeAppProtection() { + + } + +} \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationType.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationType.kt new file mode 100644 index 00000000..ae316795 --- /dev/null +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/authentication/AuthenticationType.kt @@ -0,0 +1,12 @@ +package net.dankito.banking.ui.android.authentication + + +enum class AuthenticationType { + + None, + + Password, + + Biometric + +} \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingComponent.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingComponent.kt index 7fa2a8ae..97d571b3 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingComponent.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingComponent.kt @@ -3,10 +3,13 @@ package net.dankito.banking.ui.android.di import dagger.Component import net.dankito.banking.ui.android.MainActivity import net.dankito.banking.ui.android.activities.BaseActivity +import net.dankito.banking.ui.android.activities.LandingActivity +import net.dankito.banking.ui.android.activities.LoginActivity import net.dankito.banking.ui.android.dialogs.AddAccountDialog import net.dankito.banking.ui.android.dialogs.EnterTanDialog import net.dankito.banking.ui.android.dialogs.SendMessageLogDialog import net.dankito.banking.ui.android.dialogs.TransferMoneyDialog +import net.dankito.banking.ui.android.dialogs.settings.ProtectAppSettingsDialog import net.dankito.banking.ui.android.dialogs.settings.SettingsDialogBase import net.dankito.banking.ui.android.home.HomeFragment import javax.inject.Singleton @@ -23,6 +26,10 @@ interface BankingComponent { fun inject(baseActivity: BaseActivity) + fun inject(landingActivity: LandingActivity) + + fun inject(loginActivity: LoginActivity) + fun inject(mainActivity: MainActivity) fun inject(homeFragment: HomeFragment) @@ -35,6 +42,8 @@ interface BankingComponent { fun inject(settingsDialogBase: SettingsDialogBase) + fun inject(protectAppSettingsDialog: ProtectAppSettingsDialog) + fun inject(sendMessageLogDialog: SendMessageLogDialog) } \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingModule.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingModule.kt index 3f1ece26..ba920e4f 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingModule.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/di/BankingModule.kt @@ -17,6 +17,7 @@ import net.dankito.banking.bankfinder.IBankFinder import net.dankito.banking.bankfinder.LuceneBankFinder import net.dankito.banking.persistence.RoomBankingPersistence import net.dankito.banking.persistence.model.RoomModelCreator +import net.dankito.banking.ui.android.authentication.AuthenticationService import net.dankito.banking.ui.model.mapper.IModelCreator import net.dankito.banking.ui.util.CurrencyInfoProvider import net.dankito.utils.multiplatform.toFile @@ -84,6 +85,13 @@ class BankingModule(private val applicationContext: Context) { } + @Provides + @Singleton + fun provideAuthenticationService(@Named(DataFolderKey) dataFolder: File, serializer: ISerializer) : AuthenticationService { + return AuthenticationService(dataFolder, serializer) + } + + @Provides @Singleton fun provideBankingPresenter(bankingClientCreator: IBankingClientCreator, bankFinder: IBankFinder, diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/ProtectAppSettingsDialog.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/ProtectAppSettingsDialog.kt new file mode 100644 index 00000000..c747f664 --- /dev/null +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/ProtectAppSettingsDialog.kt @@ -0,0 +1,159 @@ +package net.dankito.banking.ui.android.dialogs.settings + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.doOnNextLayout +import kotlinx.android.synthetic.main.dialog_protect_app_settings.* +import kotlinx.android.synthetic.main.dialog_protect_app_settings.view.* +import net.dankito.banking.ui.android.R +import net.dankito.banking.ui.android.authentication.AuthenticationService +import net.dankito.banking.ui.android.authentication.AuthenticationType +import net.dankito.banking.ui.android.di.BankingComponent +import net.dankito.banking.ui.android.util.StandardTextWatcher +import net.dankito.utils.android.extensions.hideKeyboardDelayed +import org.slf4j.LoggerFactory +import javax.inject.Inject + + +open class ProtectAppSettingsDialog : SettingsDialogBase() { + + companion object { + const val DialogTag = "ProtectAppSettingsDialog" + + private val log = LoggerFactory.getLogger(ProtectAppSettingsDialog::class.java) + } + + + @Inject + protected lateinit var authenticationService: AuthenticationService + + + init { + BankingComponent.component.inject(this) + } + + + fun show(activity: AppCompatActivity) { + show(activity, SettingsDialog.DialogTag) + } + + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + return inflater.inflate(R.layout.dialog_protect_app_settings, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + setupUI(view) + } + + protected open fun setupUI(rootView: View) { + rootView.apply { + toolbar.apply { + setupToolbar(this, context.getString(R.string.settings), false) + } + + val authenticationType = authenticationService.getAuthenticationType() + val isBiometricAuthenticationSupported = authenticationService.isBiometricAuthenticationSupported + + segmentedGroup.doOnNextLayout { + val segmentedControlButtonWidth = segmentedGroup.measuredWidth / 3 + btnShowBiometricAuthenticationSection.layoutParams.width = segmentedControlButtonWidth + btnShowPasswordAuthenticationSection.layoutParams.width = segmentedControlButtonWidth + btnShowRemoveAppProtectionSection.layoutParams.width = segmentedControlButtonWidth + } + + btnShowBiometricAuthenticationSection.visibility = if (isBiometricAuthenticationSupported) View.VISIBLE else View.GONE + btnShowBiometricAuthenticationSection.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + showAuthenticationLayout(rootView, lytBiometricAuthentication, false) + } + } + + btnShowPasswordAuthenticationSection.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + showAuthenticationLayout(rootView, lytPasswordAuthentication, false) + checkIfEnteredPasswordsMatch() + } + } + + btnShowRemoveAppProtectionSection.setOnCheckedChangeListener { _, isChecked -> + if (isChecked) { + showAuthenticationLayout(rootView, lytRemoveAppProtection, true) + } + } + + btnBiometricAuthentication.authenticationSuccessful = { btnSetAuthenticationMethod.isEnabled = true } + + edtxtPassword.actualEditText.addTextChangedListener(StandardTextWatcher { checkIfEnteredPasswordsMatch() } ) + edtxtPasswordConfirmation.actualEditText.addTextChangedListener(StandardTextWatcher { checkIfEnteredPasswordsMatch() } ) + + btnSetAuthenticationMethod.setOnClickListener { setAuthenticationMethod() } + + if (authenticationService.isBiometricAuthenticationSupported && authenticationType == AuthenticationType.Biometric) { + btnShowBiometricAuthenticationSection.isChecked = true + } + else { + btnShowPasswordAuthenticationSection.isChecked = true + } + + } + } + + protected open fun showAuthenticationLayout(rootView: View, authenticationLayoutToShow: ViewGroup, isRemoveAppProtectionLayout: Boolean) { + lytBiometricAuthentication.visibility = View.GONE + lytPasswordAuthentication.visibility = View.GONE + lytRemoveAppProtection.visibility = View.GONE + + authenticationLayoutToShow.visibility = View.VISIBLE + + if (isRemoveAppProtectionLayout) { + btnSetAuthenticationMethod.setText(R.string.dialog_protect_app_settings_button_remove_app_protection_title) + btnSetAuthenticationMethod.setBackgroundResource(R.color.destructiveColor) + btnSetAuthenticationMethod.isEnabled = true + } + else { + btnSetAuthenticationMethod.setText(R.string.dialog_protect_app_settings_button_set_new_authentication_method_title) + btnSetAuthenticationMethod.setBackgroundResource(R.drawable.conditionally_disabled_view_background) + btnSetAuthenticationMethod.isEnabled = false + } + + authenticationLayoutToShow.hideKeyboardDelayed(10) + } + + + protected open fun checkIfEnteredPasswordsMatch() { + val enteredPassword = edtxtPassword.text + + if (enteredPassword.isNotBlank() && enteredPassword == edtxtPasswordConfirmation.text) { + btnSetAuthenticationMethod.isEnabled = true + } + else { + btnSetAuthenticationMethod.isEnabled = false + } + } + + protected open fun setAuthenticationMethod() { + when { + btnShowBiometricAuthenticationSection.isChecked -> authenticationService.setAuthenticationMethodToBiometric() + btnShowPasswordAuthenticationSection.isChecked -> authenticationService.setAuthenticationMethodToPassword(edtxtPassword.text) + btnShowRemoveAppProtectionSection.isChecked -> authenticationService.removeAppProtection() + else -> log.error("This should never occur! Has there a new authentication method been added?") + } + + closeDialog() + } + + + override val hasUnsavedChanges: Boolean + get() = false + + override fun saveChanges() { + + } + +} \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/SettingsDialog.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/SettingsDialog.kt index 163ccbf4..ddd153ab 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/SettingsDialog.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/dialogs/settings/SettingsDialog.kt @@ -50,7 +50,9 @@ open class SettingsDialog : SettingsDialogBase() { banksAdapter.onClickListener = { navigationToBankSettingsDialog(it.bank) } banksAdapter.itemDropped = { oldPosition, oldItem, newPosition, newItem -> reorderedBanks(oldPosition, oldItem.bank, newPosition, newItem.bank) } - rootView.btnShowSendMessageLogDialog.setOnClickListener { presenter.showSendMessageLogDialog() } + btnSetAppProtection.setOnClickListener { ProtectAppSettingsDialog().show(requireActivity() as AppCompatActivity) } + + btnShowSendMessageLogDialog.setOnClickListener { presenter.showSendMessageLogDialog() } } } diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/BiometricAuthenticationButton.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/BiometricAuthenticationButton.kt new file mode 100644 index 00000000..61c8ee38 --- /dev/null +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/BiometricAuthenticationButton.kt @@ -0,0 +1,36 @@ +package net.dankito.banking.ui.android.views + +import android.content.Context +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.LinearLayout +import kotlinx.android.synthetic.main.view_biometric_authentication_button.view.* +import net.dankito.banking.ui.android.R + + +open class BiometricAuthenticationButton @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + + open var authenticationSuccessful: (() -> Unit)? = null + + + init { + setupUi(context) + } + + private fun setupUi(context: Context) { + val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + val rootView = inflater.inflate(R.layout.view_biometric_authentication_button, this, true) + + rootView.apply { + btnStartBiometricAuthentication.setOnClickListener { doBiometricAuthenticationAndLogIn() } + } + } + + protected open fun doBiometricAuthenticationAndLogIn() { + authenticationSuccessful?.invoke() + } + +} \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/FormEditText.kt b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/FormEditText.kt index 4820903f..b01bc3b3 100644 --- a/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/FormEditText.kt +++ b/ui/BankingAndroidApp/src/main/java/net/dankito/banking/ui/android/views/FormEditText.kt @@ -33,6 +33,9 @@ open class FormEditText @JvmOverloads constructor( try { textInputLayout.hint = getString(R.styleable.FormEditText_android_hint) + if (getBoolean(R.styleable.FormEditText_showPasswordToggle, false)) { + textInputLayout.endIconMode = END_ICON_PASSWORD_TOGGLE + } textInputEditText.setText(getString(R.styleable.FormEditText_android_text)) textInputEditText.inputType = getInt(R.styleable.FormEditText_android_inputType, EditorInfo.TYPE_TEXT_VARIATION_NORMAL) diff --git a/ui/BankingAndroidApp/src/main/res/drawable/conditionally_disabled_view_background.xml b/ui/BankingAndroidApp/src/main/res/drawable/conditionally_disabled_view_background.xml new file mode 100644 index 00000000..cd80b853 --- /dev/null +++ b/ui/BankingAndroidApp/src/main/res/drawable/conditionally_disabled_view_background.xml @@ -0,0 +1,14 @@ + + + + + + + + \ No newline at end of file diff --git a/ui/BankingAndroidApp/src/main/res/layout/activity_login.xml b/ui/BankingAndroidApp/src/main/res/layout/activity_login.xml new file mode 100644 index 00000000..e74059db --- /dev/null +++ b/ui/BankingAndroidApp/src/main/res/layout/activity_login.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + +