├── .github └── workflows │ └── testing.yml ├── .gitignore ├── LICENSE ├── README.MD ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── ic_launcher-playstore.png │ ├── java │ └── com │ │ └── redmadrobot │ │ └── sample │ │ ├── App.kt │ │ ├── MainActivity.kt │ │ ├── MainFragment.kt │ │ ├── create_pin │ │ ├── CreatePinFragment.kt │ │ └── CreatePinViewModel.kt │ │ ├── input_pin │ │ ├── InputPinFragment.kt │ │ └── InputPinViewModel.kt │ │ └── internal │ │ ├── HiltModule.kt │ │ └── SingleLiveEvent.kt │ └── res │ ├── layout │ ├── activity_main.xml │ ├── create_pin_fragment.xml │ ├── input_pin_fragment.xml │ └── main_fragment.xml │ ├── mipmap-anydpi-v26 │ ├── ic_launcher.xml │ └── ic_launcher_round.xml │ ├── mipmap-hdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-mdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── mipmap-xxxhdpi │ ├── ic_launcher.png │ ├── ic_launcher_foreground.png │ └── ic_launcher_round.png │ ├── navigation │ └── nav_graph.xml │ └── values │ ├── colors.xml │ ├── ic_launcher_background.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ ├── Android.kt │ ├── Dependencies.kt │ ├── GradlePlugin.kt │ ├── Kotlin.kt │ ├── PublishPlugin.kt │ └── TestDependencies.kt ├── detekt ├── config.yml └── detekt-baseline.xml ├── gradle.properties ├── gradle ├── publish.properties └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── img └── logo.svg ├── pinkman-coroutines ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── library.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── redmadrobot │ │ └── pinkman_coroutines │ │ └── CoroutinesPinkmanTest.kt │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── redmadrobot │ └── pinkman_coroutines │ └── CoroutinesPinkman.kt ├── pinkman-rx3 ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── library.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── redmadrobot │ │ └── pinkman_rx3 │ │ └── RxPinkmanTest.kt │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── redmadrobot │ └── pinkman_rx3 │ └── RxPinkman.kt ├── pinkman-ui ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── library.properties ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── redmadrobot │ │ └── pinkman_ui │ │ ├── KeyClickListener.kt │ │ ├── PinKeyboard.kt │ │ ├── PinView.kt │ │ └── internal │ │ └── Resources.kt │ └── res │ ├── drawable │ ├── pinkman_circle_grey.xml │ └── pinkman_circle_red.xml │ └── values │ ├── colors.xml │ ├── pin_keyboard_attrs.xml │ ├── pin_view_attrs.xml │ └── styles.xml ├── pinkman ├── .gitignore ├── build.gradle.kts ├── consumer-rules.pro ├── library.properties ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── redmadrobot │ │ └── pinkman │ │ ├── PinkmanBlacklistedTest.kt │ │ └── PinkmanTest.kt │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── redmadrobot │ └── pinkman │ ├── Pinkman.kt │ ├── exception │ ├── BlacklistedPinException.kt │ └── CorruptedStorageException.kt │ └── internal │ ├── Salt.kt │ ├── argon2 │ └── Argon2.kt │ ├── exception │ └── BadHashException.kt │ └── pbkdf2 │ ├── Pbkdf2Factory.kt │ └── Pbkdf2Key.kt └── settings.gradle.kts /.github/workflows/testing.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | on: 6 | push: 7 | branches: 8 | - master 9 | - release-** 10 | pull_request: 11 | branches: 12 | - master 13 | - release-** 14 | 15 | jobs: 16 | testing: 17 | name: Code quality and testing 18 | runs-on: macos-latest 19 | 20 | steps: 21 | - name: Clone repository 22 | uses: actions/checkout@v2 23 | 24 | - name: Setup Java JDK 25 | uses: actions/setup-java@v1.4.3 26 | with: 27 | java-version: 1.8 28 | 29 | - name: Run Android Linter 30 | run: ./gradlew :pinkman:lint :pinkman-ui:lint 31 | 32 | - name: Run Detekt 33 | run: ./gradlew detekt 34 | 35 | - name: Android Emulator Runner 36 | uses: ReactiveCircus/android-emulator-runner@v2.14.3 37 | with: 38 | api-level: 29 39 | script: ./gradlew connectedCheck 40 | 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Android template 3 | # Built application files 4 | *.apk 5 | *.aar 6 | *.ap_ 7 | *.aab 8 | 9 | # Files for the ART/Dalvik VM 10 | *.dex 11 | 12 | # Java class files 13 | *.class 14 | 15 | # Generated files 16 | bin/ 17 | gen/ 18 | out/ 19 | # Uncomment the following line in case you need and you don't have the release build type files in your app 20 | # release/ 21 | 22 | # Gradle files 23 | .gradle/ 24 | build/ 25 | 26 | # Local configuration file (sdk path, etc) 27 | local.properties 28 | 29 | # Proguard folder generated by Eclipse 30 | proguard/ 31 | 32 | # Log Files 33 | *.log 34 | 35 | # Android Studio Navigation editor temp files 36 | .navigation/ 37 | 38 | # Android Studio captures folder 39 | captures/ 40 | 41 | # IntelliJ 42 | *.iml 43 | .idea/workspace.xml 44 | .idea/tasks.xml 45 | .idea/gradle.xml 46 | .idea/assetWizardSettings.xml 47 | .idea/dictionaries 48 | .idea/libraries 49 | # Android Studio 3 in .gitignore file. 50 | .idea/caches 51 | .idea/modules.xml 52 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 53 | .idea/navEditor.xml 54 | 55 | # Keystore files 56 | # Uncomment the following lines if you do not want to check your keystore files in. 57 | #*.jks 58 | #*.keystore 59 | 60 | # External native build folder generated in Android Studio 2.2 and later 61 | .externalNativeBuild 62 | .cxx/ 63 | 64 | # Google Services (e.g. APIs or Firebase) 65 | # google-services.json 66 | 67 | # Freeline 68 | freeline.py 69 | freeline/ 70 | freeline_project_description.json 71 | 72 | # fastlane 73 | fastlane/report.xml 74 | fastlane/Preview.html 75 | fastlane/screenshots 76 | fastlane/test_output 77 | fastlane/readme.md 78 | 79 | # Version control 80 | vcs.xml 81 | 82 | # lint 83 | lint/intermediates/ 84 | lint/generated/ 85 | lint/outputs/ 86 | lint/tmp/ 87 | # lint/reports/ 88 | 89 | 90 | ### JetBrains template 91 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 92 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 93 | 94 | # User-specific stuff 95 | .idea/**/workspace.xml 96 | .idea/**/tasks.xml 97 | .idea/**/usage.statistics.xml 98 | .idea/**/dictionaries 99 | .idea/**/shelf 100 | 101 | # Generated files 102 | .idea/**/contentModel.xml 103 | 104 | # Sensitive or high-churn files 105 | .idea/**/dataSources/ 106 | .idea/**/dataSources.ids 107 | .idea/**/dataSources.local.xml 108 | .idea/**/sqlDataSources.xml 109 | .idea/**/dynamic.xml 110 | .idea/**/uiDesigner.xml 111 | .idea/**/dbnavigator.xml 112 | 113 | # Gradle 114 | .idea/**/gradle.xml 115 | .idea/**/libraries 116 | 117 | # Gradle and Maven with auto-import 118 | # When using Gradle or Maven with auto-import, you should exclude module files, 119 | # since they will be recreated, and may cause churn. Uncomment if using 120 | # auto-import. 121 | # .idea/artifacts 122 | # .idea/compiler.xml 123 | # .idea/modules.xml 124 | # .idea/*.iml 125 | # .idea/modules 126 | # *.iml 127 | # *.ipr 128 | 129 | # CMake 130 | cmake-build-*/ 131 | 132 | # Mongo Explorer plugin 133 | .idea/**/mongoSettings.xml 134 | 135 | # File-based project format 136 | *.iws 137 | 138 | # IntelliJ 139 | 140 | # mpeltonen/sbt-idea plugin 141 | .idea_modules/ 142 | 143 | # JIRA plugin 144 | atlassian-ide-plugin.xml 145 | 146 | # Cursive Clojure plugin 147 | .idea/replstate.xml 148 | 149 | # Crashlytics plugin (for Android Studio and IntelliJ) 150 | com_crashlytics_export_strings.xml 151 | crashlytics.properties 152 | crashlytics-build.properties 153 | fabric.properties 154 | 155 | # Editor-based Rest Client 156 | .idea/httpRequests 157 | 158 | # Android studio 3.1+ serialized cache file 159 | .idea/caches/build_file_checksums.ser 160 | 161 | .idea/ 162 | deploy.sh 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Redmadrobot 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 | 23 | -------------------------------------------------------------------------------- /README.MD: -------------------------------------------------------------------------------- 1 | ![](img/logo.svg) 2 | 3 | # PINkman 4 | [![API](https://img.shields.io/badge/API-23%2B-red.svg?style=flat)](https://android-arsenal.com/api?level=23) 5 | ![CI](https://github.com/RedMadRobot/PINkman/workflows/CI/badge.svg) 6 | ![Maven Central](https://img.shields.io/maven-central/v/com.redmadrobot/pinkman) 7 | 8 | 9 | Implementing an authentication by a PIN code is an ordinary task for a 10 | mobile applications developer. You can even think of it as some kind of 11 | boilerplate code. But it's a trap. Such tasks have a number of security 12 | gotchas. Therefore there's a high risk of implementing it in an insecure 13 | manner. Don't worry, Pinkman to the rescue! 14 | 15 | ## What is it? 16 | 17 | PINkman is a library to help implementing an authentication by a PIN 18 | code in a secure manner. The library derives hash from the user's PIN 19 | using [Argon2](https://github.com/P-H-C/phc-winner-argon2) hash function 20 | and stores it in an encrypted file. The file is encrypted with the 21 | AES-256 algorithm in the GCM mode and keys are stored in the 22 | AndroidKeystore. 23 | 24 | ## How it works? 25 | 26 | This library doesn't reinvent it's own cryptograhy and just stands on 27 | the shoulders of giants. Here's the description of the used technologies 28 | and their params. 29 | 30 | ##### Deriving a hash from a PIN code 31 | 32 | For getting the hash, the Argon2 function is used with following params: 33 | 34 | - **Mode**: Argon2i 35 | - **Time cost in iterations**: 5 36 | - **Memory cost in KBytes**: 65 536 37 | - **Parallelism**: 2 38 | - **Derived hash length**: 128bit 39 | 40 | ##### Encrypted files 41 | 42 | To store data securely, this library is using the 43 | [Jetpack security](https://developer.android.com/jetpack/androidx/releases/security) 44 | library from the 45 | [Android Jetpack libraries suite](https://developer.android.com/jetpack). 46 | That library, in turn, is using the other awesome library - 47 | [Tink](https://github.com/google/tink), so you can be sure that storing 48 | data of a PIN code is organized quite secure. Or you can verify it 49 | yourself ;) 50 | 51 | 52 | ## Quick start 53 | 54 | Add this library to your gradle config 55 | 56 | ```groovy 57 | implementation 'com.redmadrobot:pinkman:$pinkman_version' 58 | ``` 59 | 60 | Create an instance of the `Pinkman` class (use a DI please) and 61 | integrate it to your authentication logic. 62 | 63 | ```kotlin 64 | val pinkman = Pinkman(application.applicationContext) 65 | 66 | ... 67 | 68 | class CreatePinViewModel(private val pinkman: Pinkman) : ViewModel() { 69 | 70 | val pinIsCreated = MutableLiveData() 71 | 72 | fun createPin(pin: String) { 73 | pinkman.createPin(pin) 74 | 75 | pinIsCreated.postValue(true) 76 | } 77 | } 78 | 79 | ... 80 | 81 | class InputPinViewModel(private val pinkman: Pinkman) : ViewModel() { 82 | 83 | val pinIsValid = MutableLiveData() 84 | 85 | fun validatePin(pin: String) { 86 | pinIsValid.value = pinkman.isValidPin(pin) 87 | } 88 | } 89 | ``` 90 | 91 | Pinkman uses [StrongBox](https://developer.android.com/training/articles/keystore#HardwareSecurityModule) by default. 92 | If you want to turn it off you can do it in `Pinkman.Config` class. 93 | 94 | ```kotlin 95 | val pinkman = Pinkman( 96 | application.applicationContext, 97 | config = Pinkman.Config(useStrongBoxIfPossible = false) 98 | ) 99 | ``` 100 | 101 | Also you can do all these things even sipmler with the UI components 102 | (`PinView` and `PinKeyboard`) supplied by this library. You need to add 103 | this dependency to use them 104 | 105 | ```groovy 106 | implementation 'com.redmadrobot:pinkman-ui:$pinkman_version' 107 | ``` 108 | 109 | ```xml 110 | 111 | 115 | 116 | 130 | 131 | 137 | 138 | 139 | 140 | ``` 141 | 142 | And integrate them with the logic written before 143 | 144 | ```kotlin 145 | class CreatePinFragment : Fragment() { 146 | 147 | private val viewModel: CreatePinViewModel by viewModels() 148 | 149 | ... 150 | 151 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 152 | super.onViewCreated(view, savedInstanceState) 153 | 154 | viewModel.pinIsCreated.observe(viewLifecycleOwner, Observer { isCreated -> 155 | findNavController().popBackStack(R.id.mainFragment, false) 156 | }) 157 | 158 | pin_view.onFilledListener = { viewModel.createPin(it) } 159 | keyboard.keyboardClickListener = { pin_view.add(it) } 160 | 161 | } 162 | } 163 | ``` 164 | 165 | ⚠️ 166 | Hash deriving operations can take significant time on some devices. In 167 | order to avoid ANR in your application you shouldn't run methods 168 | `createPin()`, `changePin()`, `isValidPin()` on the main thread. 169 | 170 | This library has already provided two extensions to run these methods 171 | asynchronously. You can choose one depending on your specific needs (or 172 | tech stack). 173 | 174 | You need to add this dependency if you prefer RxJava: 175 | 176 | ```groovy 177 | implementation 'com.redmadrobot:pinkman-rx3:$pinkman_version' 178 | ``` 179 | 180 | But if you're on the bleeding edge technologies, you should use a 181 | dependency with 182 | [Kotlin Coroutines](https://kotlinlang.org/docs/reference/coroutines-overview.html) 183 | support: 184 | 185 | ```groovy 186 | implementation 'com.redmadrobot:pinkman-coroutines:$pinkman_version' 187 | ``` 188 | 189 | As a result, you'll get RxJava specific or Coroutines specific method's 190 | set: 191 | 192 | ```kotlin 193 | // RxJava3 194 | fun createPinAsync(...): Completable 195 | fun changePinAsync(...): Completable 196 | fun isValidPinAsync(...): Single 197 | 198 | // Coroutines 199 | suspend fun createPinAsync(...) 200 | suspend fun changePinAsync(...) 201 | suspend fun isValidPinAsync(...): Boolean 202 | ``` 203 | 204 | ## Feedback 205 | 206 | In case you have faced any bugs or have any useful suggestions for 207 | improvement of this library, feel free to create an 208 | [issue](https://github.com/RedMadRobot/PINkman/issues). 209 | 210 | ## LICENSE 211 | 212 | >THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 213 | >OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 214 | >MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 215 | >IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 216 | >CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 217 | >TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 218 | >SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 219 | 220 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id(Android.applicationPlugin) 3 | 4 | kotlin(Kotlin.androidPlugin) 5 | kotlin(Kotlin.androidExtensions) 6 | kotlin(Kotlin.kapt) 7 | 8 | id(Dependencies.App.hiltAppPlugin) 9 | } 10 | 11 | android { 12 | compileSdkVersion(Android.compileSdk) 13 | buildToolsVersion(Android.buildTools) 14 | 15 | defaultConfig { 16 | applicationId = Android.DefaultConfig.applicationId 17 | 18 | minSdkVersion(Android.DefaultConfig.minSdk) 19 | targetSdkVersion(Android.DefaultConfig.targetSdk) 20 | 21 | versionCode = Android.DefaultConfig.versionCode 22 | versionName = Android.DefaultConfig.versionName 23 | 24 | testInstrumentationRunner = Android.DefaultConfig.instrumentationRunner 25 | 26 | 27 | buildTypes { 28 | getByName(Android.BuildTypes.release) { 29 | isMinifyEnabled = false 30 | 31 | proguardFiles( 32 | getDefaultProguardFile(Android.Proguard.androidOptimizedRules), 33 | Android.Proguard.projectRules 34 | ) 35 | } 36 | } 37 | 38 | compileOptions { 39 | sourceCompatibility = JavaVersion.VERSION_1_8 40 | targetCompatibility = JavaVersion.VERSION_1_8 41 | } 42 | 43 | kotlinOptions { 44 | jvmTarget = "1.8" 45 | } 46 | 47 | kapt { 48 | correctErrorTypes = true 49 | } 50 | } 51 | } 52 | 53 | dependencies { 54 | implementation(project(":pinkman")) 55 | implementation(project(":pinkman-ui")) 56 | 57 | implementation(Kotlin.stdLib) 58 | implementation(Dependencies.Common.appCompat) 59 | 60 | implementation(Dependencies.App.hiltAndroid) 61 | implementation(Dependencies.App.hiltLifecycleViewmodel) 62 | 63 | implementation(Dependencies.App.navigationFragmentKtx) 64 | implementation(Dependencies.App.navigationUiKtx) 65 | 66 | implementation(Dependencies.App.lifecycleViewmodelKtx) 67 | implementation(Dependencies.App.lifecycleLivedataKtx) 68 | 69 | implementation(Dependencies.App.coreKtx) 70 | implementation(Dependencies.App.constraintlayout) 71 | 72 | kapt(Dependencies.App.hiltCompiler) 73 | kapt(Dependencies.App.hiltAndroidCompiler) 74 | 75 | testImplementation(TestDependencies.junit) 76 | 77 | androidTestImplementation(TestDependencies.junitExt) 78 | androidTestImplementation(TestDependencies.espresso) 79 | } 80 | 81 | -------------------------------------------------------------------------------- /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.kts.kts.kts. 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 22 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RedMadRobot/PINkman/137c3d0ffe8504eaa90b6bc99fc72e448f80d8ee/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/sample/App.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.sample 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App : Application() 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/sample/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.sample 2 | 3 | import android.os.Bundle 4 | import androidx.appcompat.app.AppCompatActivity 5 | import dagger.hilt.android.AndroidEntryPoint 6 | 7 | @AndroidEntryPoint 8 | class MainActivity : AppCompatActivity() { 9 | override fun onCreate(savedInstanceState: Bundle?) { 10 | super.onCreate(savedInstanceState) 11 | setContentView(R.layout.activity_main) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/sample/MainFragment.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.sample 2 | 3 | import android.content.Context 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import androidx.navigation.fragment.findNavController 10 | import com.redmadrobot.pinkman.Pinkman 11 | import dagger.hilt.android.AndroidEntryPoint 12 | import kotlinx.android.synthetic.main.main_fragment.* 13 | import javax.inject.Inject 14 | 15 | @AndroidEntryPoint 16 | class MainFragment : Fragment() { 17 | 18 | @Inject 19 | lateinit var pinkman: Pinkman 20 | 21 | override fun onAttach(context: Context) { 22 | super.onAttach(context) 23 | 24 | if (pinkman.isPinSet()) { 25 | findNavController().navigate(R.id.inputPinFragment) 26 | } 27 | } 28 | 29 | override fun onCreateView( 30 | inflater: LayoutInflater, 31 | container: ViewGroup?, 32 | savedInstanceState: Bundle? 33 | ): View? { 34 | return inflater.inflate(R.layout.main_fragment, container, false) 35 | } 36 | 37 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 38 | super.onViewCreated(view, savedInstanceState) 39 | 40 | if (pinkman.isPinSet()) { 41 | pin_button.text = "Remove PIN" 42 | pin_button.setOnClickListener { 43 | pinkman.removePin() 44 | 45 | parentFragmentManager.beginTransaction() 46 | .detach(this) 47 | .attach(this) 48 | .commit() 49 | } 50 | } else { 51 | pin_button.text = "Create PIN" 52 | pin_button.setOnClickListener { findNavController().navigate(R.id.createPinFragment) } 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/sample/create_pin/CreatePinFragment.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.sample.create_pin 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import androidx.fragment.app.viewModels 9 | import androidx.navigation.fragment.findNavController 10 | import com.redmadrobot.pinkman_ui.KeyClickListener 11 | import com.redmadrobot.sample.R 12 | import dagger.hilt.android.AndroidEntryPoint 13 | import kotlinx.android.synthetic.main.create_pin_fragment.* 14 | 15 | @AndroidEntryPoint 16 | class CreatePinFragment : Fragment() { 17 | 18 | private val viewModel: CreatePinViewModel by viewModels() 19 | 20 | override fun onCreateView( 21 | inflater: LayoutInflater, 22 | container: ViewGroup?, 23 | savedInstanceState: Bundle? 24 | ): View? { 25 | return inflater.inflate(R.layout.create_pin_fragment, container, false) 26 | } 27 | 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onViewCreated(view, savedInstanceState) 30 | 31 | viewModel.pinIsCreated.observe(viewLifecycleOwner) { 32 | findNavController().popBackStack(R.id.mainFragment, false) 33 | } 34 | 35 | pin_view.onFilledListener = { viewModel.createPin(it) } 36 | keyboard.keyClickListener = KeyClickListener { pin_view.add(it) } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/sample/create_pin/CreatePinViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.sample.create_pin 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import com.redmadrobot.pinkman.Pinkman 7 | 8 | class CreatePinViewModel @ViewModelInject constructor(private val pinkman: Pinkman) : ViewModel() { 9 | 10 | val pinIsCreated = MutableLiveData() 11 | 12 | fun createPin(pin: String) { 13 | pinkman.createPin(pin) 14 | 15 | pinIsCreated.postValue(true) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/sample/input_pin/InputPinFragment.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.sample.input_pin 2 | 3 | import android.os.Bundle 4 | import android.os.Handler 5 | import android.os.Looper 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.Toast 10 | import androidx.fragment.app.Fragment 11 | import androidx.fragment.app.viewModels 12 | import androidx.navigation.fragment.findNavController 13 | import com.redmadrobot.pinkman_ui.KeyClickListener 14 | import com.redmadrobot.sample.R 15 | import dagger.hilt.android.AndroidEntryPoint 16 | import kotlinx.android.synthetic.main.create_pin_fragment.* 17 | 18 | @AndroidEntryPoint 19 | class InputPinFragment : Fragment() { 20 | 21 | private val viewModel: InputPinViewModel by viewModels() 22 | 23 | override fun onCreateView( 24 | inflater: LayoutInflater, 25 | container: ViewGroup?, 26 | savedInstanceState: Bundle? 27 | ): View? { 28 | return inflater.inflate(R.layout.input_pin_fragment, container, false) 29 | } 30 | 31 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 32 | super.onViewCreated(view, savedInstanceState) 33 | 34 | viewModel.pinIsValid.observe(viewLifecycleOwner) { isValid -> 35 | if (isValid) { 36 | findNavController().popBackStack(R.id.mainFragment, false) 37 | } else { 38 | Toast.makeText(context, "Invalid PIN", Toast.LENGTH_SHORT).show() 39 | @Suppress("MagicNumber") 40 | Handler(Looper.getMainLooper()).postDelayed({ pin_view.empty() }, 500) 41 | } 42 | } 43 | 44 | pin_view.onFilledListener = { viewModel.validatePin(it) } 45 | keyboard.keyClickListener = KeyClickListener { pin_view.add(it) } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/sample/input_pin/InputPinViewModel.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.sample.input_pin 2 | 3 | import androidx.hilt.lifecycle.ViewModelInject 4 | import androidx.lifecycle.MutableLiveData 5 | import androidx.lifecycle.ViewModel 6 | import com.redmadrobot.pinkman.Pinkman 7 | 8 | class InputPinViewModel @ViewModelInject constructor(private val pinkman: Pinkman) : ViewModel() { 9 | 10 | val pinIsValid = MutableLiveData() 11 | 12 | fun validatePin(pin: String) { 13 | pinIsValid.value = pinkman.isValidPin(pin) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/sample/internal/HiltModule.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.sample.internal 2 | 3 | import android.app.Application 4 | import com.redmadrobot.pinkman.Pinkman 5 | import dagger.Module 6 | import dagger.Provides 7 | import dagger.hilt.InstallIn 8 | import dagger.hilt.android.components.ApplicationComponent 9 | import javax.inject.Singleton 10 | 11 | @Module 12 | @InstallIn(ApplicationComponent::class) 13 | object HiltModule { 14 | 15 | @Singleton 16 | @Provides 17 | fun providePinkman(application: Application): Pinkman { 18 | return Pinkman(application.applicationContext) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/src/main/java/com/redmadrobot/sample/internal/SingleLiveEvent.kt: -------------------------------------------------------------------------------- 1 | package com.redmadrobot.sample.internal 2 | 3 | open class SingleLiveEvent(private val content: T) { 4 | 5 | var hasBeenHandled = false 6 | private set // Allow external read but not write 7 | 8 | /** 9 | * Returns the content and prevents its use again. 10 | */ 11 | fun getContentIfNotHandled(): T? { 12 | return if (hasBeenHandled) { 13 | null 14 | } else { 15 | hasBeenHandled = true 16 | content 17 | } 18 | } 19 | 20 | /** 21 | * Returns the content, even if it's already been handled. 22 | */ 23 | fun peekContent(): T = content 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/res/layout/create_pin_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 17 | 18 | 32 | 33 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/input_pin_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 17 | 18 | 32 | 33 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 |