├── app
├── .gitignore
├── src
│ ├── main
│ │ ├── res
│ │ │ ├── mipmap-hdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-mdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── mipmap-xxxhdpi
│ │ │ │ ├── ic_launcher.webp
│ │ │ │ └── ic_launcher_round.webp
│ │ │ ├── values
│ │ │ │ ├── themes.xml
│ │ │ │ ├── colors.xml
│ │ │ │ └── strings.xml
│ │ │ ├── mipmap-anydpi-v26
│ │ │ │ ├── ic_launcher.xml
│ │ │ │ └── ic_launcher_round.xml
│ │ │ ├── xml
│ │ │ │ ├── backup_rules.xml
│ │ │ │ └── data_extraction_rules.xml
│ │ │ ├── values-en
│ │ │ │ └── strings.xml
│ │ │ ├── values-tr
│ │ │ │ └── strings.xml
│ │ │ ├── values-de
│ │ │ │ └── strings.xml
│ │ │ ├── drawable-v24
│ │ │ │ └── ic_launcher_foreground.xml
│ │ │ └── drawable
│ │ │ │ └── ic_launcher_background.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── android
│ │ │ │ └── authentication
│ │ │ │ ├── ui
│ │ │ │ ├── theme
│ │ │ │ │ ├── Color.kt
│ │ │ │ │ ├── Type.kt
│ │ │ │ │ └── Theme.kt
│ │ │ │ └── screens
│ │ │ │ │ └── StartScreen.kt
│ │ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── android
│ │ │ └── authentication
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── android
│ │ └── authentication
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── pinlibrary
├── .gitignore
├── consumer-rules.pro
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── android
│ │ │ │ └── pinlibrary
│ │ │ │ ├── utils
│ │ │ │ ├── enums
│ │ │ │ │ └── PinCodeScenario.kt
│ │ │ │ ├── preferences
│ │ │ │ │ ├── IAttemptCounter.kt
│ │ │ │ │ ├── IPinCodeManager.kt
│ │ │ │ │ ├── ISettingsManager.kt
│ │ │ │ │ ├── AttemptCounter.kt
│ │ │ │ │ ├── SettingsManager.kt
│ │ │ │ │ └── PinCodeManager.kt
│ │ │ │ ├── listeners
│ │ │ │ │ └── NumberListener.kt
│ │ │ │ ├── state
│ │ │ │ │ ├── deletepin
│ │ │ │ │ │ ├── DeletePinScreenIntent.kt
│ │ │ │ │ │ └── DeletePinScreenState.kt
│ │ │ │ │ ├── validationpin
│ │ │ │ │ │ ├── ValidationPinScreenIntent.kt
│ │ │ │ │ │ └── ValidationPinScreenState.kt
│ │ │ │ │ ├── createpin
│ │ │ │ │ │ ├── CreatePinScreenIntent.kt
│ │ │ │ │ │ └── CreatePinScreenState.kt
│ │ │ │ │ ├── changepin
│ │ │ │ │ │ ├── ChangePinScreenIntent.kt
│ │ │ │ │ │ └── ChangePinScreenState.kt
│ │ │ │ │ ├── IPinCodeStateManager.kt
│ │ │ │ │ └── PinCodeStateManager.kt
│ │ │ │ ├── keyboard
│ │ │ │ │ └── KeyboardButtonEnum.kt
│ │ │ │ ├── encryption
│ │ │ │ │ ├── enums
│ │ │ │ │ │ └── Algorithm.kt
│ │ │ │ │ └── Encryptor.kt
│ │ │ │ ├── helpers
│ │ │ │ │ └── HandleButtonClick.kt
│ │ │ │ └── biometric
│ │ │ │ │ └── BiometricHelper.kt
│ │ │ │ ├── ui
│ │ │ │ ├── systemdesign
│ │ │ │ │ ├── theme
│ │ │ │ │ │ └── Dimens.kt
│ │ │ │ │ ├── dialog
│ │ │ │ │ │ └── ShowDialog.kt
│ │ │ │ │ ├── indicator
│ │ │ │ │ │ └── RoundedPinCodeIndicator.kt
│ │ │ │ │ └── ripple
│ │ │ │ │ │ └── RippleEffect.kt
│ │ │ │ ├── components
│ │ │ │ │ ├── PinCodeTitle.kt
│ │ │ │ │ ├── PinCodeNotification.kt
│ │ │ │ │ ├── PinCodeForgot.kt
│ │ │ │ │ ├── Keyboard.kt
│ │ │ │ │ ├── PinCodeContent.kt
│ │ │ │ │ └── KeyboardButton.kt
│ │ │ │ └── screens
│ │ │ │ │ ├── MainScreen.kt
│ │ │ │ │ ├── DeletePinScreen.kt
│ │ │ │ │ ├── CreatePinScreen.kt
│ │ │ │ │ ├── ValidationPinScreen.kt
│ │ │ │ │ └── ChangePinScreen.kt
│ │ │ │ └── viewmodel
│ │ │ │ └── PinViewModel.kt
│ │ └── res
│ │ │ ├── drawable
│ │ │ ├── ic_clear_30.xml
│ │ │ ├── ic_clear_white_30.xml
│ │ │ ├── ic_fingerprint_30.xml
│ │ │ ├── ic_fingerprint_white_30.xml
│ │ │ └── ic_fingerprint_transparent_30.xml
│ │ │ ├── values-en
│ │ │ └── strings.xml
│ │ │ ├── values-tr
│ │ │ └── strings.xml
│ │ │ ├── values-de
│ │ │ └── strings.xml
│ │ │ └── values
│ │ │ └── strings.xml
│ ├── test
│ │ └── java
│ │ │ └── com
│ │ │ └── android
│ │ │ └── pinlibrary
│ │ │ └── ExampleUnitTest.kt
│ └── androidTest
│ │ └── java
│ │ └── com
│ │ └── android
│ │ └── pinlibrary
│ │ └── ExampleInstrumentedTest.kt
├── proguard-rules.pro
└── build.gradle
├── .idea
├── .name
├── .gitignore
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── vcs.xml
├── compiler.xml
├── kotlinc.xml
├── deploymentTargetDropDown.xml
├── migrations.xml
├── misc.xml
├── gradle.xml
└── inspectionProfiles
│ └── Project_Default.xml
├── screens
├── preview_russian.gif
├── preview_english_01.png
├── preview_english_02.png
└── preview_english_03.png
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
├── LICENSE.md
├── gradle.properties
├── gradlew.bat
├── README.md
├── gradlew
└── README_RU.md
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/pinlibrary/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/.idea/.name:
--------------------------------------------------------------------------------
1 | Pin Authentication
--------------------------------------------------------------------------------
/pinlibrary/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.idea/.gitignore:
--------------------------------------------------------------------------------
1 | # Default ignored files
2 | /shelf/
3 | /workspace.xml
4 |
--------------------------------------------------------------------------------
/screens/preview_russian.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/screens/preview_russian.gif
--------------------------------------------------------------------------------
/screens/preview_english_01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/screens/preview_english_01.png
--------------------------------------------------------------------------------
/screens/preview_english_02.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/screens/preview_english_02.png
--------------------------------------------------------------------------------
/screens/preview_english_03.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/screens/preview_english_03.png
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Khokhlinvladimir/android-pin-authentication/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.idea/kotlinc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/enums/PinCodeScenario.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.enums
2 |
3 | enum class PinCodeScenario {
4 | CREATION,
5 | CHANGE,
6 | DELETION,
7 | VALIDATION,
8 | STUB;
9 | }
--------------------------------------------------------------------------------
/.idea/deploymentTargetDropDown.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Oct 25 16:26:40 MSK 2023
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
5 | zipStoreBase=GRADLE_USER_HOME
6 | zipStorePath=wrapper/dists
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/preferences/IAttemptCounter.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.preferences
2 |
3 | interface IAttemptCounter {
4 | fun saveAttempts(attempts: Int)
5 | fun getAttempts(): Int
6 | fun decrementAttempts()
7 | fun resetAttempts()
8 | }
9 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/listeners/NumberListener.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.listeners
2 |
3 | import com.android.pinlibrary.utils.keyboard.KeyboardButtonEnum
4 |
5 | fun interface NumberListener {
6 | fun onNumberTriggered(keyboardEnum: KeyboardButtonEnum)
7 | }
8 |
--------------------------------------------------------------------------------
/.idea/migrations.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
10 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/preferences/IPinCodeManager.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.preferences
2 |
3 | interface IPinCodeManager {
4 | fun savePinCode(pinCode: String)
5 | fun loadPinCode(): String?
6 | fun clearPinCode()
7 | fun isPinCodeCorrect(enteredPinCode: String): Boolean
8 | }
9 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/state/deletepin/DeletePinScreenIntent.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.state.deletepin
2 |
3 | sealed class DeletePinScreenIntent {
4 |
5 | data object InitialState : DeletePinScreenIntent()
6 | data class EnterPin(val pin: String) : DeletePinScreenIntent()
7 | data object DeletePin : DeletePinScreenIntent()
8 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/authentication/ui/theme/Color.kt:
--------------------------------------------------------------------------------
1 | package com.android.authentication.ui.theme
2 |
3 | import androidx.compose.ui.graphics.Color
4 |
5 | val Purple80 = Color(0xFFD0BCFF)
6 | val PurpleGrey80 = Color(0xFFCCC2DC)
7 | val Pink80 = Color(0xFFEFB8C8)
8 |
9 | val Purple40 = Color(0xFF6650a4)
10 | val PurpleGrey40 = Color(0xFF625b71)
11 | val Pink40 = Color(0xFF7D5260)
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/state/validationpin/ValidationPinScreenIntent.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.state.validationpin
2 |
3 | sealed class ValidationPinScreenIntent {
4 |
5 | data object InitialState : ValidationPinScreenIntent()
6 | data class EnterPin(val pin: String) : ValidationPinScreenIntent()
7 | data object ValidatePin : ValidationPinScreenIntent()
8 | }
--------------------------------------------------------------------------------
/.idea/misc.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | pluginManagement {
2 | repositories {
3 | google()
4 | mavenCentral()
5 | gradlePluginPortal()
6 | }
7 | }
8 | dependencyResolutionManagement {
9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
10 | repositories {
11 | google()
12 | mavenCentral()
13 | }
14 | }
15 | rootProject.name = "Pin Authentication"
16 | include ':app'
17 | include ':pinlibrary'
18 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/state/createpin/CreatePinScreenIntent.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.state.createpin
2 |
3 | sealed class CreatePinScreenIntent {
4 |
5 | data object InitialState : CreatePinScreenIntent()
6 | data class EnterPin(val pin: String) : CreatePinScreenIntent()
7 | data class ConfirmPin(val pin: String) : CreatePinScreenIntent()
8 | data object CreatePin : CreatePinScreenIntent()
9 | }
10 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/keyboard/KeyboardButtonEnum.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.keyboard
2 |
3 | enum class KeyboardButtonEnum(val buttonValue: Int) {
4 | BUTTON_0(0),
5 | BUTTON_1(1),
6 | BUTTON_2(2),
7 | BUTTON_3(3),
8 | BUTTON_4(4),
9 | BUTTON_5(5),
10 | BUTTON_6(6),
11 | BUTTON_7(7),
12 | BUTTON_8(8),
13 | BUTTON_9(9),
14 | BUTTON_FINGERPRINT(10),
15 | BUTTON_CLEAR(-1);
16 | }
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/state/deletepin/DeletePinScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.state.deletepin
2 |
3 | sealed class DeletePinScreenState {
4 |
5 | data object InitialState : DeletePinScreenState()
6 | data class EnteringPinState(val pin: String) : DeletePinScreenState()
7 | data object PinDeletedState : DeletePinScreenState()
8 | data class ErrorState(val errorMessage: String) : DeletePinScreenState()
9 | }
--------------------------------------------------------------------------------
/pinlibrary/src/test/java/com/android/pinlibrary/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/test/java/com/android/authentication/ExampleUnitTest.kt:
--------------------------------------------------------------------------------
1 | package com.android.authentication
2 |
3 | import org.junit.Test
4 |
5 | import org.junit.Assert.*
6 |
7 | /**
8 | * Example local unit test, which will execute on the development machine (host).
9 | *
10 | * See [testing documentation](http://d.android.com/tools/testing).
11 | */
12 | class ExampleUnitTest {
13 | @Test
14 | fun addition_isCorrect() {
15 | assertEquals(4, 2 + 2)
16 | }
17 | }
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/state/validationpin/ValidationPinScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.state.validationpin
2 |
3 | sealed class ValidationPinScreenState {
4 |
5 | data object InitialState : ValidationPinScreenState()
6 | data class EnteringPinState(val pin: String) : ValidationPinScreenState()
7 | data class ErrorState(val errorMessage: String) : ValidationPinScreenState()
8 | data object PinValidatedState : ValidationPinScreenState()
9 | }
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/systemdesign/theme/Dimens.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.systemdesign.theme
2 |
3 | import androidx.compose.ui.unit.dp
4 | import androidx.compose.ui.unit.sp
5 |
6 | object Dimens {
7 | val verticalKeyboardButtonPadding = 10.dp
8 | val horizontalKeyboardButtonPadding = 23.dp
9 | val verticalKeyboardPadding = 16.dp
10 | val horizontalKeyboardPadding = 20.dp
11 | val keyBoardButtonSize = 50.dp
12 | val keyBoardButtonFontSize = 25.sp
13 | }
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/preferences/ISettingsManager.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.preferences
2 |
3 | interface ISettingsManager {
4 | fun isBiometricEnabled(): Boolean
5 | fun isAutoLaunchBiometricEnabled(): Boolean
6 | fun setAutoLaunchBiometricEnabled(enabled: Boolean)
7 | fun setBiometricEnabled(enabled: Boolean)
8 | fun getMaxPinAttempts(): Int
9 | fun setMaxPinAttempts(maxAttempts: Int)
10 | fun getPinLength(): Int
11 | fun setPinLength(pinLength: Int)
12 | }
13 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/state/changepin/ChangePinScreenIntent.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.state.changepin
2 |
3 | sealed class ChangePinScreenIntent {
4 |
5 | data object InitialState : ChangePinScreenIntent()
6 | data class EnterCurrentPin(val currentPin: String) : ChangePinScreenIntent()
7 | data class EnterNewPin(val newPin: String) : ChangePinScreenIntent()
8 | data class ConfirmNewPin(val newPin: String) : ChangePinScreenIntent()
9 | data object ChangePin : ChangePinScreenIntent()
10 | }
--------------------------------------------------------------------------------
/app/src/main/res/xml/backup_rules.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
13 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/state/createpin/CreatePinScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.state.createpin
2 |
3 | sealed class CreatePinScreenState {
4 |
5 | data object InitialState : CreatePinScreenState()
6 | data class EnteringPinState(val pin: String) : CreatePinScreenState()
7 | data class ConfirmingPinState(val pin: String) : CreatePinScreenState()
8 | data object PinCreatedState : CreatePinScreenState()
9 | data class ErrorState(val errorMessage: String) : CreatePinScreenState()
10 | }
11 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/encryption/enums/Algorithm.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.encryption.enums
2 |
3 | enum class Algorithm(val value: String) {
4 | SHA1("1"), SHA256("2");
5 |
6 | companion object {
7 | @JvmStatic
8 | fun getFromText(text: String): Algorithm {
9 | for (algorithm in values()) {
10 | if (algorithm.value == text) {
11 | return algorithm
12 | }
13 | }
14 | return SHA1
15 | }
16 | }
17 | }
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Pin Authentication
3 |
4 | Текущий статус: %s
5 | Создать Pin-код
6 | Валидировать Pin-код
7 | Изменить Pin-код
8 | Удалить Pin-код
9 | Pin-код создан
10 | Pin-код не создан
11 |
--------------------------------------------------------------------------------
/app/src/main/res/values-en/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Pin Authentication
4 |
5 | Current status: %s
6 | Create Pin Code
7 | Validate Pin Code
8 | Change Pin Code
9 | Delete Pin Code
10 | Pin Code Created
11 | Pin Code Not Created
12 |
--------------------------------------------------------------------------------
/app/src/main/res/values-tr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Pin Authentication
4 |
5 | Mevcut durum: %s
6 | Pin Kodu Oluştur
7 | Pin Kodunu Doğrula
8 | Pin Kodunu Değiştir
9 | Pin Kodunu Sil
10 | Pin Kodu Oluşturuldu
11 | Pin Kodu Oluşturulmadı
12 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/state/changepin/ChangePinScreenState.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.state.changepin
2 |
3 | sealed class ChangePinScreenState {
4 | data object InitialState : ChangePinScreenState()
5 | data class EnteringCurrentPin(val currentPin: String) : ChangePinScreenState()
6 | data class EnteringNewPin(val newPin: String) : ChangePinScreenState()
7 | data class ConfirmingNewPin(val newPin: String) : ChangePinScreenState()
8 | data object PinChangedSuccess : ChangePinScreenState()
9 | data class ErrorState(val errorMessage: String) : ChangePinScreenState()
10 | }
--------------------------------------------------------------------------------
/app/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Pin Authentication
4 |
5 | Aktueller Status: %s
6 | PIN-Code erstellen
7 | PIN-Code überprüfen
8 | PIN-Code ändern
9 | PIN-Code löschen
10 | PIN-Code erstellt
11 | PIN-Code nicht erstellt
12 |
--------------------------------------------------------------------------------
/app/src/main/res/xml/data_extraction_rules.xml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
12 |
13 |
19 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/app/src/androidTest/java/com/android/authentication/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.android.authentication
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.android.authentication", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/pinlibrary/src/androidTest/java/com/android/pinlibrary/ExampleInstrumentedTest.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary
2 |
3 | import androidx.test.platform.app.InstrumentationRegistry
4 | import androidx.test.ext.junit.runners.AndroidJUnit4
5 |
6 | import org.junit.Test
7 | import org.junit.runner.RunWith
8 |
9 | import org.junit.Assert.*
10 |
11 | /**
12 | * Instrumented test, which will execute on an Android device.
13 | *
14 | * See [testing documentation](http://d.android.com/tools/testing).
15 | */
16 | @RunWith(AndroidJUnit4::class)
17 | class ExampleInstrumentedTest {
18 | @Test
19 | fun useAppContext() {
20 | // Context of the app under test.
21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext
22 | assertEquals("com.android.pinlibrary.test", appContext.packageName)
23 | }
24 | }
--------------------------------------------------------------------------------
/pinlibrary/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/components/PinCodeTitle.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.components
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.foundation.layout.wrapContentSize
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.text.font.FontWeight
9 | import androidx.compose.ui.text.style.TextAlign
10 | import androidx.compose.ui.unit.dp
11 | import androidx.compose.ui.unit.sp
12 |
13 | @Composable
14 | fun PinCodeScreenHeader(text: String) {
15 | Text(
16 | text = text,
17 | fontWeight = FontWeight.Bold,
18 | fontSize = 24.sp,
19 | textAlign = TextAlign.Center,
20 | modifier = Modifier
21 | .wrapContentSize()
22 | .padding(horizontal = 16.dp, vertical = 0.dp)
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/res/drawable/ic_clear_30.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/res/drawable/ic_clear_white_30.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/helpers/HandleButtonClick.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.helpers
2 |
3 | import com.android.pinlibrary.utils.keyboard.KeyboardButtonEnum
4 |
5 | fun fillArrayWithButtons(
6 | button: KeyboardButtonEnum,
7 | array: MutableList,
8 | maxLength: Int,
9 | callback: (List) -> Unit
10 | ) {
11 |
12 | if (button == KeyboardButtonEnum.BUTTON_FINGERPRINT) {
13 | return
14 | }
15 |
16 | if (button == KeyboardButtonEnum.BUTTON_CLEAR) {
17 | /** If the "Clear" button was pressed, remove the last element from the array **/
18 | if (array.isNotEmpty()) {
19 | array.removeAt(array.size - 1)
20 | }
21 | } else {
22 | /** If there is no "Clear" button, add the button value to the array **/
23 | if (array.size < maxLength) {
24 | array.add(button.buttonValue)
25 | }
26 | }
27 |
28 | /** Call callback to notify about array changes **/
29 | callback(array)
30 | }
31 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/components/PinCodeNotification.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.components
2 |
3 | import androidx.compose.foundation.layout.padding
4 | import androidx.compose.foundation.layout.wrapContentSize
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.Modifier
8 | import androidx.compose.ui.graphics.Color
9 | import androidx.compose.ui.text.font.FontWeight
10 | import androidx.compose.ui.text.style.TextAlign
11 | import androidx.compose.ui.text.style.TextOverflow
12 | import androidx.compose.ui.unit.dp
13 | import androidx.compose.ui.unit.sp
14 |
15 | @Composable
16 | fun PinCodeScreenNotification(text: String) {
17 | Text(
18 | text = text,
19 | fontWeight = FontWeight.Normal,
20 | fontSize = 15.sp,
21 | color = Color.Red,
22 | textAlign = TextAlign.Center,
23 | modifier = Modifier
24 | .wrapContentSize()
25 | .padding(horizontal = 16.dp, vertical = 20.dp),
26 | overflow = TextOverflow.Visible
27 | )
28 | }
29 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [2023] [Khokhlin Vladimir]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
15 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/app/src/main/java/com/android/authentication/ui/theme/Type.kt:
--------------------------------------------------------------------------------
1 | package com.android.authentication.ui.theme
2 |
3 | import androidx.compose.material3.Typography
4 | import androidx.compose.ui.text.TextStyle
5 | import androidx.compose.ui.text.font.FontFamily
6 | import androidx.compose.ui.text.font.FontWeight
7 | import androidx.compose.ui.unit.sp
8 |
9 | // Set of Material typography styles to start with
10 | val Typography = Typography(
11 | bodyLarge = TextStyle(
12 | fontFamily = FontFamily.Default,
13 | fontWeight = FontWeight.Normal,
14 | fontSize = 16.sp,
15 | lineHeight = 24.sp,
16 | letterSpacing = 0.5.sp
17 | )
18 | /* Other default text styles to override
19 | titleLarge = TextStyle(
20 | fontFamily = FontFamily.Default,
21 | fontWeight = FontWeight.Normal,
22 | fontSize = 22.sp,
23 | lineHeight = 28.sp,
24 | letterSpacing = 0.sp
25 | ),
26 | labelSmall = TextStyle(
27 | fontFamily = FontFamily.Default,
28 | fontWeight = FontWeight.Medium,
29 | fontSize = 11.sp,
30 | lineHeight = 16.sp,
31 | letterSpacing = 0.5.sp
32 | )
33 | */
34 | )
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/screens/MainScreen.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.screens
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.livedata.observeAsState
5 | import androidx.lifecycle.viewmodel.compose.viewModel
6 | import com.android.pinlibrary.utils.enums.PinCodeScenario
7 | import com.android.pinlibrary.utils.state.PinCodeStateManager
8 | import com.android.pinlibrary.viewmodel.PinViewModel
9 |
10 | @Composable
11 | fun PinCodeScreen() {
12 |
13 | val pinCodeStateManager = PinCodeStateManager.getInstance()
14 | val currentScenario = pinCodeStateManager.currentScenario.observeAsState()
15 | val pinViewModel = viewModel()
16 |
17 | when (currentScenario.value) {
18 | PinCodeScenario.CREATION -> CreatePinScreen(pinViewModel, pinCodeStateManager)
19 | PinCodeScenario.CHANGE -> ChangePinScreen(pinViewModel, pinCodeStateManager)
20 | PinCodeScenario.DELETION -> DeletePinScreen(pinViewModel, pinCodeStateManager)
21 | PinCodeScenario.VALIDATION -> ValidationPinScreen(
22 | pinViewModel,
23 | pinCodeStateManager,
24 | PinCodeScenario.VALIDATION
25 | )
26 |
27 | else -> {}
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/preferences/AttemptCounter.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.preferences
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 |
6 | class AttemptCounter(val context: Context) : IAttemptCounter {
7 |
8 | private val sharedPreferences: SharedPreferences =
9 | context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
10 | private val settingsManager = SettingsManager(context = context)
11 |
12 | override fun saveAttempts(attempts: Int) {
13 | sharedPreferences.edit().putInt(KEY_ATTEMPTS, attempts).apply()
14 | }
15 |
16 | override fun getAttempts(): Int {
17 | return sharedPreferences.getInt(KEY_ATTEMPTS, settingsManager.getMaxPinAttempts())
18 | }
19 |
20 | override fun decrementAttempts() {
21 | val currentAttempts = getAttempts()
22 | if (currentAttempts > 0) {
23 | saveAttempts(currentAttempts - 1)
24 | }
25 | }
26 |
27 | override fun resetAttempts() {
28 | saveAttempts(settingsManager.getMaxPinAttempts())
29 | }
30 |
31 | companion object {
32 | private const val PREFS_NAME = "AttemptPrefs"
33 | private const val KEY_ATTEMPTS = "attempts"
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/systemdesign/dialog/ShowDialog.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.systemdesign.dialog
2 |
3 | import androidx.compose.material3.AlertDialog
4 | import androidx.compose.material3.Button
5 | import androidx.compose.material3.Text
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.ui.res.stringResource
8 | import com.android.pinlibrary.R
9 | import com.android.pinlibrary.utils.state.PinCodeStateManager
10 |
11 | @Composable
12 | fun ShowDialog(
13 | title: String,
14 | description: String,
15 | showDialog: Boolean,
16 | pinCodeStateManager: PinCodeStateManager,
17 | onDismiss: () -> Unit
18 | ) {
19 | if (showDialog) {
20 | AlertDialog(
21 | onDismissRequest = { onDismiss() },
22 | title = {
23 | Text(text = title)
24 | },
25 | text = {
26 | Text(text = description)
27 | },
28 | confirmButton = {
29 | Button(
30 | onClick = {
31 | pinCodeStateManager.setResetPassword()
32 | onDismiss()
33 | }
34 | ) {
35 | Text(text = stringResource(id = R.string.ok_button))
36 | }
37 | }
38 | )
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Kotlin code style for this project: "official" or "obsolete":
19 | kotlin.code.style=official
20 | # Enables namespacing of each library's R class so that its R class includes only the
21 | # resources declared in the library itself and none from the library's dependencies,
22 | # thereby reducing the size of the R class for that library
23 | android.nonTransitiveRClass=true
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/state/IPinCodeStateManager.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.state
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.LiveData
5 | import com.android.pinlibrary.utils.enums.PinCodeScenario
6 |
7 | interface IPinCodeStateManager {
8 | val currentScenario: LiveData
9 |
10 | fun setScenario(scenario: PinCodeScenario)
11 |
12 | fun onCreationSuccess(callback: (isSuccess: Boolean) -> Unit)
13 |
14 | fun onValidationSuccess(callback: (isSuccess: Boolean) -> Unit)
15 |
16 | fun onChangeSuccess(callback: (isSuccess: Boolean) -> Unit)
17 |
18 | fun onDeletionSuccess(callback: (isSuccess: Boolean) -> Unit)
19 |
20 | fun onLoginAttemptsExpended(callback: () -> Unit)
21 | fun onBiometricAuthentication(callback: (isSuccess: Boolean) -> Unit)
22 |
23 | fun onResetPassword(callback: () -> Unit)
24 |
25 | fun clearConfiguration(application: Application)
26 | fun isPinCodeSaved(application: Application): Boolean
27 |
28 | fun setBiometricEnabled(enabled: Boolean, application: Application): Boolean
29 | fun autoLaunchBiometricEnabled(enabled: Boolean, application: Application): Boolean
30 | fun setMaxPinAttempts(maxAttempts: Int, application: Application): Int
31 | fun setPinLength(pinLength: Int, application: Application): Int
32 |
33 | fun setTemporaryPinValidationState(isEnabled: Boolean)
34 | }
35 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/res/values-en/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Forgot?
4 | Disable Pincode
5 | Create a PIN code
6 | Enter Pincode
7 | Enter Pincode
8 | Confirm Pincode
9 | Fingerprint
10 | Fingerprint recognized
11 | Fingerprint not recognized. Try again
12 | Incorrect PIN. Attempts left: %1$d
13 | PIN codes do not match
14 | Password successfully saved
15 |
16 | Authorization
17 | Use biometrics for authorization
18 | Confirm your identity
19 | Cancel
20 |
21 | Forgot PIN?
22 | If you forgot your PIN, you can reset it by re-authenticating
23 | OK
24 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/res/values-tr/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | PIN\'ı unuttunuz mu?
4 | %d haneli PIN\'i devre dışı bırak
5 | Bir PIN kodu oluşturun
6 | PIN girin
7 | PIN girin
8 | PIN\'inizi onaylayın
9 | Parmak izi
10 | Parmak izi kabul edildi
11 | Parmak izi tanınmadı. Tekrar deneyin.
12 | Yanlış PIN. Kalan deneme sayısı: %1$d
13 | PIN kodları uyuşmuyor
14 | Parola başarıyla kaydedildi
15 |
16 | Yetkilendirme
17 | Yetkilendirme için biyometriyi kullanın
18 | Kimliğinizi doğrulayın
19 | İptal
20 |
21 | PIN\'ı unuttunuz mu?
22 | PIN kodunuzu unuttuysanız, yeniden yetkilendirme yaparak sıfırlayabilirsiniz
23 | Tamam
24 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/res/values-de/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | PIN vergessen?
4 | %d-stellige PIN deaktivieren
5 | Erstellen Sie einen PIN-Code
6 | PIN eingeben
7 | PIN eingeben
8 | Bestätigen Sie Ihre PIN
9 | Fingerabdruck
10 | Fingerabdruck akzeptiert
11 | Fingerabdruck nicht erkannt. Versuchen Sie es erneut.
12 | Falsche PIN. Versuche übrig: %1$d
13 | PIN-Codes stimmen nicht überein
14 | Passwort erfolgreich gespeichert
15 |
16 | Authentifizierung
17 | Verwenden Sie die Biometrie zur Authentifizierung
18 | Bestätigen Sie Ihre Identität
19 | Abbrechen
20 |
21 | PIN vergessen?
22 | Wenn Sie Ihren PIN vergessen haben, können Sie ihn durch erneute Authentifizierung zurücksetzen
23 | OK
24 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/encryption/Encryptor.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.encryption
2 |
3 | import com.android.pinlibrary.utils.encryption.enums.Algorithm
4 | import java.security.MessageDigest
5 | import java.util.Locale
6 |
7 | class Encryptor {
8 |
9 | fun getSHA(text: String?, algorithm: Algorithm): String {
10 | val sha = ""
11 | if (text.isNullOrEmpty()) {
12 | return sha
13 | }
14 |
15 | val shaDigest = getShaDigest(algorithm)
16 |
17 | if (shaDigest != null) {
18 | val textBytes = text.toByteArray(Charsets.UTF_8)
19 | shaDigest.update(textBytes, 0, textBytes.size)
20 | val shaHash = shaDigest.digest()
21 | return bytesToHex(shaHash)
22 | }
23 |
24 | return ""
25 | }
26 |
27 | private companion object {
28 |
29 | private fun getShaDigest(algorithm: Algorithm): MessageDigest? {
30 | return try {
31 | when (algorithm) {
32 | Algorithm.SHA256 -> MessageDigest.getInstance("SHA-256")
33 | else -> MessageDigest.getInstance("SHA-1")
34 | }
35 | } catch (e: Exception) {
36 | null
37 | }
38 | }
39 |
40 | private fun bytesToHex(bytes: ByteArray): String {
41 | val stringBuilder = StringBuilder()
42 | var stringTemp: String
43 | for (aByte in bytes) {
44 | stringTemp = (Integer.toHexString(aByte.toInt() and 0xFF))
45 | if (stringTemp.length == 1) {
46 | stringBuilder.append("0").append(stringTemp)
47 | } else {
48 | stringBuilder.append(stringTemp)
49 | }
50 | }
51 | return stringBuilder.toString().lowercase(Locale.ENGLISH)
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/components/PinCodeForgot.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.components
2 |
3 | import androidx.compose.foundation.clickable
4 | import androidx.compose.foundation.layout.padding
5 | import androidx.compose.foundation.layout.wrapContentSize
6 | import androidx.compose.material3.Text
7 | import androidx.compose.runtime.Composable
8 | import androidx.compose.runtime.getValue
9 | import androidx.compose.runtime.mutableStateOf
10 | import androidx.compose.runtime.remember
11 | import androidx.compose.runtime.setValue
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.res.stringResource
14 | import androidx.compose.ui.text.font.FontWeight
15 | import androidx.compose.ui.text.style.TextAlign
16 | import androidx.compose.ui.unit.dp
17 | import androidx.compose.ui.unit.sp
18 | import com.android.pinlibrary.R
19 | import com.android.pinlibrary.ui.systemdesign.dialog.ShowDialog
20 | import com.android.pinlibrary.utils.state.PinCodeStateManager
21 |
22 | @Composable
23 | fun PinCodeScreenForgot(
24 | text: String,
25 | pinCodeStateManager: PinCodeStateManager
26 | ) {
27 | var showDialog by remember { mutableStateOf(false) }
28 |
29 | Text(
30 | text = text,
31 | fontWeight = FontWeight.Normal,
32 | fontSize = 18.sp,
33 | textAlign = TextAlign.Center,
34 | modifier = Modifier
35 | .wrapContentSize()
36 | .padding(horizontal = 16.dp, vertical = 20.dp)
37 | .clickable {
38 | showDialog = true
39 | }
40 | )
41 |
42 | ShowDialog(
43 | title = stringResource(id = R.string.forgot_pin),
44 | description = stringResource(id = R.string.forgot_pin_instruction),
45 | showDialog = showDialog,
46 | pinCodeStateManager = pinCodeStateManager,
47 | onDismiss = {
48 | showDialog = false
49 | }
50 | )
51 | }
52 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/systemdesign/indicator/RoundedPinCodeIndicator.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.systemdesign.indicator
2 |
3 | import androidx.compose.foundation.background
4 | import androidx.compose.foundation.border
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.foundation.layout.Arrangement
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.foundation.layout.wrapContentSize
11 | import androidx.compose.foundation.lazy.LazyRow
12 | import androidx.compose.foundation.shape.CircleShape
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.Alignment
15 | import androidx.compose.ui.Modifier
16 | import androidx.compose.ui.graphics.Color
17 | import androidx.compose.ui.unit.dp
18 |
19 | @Composable
20 | fun RoundedBoxesRow(startQuantity: Int, quantity: Int) {
21 | LazyRow(
22 | modifier = Modifier
23 | .wrapContentSize()
24 | .padding(horizontal = 16.dp, vertical = 20.dp),
25 | horizontalArrangement = Arrangement.SpaceBetween,
26 | ) {
27 | items(quantity) {
28 | RoundedBox(isFilled = true)
29 | }
30 | items(startQuantity - quantity) {
31 | RoundedBox(isFilled = false)
32 | }
33 | }
34 | }
35 |
36 | @Composable
37 | fun RoundedBox(isFilled: Boolean) {
38 | val color = if (isSystemInDarkTheme()) Color.White else Color.Black
39 | val boxModifier = Modifier
40 | .size(35.dp)
41 | .padding(9.dp)
42 | .let {
43 | if (isFilled) it.background(color, shape = CircleShape)
44 | else it.border(1.dp, color, shape = CircleShape)
45 | }
46 |
47 | Box(
48 | modifier = boxModifier,
49 | contentAlignment = Alignment.Center
50 | ) {
51 | /** You can add additional content inside the round Box if necessary **/
52 | }
53 | }
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/preferences/SettingsManager.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.preferences
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 |
6 | class SettingsManager(context: Context) : ISettingsManager {
7 | private val sharedPreferences: SharedPreferences =
8 | context.getSharedPreferences("AppSettings", Context.MODE_PRIVATE)
9 |
10 | override fun isBiometricEnabled(): Boolean {
11 | return sharedPreferences.getBoolean(BIOMETRIC_ENABLED_KEY, true)
12 | /** Enabled by default */
13 | }
14 |
15 | override fun setBiometricEnabled(enabled: Boolean) {
16 | val editor = sharedPreferences.edit()
17 | editor.putBoolean(BIOMETRIC_ENABLED_KEY, enabled)
18 | editor.apply()
19 | }
20 |
21 | override fun isAutoLaunchBiometricEnabled(): Boolean {
22 | return sharedPreferences.getBoolean(AUTO_LAUNCH_BIOMETRIC_ENABLED_KEY, true)
23 | /** Enabled by default */
24 | }
25 |
26 | override fun setAutoLaunchBiometricEnabled(enabled: Boolean) {
27 | val editor = sharedPreferences.edit()
28 | editor.putBoolean(AUTO_LAUNCH_BIOMETRIC_ENABLED_KEY, enabled)
29 | editor.apply()
30 | }
31 |
32 | override fun getMaxPinAttempts(): Int {
33 | return sharedPreferences.getInt(MAX_PIN_ATTEMPTS_KEY, 5)
34 | /** Default 5 attempts */
35 | }
36 |
37 | override fun setMaxPinAttempts(maxAttempts: Int) {
38 | val editor = sharedPreferences.edit()
39 | editor.putInt(MAX_PIN_ATTEMPTS_KEY, maxAttempts)
40 | editor.apply()
41 | }
42 |
43 | override fun getPinLength(): Int {
44 | return sharedPreferences.getInt(PIN_LENGTH_KEY, 4)
45 | /** Default 4 characters */
46 | }
47 |
48 | override fun setPinLength(pinLength: Int) {
49 | val editor = sharedPreferences.edit()
50 | editor.putInt(PIN_LENGTH_KEY, pinLength)
51 | editor.apply()
52 | }
53 |
54 | companion object {
55 | private const val BIOMETRIC_ENABLED_KEY = "biometric_enabled"
56 | private const val AUTO_LAUNCH_BIOMETRIC_ENABLED_KEY = "auto_launch_biometric_enabled"
57 | private const val MAX_PIN_ATTEMPTS_KEY = "max_pin_attempts"
58 | private const val PIN_LENGTH_KEY = "pin_length"
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'org.jetbrains.kotlin.android'
4 | }
5 |
6 | android {
7 | namespace 'com.android.authentication'
8 | compileSdk 34
9 |
10 | defaultConfig {
11 | applicationId "com.android.authentication"
12 | minSdk 24
13 | targetSdk 34
14 | versionCode 1
15 | versionName "1.0"
16 |
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | vectorDrawables {
19 | useSupportLibrary true
20 | }
21 | }
22 |
23 | buildTypes {
24 | release {
25 | minifyEnabled false
26 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
27 | }
28 | }
29 | compileOptions {
30 | sourceCompatibility JavaVersion.VERSION_17
31 | targetCompatibility JavaVersion.VERSION_17
32 | }
33 | kotlinOptions {
34 | jvmTarget = '17'
35 | }
36 | buildFeatures {
37 | compose true
38 | }
39 | composeOptions {
40 | kotlinCompilerExtensionVersion '1.5.5'
41 | }
42 | packagingOptions {
43 | resources {
44 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
45 | }
46 | }
47 | }
48 |
49 | dependencies {
50 |
51 | implementation project(":pinlibrary")
52 |
53 | implementation 'androidx.core:core-ktx:1.13.1'
54 | implementation platform('org.jetbrains.kotlin:kotlin-bom:1.8.0')
55 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.2'
56 | implementation 'androidx.activity:activity-compose:1.9.0'
57 | implementation platform('androidx.compose:compose-bom:2022.10.00')
58 | implementation 'androidx.compose.ui:ui'
59 | implementation 'androidx.compose.ui:ui-graphics'
60 | implementation 'androidx.compose.ui:ui-tooling-preview'
61 | implementation 'androidx.compose.material3:material3'
62 | testImplementation 'junit:junit:4.13.2'
63 | androidTestImplementation 'androidx.test.ext:junit:1.2.1'
64 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
65 | androidTestImplementation platform('androidx.compose:compose-bom:2022.10.00')
66 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
67 | debugImplementation 'androidx.compose.ui:ui-tooling'
68 | debugImplementation 'androidx.compose.ui:ui-test-manifest'
69 | implementation "androidx.compose.runtime:runtime-livedata:1.6.8"
70 | }
--------------------------------------------------------------------------------
/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/app/src/main/java/com/android/authentication/ui/theme/Theme.kt:
--------------------------------------------------------------------------------
1 | package com.android.authentication.ui.theme
2 |
3 | import android.app.Activity
4 | import android.os.Build
5 | import androidx.compose.foundation.isSystemInDarkTheme
6 | import androidx.compose.material3.MaterialTheme
7 | import androidx.compose.material3.darkColorScheme
8 | import androidx.compose.material3.dynamicDarkColorScheme
9 | import androidx.compose.material3.dynamicLightColorScheme
10 | import androidx.compose.material3.lightColorScheme
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.SideEffect
13 | import androidx.compose.ui.graphics.toArgb
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.compose.ui.platform.LocalView
16 | import androidx.core.view.WindowCompat
17 |
18 | private val DarkColorScheme = darkColorScheme(
19 | primary = Purple80,
20 | secondary = PurpleGrey80,
21 | tertiary = Pink80
22 | )
23 |
24 | private val LightColorScheme = lightColorScheme(
25 | primary = Purple40,
26 | secondary = PurpleGrey40,
27 | tertiary = Pink40
28 |
29 | /* Other default colors to override
30 | background = Color(0xFFFFFBFE),
31 | surface = Color(0xFFFFFBFE),
32 | onPrimary = Color.White,
33 | onSecondary = Color.White,
34 | onTertiary = Color.White,
35 | onBackground = Color(0xFF1C1B1F),
36 | onSurface = Color(0xFF1C1B1F),
37 | */
38 | )
39 |
40 | @Composable
41 | fun PinAuthenticationTheme(
42 | darkTheme: Boolean = isSystemInDarkTheme(),
43 | // Dynamic color is available on Android 12+
44 | dynamicColor: Boolean = true,
45 | content: @Composable () -> Unit
46 | ) {
47 | val colorScheme = when {
48 | dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
49 | val context = LocalContext.current
50 | if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
51 | }
52 |
53 | darkTheme -> DarkColorScheme
54 | else -> LightColorScheme
55 | }
56 | val view = LocalView.current
57 | if (!view.isInEditMode) {
58 | SideEffect {
59 | val window = (view.context as Activity).window
60 | window.statusBarColor = colorScheme.primary.toArgb()
61 | WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
62 | }
63 | }
64 |
65 | MaterialTheme(
66 | colorScheme = colorScheme,
67 | typography = Typography,
68 | content = content
69 | )
70 | }
--------------------------------------------------------------------------------
/pinlibrary/src/main/res/drawable/ic_fingerprint_30.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/res/drawable/ic_fingerprint_white_30.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/res/drawable/ic_fingerprint_transparent_30.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/java/com/android/authentication/ui/screens/StartScreen.kt:
--------------------------------------------------------------------------------
1 | package com.android.authentication.ui.screens
2 |
3 | import androidx.compose.foundation.layout.Arrangement
4 | import androidx.compose.foundation.layout.Column
5 | import androidx.compose.foundation.layout.Spacer
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.foundation.layout.fillMaxWidth
8 | import androidx.compose.foundation.layout.height
9 | import androidx.compose.foundation.layout.padding
10 | import androidx.compose.material3.Button
11 | import androidx.compose.material3.Text
12 | import androidx.compose.runtime.Composable
13 | import androidx.compose.ui.Alignment
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.res.stringResource
16 | import androidx.compose.ui.unit.dp
17 | import com.android.authentication.R
18 | import com.android.pinlibrary.utils.enums.PinCodeScenario
19 |
20 | @Composable
21 | fun PinCodeStartScreen(isPinCodeCreated: Boolean, onButtonClick: (PinCodeScenario) -> Unit) {
22 | Column(
23 | modifier = Modifier.fillMaxSize(),
24 | verticalArrangement = Arrangement.Center,
25 | horizontalAlignment = Alignment.CenterHorizontally
26 | ) {
27 | Text(
28 | text = stringResource(
29 | id = if (isPinCodeCreated) R.string.pin_code_created
30 | else R.string.pin_code_not_created
31 | )
32 | )
33 |
34 | Spacer(modifier = Modifier.height(16.dp))
35 |
36 | if (!isPinCodeCreated) {
37 | CreatePinCodeButton(
38 | PinCodeScenario.CREATION,
39 | R.string.create_pin_code, onButtonClick
40 | )
41 | } else {
42 | CreatePinCodeButton(
43 | PinCodeScenario.VALIDATION,
44 | R.string.validate_pin_code,
45 | onButtonClick
46 | )
47 |
48 | Spacer(modifier = Modifier.height(16.dp))
49 |
50 | CreatePinCodeButton(
51 | PinCodeScenario.CHANGE,
52 | R.string.change_pin_code, onButtonClick
53 | )
54 |
55 | Spacer(modifier = Modifier.height(16.dp))
56 |
57 | CreatePinCodeButton(
58 | PinCodeScenario.DELETION,
59 | R.string.delete_pin_code, onButtonClick
60 | )
61 | }
62 | }
63 | }
64 |
65 | @Composable
66 | fun CreatePinCodeButton(
67 | scenario: PinCodeScenario,
68 | stringResId: Int,
69 | onButtonClick: (PinCodeScenario) -> Unit
70 | ) {
71 | Button(
72 | onClick = { onButtonClick(scenario) },
73 | modifier = Modifier
74 | .fillMaxWidth()
75 | .padding(horizontal = 30.dp)
76 | ) {
77 | Text(text = stringResource(id = stringResId))
78 | }
79 | }
80 |
81 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 0
4 | 1
5 | 2
6 | ABC
7 | 3
8 | DEF
9 | 4
10 | GHI
11 | 5
12 | JKL
13 | 6
14 | MNO
15 | 7
16 | PQRS
17 | 8
18 | TUV
19 | 9
20 | WXYZ
21 |
22 |
23 | Забыли PIN-код?
24 | Отключите %d-значный пин-код
25 | Придумайте PIN-код
26 | Введите PIN-код
27 | Введите PIN-код
28 | Повторите ваш PIN-код
29 | Отпечаток пальца
30 | Отпечаток принят
31 | Отпечаток не принят. Попробуйте снова.
32 | Неправильный PIN. Попыток осталось: %1$d
33 | PIN-коды не совпадают
34 | Пароль успешно сохранен
35 |
36 |
37 | Авторизация
38 | Используйте биометрию для авторизации
39 | Подтвердите свою личность
40 | Отмена
41 |
42 | Забыли PIN-код?
43 | Если вы забыли свой PIN-код, вы можете восстановить его, заново пройдя авторизацию.
44 | Ок
45 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/systemdesign/ripple/RippleEffect.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.systemdesign.ripple
2 |
3 | import androidx.compose.animation.core.animateFloatAsState
4 | import androidx.compose.animation.core.tween
5 | import androidx.compose.foundation.background
6 | import androidx.compose.foundation.gestures.detectTapGestures
7 | import androidx.compose.foundation.layout.Box
8 | import androidx.compose.foundation.layout.offset
9 | import androidx.compose.foundation.layout.size
10 | import androidx.compose.foundation.shape.RoundedCornerShape
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.runtime.getValue
13 | import androidx.compose.runtime.mutableFloatStateOf
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.runtime.setValue
17 | import androidx.compose.ui.Modifier
18 | import androidx.compose.ui.geometry.Offset
19 | import androidx.compose.ui.graphics.Brush
20 | import androidx.compose.ui.graphics.Color
21 | import androidx.compose.ui.input.pointer.pointerInput
22 | import androidx.compose.ui.unit.IntOffset
23 | import androidx.compose.ui.unit.dp
24 | import kotlin.math.max
25 |
26 | @Composable
27 | fun RippleView(
28 | modifier: Modifier = Modifier,
29 | content: @Composable () -> Unit
30 | ) {
31 | var isRippleAnimating by remember { mutableStateOf(false) }
32 | var rippleSize by remember { mutableFloatStateOf(0f) }
33 | var ripplePosition by remember { mutableStateOf(Offset(0f, 0f)) }
34 |
35 | val rippleAlpha by animateFloatAsState(
36 | targetValue = if (isRippleAnimating) 0f else 0.3f,
37 | animationSpec = tween(durationMillis = 400), label = ""
38 | )
39 |
40 | Box(
41 | modifier = modifier
42 | .pointerInput(Unit) {
43 | detectTapGestures { offset ->
44 | isRippleAnimating = true
45 | rippleSize = 2f * max(size.width, size.height)
46 | ripplePosition = offset
47 | }
48 | }
49 | ) {
50 |
51 | /** Nested content goes here **/
52 | content()
53 |
54 | if (isRippleAnimating) {
55 | Box(
56 | modifier = Modifier
57 | .size(rippleSize.dp)
58 | .offset {
59 | IntOffset(
60 | (ripplePosition.x - rippleSize / 2).toInt(),
61 | (ripplePosition.y - rippleSize / 2).toInt()
62 | )
63 | }
64 | .background(
65 | Brush.radialGradient(
66 | colors = listOf(
67 | Color.Black.copy(alpha = rippleAlpha),
68 | Color.Transparent
69 | ),
70 | radius = (rippleSize / 2),
71 | center = Offset((rippleSize / 2), (rippleSize / 2))
72 | ),
73 | shape = RoundedCornerShape(percent = 50)
74 | )
75 | )
76 | }
77 | }
78 | }
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/preferences/PinCodeManager.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.preferences
2 |
3 | import android.content.Context
4 | import android.content.SharedPreferences
5 | import android.os.Build
6 | import android.util.Base64
7 | import com.android.pinlibrary.utils.encryption.Encryptor
8 | import com.android.pinlibrary.utils.encryption.enums.Algorithm
9 | import java.security.SecureRandom
10 |
11 | class PinCodeManager(context: Context) : IPinCodeManager {
12 |
13 | private val sharedPreferences: SharedPreferences =
14 | context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
15 |
16 | override fun savePinCode(pinCode: String) {
17 | val salt = getSalt()
18 | val saltedPinCode = salt + pinCode + salt
19 | setAlgorithm(Algorithm.SHA256)
20 | val hashedPinCode = Encryptor().getSHA(saltedPinCode, Algorithm.SHA256)
21 |
22 | val editor = sharedPreferences.edit()
23 | editor.putString(PIN_CODE_KEY, hashedPinCode)
24 | editor.apply()
25 | }
26 |
27 | override fun loadPinCode(): String? {
28 | return sharedPreferences.getString(PIN_CODE_KEY, null)
29 | }
30 |
31 | override fun clearPinCode() {
32 | val editor = sharedPreferences.edit()
33 | editor.remove(PIN_CODE_KEY)
34 | editor.apply()
35 | }
36 |
37 | override fun isPinCodeCorrect(enteredPinCode: String): Boolean {
38 | val savedHashedPinCode = loadPinCode() ?: return false
39 |
40 | /**PIN was not saved*/
41 | val salt = getSalt()
42 | val saltedEnteredPinCode = salt + enteredPinCode + salt
43 | setAlgorithm(Algorithm.SHA256)
44 | val hashedEnteredPinCode = Encryptor().getSHA(saltedEnteredPinCode, Algorithm.SHA256)
45 | return savedHashedPinCode == hashedEnteredPinCode
46 | }
47 |
48 |
49 | private fun setAlgorithm(algorithm: Algorithm) {
50 | val editor = sharedPreferences.edit()
51 | editor.putString(PASSWORD_ALGORITHM_PREFERENCE_KEY, algorithm.value)
52 | editor.apply()
53 | }
54 |
55 | private fun getSalt(): String {
56 | var salt = sharedPreferences.getString(PASSWORD_SALT_PREFERENCE_KEY, null)
57 | if (salt == null) {
58 | salt = generateSalt()
59 | setSalt(salt)
60 | }
61 | return salt
62 | }
63 |
64 | private fun setSalt(salt: String) {
65 | val editor = sharedPreferences.edit()
66 | editor.putString(PASSWORD_SALT_PREFERENCE_KEY, salt)
67 | editor.apply()
68 | }
69 |
70 | private fun generateSalt(): String {
71 | val salt = ByteArray(KEY_LENGTH)
72 | return try {
73 | val sr =
74 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) SecureRandom.getInstanceStrong()
75 | else SecureRandom.getInstance("SHA1PRNG")
76 | sr.nextBytes(salt)
77 | Base64.encodeToString(salt, Base64.DEFAULT)
78 | } catch (e: Exception) {
79 | DEFAULT_PASSWORD_SALT
80 | }
81 | }
82 |
83 | companion object {
84 | private const val PREFS_NAME = "PinCodePrefs"
85 | private const val PIN_CODE_KEY = "pin_code"
86 | private const val DEFAULT_PASSWORD_SALT = "7xn7@c$"
87 | private const val KEY_LENGTH = 256
88 | private const val PASSWORD_SALT_PREFERENCE_KEY = "PASSWORD_SALT_PREFERENCE_KEY"
89 | private const val PASSWORD_ALGORITHM_PREFERENCE_KEY = "ALGORITHM"
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/screens/DeletePinScreen.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.screens
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.livedata.observeAsState
6 | import androidx.compose.runtime.mutableIntStateOf
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.ui.platform.LocalContext
11 | import androidx.compose.ui.res.stringResource
12 | import com.android.pinlibrary.R
13 | import com.android.pinlibrary.ui.components.PinCodeContent
14 | import com.android.pinlibrary.utils.preferences.AttemptCounter
15 | import com.android.pinlibrary.utils.preferences.PinCodeManager
16 | import com.android.pinlibrary.utils.state.PinCodeStateManager
17 | import com.android.pinlibrary.utils.state.deletepin.DeletePinScreenIntent
18 | import com.android.pinlibrary.utils.state.deletepin.DeletePinScreenState
19 | import com.android.pinlibrary.viewmodel.PinViewModel
20 |
21 | @Composable
22 | fun DeletePinScreen(
23 | viewModel: PinViewModel,
24 | pinCodeStateManager: PinCodeStateManager
25 | ) {
26 |
27 | val context = LocalContext.current
28 | val pinCodeManager = PinCodeManager(context = context)
29 | val attemptCounter = AttemptCounter(context = context)
30 | var isInitScenario by remember { mutableStateOf(false) }
31 | var isError by remember { mutableStateOf(false) }
32 | var headerId by remember { mutableIntStateOf(R.string.pin_code_step_create) }
33 | val notificationText = stringResource(id = R.string.empty)
34 | var notification by remember { mutableStateOf(notificationText) }
35 | val forgotMessageId by remember { mutableIntStateOf(R.string.pin_code_forgot_text) }
36 |
37 | if (!isInitScenario) {
38 | viewModel.processIntent(DeletePinScreenIntent.InitialState)
39 | isInitScenario = true
40 | }
41 |
42 | when (val state = viewModel.deletePinScreenState.observeAsState().value) {
43 | is DeletePinScreenState.InitialState -> {
44 | headerId = R.string.pin_code_step_unlock
45 | }
46 |
47 | is DeletePinScreenState.EnteringPinState -> {
48 | if (pinCodeManager.isPinCodeCorrect(state.pin)) {
49 | viewModel.processIntent(DeletePinScreenIntent.DeletePin)
50 | attemptCounter.resetAttempts()
51 | } else {
52 | if (!isError) {
53 | attemptCounter.decrementAttempts()
54 | notification = stringResource(
55 | id = R.string.pin_code_attempts,
56 | attemptCounter.getAttempts()
57 | )
58 | if (attemptCounter.getAttempts() == 0) {
59 | pinCodeStateManager.setLoginAttemptsExpended()
60 | }
61 | isError = true
62 | }
63 | }
64 | }
65 |
66 | is DeletePinScreenState.PinDeletedState -> {
67 | pinCodeStateManager.setDeletionSuccess(true)
68 | }
69 |
70 | is DeletePinScreenState.ErrorState -> {
71 | pinCodeStateManager.setDeletionSuccess(false)
72 | }
73 |
74 | else -> {}
75 | }
76 |
77 | PinCodeContent(
78 | headerId = headerId,
79 | notification = notification,
80 | forgotMessageId = forgotMessageId,
81 | pinCodeStateManager = pinCodeStateManager
82 | ) {
83 | val pinCode = it.toList().joinToString("")
84 | viewModel.processIntent(DeletePinScreenIntent.EnterPin(pinCode))
85 | isError = false
86 | }
87 | }
--------------------------------------------------------------------------------
/pinlibrary/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'maven-publish'
5 | }
6 |
7 | android {
8 | namespace 'com.android.pinlibrary'
9 | compileSdk 34
10 |
11 | defaultConfig {
12 | minSdk 24
13 | targetSdkVersion 34
14 |
15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
16 | consumerProguardFiles "consumer-rules.pro"
17 | }
18 |
19 | buildTypes {
20 | release {
21 | minifyEnabled false
22 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
23 | }
24 | }
25 | compileOptions {
26 | sourceCompatibility JavaVersion.VERSION_17
27 | targetCompatibility JavaVersion.VERSION_17
28 | }
29 | kotlinOptions {
30 | jvmTarget = '17'
31 | }
32 |
33 | buildFeatures {
34 | compose true
35 | }
36 | composeOptions {
37 | kotlinCompilerExtensionVersion '1.5.5'
38 | }
39 | packagingOptions {
40 | resources {
41 | excludes += '/META-INF/{AL2.0,LGPL2.1}'
42 | }
43 | }
44 |
45 | publishing {
46 | singleVariant("release") {
47 | withSourcesJar()
48 | withJavadocJar()
49 | }
50 | }
51 |
52 | }
53 | afterEvaluate {
54 | publishing {
55 | publications {
56 | release(MavenPublication) {
57 | from components.release
58 | groupId = 'com.android.pinlibrary'
59 | artifactId = 'pinlibrary'
60 | version = '1.0.5'
61 | }
62 | }
63 | repositories {
64 | maven {
65 | name = "JitPack"
66 | url = uri("https://jitpack.io")
67 | }
68 | }
69 | }
70 | }
71 |
72 | dependencies {
73 | // AndroidX Core и Kotlin Extensions
74 | implementation 'androidx.core:core-ktx:1.13.1'
75 | implementation platform('org.jetbrains.kotlin:kotlin-bom:1.9.24')
76 |
77 | // AndroidX Lifecycle
78 | implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.8.4'
79 | implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.8.4"
80 |
81 | // AndroidX Activity Compose
82 | implementation 'androidx.activity:activity-compose:1.9.1'
83 |
84 | // Unit-test с JUnit
85 | testImplementation 'junit:junit:4.13.2'
86 |
87 | // AndroidX Test с JUnit
88 | androidTestImplementation 'androidx.test.ext:junit:1.2.1'
89 |
90 | // Espresso для UI-test
91 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.6.1'
92 |
93 | // AndroidX Compose для UI-test
94 | androidTestImplementation platform('androidx.compose:compose-bom:2024.06.00')
95 | androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
96 |
97 | // AndroidX Compose UI Tooling (только для отладки)
98 | debugImplementation 'androidx.compose.ui:ui-tooling'
99 |
100 | // Compose version
101 | implementation platform('androidx.compose:compose-bom:2024.06.00')
102 |
103 | // Compose UI
104 | implementation 'androidx.compose.ui:ui'
105 |
106 | // Compose Graphics
107 | implementation 'androidx.compose.ui:ui-graphics'
108 |
109 | // Compose UI Tooling
110 | implementation 'androidx.compose.ui:ui-tooling-preview'
111 |
112 | // Compose Material3
113 | implementation 'androidx.compose.material3:material3'
114 |
115 | // Compose Runtime
116 | implementation "androidx.compose.runtime:runtime:1.6.8"
117 | implementation "androidx.compose.runtime:runtime-livedata:1.6.8"
118 |
119 | // Biometric
120 | implementation "androidx.biometric:biometric-ktx:1.2.0-alpha05"
121 | }
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/screens/CreatePinScreen.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.screens
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.livedata.observeAsState
6 | import androidx.compose.runtime.mutableIntStateOf
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.ui.platform.LocalContext
11 | import androidx.compose.ui.res.stringResource
12 | import com.android.pinlibrary.R
13 | import com.android.pinlibrary.ui.components.PinCodeContent
14 | import com.android.pinlibrary.utils.preferences.AttemptCounter
15 | import com.android.pinlibrary.utils.preferences.PinCodeManager
16 | import com.android.pinlibrary.utils.state.PinCodeStateManager
17 | import com.android.pinlibrary.utils.state.createpin.CreatePinScreenIntent
18 | import com.android.pinlibrary.utils.state.createpin.CreatePinScreenState
19 | import com.android.pinlibrary.viewmodel.PinViewModel
20 |
21 | private var tempPinCode: String = ""
22 |
23 | @Composable
24 | fun CreatePinScreen(
25 | viewModel: PinViewModel,
26 | pinCodeStateManager: PinCodeStateManager
27 | ) {
28 | var isInitScenario by remember { mutableStateOf(false) }
29 |
30 | if (!isInitScenario) {
31 | viewModel.processIntent(CreatePinScreenIntent.InitialState)
32 | isInitScenario = true
33 | }
34 |
35 | val context = LocalContext.current
36 | val pinCodeManager = PinCodeManager(context = context)
37 | val attemptCounter = AttemptCounter(context = context)
38 |
39 | var headerId by remember { mutableIntStateOf(R.string.pin_code_step_create) }
40 | val notificationText = stringResource(id = R.string.empty)
41 | var notification by remember { mutableStateOf(notificationText) }
42 | val forgotMessageId by remember { mutableIntStateOf(R.string.pin_code_forgot_text) }
43 | var isPinEntering by remember { mutableStateOf(false) }
44 |
45 | when (val state = viewModel.createPinScreenState.observeAsState().value) {
46 |
47 | is CreatePinScreenState.InitialState -> {
48 | headerId = R.string.pin_code_step_create
49 | isPinEntering = false
50 | }
51 |
52 | is CreatePinScreenState.EnteringPinState -> {
53 | headerId = R.string.pin_code_step_enable_confirm
54 | notification = stringResource(id = R.string.empty)
55 | isPinEntering = true
56 | tempPinCode = state.pin
57 | }
58 |
59 | is CreatePinScreenState.ConfirmingPinState -> {
60 | if (tempPinCode == state.pin) {
61 | pinCodeManager.savePinCode(state.pin)
62 | viewModel.processIntent(CreatePinScreenIntent.CreatePin)
63 | } else {
64 | notification = stringResource(id = R.string.pin_codes_do_not_match)
65 | viewModel.processIntent(CreatePinScreenIntent.InitialState)
66 | }
67 | }
68 |
69 | is CreatePinScreenState.PinCreatedState -> {
70 | pinCodeStateManager.setCreationSuccess(true)
71 | attemptCounter.resetAttempts()
72 | }
73 |
74 | is CreatePinScreenState.ErrorState -> {
75 | pinCodeStateManager.setCreationSuccess(false)
76 | notification = stringResource(id = R.string.pin_codes_do_not_match)
77 | }
78 |
79 | else -> {}
80 | }
81 |
82 | PinCodeContent(
83 | headerId = headerId,
84 | notification = notification,
85 | forgotMessageId = forgotMessageId
86 | ) {
87 | val pinCode = it.toList().joinToString("")
88 | if (!isPinEntering) {
89 | viewModel.processIntent(CreatePinScreenIntent.EnterPin(pinCode))
90 | } else {
91 | viewModel.processIntent(CreatePinScreenIntent.ConfirmPin(pinCode))
92 | }
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/biometric/BiometricHelper.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.biometric
2 |
3 | import android.Manifest
4 | import android.content.Context
5 | import android.content.pm.PackageManager
6 | import android.hardware.biometrics.BiometricPrompt
7 | import android.os.Build
8 | import android.os.CancellationSignal
9 | import androidx.activity.compose.rememberLauncherForActivityResult
10 | import androidx.activity.result.contract.ActivityResultContracts
11 | import androidx.annotation.RequiresApi
12 | import androidx.biometric.BiometricManager
13 | import androidx.compose.runtime.Composable
14 | import androidx.compose.ui.platform.LocalContext
15 | import androidx.core.content.ContextCompat
16 | import com.android.pinlibrary.R
17 | import java.util.concurrent.Executor
18 |
19 | /**
20 | * Field requires API level 28 (current min is 24): android
21 | */
22 | @RequiresApi(Build.VERSION_CODES.P)
23 | class BiometricHelper(private val context: Context) {
24 |
25 | private var executor: Executor? = null
26 |
27 | private fun isBiometricSupported(): Boolean {
28 | val biometricManager = BiometricManager.from(context)
29 | return biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
30 | }
31 |
32 | private fun startAuthentication(authenticationCallback: BiometricPrompt.AuthenticationCallback) {
33 | executor = ContextCompat.getMainExecutor(context)
34 | val cancellationSignal = CancellationSignal()
35 | val biometricPrompt = executor?.let {
36 | BiometricPrompt.Builder(context)
37 | .setTitle(context.getString(R.string.authorization))
38 | .setSubtitle(context.getString(R.string.use_biometry))
39 | .setDescription(context.getString(R.string.confirm_identity))
40 | .setNegativeButton(context.getString(R.string.cancel_action), it) { _, _ -> }
41 | .build()
42 | }
43 |
44 | executor?.let {
45 | biometricPrompt?.authenticate(
46 | cancellationSignal,
47 | it,
48 | authenticationCallback
49 | )
50 | }
51 | }
52 |
53 | fun openBiometricScanner(authenticationCallback: BiometricPrompt.AuthenticationCallback) {
54 |
55 | if (isBiometricSupported()) {
56 | startAuthentication(authenticationCallback)
57 | } else {
58 | // Биометрическая аутентификация не поддерживается
59 | }
60 | }
61 | }
62 |
63 | /**
64 | * Field requires API level 28 (current min is 24): android
65 | */
66 | @Composable
67 | @RequiresApi(Build.VERSION_CODES.P)
68 | fun BiometricScannerScreen(authenticationCallback: BiometricPrompt.AuthenticationCallback) {
69 | val context = LocalContext.current
70 | val biometricHelper = BiometricHelper(context)
71 |
72 | /**
73 | * The process of creating the requestPermissionLauncher that is used to request permission
74 | */
75 | val requestPermissionLauncher = rememberLauncherForActivityResult(
76 | contract = ActivityResultContracts.RequestPermission()
77 | ) { isGranted ->
78 | if (isGranted) {
79 | biometricHelper.openBiometricScanner(authenticationCallback)
80 | } else {
81 | /**
82 | * Perform actions if not granted
83 | */
84 | }
85 | }
86 |
87 | when (ContextCompat.checkSelfPermission(
88 | context,
89 | Manifest.permission.USE_BIOMETRIC
90 | )) {
91 | PackageManager.PERMISSION_GRANTED -> {
92 | biometricHelper.openBiometricScanner(authenticationCallback)
93 | }
94 |
95 | else -> {
96 | /**
97 | * Request permission if not granted
98 | */
99 | requestPermissionLauncher.launch(Manifest.permission.USE_BIOMETRIC)
100 | }
101 | }
102 | }
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | xmlns:android
18 |
19 | ^$
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | xmlns:.*
29 |
30 | ^$
31 |
32 |
33 | BY_NAME
34 |
35 |
36 |
37 |
38 |
39 |
40 | .*:id
41 |
42 | http://schemas.android.com/apk/res/android
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | .*:name
52 |
53 | http://schemas.android.com/apk/res/android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | name
63 |
64 | ^$
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | style
74 |
75 | ^$
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | .*
85 |
86 | ^$
87 |
88 |
89 | BY_NAME
90 |
91 |
92 |
93 |
94 |
95 |
96 | .*
97 |
98 | http://schemas.android.com/apk/res/android
99 |
100 |
101 | ANDROID_ATTRIBUTE_ORDER
102 |
103 |
104 |
105 |
106 |
107 |
108 | .*
109 |
110 | .*
111 |
112 |
113 | BY_NAME
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/components/Keyboard.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.components
2 |
3 | import android.os.Build
4 | import androidx.compose.foundation.isSystemInDarkTheme
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.Row
8 | import androidx.compose.foundation.layout.padding
9 | import androidx.compose.foundation.layout.wrapContentSize
10 | import androidx.compose.runtime.Composable
11 | import androidx.compose.ui.Alignment
12 | import androidx.compose.ui.Modifier
13 | import androidx.compose.ui.platform.LocalContext
14 | import androidx.compose.ui.res.stringResource
15 | import com.android.pinlibrary.R
16 | import com.android.pinlibrary.ui.systemdesign.theme.Dimens
17 | import com.android.pinlibrary.utils.enums.PinCodeScenario
18 | import com.android.pinlibrary.utils.keyboard.KeyboardButtonEnum
19 | import com.android.pinlibrary.utils.preferences.SettingsManager
20 |
21 | @Composable
22 | fun Keyboard(pinCodeScenario: PinCodeScenario) {
23 |
24 | val rowModifier = Modifier.wrapContentSize()
25 |
26 | Column(
27 | modifier = Modifier
28 | .wrapContentSize()
29 | .padding(
30 | vertical = Dimens.verticalKeyboardPadding,
31 | horizontal = Dimens.horizontalKeyboardPadding
32 | ),
33 | verticalArrangement = Arrangement.Center,
34 | horizontalAlignment = Alignment.CenterHorizontally
35 | ) {
36 | Row(
37 | modifier = rowModifier
38 | ) {
39 | NumberButton(
40 | stringResource(id = R.string.button1_large_text),
41 | KeyboardButtonEnum.BUTTON_1
42 | )
43 | NumberButton(
44 | stringResource(id = R.string.button2_large_text),
45 | KeyboardButtonEnum.BUTTON_2
46 | )
47 | NumberButton(
48 | stringResource(id = R.string.button3_large_text),
49 | KeyboardButtonEnum.BUTTON_3
50 | )
51 | }
52 | Row(
53 | modifier = rowModifier
54 | ) {
55 | NumberButton(
56 | stringResource(id = R.string.button4_large_text),
57 | KeyboardButtonEnum.BUTTON_4
58 | )
59 | NumberButton(
60 | stringResource(id = R.string.button5_large_text),
61 | KeyboardButtonEnum.BUTTON_5
62 | )
63 | NumberButton(
64 | stringResource(id = R.string.button6_large_text),
65 | KeyboardButtonEnum.BUTTON_6
66 | )
67 | }
68 | Row(
69 | modifier = rowModifier
70 | ) {
71 | NumberButton(
72 | stringResource(id = R.string.button7_large_text),
73 | KeyboardButtonEnum.BUTTON_7
74 | )
75 | NumberButton(
76 | stringResource(id = R.string.button8_large_text),
77 | KeyboardButtonEnum.BUTTON_8
78 | )
79 | NumberButton(
80 | stringResource(id = R.string.button9_large_text),
81 | KeyboardButtonEnum.BUTTON_9
82 | )
83 | }
84 | Row(
85 | modifier = rowModifier
86 | ) {
87 | val settingsManager = SettingsManager(context = LocalContext.current)
88 | if (
89 | pinCodeScenario == PinCodeScenario.VALIDATION
90 | && settingsManager.isBiometricEnabled()
91 | && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P
92 | ) {
93 | ImageButton(
94 | if (isSystemInDarkTheme()) R.drawable.ic_fingerprint_white_30 else R.drawable.ic_fingerprint_30,
95 | KeyboardButtonEnum.BUTTON_FINGERPRINT
96 | )
97 | } else {
98 | ImageButtonStub(R.drawable.ic_fingerprint_transparent_30)
99 | }
100 | NumberButton(
101 | stringResource(id = R.string.button0_large_text),
102 | KeyboardButtonEnum.BUTTON_0
103 | )
104 | ImageButton(
105 | if (isSystemInDarkTheme()) R.drawable.ic_clear_white_30 else R.drawable.ic_clear_30,
106 | KeyboardButtonEnum.BUTTON_CLEAR
107 | )
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/components/PinCodeContent.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.components
2 |
3 | import android.hardware.biometrics.BiometricPrompt
4 | import android.os.Build
5 | import androidx.compose.foundation.layout.Arrangement
6 | import androidx.compose.foundation.layout.Column
7 | import androidx.compose.foundation.layout.fillMaxSize
8 | import androidx.compose.runtime.Composable
9 | import androidx.compose.runtime.DisposableEffect
10 | import androidx.compose.runtime.LaunchedEffect
11 | import androidx.compose.runtime.getValue
12 | import androidx.compose.runtime.mutableIntStateOf
13 | import androidx.compose.runtime.mutableStateListOf
14 | import androidx.compose.runtime.mutableStateOf
15 | import androidx.compose.runtime.remember
16 | import androidx.compose.runtime.setValue
17 | import androidx.compose.runtime.snapshots.SnapshotStateList
18 | import androidx.compose.ui.Alignment
19 | import androidx.compose.ui.Modifier
20 | import androidx.compose.ui.platform.LocalContext
21 | import androidx.compose.ui.platform.LocalLifecycleOwner
22 | import androidx.compose.ui.res.stringResource
23 | import androidx.lifecycle.Lifecycle
24 | import androidx.lifecycle.LifecycleEventObserver
25 | import com.android.pinlibrary.ui.systemdesign.indicator.RoundedBoxesRow
26 | import com.android.pinlibrary.utils.biometric.BiometricScannerScreen
27 | import com.android.pinlibrary.utils.enums.PinCodeScenario
28 | import com.android.pinlibrary.utils.helpers.fillArrayWithButtons
29 | import com.android.pinlibrary.utils.keyboard.KeyboardButtonEnum
30 | import com.android.pinlibrary.utils.listeners.NumberListener
31 | import com.android.pinlibrary.utils.preferences.SettingsManager
32 | import com.android.pinlibrary.utils.state.PinCodeStateManager
33 |
34 | @Composable
35 | fun PinCodeContent(
36 | headerId: Int,
37 | notification: String,
38 | forgotMessageId: Int,
39 | pinCodeStateManager: PinCodeStateManager? = null,
40 | pinCodeScenario: PinCodeScenario = PinCodeScenario.STUB,
41 | authenticationCallback: BiometricPrompt.AuthenticationCallback? = null,
42 | onClick: (buttonArray: SnapshotStateList) -> Unit
43 | ) {
44 | val settingsManager = SettingsManager(context = LocalContext.current)
45 | val pinLength = settingsManager.getPinLength()
46 | val buttonArray = remember { mutableStateListOf() }
47 | var quantity by remember { mutableIntStateOf(0) }
48 | var showBiometricScreen by remember { mutableStateOf(false) }
49 | val lifecycleOwner = LocalLifecycleOwner.current
50 |
51 | DisposableEffect(lifecycleOwner) {
52 | val observer = LifecycleEventObserver { _, event ->
53 | if (event == Lifecycle.Event.ON_CREATE) {
54 | showBiometricScreen = settingsManager.isAutoLaunchBiometricEnabled()
55 | }
56 | }
57 | lifecycleOwner.lifecycle.addObserver(observer)
58 | onDispose {
59 | lifecycleOwner.lifecycle.removeObserver(observer)
60 | }
61 | }
62 |
63 | LaunchedEffect(onNumberClickListener) {
64 | onNumberClickListener = NumberListener { keyboardEnum ->
65 | when (keyboardEnum) {
66 | KeyboardButtonEnum.BUTTON_FINGERPRINT -> showBiometricScreen = true
67 | else -> {
68 | fillArrayWithButtons(keyboardEnum, buttonArray, pinLength) { updatedArray ->
69 | quantity = updatedArray.size
70 | }
71 | if (quantity == pinLength) {
72 | onClick(buttonArray)
73 | quantity = 0
74 | buttonArray.clear()
75 | }
76 | showBiometricScreen = false
77 | }
78 | }
79 | }
80 | }
81 |
82 | if (showBiometricScreen && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && authenticationCallback != null) {
83 | BiometricScannerScreen(authenticationCallback = authenticationCallback)
84 | showBiometricScreen = false
85 | }
86 |
87 | Column(
88 | modifier = Modifier.fillMaxSize(),
89 | verticalArrangement = Arrangement.Center,
90 | horizontalAlignment = Alignment.CenterHorizontally
91 | ) {
92 | PinCodeScreenHeader(stringResource(id = headerId))
93 | PinCodeScreenNotification(text = notification)
94 | RoundedBoxesRow(startQuantity = pinLength, quantity = quantity)
95 | Keyboard(pinCodeScenario = pinCodeScenario)
96 | pinCodeStateManager?.let {
97 | PinCodeScreenForgot(stringResource(id = forgotMessageId), pinCodeStateManager = it)
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/components/KeyboardButton.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.components
2 |
3 | import androidx.compose.foundation.Image
4 | import androidx.compose.foundation.clickable
5 | import androidx.compose.foundation.layout.Box
6 | import androidx.compose.foundation.layout.padding
7 | import androidx.compose.foundation.layout.size
8 | import androidx.compose.foundation.layout.wrapContentSize
9 | import androidx.compose.foundation.shape.CircleShape
10 | import androidx.compose.material3.Text
11 | import androidx.compose.runtime.Composable
12 | import androidx.compose.ui.Alignment
13 | import androidx.compose.ui.Modifier
14 | import androidx.compose.ui.draw.clip
15 | import androidx.compose.ui.res.painterResource
16 | import androidx.compose.ui.text.TextStyle
17 | import com.android.pinlibrary.ui.systemdesign.ripple.RippleView
18 | import com.android.pinlibrary.ui.systemdesign.theme.Dimens
19 | import com.android.pinlibrary.utils.keyboard.KeyboardButtonEnum
20 | import com.android.pinlibrary.utils.listeners.NumberListener
21 |
22 | @Composable
23 | fun NumberButton(
24 | number: String,
25 | keyboardEnum: KeyboardButtonEnum
26 | ) {
27 | Box(
28 | modifier = Modifier
29 | .wrapContentSize()
30 | .padding(
31 | vertical = Dimens.verticalKeyboardButtonPadding,
32 | horizontal = Dimens.horizontalKeyboardButtonPadding
33 | )
34 | ) {
35 | RippleView(
36 | modifier = Modifier.size(Dimens.keyBoardButtonSize)
37 | ) {
38 | Box(
39 | modifier = Modifier
40 | .size(Dimens.keyBoardButtonSize)
41 | .clip(CircleShape)
42 | .clickable {
43 | setNumberClickListener(
44 | onNumberClickListener = onNumberClickListener,
45 | keyboardEnum = keyboardEnum
46 | )
47 | },
48 | contentAlignment = Alignment.Center
49 | ) {
50 | Text(
51 | text = number,
52 | style = TextStyle(fontSize = Dimens.keyBoardButtonFontSize)
53 | )
54 | }
55 | }
56 | }
57 | }
58 |
59 | @Composable
60 | fun ImageButton(
61 | resourceId: Int,
62 | keyboardEnum: KeyboardButtonEnum
63 | ) {
64 | val painter = painterResource(id = resourceId)
65 | Box(
66 | modifier = Modifier
67 | .wrapContentSize()
68 | .padding(
69 | vertical = Dimens.verticalKeyboardButtonPadding,
70 | horizontal = Dimens.horizontalKeyboardButtonPadding
71 | )
72 | ) {
73 | RippleView(
74 | modifier = Modifier.size(Dimens.keyBoardButtonSize)
75 | ) {
76 | Box(
77 | modifier = Modifier
78 | .size(Dimens.keyBoardButtonSize)
79 | .clip(CircleShape)
80 | .clickable {
81 | setNumberClickListener(
82 | onNumberClickListener = onNumberClickListener,
83 | keyboardEnum = keyboardEnum
84 | )
85 | },
86 | contentAlignment = Alignment.Center
87 | ) {
88 | Image(
89 | painter = painter,
90 | contentDescription = null
91 | )
92 | }
93 | }
94 | }
95 | }
96 |
97 | @Composable
98 | fun ImageButtonStub(
99 | resourceId: Int
100 | ) {
101 | val painter = painterResource(id = resourceId)
102 | Box(
103 | modifier = Modifier
104 | .wrapContentSize()
105 | .padding(
106 | vertical = Dimens.verticalKeyboardButtonPadding,
107 | horizontal = Dimens.horizontalKeyboardButtonPadding
108 | )
109 | ) {
110 | Box(
111 | modifier = Modifier
112 | .size(Dimens.keyBoardButtonSize)
113 | .clip(CircleShape),
114 | contentAlignment = Alignment.Center
115 | ) {
116 | Image(
117 | painter = painter,
118 | contentDescription = null
119 | )
120 | }
121 | }
122 | }
123 |
124 | internal var onNumberClickListener: NumberListener? = null
125 | internal fun setNumberClickListener(
126 | onNumberClickListener: NumberListener?,
127 | keyboardEnum: KeyboardButtonEnum
128 | ) {
129 | onNumberClickListener?.onNumberTriggered(keyboardEnum)
130 | }
131 |
132 |
133 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/screens/ValidationPinScreen.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.screens
2 |
3 | import android.hardware.biometrics.BiometricPrompt
4 | import android.os.Build
5 | import androidx.annotation.RequiresApi
6 | import androidx.compose.runtime.Composable
7 | import androidx.compose.runtime.getValue
8 | import androidx.compose.runtime.livedata.observeAsState
9 | import androidx.compose.runtime.mutableIntStateOf
10 | import androidx.compose.runtime.mutableStateOf
11 | import androidx.compose.runtime.remember
12 | import androidx.compose.runtime.setValue
13 | import androidx.compose.ui.platform.LocalContext
14 | import androidx.compose.ui.res.stringResource
15 | import com.android.pinlibrary.R
16 | import com.android.pinlibrary.ui.components.PinCodeContent
17 | import com.android.pinlibrary.utils.enums.PinCodeScenario
18 | import com.android.pinlibrary.utils.preferences.AttemptCounter
19 | import com.android.pinlibrary.utils.preferences.PinCodeManager
20 | import com.android.pinlibrary.utils.state.PinCodeStateManager
21 | import com.android.pinlibrary.utils.state.validationpin.ValidationPinScreenIntent
22 | import com.android.pinlibrary.utils.state.validationpin.ValidationPinScreenState
23 | import com.android.pinlibrary.viewmodel.PinViewModel
24 |
25 | @Composable
26 | fun ValidationPinScreen(
27 | viewModel: PinViewModel,
28 | pinCodeStateManager: PinCodeStateManager,
29 | pinCodeScenario: PinCodeScenario
30 | ) {
31 |
32 | val context = LocalContext.current
33 | val pinCodeManager = PinCodeManager(context = context)
34 | val attemptCounter = AttemptCounter(context = context)
35 | var isInitScenario by remember { mutableStateOf(false) }
36 | var isError by remember { mutableStateOf(false) }
37 | var headerId by remember { mutableIntStateOf(R.string.pin_code_step_create) }
38 | val notificationText = stringResource(id = R.string.empty)
39 | var notification by remember { mutableStateOf(notificationText) }
40 | val forgotMessageId by remember { mutableIntStateOf(R.string.pin_code_forgot_text) }
41 |
42 | if (!isInitScenario && pinCodeStateManager.isValidationEnabled) {
43 | viewModel.processIntent(ValidationPinScreenIntent.InitialState)
44 | isInitScenario = true
45 | }
46 |
47 | when (val state = viewModel.validationPinScreenState.observeAsState().value) {
48 | is ValidationPinScreenState.InitialState -> {
49 | headerId = R.string.pin_code_step_unlock
50 | }
51 |
52 | is ValidationPinScreenState.EnteringPinState -> {
53 | if (pinCodeManager.isPinCodeCorrect(state.pin)) {
54 | viewModel.processIntent(ValidationPinScreenIntent.ValidatePin)
55 | attemptCounter.resetAttempts()
56 | } else {
57 | if (!isError) {
58 | attemptCounter.decrementAttempts()
59 | notification = stringResource(
60 | id = R.string.pin_code_attempts,
61 | attemptCounter.getAttempts()
62 | )
63 | if (attemptCounter.getAttempts() == 0) {
64 | pinCodeStateManager.setLoginAttemptsExpended()
65 | }
66 | isError = true
67 | }
68 | }
69 | }
70 |
71 | is ValidationPinScreenState.PinValidatedState -> {
72 | pinCodeStateManager.setValidationSuccess(true)
73 | }
74 |
75 | is ValidationPinScreenState.ErrorState -> {
76 | pinCodeStateManager.setValidationSuccess(false)
77 | }
78 |
79 | else -> {}
80 | }
81 |
82 | val authenticationCallback = @RequiresApi(Build.VERSION_CODES.P)
83 | object : BiometricPrompt.AuthenticationCallback() {
84 | override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
85 | pinCodeStateManager.setBiometricAuthentication(false)
86 | }
87 |
88 | override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
89 | pinCodeStateManager.setBiometricAuthentication(true)
90 | }
91 |
92 | override fun onAuthenticationFailed() {
93 | pinCodeStateManager.setBiometricAuthentication(false)
94 | }
95 | }
96 |
97 | PinCodeContent(
98 | headerId = headerId,
99 | notification = notification,
100 | pinCodeScenario = pinCodeScenario,
101 | authenticationCallback = authenticationCallback,
102 | forgotMessageId = forgotMessageId,
103 | pinCodeStateManager = pinCodeStateManager
104 | ) { pinValue ->
105 | val pinCode = pinValue.toList().joinToString("")
106 | viewModel.processIntent(ValidationPinScreenIntent.EnterPin(pinCode))
107 | isError = false
108 | }
109 | }
110 |
111 |
112 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/viewmodel/PinViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.viewmodel
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import com.android.pinlibrary.utils.state.changepin.ChangePinScreenIntent
7 | import com.android.pinlibrary.utils.state.changepin.ChangePinScreenState
8 | import com.android.pinlibrary.utils.state.createpin.CreatePinScreenIntent
9 | import com.android.pinlibrary.utils.state.createpin.CreatePinScreenState
10 | import com.android.pinlibrary.utils.state.deletepin.DeletePinScreenIntent
11 | import com.android.pinlibrary.utils.state.deletepin.DeletePinScreenState
12 | import com.android.pinlibrary.utils.state.validationpin.ValidationPinScreenIntent
13 | import com.android.pinlibrary.utils.state.validationpin.ValidationPinScreenState
14 |
15 | class PinViewModel : ViewModel() {
16 |
17 | private val _changePinScreenState = MutableLiveData()
18 | val changePinScreenState: LiveData
19 | get() = _changePinScreenState
20 |
21 | private val _createPinScreenState = MutableLiveData()
22 | val createPinScreenState: LiveData
23 | get() = _createPinScreenState
24 |
25 | private val _deletePinScreenState = MutableLiveData()
26 | val deletePinScreenState: LiveData
27 | get() = _deletePinScreenState
28 |
29 | private val _validationPinScreenState = MutableLiveData()
30 | val validationPinScreenState: LiveData
31 | get() = _validationPinScreenState
32 |
33 | fun processIntent(intent: ChangePinScreenIntent) {
34 | when (intent) {
35 | is ChangePinScreenIntent.InitialState -> {
36 | _changePinScreenState.value = ChangePinScreenState.InitialState
37 | }
38 |
39 | is ChangePinScreenIntent.EnterCurrentPin -> {
40 | _changePinScreenState.value =
41 | ChangePinScreenState.EnteringCurrentPin(intent.currentPin)
42 | }
43 |
44 | is ChangePinScreenIntent.EnterNewPin -> {
45 | _changePinScreenState.value = ChangePinScreenState.EnteringNewPin(intent.newPin)
46 | }
47 |
48 | is ChangePinScreenIntent.ConfirmNewPin -> {
49 | _changePinScreenState.value = ChangePinScreenState.ConfirmingNewPin(intent.newPin)
50 | }
51 |
52 | is ChangePinScreenIntent.ChangePin -> {
53 | _changePinScreenState.value = ChangePinScreenState.PinChangedSuccess
54 | }
55 | }
56 | }
57 |
58 | fun processIntent(intent: CreatePinScreenIntent) {
59 | when (intent) {
60 | is CreatePinScreenIntent.InitialState -> {
61 | _createPinScreenState.value = CreatePinScreenState.InitialState
62 | }
63 |
64 | is CreatePinScreenIntent.EnterPin -> {
65 | _createPinScreenState.value = CreatePinScreenState.EnteringPinState(intent.pin)
66 | }
67 |
68 | is CreatePinScreenIntent.ConfirmPin -> {
69 | _createPinScreenState.value = CreatePinScreenState.ConfirmingPinState(intent.pin)
70 | }
71 |
72 | is CreatePinScreenIntent.CreatePin -> {
73 | _createPinScreenState.value = CreatePinScreenState.PinCreatedState
74 | }
75 | }
76 | }
77 |
78 | fun processIntent(intent: DeletePinScreenIntent) {
79 | when (intent) {
80 | is DeletePinScreenIntent.InitialState -> {
81 | _deletePinScreenState.value = DeletePinScreenState.InitialState
82 | }
83 |
84 | is DeletePinScreenIntent.EnterPin -> {
85 | _deletePinScreenState.value = DeletePinScreenState.EnteringPinState(intent.pin)
86 | }
87 |
88 | is DeletePinScreenIntent.DeletePin -> {
89 | _deletePinScreenState.value = DeletePinScreenState.PinDeletedState
90 | }
91 | }
92 | }
93 |
94 | fun processIntent(intent: ValidationPinScreenIntent) {
95 | when (intent) {
96 | is ValidationPinScreenIntent.InitialState -> {
97 | _validationPinScreenState.value = ValidationPinScreenState.InitialState
98 | }
99 |
100 | is ValidationPinScreenIntent.EnterPin -> {
101 | _validationPinScreenState.value =
102 | ValidationPinScreenState.EnteringPinState(intent.pin)
103 | }
104 |
105 | is ValidationPinScreenIntent.ValidatePin -> {
106 | _validationPinScreenState.value = ValidationPinScreenState.PinValidatedState
107 | }
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/ui/screens/ChangePinScreen.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.ui.screens
2 |
3 | import androidx.compose.runtime.Composable
4 | import androidx.compose.runtime.getValue
5 | import androidx.compose.runtime.livedata.observeAsState
6 | import androidx.compose.runtime.mutableIntStateOf
7 | import androidx.compose.runtime.mutableStateOf
8 | import androidx.compose.runtime.remember
9 | import androidx.compose.runtime.setValue
10 | import androidx.compose.ui.platform.LocalContext
11 | import androidx.compose.ui.res.stringResource
12 | import com.android.pinlibrary.R
13 | import com.android.pinlibrary.ui.components.PinCodeContent
14 | import com.android.pinlibrary.utils.preferences.AttemptCounter
15 | import com.android.pinlibrary.utils.preferences.PinCodeManager
16 | import com.android.pinlibrary.utils.state.PinCodeStateManager
17 | import com.android.pinlibrary.utils.state.changepin.ChangePinScreenIntent
18 | import com.android.pinlibrary.utils.state.changepin.ChangePinScreenState
19 | import com.android.pinlibrary.viewmodel.PinViewModel
20 |
21 | private var tempPinCode: String = ""
22 |
23 | @Composable
24 | fun ChangePinScreen(
25 | viewModel: PinViewModel,
26 | pinCodeStateManager: PinCodeStateManager
27 | ) {
28 |
29 | val context = LocalContext.current
30 | val pinCodeManager = PinCodeManager(context = context)
31 | val attemptCounter = AttemptCounter(context = context)
32 | var isError by remember { mutableStateOf(false) }
33 | var isInitScenario by remember { mutableStateOf(false) }
34 | var headerId by remember { mutableIntStateOf(R.string.pin_code_step_create) }
35 | val notificationText = stringResource(id = R.string.empty)
36 | var notification by remember { mutableStateOf(notificationText) }
37 | val forgotMessageId by remember { mutableIntStateOf(R.string.pin_code_forgot_text) }
38 | var isPinEntering by remember { mutableStateOf(false) }
39 | var isNewPinEntering by remember { mutableStateOf(false) }
40 |
41 |
42 | if (!isInitScenario) {
43 | viewModel.processIntent(ChangePinScreenIntent.InitialState)
44 | isInitScenario = true
45 | }
46 |
47 | when (val state = viewModel.changePinScreenState.observeAsState().value) {
48 | is ChangePinScreenState.InitialState -> {
49 | headerId = R.string.pin_code_step_unlock
50 | isPinEntering = false
51 | isNewPinEntering = false
52 | }
53 |
54 | is ChangePinScreenState.EnteringCurrentPin -> {
55 | if (pinCodeManager.isPinCodeCorrect(state.currentPin)) {
56 | headerId = R.string.pin_code_step_create
57 | isPinEntering = true
58 | attemptCounter.resetAttempts()
59 | notification = stringResource(id = R.string.empty)
60 | } else {
61 | if (!isError) {
62 | attemptCounter.decrementAttempts()
63 | notification = stringResource(
64 | id = R.string.pin_code_attempts,
65 | attemptCounter.getAttempts()
66 | )
67 | if (attemptCounter.getAttempts() == 0) {
68 | pinCodeStateManager.setLoginAttemptsExpended()
69 | }
70 | isError = true
71 | }
72 | }
73 | }
74 |
75 | is ChangePinScreenState.EnteringNewPin -> {
76 | headerId = R.string.pin_code_step_enable_confirm
77 | isNewPinEntering = true
78 | tempPinCode = state.newPin
79 | }
80 |
81 | is ChangePinScreenState.ConfirmingNewPin -> {
82 | if (tempPinCode == state.newPin) {
83 | pinCodeManager.savePinCode(state.newPin)
84 | viewModel.processIntent(ChangePinScreenIntent.ChangePin)
85 | } else {
86 | notification = stringResource(id = R.string.pin_codes_do_not_match)
87 | viewModel.processIntent(ChangePinScreenIntent.InitialState)
88 | }
89 | }
90 |
91 | is ChangePinScreenState.PinChangedSuccess -> {
92 | pinCodeStateManager.setChangeSuccess(true)
93 | }
94 |
95 | is ChangePinScreenState.ErrorState -> {
96 | pinCodeStateManager.setChangeSuccess(false)
97 | }
98 |
99 | else -> {}
100 | }
101 |
102 | PinCodeContent(
103 | headerId = headerId,
104 | notification = notification,
105 | forgotMessageId = forgotMessageId,
106 | pinCodeStateManager = pinCodeStateManager
107 | ) {
108 | val pinCode = it.toList().joinToString("")
109 |
110 | if (!isPinEntering) {
111 | viewModel.processIntent(ChangePinScreenIntent.EnterCurrentPin(pinCode))
112 | } else {
113 | viewModel.processIntent(ChangePinScreenIntent.EnterNewPin(pinCode))
114 | }
115 | if (isNewPinEntering) {
116 | viewModel.processIntent(ChangePinScreenIntent.ConfirmNewPin(pinCode))
117 | }
118 | isError = false
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # PIN Authentication Library on Jetpack Compose
2 |
3 | [](https://jitpack.io/#Khokhlinvladimir/android-pin-authentication)
4 |
5 | [Версия на русском языке](https://github.com/Khokhlinvladimir/android-pin-authentication/blob/main/README_RU.md)
6 |
7 | ## Description
8 |
9 | Library for user authentication using PIN code. This library provides convenient and secure ways to verify users' identities using a simple numeric PIN. It can be used by developers when creating applications that require an additional layer of security or user authentication.
10 |
11 |
12 |
13 | ## Main features
14 |
15 | 1. Simple and intuitive PIN authentication.
16 | 2. Reliable storage and protection of user PIN codes using encryption.
17 | 3. Ability to customize the required length and complexity of the PIN code.
18 | 4. Limit the number of attempts to enter a PIN code to prevent brute force attacks.
19 | 5. Advanced configuration options and event notifications.
20 |
21 | # Instructions for using the library for PIN code authentication
22 |
23 | This tutorial will help you become familiar with using the PIN authentication library in your application. The library provides a simple and reliable way to protect access to your application using a PIN code. Let's figure out how it works:
24 |
25 | ## Step 1: Installing the library
26 |
27 | Installing a PIN authentication library is the first step to securely protecting your application.
28 |
29 | 1. Add the dependency to your build.gradle file:
30 |
31 | ```gradle
32 | dependencies {
33 | implementation 'com.github.Khokhlinvladimir:android-pin-authentication:v1.0.5'
34 | }
35 | ```
36 | With this simple step, you will enable a powerful authentication tool in your application, making it reliable and secure.
37 |
38 | 2. Specifying the repository in the settings.gradle file:
39 |
40 | ```gradle
41 | dependencyResolutionManagement {
42 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
43 | repositories {
44 | mavenCentral()
45 | maven { url 'https://jitpack.io' }
46 | }
47 | }
48 | ```
49 | Don't forget to add this setting to your settings.gradle file to ensure the library will be available for download.
50 | ## Step 2: Initialize the library
51 |
52 | In your activity or fragment, initialize the library like this:
53 |
54 | ```kotlin
55 | val pinCodeStateManager = PinCodeStateManager.getInstance()
56 | ```
57 |
58 | ## Step 3: Configure Security Settings
59 |
60 | Set security options such as maximum number of PIN attempts, PIN length, and biometric authentication activation:
61 |
62 | ```kotlin
63 | pinCodeStateManager.setMaxPinAttempts(maxAttempts = 4, application = application)
64 | pinCodeStateManager.setPinLength(pinLength = 4, application = application)
65 | pinCodeStateManager.setBiometricEnabled(enabled = true, application = application)
66 | pinCodeStateManager.autoLaunchBiometricEnabled(enabled = true,application = application)
67 | ```
68 |
69 | ## Step 4: Create, validate, change and delete PIN code
70 |
71 | The library supports four main scenarios:
72 |
73 | ### 1. Create a PIN code
74 |
75 | ```kotlin
76 | pinCodeStateManager.setScenario(PinCodeScenario.CREATION)
77 | ```
78 |
79 | Allows the user to create a new PIN.
80 |
81 | ### 2. PIN code validation
82 |
83 | ```kotlin
84 | pinCodeStateManager.setScenario(PinCodeScenario.VALIDATION)
85 | ```
86 |
87 | The user can log into the application by entering their existing PIN. The library will check the entered PIN code for compliance and, if validated successfully, will provide access to the application.
88 |
89 | ### 3. Change PIN code
90 |
91 | ```kotlin
92 | pinCodeStateManager.setScenario(PinCodeScenario.CHANGE)
93 | ```
94 |
95 | Allows the user to change their current PIN to a new one.
96 |
97 | ### 4. Removing the PIN code
98 |
99 | ```kotlin
100 | pinCodeStateManager.setScenario(PinCodeScenario.DELETION)
101 | ```
102 |
103 | The user can delete their current PIN.
104 |
105 | ## Step 5: Event Handling
106 |
107 | Handle events such as successful PIN creation, validation, change, and deletion, as well as input attempts exhaustion:
108 |
109 | ```kotlin
110 | pinCodeStateManager.onCreationSuccess {
111 | // Handle successful PIN setting
112 | }
113 |
114 | pinCodeStateManager.onValidationSuccess {
115 | // Handle successful PIN validation
116 | }
117 |
118 | pinCodeStateManager.onLoginAttemptsExpended {
119 | // Handle exhaustion of PIN entry attempts
120 | }
121 |
122 | // and so on for other events
123 | ```
124 |
125 | ## Step 6: Integration with the application interface
126 |
127 | Integrate the library with your application's interface
128 |
129 | ```kotlin
130 | Surface(
131 | modifier = Modifier.fillMaxSize(),
132 | color = MaterialTheme.colorScheme.background
133 | ) {
134 | // Display a screen for entering or validating a PIN code
135 | PinCodeScreen()
136 | }
137 | ```
138 |
139 | These are the basic steps to use the library for PIN authentication in your Android application. Customize the library and handle events according to your needs to create a secure and seamless user experience.
140 |
141 | ## Technical specifications:
142 |
143 | Minimum SDK version (minSdk): 24
144 |
145 | Target SDK version (targetSdk): 34
146 |
147 | Gradle version: 8.5.2
148 |
149 | OpenJDK Version: 17.0.1
150 |
151 | ## License
152 |
153 | This library is distributed under the MIT license. Details can be found in the LICENSE file.
154 |
155 | ## Author
156 |
157 | The library is developed and maintained by Khokhlin Vladimir. You can contact me via telegram [@vkhokhlin](https://t.me/vkhokhlin).
158 |
159 | ## Assistance
160 |
161 | If you have suggestions for improving the library or find a bug, please create an issue or send a pull request to the GitHub repository.
162 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/README_RU.md:
--------------------------------------------------------------------------------
1 | # PIN Authentication Library on Jetpack Compose
2 |
3 | [](https://jitpack.io/#Khokhlinvladimir/android-pin-authentication)
4 |
5 | [Версия на английском языке](https://github.com/Khokhlinvladimir/android-pin-authentication/blob/main/README.md)
6 |
7 | ## Описание
8 |
9 | Библиотека для аутентификации пользователей с использованием PIN-кода. Эта библиотека предоставляет удобные и безопасные способы проверки личности пользователей с помощью простого числового PIN-кода. Она может быть использована разработчиками при создании приложений, требующих дополнительного слоя безопасности или идентификации пользователей.
10 |
11 |
12 |
13 | ## Основные возможности
14 |
15 | 1. Простая и интуитивно понятная аутентификация с помощью PIN-кода.
16 | 2. Надежное хранение и защита PIN-кодов пользователей при помощи кодирования.
17 | 3. Возможность настройки требуемой длины и сложности PIN-кода.
18 | 4. Ограничение количества попыток ввода PIN-кода для предотвращения атак брутфорса.
19 | 5. Расширенные опции настройки и уведомлений о событиях.
20 |
21 | # Инструкция по использованию библиотеки для аутентификации по PIN-коду
22 |
23 | Эта инструкция поможет вам ознакомиться с использованием библиотеки для аутентификации по PIN-коду в вашем приложении. Библиотека предоставляет простой и надежный способ защиты доступа к вашему приложению с помощью PIN-кода. Давайте разберемся, как это работает:
24 |
25 | ## Шаг 1: Установка библиотеки
26 |
27 | Установка библиотеки для аутентификации по PIN-коду — первый шаг к безопасной защите вашего приложения.
28 |
29 | 1. Внесите зависимость в файл build.gradle:
30 |
31 | ```gradle
32 | dependencies {
33 | implementation 'com.github.Khokhlinvladimir:android-pin-authentication:v1.0.5'
34 | }
35 | ```
36 | С этим простым шагом, вы включите мощный инструмент аутентификации в вашем приложении, делая его надежным и безопасным.
37 |
38 | 2. Указание репозитория в файле settings.gradle:
39 |
40 | ```gradle
41 | dependencyResolutionManagement {
42 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
43 | repositories {
44 | mavenCentral()
45 | maven { url 'https://jitpack.io' }
46 | }
47 | }
48 | ```
49 | Не забудьте добавить эту настройку в ваш файл settings.gradle, чтобы убедиться, что библиотека будет доступна для загрузки.
50 | ## Шаг 2: Инициализация библиотеки
51 |
52 | В вашей активности или фрагменте инициализируйте библиотеку следующим образом:
53 |
54 | ```kotlin
55 | val pinCodeStateManager = PinCodeStateManager.getInstance()
56 | ```
57 |
58 | ## Шаг 3: Настройка параметров безопасности
59 |
60 | Установите параметры безопасности, такие как максимальное количество попыток ввода PIN-кода, длина PIN-кода и активация биометрической аутентификации:
61 |
62 | ```kotlin
63 | pinCodeStateManager.setMaxPinAttempts(maxAttempts = 4, application = application)
64 | pinCodeStateManager.setPinLength(pinLength = 4, application = application)
65 | pinCodeStateManager.setBiometricEnabled(enabled = true, application = application)
66 | pinCodeStateManager.autoLaunchBiometricEnabled(enabled = true,application = application)
67 | ```
68 |
69 | ## Шаг 4: Создание, валидация, изменение и удаление PIN-кода
70 |
71 | Библиотека поддерживает четыре основных сценария:
72 |
73 | ### 1. Создание PIN-кода
74 |
75 | ```kotlin
76 | pinCodeStateManager.setScenario(PinCodeScenario.CREATION)
77 | ```
78 |
79 | Позволяет пользователю создать новый PIN-код.
80 |
81 | ### 2. Валидация PIN-кода
82 |
83 | ```kotlin
84 | pinCodeStateManager.setScenario(PinCodeScenario.VALIDATION)
85 | ```
86 |
87 | Пользователь может войти в приложение, введя свой существующий PIN-код. Библиотека проверит введенный PIN-код на соответствие и, при успешной валидации, предоставит доступ к приложению.
88 |
89 | ### 3. Изменение PIN-кода
90 |
91 | ```kotlin
92 | pinCodeStateManager.setScenario(PinCodeScenario.CHANGE)
93 | ```
94 |
95 | Позволяет пользователю изменить свой текущий PIN-код на новый.
96 |
97 | ### 4. Удаление PIN-кода
98 |
99 | ```kotlin
100 | pinCodeStateManager.setScenario(PinCodeScenario.DELETION)
101 | ```
102 |
103 | Пользователь может удалить свой текущий PIN-код.
104 |
105 | ## Шаг 5: Обработка событий
106 |
107 | Обработайте события, такие как успешное создание, валидация, изменение и удаление PIN-кода, а также исчерпание попыток ввода:
108 |
109 | ```kotlin
110 | pinCodeStateManager.onCreationSuccess {
111 | // Обработка успешной установки PIN-кода
112 | }
113 |
114 | pinCodeStateManager.onValidationSuccess {
115 | // Обработка успешной валидации PIN-кода
116 | }
117 |
118 | pinCodeStateManager.onLoginAttemptsExpended {
119 | // Обработка исчерпания попыток ввода PIN-кода
120 | }
121 |
122 | // и так далее для других событий
123 | ```
124 |
125 | ## Шаг 6: Интеграция с интерфейсом приложения
126 |
127 | Интегрируйте библиотеку с интерфейсом вашего приложения
128 |
129 | ```kotlin
130 | Surface(
131 | modifier = Modifier.fillMaxSize(),
132 | color = MaterialTheme.colorScheme.background
133 | ) {
134 | // Вывести экран для ввода или валидации PIN-кода
135 | PinCodeScreen()
136 | }
137 | ```
138 |
139 | Это основные шаги по использованию библиотеки для аутентификации по PIN-коду в вашем Android-приложении. Настраивайте библиотеку и обрабатывайте события в соответствии с вашими потребностями для создания безопасного и удобного опыта для пользователей.
140 |
141 | ## Технические спецификации:
142 |
143 | Минимальная версия SDK (minSdk): 24
144 |
145 | Целевая версия SDK (targetSdk): 34
146 |
147 | Версия Gradle: 8.5.2
148 |
149 | Версия OpenJDK: 17.0.1
150 |
151 | ## Лицензия
152 |
153 | Эта библиотека распространяется под лицензией MIT. Подробности можно найти в файле LICENSE.
154 |
155 | ## Автор
156 |
157 | Библиотека разработана и поддерживается Khokhlin Vladimir. Вы можете связаться со мной через telegram [@vkhokhlin](https://t.me/vkhokhlin).
158 |
159 | ## Содействие
160 |
161 | Если у вас есть предложения по улучшению библиотеки или вы обнаружили ошибку, пожалуйста, создайте issue или отправьте пулл-реквест в репозиторий на GitHub.
162 |
--------------------------------------------------------------------------------
/pinlibrary/src/main/java/com/android/pinlibrary/utils/state/PinCodeStateManager.kt:
--------------------------------------------------------------------------------
1 | package com.android.pinlibrary.utils.state
2 |
3 | import android.app.Application
4 | import androidx.lifecycle.LiveData
5 | import androidx.lifecycle.MutableLiveData
6 | import com.android.pinlibrary.utils.enums.PinCodeScenario
7 | import com.android.pinlibrary.utils.preferences.AttemptCounter
8 | import com.android.pinlibrary.utils.preferences.PinCodeManager
9 | import com.android.pinlibrary.utils.preferences.SettingsManager
10 |
11 | /**
12 | * val pinCodeStateManager = PinCodeStateManager.getInstance()
13 | *
14 | * Installing the scenario
15 | * pinCodeStateManager.setScenario(PinCodeScenario.CREATION)
16 | *
17 | * Getting the current scenario using [LiveData]
18 | *
19 | * val currentScenarioLiveData = pinCodeStateManager.currentScenario
20 | * currentScenarioLiveData.observe(owner) { scenario ->
21 | * Handling a scenario change
22 | * }
23 | */
24 |
25 | class PinCodeStateManager private constructor() : IPinCodeStateManager {
26 |
27 | private val _currentScenario = MutableLiveData()
28 | override val currentScenario: LiveData = _currentScenario
29 |
30 | private var onLoginAttemptsExpended: (() -> Unit)? = null
31 | private var onCreationSuccess: ((isSuccess: Boolean) -> Unit)? = null
32 | private var onValidationSuccess: ((isSuccess: Boolean) -> Unit)? = null
33 | private var onChangeSuccess: ((isSuccess: Boolean) -> Unit)? = null
34 | private var onDeletionSuccess: ((isSuccess: Boolean) -> Unit)? = null
35 | private var onBiometricAuthentication: ((isSuccess: Boolean) -> Unit)? = null
36 | private var onResetPassword: (() -> Unit)? = null
37 | var isValidationEnabled: Boolean = true
38 |
39 | companion object {
40 | private var instance: PinCodeStateManager? = null
41 | fun getInstance(): PinCodeStateManager {
42 | return instance ?: synchronized(this) {
43 | instance ?: PinCodeStateManager().also { instance = it }
44 | }
45 | }
46 | }
47 |
48 | override fun setScenario(scenario: PinCodeScenario) {
49 | _currentScenario.value = scenario
50 | }
51 |
52 | internal fun setCreationSuccess(isSuccess: Boolean) {
53 | onCreationSuccess?.invoke(isSuccess)
54 | }
55 |
56 | override fun onCreationSuccess(callback: (isSuccess: Boolean) -> Unit) {
57 | this.onCreationSuccess = callback
58 | }
59 |
60 | internal fun setValidationSuccess(isSuccess: Boolean) {
61 | onValidationSuccess?.invoke(isSuccess)
62 | }
63 |
64 | override fun onValidationSuccess(callback: (isSuccess: Boolean) -> Unit) {
65 | this.onValidationSuccess = callback
66 | }
67 |
68 | internal fun setChangeSuccess(isSuccess: Boolean) {
69 | onChangeSuccess?.invoke(isSuccess)
70 | }
71 |
72 | override fun onChangeSuccess(callback: (isSuccess: Boolean) -> Unit) {
73 | this.onChangeSuccess = callback
74 | }
75 |
76 | internal fun setDeletionSuccess(isSuccess: Boolean) {
77 | onDeletionSuccess?.invoke(isSuccess)
78 | }
79 |
80 | override fun onDeletionSuccess(callback: (isSuccess: Boolean) -> Unit) {
81 | this.onDeletionSuccess = callback
82 | }
83 |
84 | internal fun setLoginAttemptsExpended() {
85 | onLoginAttemptsExpended?.invoke()
86 | }
87 |
88 | override fun onLoginAttemptsExpended(callback: () -> Unit) {
89 | this.onLoginAttemptsExpended = callback
90 | }
91 |
92 | internal fun setBiometricAuthentication(isSuccess: Boolean) {
93 | onBiometricAuthentication?.invoke(isSuccess)
94 | }
95 |
96 | override fun onBiometricAuthentication(callback: (isSuccess: Boolean) -> Unit) {
97 | this.onBiometricAuthentication = callback
98 | }
99 |
100 | internal fun setResetPassword() {
101 | onResetPassword?.invoke()
102 | }
103 |
104 | override fun onResetPassword(callback: () -> Unit) {
105 | this.onResetPassword = callback
106 | }
107 |
108 | override fun clearConfiguration(application: Application) {
109 | val pinCodeManager = PinCodeManager(context = application)
110 | val attemptCounter = AttemptCounter(context = application)
111 | pinCodeManager.clearPinCode()
112 | attemptCounter.resetAttempts()
113 | }
114 |
115 | override fun isPinCodeSaved(application: Application): Boolean {
116 | val pinCodeManager = PinCodeManager(context = application)
117 | return pinCodeManager.loadPinCode() != null
118 | }
119 |
120 | /**
121 | * Set the value of the "Enable biometrics" option and return the current value.
122 | *
123 | * @param enabled true to enable biometrics; false to turn off.
124 | * @param application Application context.
125 | * @return true if biometrics is enabled; false otherwise.
126 | */
127 | override fun setBiometricEnabled(enabled: Boolean, application: Application): Boolean {
128 | val settingsManager = SettingsManager(context = application)
129 | settingsManager.setBiometricEnabled(enabled)
130 | return settingsManager.isBiometricEnabled()
131 | }
132 |
133 | /**
134 | * Set the value of the "Auto-launch biometric authentication" option and return the current value.
135 | *
136 | * @param enabled true to enable automatic biometric dialog on validation start; false to disable.
137 | * @param application Application context.
138 | * @return true if automatic biometric launch is enabled; false otherwise.
139 | */
140 | override fun autoLaunchBiometricEnabled(enabled: Boolean, application: Application): Boolean {
141 | val settingsManager = SettingsManager(context = application)
142 | settingsManager.setAutoLaunchBiometricEnabled(enabled)
143 | return settingsManager.isAutoLaunchBiometricEnabled()
144 | }
145 |
146 | /**
147 | * Set the maximum number of pin code entry attempts and return the current value.
148 | *
149 | * @param maxAttempts New maximum number of attempts.
150 | * @param application Application context.
151 | * @return The current maximum number of retries.
152 | */
153 | override fun setMaxPinAttempts(maxAttempts: Int, application: Application): Int {
154 | val settingsManager = SettingsManager(context = application)
155 | settingsManager.setMaxPinAttempts(maxAttempts)
156 | return settingsManager.getMaxPinAttempts()
157 | }
158 |
159 | /**
160 | * Set pin length and return current value.
161 | *
162 | * @param pinLength New pin length.
163 | * @param application Application context.
164 | * @return Current pin length.
165 | */
166 | override fun setPinLength(pinLength: Int, application: Application): Int {
167 | val settingsManager = SettingsManager(context = application)
168 | settingsManager.setPinLength(pinLength)
169 | return settingsManager.getPinLength()
170 | }
171 |
172 | override fun setTemporaryPinValidationState(isEnabled: Boolean) {
173 | isValidationEnabled = isEnabled
174 | }
175 | }
--------------------------------------------------------------------------------
/app/src/main/java/com/android/authentication/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.android.authentication
2 |
3 | import android.os.Bundle
4 | import androidx.activity.ComponentActivity
5 | import androidx.activity.compose.setContent
6 | import androidx.compose.foundation.layout.fillMaxSize
7 | import androidx.compose.material3.MaterialTheme
8 | import androidx.compose.material3.Surface
9 | import androidx.compose.runtime.Composable
10 | import androidx.compose.runtime.getValue
11 | import androidx.compose.runtime.mutableStateOf
12 | import androidx.compose.runtime.remember
13 | import androidx.compose.runtime.setValue
14 | import androidx.compose.ui.Modifier
15 | import androidx.compose.ui.tooling.preview.Preview
16 | import com.android.authentication.ui.screens.PinCodeStartScreen
17 | import com.android.authentication.ui.theme.PinAuthenticationTheme
18 | import com.android.pinlibrary.ui.screens.PinCodeScreen
19 | import com.android.pinlibrary.utils.state.PinCodeStateManager
20 |
21 | class MainActivity : ComponentActivity() {
22 |
23 | override fun onCreate(savedInstanceState: Bundle?) {
24 | super.onCreate(savedInstanceState)
25 |
26 | val pinCodeStateManager = PinCodeStateManager.getInstance()
27 |
28 | setContent {
29 | PinAuthenticationTheme {
30 |
31 | var isPinCodeCreated by remember { mutableStateOf(false) }
32 | var isPinCodeScreenVisible by remember { mutableStateOf(false) }
33 |
34 | /**
35 | * Handler invoked when all login attempts with the PIN code have been exhausted.
36 | * This method is responsible for handling the event when all login attempts are used up.
37 | */
38 | pinCodeStateManager.onLoginAttemptsExpended {
39 | // Your handling logic here
40 | isPinCodeScreenVisible = false
41 | isPinCodeCreated = false
42 | pinCodeStateManager.clearConfiguration(application = application)
43 | }
44 |
45 | /**
46 | * Handler invoked upon successful biometric authentication.
47 | * This method is responsible for handling the event of successful biometric authentication of the user.
48 | */
49 | pinCodeStateManager.onBiometricAuthentication {
50 | // Your handling logic here
51 | isPinCodeScreenVisible = !it
52 | }
53 |
54 | /**
55 | * Handler invoked upon successful PIN code creation or setting.
56 | * This method is responsible for handling the event of successful PIN code creation or setting.
57 | */
58 | pinCodeStateManager.onCreationSuccess {
59 | // Your handling logic here
60 | isPinCodeScreenVisible = false
61 | isPinCodeCreated = true
62 | }
63 |
64 | /**
65 | * Handler invoked upon successful PIN code change.
66 | * This method is responsible for handling the event of successful PIN code change.
67 | */
68 | pinCodeStateManager.onChangeSuccess {
69 | // Your handling logic here
70 | isPinCodeScreenVisible = false
71 | }
72 |
73 | /**
74 | * Handler invoked upon successful PIN code validation.
75 | * This method is responsible for handling the event of successful validation of the entered PIN code.
76 | */
77 | pinCodeStateManager.onValidationSuccess {
78 | // Your handling logic here
79 | isPinCodeScreenVisible = false
80 | }
81 |
82 | /**
83 | * Handler invoked upon successful PIN code deletion.
84 | * This method is responsible for handling the event of successful PIN code deletion.
85 | */
86 | pinCodeStateManager.onDeletionSuccess {
87 | // Your handling logic here
88 | isPinCodeScreenVisible = false
89 | isPinCodeCreated = false
90 | }
91 |
92 | /**
93 | * Handler invoked when resetting the password.
94 | * This method is responsible for handling the event of password or PIN code reset.
95 | */
96 | pinCodeStateManager.onResetPassword {
97 | // Your handling logic here
98 | isPinCodeScreenVisible = false
99 | isPinCodeCreated = false
100 | pinCodeStateManager.clearConfiguration(application = application)
101 | }
102 |
103 | /**
104 | * Indicates whether a PIN code is saved in the application.
105 | *
106 | * @return True if a PIN code is saved, false otherwise.
107 | */
108 | val isPinCodeSaved = pinCodeStateManager.isPinCodeSaved(
109 | application = application
110 | )
111 | isPinCodeCreated = isPinCodeSaved
112 |
113 | /**
114 | * Sets the maximum number of PIN code attempts.
115 | *
116 | * @param maxAttempts The maximum number of PIN code attempts allowed.
117 | * @param application The application context.
118 | */
119 | val setMaxPinAttempts = pinCodeStateManager.setMaxPinAttempts(
120 | maxAttempts = 4,
121 | application = application
122 | )
123 |
124 | /**
125 | * Sets the length of the PIN code.
126 | *
127 | * @param pinLength The desired length for the PIN code.
128 | * @param application The application context.
129 | */
130 | val setPinLength =
131 | pinCodeStateManager.setPinLength(
132 | pinLength = 4,
133 | application = application
134 | )
135 |
136 | /**
137 | * Enables or disables biometric authentication for PIN code.
138 | *
139 | * @param enabled True to enable biometric authentication, false to disable.
140 | * @param application The application context.
141 | */
142 | val setBiometricEnabled = pinCodeStateManager.setBiometricEnabled(
143 | enabled = true,
144 | application = application
145 | )
146 |
147 | /**
148 | * Enables or disables automatic biometric authentication dialog launch on validation screen start.
149 | *
150 | * @param enabled True to enable automatic biometric launch, false to disable.
151 | * @param application The application context.
152 | */
153 | val autoLaunchBiometricEnabled = pinCodeStateManager.autoLaunchBiometricEnabled(
154 | enabled = true,
155 | application = application
156 | )
157 |
158 | Surface(
159 | modifier = Modifier.fillMaxSize(),
160 | color = MaterialTheme.colorScheme.background
161 | ) {
162 |
163 | if (isPinCodeScreenVisible) {
164 | PinCodeScreen()
165 | } else {
166 | PinCodeStartScreen(isPinCodeCreated) {
167 | pinCodeStateManager.setScenario(it)
168 | isPinCodeScreenVisible = true
169 | }
170 | }
171 | }
172 | }
173 | }
174 | }
175 | }
176 |
177 | @Preview(showBackground = true)
178 | @Composable
179 | fun GreetingPreview() {
180 | PinAuthenticationTheme {
181 | PinCodeScreen()
182 | }
183 | }
184 |
--------------------------------------------------------------------------------