├── .gitattributes ├── .github └── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── .gitignore ├── LICENSE ├── README.md ├── app ├── .DS_Store ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── in │ │ └── surajsau │ │ └── jisho │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── assets │ │ ├── emojis.json │ │ ├── style0.jpg │ │ ├── style1.jpg │ │ ├── style2.jpg │ │ ├── style3.jpg │ │ ├── style4.jpg │ │ └── style5.jpg │ ├── java │ │ └── in │ │ │ └── surajsau │ │ │ └── jisho │ │ │ ├── App.kt │ │ │ ├── MainActivity.kt │ │ │ ├── base │ │ │ ├── BitmapCache.kt │ │ │ ├── BitmapUtils.kt │ │ │ ├── BoolUtils.kt │ │ │ ├── ContextUtils.kt │ │ │ ├── FileName.kt │ │ │ ├── ListUtils.kt │ │ │ ├── NormalizeOp.kt │ │ │ ├── ObserveLifecycle.kt │ │ │ ├── Optional.kt │ │ │ └── SingleFlowViewModel.kt │ │ │ ├── data │ │ │ ├── CardDataProvider.kt │ │ │ ├── ChatDataProvider.kt │ │ │ ├── DigitalInkProvider.kt │ │ │ ├── FileProvider.kt │ │ │ ├── ScreensDataProvider.kt │ │ │ ├── StyleTransferProvider.kt │ │ │ ├── TranslatorProvider.kt │ │ │ ├── chat │ │ │ │ ├── EmojiLabellingDataProvider.kt │ │ │ │ ├── EntityExtractionProvider.kt │ │ │ │ └── ReplySuggestionProvider.kt │ │ │ ├── db │ │ │ │ ├── AppDb.kt │ │ │ │ ├── FaceImage.kt │ │ │ │ └── FacenetDAO.kt │ │ │ ├── facenet │ │ │ │ ├── FaceDetectionProvider.kt │ │ │ │ ├── FaceRecognitionProvider.kt │ │ │ │ └── FacesDataProvider.kt │ │ │ └── model │ │ │ │ ├── ChatMessageModel.kt │ │ │ │ ├── Emoji.kt │ │ │ │ ├── Screen.kt │ │ │ │ └── Suggestion.kt │ │ │ ├── di │ │ │ ├── DataModule.kt │ │ │ └── cardreader │ │ │ │ └── OnboardingScreenModule.kt │ │ │ ├── domain │ │ │ ├── Cleanup.kt │ │ │ ├── cardreader │ │ │ │ ├── GetCardDetails.kt │ │ │ │ ├── IdentifyText.kt │ │ │ │ └── processor │ │ │ │ │ ├── CCFrontProcessor.kt │ │ │ │ │ ├── IDCardBackProcessor.kt │ │ │ │ │ ├── IDCardFrontProcessor.kt │ │ │ │ │ └── TextProcessor.kt │ │ │ ├── chat │ │ │ │ ├── CheckEntityExtractorAvailability.kt │ │ │ │ ├── FetchChatDetails.kt │ │ │ │ ├── FetchLatestMessage.kt │ │ │ │ ├── FetchSuggestions.kt │ │ │ │ ├── GetEmojiSuggestions.kt │ │ │ │ ├── LoadEmojis.kt │ │ │ │ └── SendMessage.kt │ │ │ ├── facenet │ │ │ │ ├── DetectFaces.kt │ │ │ │ ├── FetchAllFaces.kt │ │ │ │ ├── FetchAllImages.kt │ │ │ │ ├── FetchAllImagesForFace.kt │ │ │ │ ├── FetchFaceNames.kt │ │ │ │ ├── Initiate.kt │ │ │ │ ├── LoadEmbeddings.kt │ │ │ │ ├── SaveFaceEmbedding.kt │ │ │ │ ├── SaveImage.kt │ │ │ │ └── SaveNewFace.kt │ │ │ └── models │ │ │ │ ├── CreditCard.kt │ │ │ │ ├── FaceModel.kt │ │ │ │ ├── FaceRecognitionResult.kt │ │ │ │ ├── GalleryModel.kt │ │ │ │ ├── IDCard.kt │ │ │ │ ├── User.kt │ │ │ │ └── chat │ │ │ │ ├── ChatAnnotation.kt │ │ │ │ ├── ChatDetails.kt │ │ │ │ ├── ChatRowModel.kt │ │ │ │ └── EntityModel.kt │ │ │ └── ui │ │ │ ├── Destinations.kt │ │ │ ├── MLApp.kt │ │ │ ├── base │ │ │ ├── AskPermissionScreen.kt │ │ │ ├── ExternalIntentHandler.kt │ │ │ ├── ForEachRow.kt │ │ │ ├── Line.kt │ │ │ └── camera │ │ │ │ ├── Camera.kt │ │ │ │ └── CameraControls.kt │ │ │ ├── cardreader │ │ │ ├── CameraScreen.kt │ │ │ ├── CardReaderScreen.kt │ │ │ ├── DetailsScreen.kt │ │ │ ├── OnBoardingScreen.kt │ │ │ └── OnBoardingViewModel.kt │ │ │ ├── chat │ │ │ ├── ChatBubble.kt │ │ │ ├── ChatClickableText.kt │ │ │ ├── SmartChatScreen.kt │ │ │ └── SmartChatViewModel.kt │ │ │ ├── digitalink │ │ │ ├── DigitalInkScreen.kt │ │ │ ├── DigitalInkViewModel.kt │ │ │ ├── DrawSpace.kt │ │ │ └── MLKitModelStatus.kt │ │ │ ├── facenet │ │ │ ├── CameraScreen.kt │ │ │ ├── CheckFaceDialog.kt │ │ │ ├── FacenetScreen.kt │ │ │ ├── FacenetViewModel.kt │ │ │ └── SavedFaceImage.kt │ │ │ ├── home │ │ │ ├── HomeScreen.kt │ │ │ └── HomeViewModel.kt │ │ │ ├── styletransfer │ │ │ ├── CameraScreen.kt │ │ │ ├── StylePreviewScreen.kt │ │ │ ├── StyleTransferScreen.kt │ │ │ └── StyleTransferViewModel.kt │ │ │ └── theme │ │ │ ├── Color.kt │ │ │ ├── Shape.kt │ │ │ ├── Theme.kt │ │ │ └── Type.kt │ ├── ml │ │ ├── facenet.tflite │ │ ├── magenta_style_predictor.tflite │ │ ├── magenta_style_transfer.tflite │ │ ├── style_predictor_model.tflite │ │ └── style_transfer_model.tflite │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── ic_switch_camera.png │ │ ├── mipmap-hdpi │ │ └── ic_launcher.png │ │ ├── mipmap-mdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxhdpi │ │ └── ic_launcher.png │ │ ├── mipmap-xxxhdpi │ │ └── ic_launcher.png │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── backup_rules.xml │ │ ├── data_extraction_rules.xml │ │ └── file_paths.xml │ └── test │ └── java │ └── in │ └── surajsau │ └── jisho │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── buildSrc ├── build.gradle.kts └── src │ └── main │ └── java │ └── Versions.kt ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── screenshots ├── card_reader.gif ├── face.gif ├── gpt2.gif └── translate_app.gif └── settings.gradle /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ## Description 2 | 3 | ## Screenshots 4 | 5 | ## Checklist: 6 | - [ ] Test 7 | - [ ] Generate Screenshot 8 | - [ ] Add entry in `ScreensProvider` 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | *.aab 5 | 6 | # Files for the ART/Dalvik VM 7 | *.dex 8 | 9 | # Java class files 10 | *.class 11 | 12 | # Generated files 13 | bin/ 14 | gen/ 15 | out/ 16 | 17 | # Gradle files 18 | .gradle/ 19 | build/ 20 | 21 | # Local configuration file (sdk path, etc) 22 | local.properties 23 | 24 | # Proguard folder generated by Eclipse 25 | proguard/ 26 | 27 | # Log Files 28 | *.log 29 | 30 | # Android Studio Navigation editor temp files 31 | .navigation/ 32 | 33 | # Android Studio captures folder 34 | captures/ 35 | 36 | # IntelliJ 37 | *.iml 38 | .idea/workspace.xml 39 | .idea/tasks.xml 40 | .idea/gradle.xml 41 | .idea/assetWizardSettings.xml 42 | .idea/dictionaries 43 | .idea/libraries 44 | .idea/caches 45 | 46 | # Keystore files 47 | # Uncomment the following lines if you do not want to check your keystore files in. 48 | #*.jks 49 | #*.keystore 50 | 51 | # External native build folder generated in Android Studio 2.2 and later 52 | .externalNativeBuild 53 | 54 | # Google Services (e.g. APIs or Firebase) 55 | google-services.json 56 | 57 | # Freeline 58 | freeline.py 59 | freeline/ 60 | freeline_project_description.json 61 | 62 | # fastlane 63 | fastlane/report.xml 64 | fastlane/Preview.html 65 | fastlane/screenshots 66 | fastlane/test_output 67 | fastlane/readme.md 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ML Android 2 | Implementing various ML usecases in Android using strictly Jetpack Compose 3 | 4 | | **Demo** | **About** | 5 | |--|--| 6 | | |

On-device Google Translate


- Character Recognition and Translation using Google MLKit
- Drawable Canvas implemented in Jetpack Compose
- [Code walkthrough](https://proandroiddev.com/on-device-google-translate-with-jetpack-compose-mlkit-7a48f5b11948) | 7 | | |

Google Photos like Face recognition


- Face Detection using MLKit
- Face recognition using [Sirius AI's Mobile Facenet](https://github.com/sirius-ai/MobileFaceNet_TF)| 8 | | |

Simplified Onboarding with Text Recognition


- Text Recognition using MLKit's Text Recognition| 9 | 10 | # License 11 | ``` 12 | Copyright 2022 Suraj Sau 13 | 14 | Licensed under the Apache License, Version 2.0 (the "License"); 15 | you may not use this file except in compliance with the License. 16 | You may obtain a copy of the License at 17 | 18 | https://www.apache.org/licenses/LICENSE-2.0 19 | 20 | Unless required by applicable law or agreed to in writing, software 21 | distributed under the License is distributed on an "AS IS" BASIS, 22 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 23 | See the License for the specific language governing permissions and 24 | limitations under the License. 25 | ``` 26 | -------------------------------------------------------------------------------- /app/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/.DS_Store -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | 3 | # tflite 4 | /src/main/assets/gpt/ 5 | /src/main/assets/facenet/ 6 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import `in`.surajsau.jisho.Dep 2 | 3 | plugins { 4 | id("com.android.application") 5 | id("dagger.hilt.android.plugin") 6 | id("kotlin-android") 7 | id("kotlin-android-extensions") 8 | id("kotlin-kapt") 9 | } 10 | 11 | android { 12 | compileSdk = 31 13 | 14 | defaultConfig { 15 | applicationId = "in.surajsau.jisho" 16 | minSdk = 24 17 | targetSdk = 31 18 | versionCode = 1 19 | versionName = "1.0" 20 | 21 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 22 | vectorDrawables { 23 | useSupportLibrary = true 24 | } 25 | } 26 | 27 | buildTypes { 28 | release { 29 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") 30 | } 31 | } 32 | compileOptions { 33 | sourceCompatibility = JavaVersion.VERSION_1_8 34 | targetCompatibility = JavaVersion.VERSION_1_8 35 | } 36 | kotlinOptions { 37 | jvmTarget = "1.8" 38 | } 39 | buildFeatures { 40 | compose = true 41 | mlModelBinding = true 42 | } 43 | composeOptions { 44 | kotlinCompilerExtensionVersion = "1.0.4" 45 | } 46 | androidResources { 47 | noCompress("tflite") 48 | } 49 | packagingOptions { 50 | resources { 51 | excludes += "/META-INF/{AL2.0,LGPL2.1}" 52 | } 53 | } 54 | } 55 | 56 | dependencies { 57 | 58 | implementation(Dep.AndroidX.Core) 59 | implementation(Dep.AndroidX.AppCompat) 60 | implementation(Dep.AndroidX.Material) 61 | implementation(Dep.AndroidX.Lifeycycle) 62 | 63 | // Temporary Fix For apps targeting Android S+, add the following 64 | constraints { 65 | implementation("androidx.work:work-runtime:2.7.0-alpha04") 66 | } 67 | 68 | implementation(Dep.Compose.Ui) 69 | implementation(Dep.Compose.Material) 70 | implementation(Dep.Compose.Tooling) 71 | implementation(Dep.Compose.LiveData) 72 | implementation(Dep.Compose.ViewModel) 73 | implementation(Dep.Compose.Activity) 74 | implementation(Dep.Compose.Navigation) 75 | 76 | implementation(Dep.CameraX.Core) 77 | implementation(Dep.CameraX.Lifecycle) 78 | implementation(Dep.CameraX.View) 79 | 80 | implementation(Dep.Accompanist.SwipeRefresh) 81 | implementation(Dep.Accompanist.Permissions) 82 | 83 | implementation(Dep.Hilt.Core) 84 | implementation(Dep.TensorFlow.Support) 85 | implementation(Dep.TensorFlow.MetaData) 86 | kapt(Dep.Hilt.Compiler) 87 | implementation(Dep.Hilt.Compose) 88 | implementation(Dep.Hilt.ViewModel) 89 | kapt(Dep.Hilt.AndroidCompiler) 90 | 91 | implementation(Dep.MLKit.DigitalInk) 92 | implementation(Dep.MLKit.Translation) 93 | implementation(Dep.MLKit.FaceDetection) 94 | implementation(Dep.MLKit.TextRecognition) 95 | implementation(Dep.MLKit.EntityExtraction) 96 | implementation(Dep.MLKit.SmartReplies) 97 | implementation(Dep.MLKit.ImageLabeling) 98 | 99 | implementation(Dep.Emoji.Core) 100 | implementation(Dep.Emoji.Views) 101 | implementation(Dep.Emoji.Helper) 102 | 103 | implementation(Dep.Coil.Compose) 104 | implementation(Dep.Coil.Gif) 105 | 106 | implementation(Dep.Klock) 107 | 108 | implementation(Dep.Gson) 109 | 110 | implementation(Dep.Room.Core) 111 | implementation(Dep.Room.Ktx) 112 | kapt(Dep.Room.Compiler) 113 | 114 | testImplementation("junit:junit:4.13.2") 115 | androidTestImplementation("androidx.test.ext:junit:1.1.3") 116 | androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") 117 | androidTestImplementation(Dep.Compose.Test) 118 | debugImplementation(Dep.Compose.ToolingTest) 119 | } -------------------------------------------------------------------------------- /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/in/surajsau/jisho/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho 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("in.surajsau.jisho", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 22 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 39 | 42 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/assets/style0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/assets/style0.jpg -------------------------------------------------------------------------------- /app/src/main/assets/style1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/assets/style1.jpg -------------------------------------------------------------------------------- /app/src/main/assets/style2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/assets/style2.jpg -------------------------------------------------------------------------------- /app/src/main/assets/style3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/assets/style3.jpg -------------------------------------------------------------------------------- /app/src/main/assets/style4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/assets/style4.jpg -------------------------------------------------------------------------------- /app/src/main/assets/style5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/assets/style5.jpg -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/App.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho 2 | 3 | import android.app.Application 4 | import dagger.hilt.android.HiltAndroidApp 5 | 6 | @HiltAndroidApp 7 | class App: Application() -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho 2 | 3 | import android.os.Bundle 4 | import androidx.activity.ComponentActivity 5 | import androidx.activity.compose.setContent 6 | import dagger.hilt.android.AndroidEntryPoint 7 | import `in`.surajsau.jisho.ui.MLApp 8 | import android.content.Intent 9 | import android.net.Uri 10 | import android.provider.Settings 11 | 12 | @AndroidEntryPoint 13 | class MainActivity : ComponentActivity() { 14 | 15 | override fun onCreate(savedInstanceState: Bundle?) { 16 | super.onCreate(savedInstanceState) 17 | setContent { MLApp( 18 | navigateToSettings = { 19 | startActivity(Intent().apply { 20 | action = Settings.ACTION_APPLICATION_DETAILS_SETTINGS 21 | data = Uri.fromParts("package", BuildConfig.APPLICATION_ID, null) 22 | addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) 23 | }) 24 | } 25 | ) } 26 | } 27 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/base/BitmapCache.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.base 2 | 3 | import android.graphics.Bitmap 4 | import androidx.collection.LruCache 5 | import androidx.compose.runtime.compositionLocalOf 6 | 7 | class BitmapCache { 8 | 9 | private val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt() 10 | 11 | private val cache: LruCache = LruCache(maxMemory/8) 12 | 13 | fun save(fileName: String, bitmap: Bitmap) { 14 | this.cache.put(fileName, bitmap) 15 | } 16 | 17 | fun has(fileName: String): Boolean = this.cache.get(fileName) != null 18 | 19 | fun get(fileName: String) = this.cache.get(fileName) ?: throw Exception("Couldn't find $fileName") 20 | 21 | fun clear() { 22 | this.cache.evictAll() 23 | } 24 | 25 | } 26 | 27 | val LocalBitmapCache = compositionLocalOf { error("BitmapCache instance not provided") } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/base/BitmapUtils.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.base 2 | 3 | import android.graphics.Bitmap 4 | import android.graphics.Matrix 5 | import android.graphics.Rect 6 | 7 | fun Bitmap.rotate(degree: Int): Bitmap { 8 | val matrix = Matrix() 9 | matrix.postRotate(degree.toFloat()) 10 | return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) 11 | } 12 | 13 | fun Rect.scale(factor: Float): Rect { 14 | val left = (this.left * factor).toInt() 15 | val right = (left + (this.width() * factor).toInt()) 16 | val top = (this.top * factor).toInt() 17 | val bottom = (top + (this.height() * factor).toInt()) 18 | 19 | return Rect(left, top, right, bottom) 20 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/base/BoolUtils.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.base 2 | 3 | fun Boolean.toInt() = if (this) 1 else 0 -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/base/ContextUtils.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.base 2 | 3 | import android.content.Context 4 | import android.net.Uri 5 | import androidx.core.content.FileProvider 6 | import java.io.File 7 | 8 | fun Context.getUriForFile(file: File) = FileProvider.getUriForFile(this, "$packageName.fileprovider", file) 9 | 10 | fun Context.getUriForImage(folderName: String, fileName: String): Uri { 11 | val file = File(folderName, fileName) 12 | return getUriForFile(file = file) 13 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/base/FileName.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.base 2 | 3 | @JvmInline 4 | value class FileName(val value: String) -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/base/ListUtils.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.base 2 | 3 | fun List.subListFrom(startIndex: Int) = this.subList(startIndex, this.size - 1) 4 | 5 | fun Map.reverseMap(): Map = this.entries.associateBy({it.value}, {it.key}) -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/base/NormalizeOp.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.base 2 | 3 | import org.tensorflow.lite.DataType 4 | import org.tensorflow.lite.support.common.TensorOperator 5 | import org.tensorflow.lite.support.tensorbuffer.TensorBuffer 6 | import org.tensorflow.lite.support.tensorbuffer.TensorBufferFloat 7 | import kotlin.math.max 8 | import kotlin.math.pow 9 | import kotlin.math.sqrt 10 | 11 | class NormalizeOp: TensorOperator { 12 | 13 | override fun apply(input: TensorBuffer): TensorBuffer { 14 | val pixels = input.floatArray 15 | val mean = pixels.average().toFloat() 16 | val standardDeviation = sqrt(pixels.map { pixel -> (pixel - mean).pow(2) }.sum() / pixels.size.toFloat()).let { 17 | max(it, 1f/ sqrt(pixels.size.toFloat())) 18 | } 19 | 20 | val normalizedPixels = pixels.map { (it - mean)/standardDeviation }.toFloatArray() 21 | val output = TensorBufferFloat.createFixedSize(input.shape, DataType.FLOAT32) 22 | output.loadArray(normalizedPixels) 23 | 24 | return output 25 | } 26 | 27 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/base/ObserveLifecycle.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.base 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.DisposableEffect 5 | import androidx.compose.ui.platform.LocalLifecycleOwner 6 | import androidx.lifecycle.Lifecycle 7 | import androidx.lifecycle.LifecycleEventObserver 8 | 9 | @Composable 10 | fun ObserveLifecycle( 11 | onEvent: (Lifecycle.Event) -> Unit 12 | ) { 13 | val lifecycleOwner = LocalLifecycleOwner.current 14 | 15 | DisposableEffect(Unit) { 16 | val observer = LifecycleEventObserver { _, event -> onEvent.invoke(event)} 17 | lifecycleOwner.lifecycle.addObserver(observer) 18 | 19 | onDispose { lifecycleOwner.lifecycle.removeObserver(observer) } 20 | } 21 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/base/Optional.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.base 2 | 3 | sealed class Optional { 4 | 5 | data class Some(val data: T): Optional() 6 | object Empty: Optional() 7 | 8 | fun getValue(): T { 9 | if (this is Empty) { 10 | throw Exception("$this is empty") 11 | } 12 | return (this as? Some)?.data!! 13 | } 14 | 15 | companion object { 16 | 17 | fun of(value: T?): Optional { 18 | if (value == null) 19 | return Empty 20 | return Some(data = value) 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/base/SingleFlowViewModel.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.base 2 | 3 | import androidx.compose.runtime.Composable 4 | import androidx.compose.runtime.collectAsState 5 | import androidx.compose.runtime.getValue 6 | import androidx.compose.runtime.remember 7 | import androidx.compose.ui.platform.LocalLifecycleOwner 8 | import androidx.lifecycle.Lifecycle 9 | import androidx.lifecycle.flowWithLifecycle 10 | import kotlinx.coroutines.flow.StateFlow 11 | 12 | interface SingleFlowViewModel { 13 | val state: StateFlow 14 | fun onEvent(event: Event) 15 | } 16 | 17 | data class ViewModelComponents( 18 | val state: State, 19 | val onEvent: (Event) -> Unit 20 | ) 21 | 22 | @Composable 23 | fun use( 24 | viewModel: SingleFlowViewModel, 25 | initialStateValue: State, 26 | ): ViewModelComponents { 27 | 28 | val lifecycleOwner = LocalLifecycleOwner.current 29 | 30 | val lifecycleAwareState = remember(viewModel.state, lifecycleOwner) { 31 | viewModel.state.flowWithLifecycle(lifecycleOwner.lifecycle, Lifecycle.State.STARTED) 32 | } 33 | 34 | val state = lifecycleAwareState.collectAsState(initialStateValue) 35 | 36 | return ViewModelComponents( 37 | state = state.value, 38 | onEvent = { viewModel.onEvent(it) } 39 | ) 40 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/CardDataProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data 2 | 3 | import android.graphics.Bitmap 4 | import android.util.Log 5 | import com.google.mlkit.vision.common.InputImage 6 | import com.google.mlkit.vision.text.Text 7 | import com.google.mlkit.vision.text.TextRecognition 8 | import com.google.mlkit.vision.text.latin.TextRecognizerOptions 9 | import kotlinx.coroutines.channels.awaitClose 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.callbackFlow 12 | import javax.inject.Inject 13 | 14 | class CardDataProviderImpl @Inject constructor(): CardDataProvider { 15 | 16 | private val recognizer by lazy { 17 | TextRecognition.getClient(TextRecognizerOptions.Builder().build()) 18 | } 19 | 20 | override fun identifyTexts(bitmap: Bitmap, language: CardDataProvider.Language): Flow = callbackFlow { 21 | val image = InputImage.fromBitmap(bitmap, 0) 22 | recognizer.process(image) 23 | .addOnSuccessListener { 24 | Log.e("Card", it.text) 25 | trySend(it) 26 | } 27 | .addOnFailureListener { 28 | it.printStackTrace() 29 | throw it 30 | } 31 | .addOnCompleteListener { close() } 32 | 33 | awaitClose { close() } 34 | } 35 | } 36 | 37 | interface CardDataProvider { 38 | 39 | fun identifyTexts(bitmap: Bitmap, language: Language = Language.EN): Flow 40 | 41 | enum class Language { EN, JP, HI } 42 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/ChatDataProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data 2 | 3 | import `in`.surajsau.jisho.data.model.ChatMessageModel 4 | import `in`.surajsau.jisho.domain.models.chat.ChatDetails 5 | import android.util.Log 6 | import kotlinx.coroutines.channels.Channel 7 | import kotlinx.coroutines.delay 8 | import kotlinx.coroutines.flow.Flow 9 | import kotlinx.coroutines.flow.flow 10 | import javax.inject.Inject 11 | import kotlin.random.Random 12 | 13 | class ChatDataProviderImpl @Inject constructor(): ChatDataProvider { 14 | 15 | private val randomDelay = Random(300L) 16 | 17 | override val latestMessage: Channel = Channel() 18 | 19 | override suspend fun sendMessage( 20 | message: String, 21 | imageUrl: String?, 22 | isMe: Boolean, 23 | ) { 24 | val ts = System.currentTimeMillis() 25 | val id = (ts/1000).toInt() 26 | val model = ChatMessageModel.Message( 27 | id = id, 28 | text = message, 29 | isMe = isMe, 30 | ts = ts, 31 | imageUrl = imageUrl, 32 | ) 33 | 34 | // add random delay to act as an api 35 | if (!isMe) { 36 | latestMessage.trySend(ChatMessageModel.Typing(ts = ts)) 37 | delay(randomDelay.nextLong(300L, 2000L)) 38 | } 39 | 40 | latestMessage.trySend(model) 41 | } 42 | 43 | override fun fetchChatDetails(): Flow = flow { 44 | emit( 45 | ChatDetails( 46 | chatName = "レオレウス", 47 | chatIconUrl = "https://i.pinimg.com/originals/76/99/4b/76994b84e7e7f53a4cf1a5a4c52736d4.jpg" 48 | ) 49 | ) 50 | } 51 | } 52 | 53 | interface ChatDataProvider { 54 | 55 | val latestMessage: Channel 56 | 57 | suspend fun sendMessage( 58 | message: String, 59 | imageUrl: String? = null, 60 | isMe: Boolean 61 | ) 62 | 63 | fun fetchChatDetails(): Flow 64 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/DigitalInkProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data 2 | 3 | import `in`.surajsau.jisho.ui.digitalink.MLKitModelStatus 4 | import com.google.mlkit.common.model.DownloadConditions 5 | import com.google.mlkit.common.model.RemoteModelManager 6 | import com.google.mlkit.vision.digitalink.* 7 | import kotlinx.coroutines.cancel 8 | import kotlinx.coroutines.channels.Channel 9 | import kotlinx.coroutines.channels.awaitClose 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.callbackFlow 12 | import javax.inject.Inject 13 | 14 | class DigitalInkProviderImpl @Inject constructor(): DigitalInkProvider { 15 | 16 | override val predictions = Channel>(4) 17 | 18 | private var strokeBuilder: Ink.Stroke.Builder = Ink.Stroke.builder() 19 | 20 | private val recognitionModel = DigitalInkRecognitionModel 21 | .builder(DigitalInkRecognitionModelIdentifier.JA) 22 | .build() 23 | 24 | private val remoteModelManager = RemoteModelManager.getInstance() 25 | 26 | private val recognizer = DigitalInkRecognition.getClient( 27 | DigitalInkRecognizerOptions 28 | .builder(this.recognitionModel) 29 | .build() 30 | ) 31 | 32 | override fun checkIfModelIsDownlaoded(): Flow = callbackFlow { 33 | trySend(MLKitModelStatus.CheckingDownload) 34 | 35 | this@DigitalInkProviderImpl.remoteModelManager 36 | .isModelDownloaded(this@DigitalInkProviderImpl.recognitionModel) 37 | .addOnSuccessListener { isDownloaded -> 38 | if (isDownloaded) 39 | trySend(MLKitModelStatus.Downloaded) 40 | else 41 | trySend(MLKitModelStatus.NotDownloaded) 42 | } 43 | .addOnCompleteListener { close() } 44 | .addOnFailureListener { 45 | it.printStackTrace() 46 | close(it) 47 | } 48 | 49 | awaitClose { cancel() } 50 | } 51 | 52 | override fun downloadModel(): Flow = callbackFlow { 53 | val downloadConditions = DownloadConditions.Builder() 54 | .build() 55 | 56 | trySend(MLKitModelStatus.Downloading) 57 | this@DigitalInkProviderImpl.remoteModelManager 58 | .download(this@DigitalInkProviderImpl.recognitionModel, downloadConditions) 59 | .addOnSuccessListener { 60 | trySend(MLKitModelStatus.Downloaded) 61 | } 62 | .addOnCompleteListener { close() } 63 | .addOnFailureListener { 64 | it.printStackTrace() 65 | close(it) 66 | } 67 | 68 | awaitClose { cancel() } 69 | } 70 | 71 | override fun record(x: Float, y: Float) { 72 | val point = Ink.Point.create(x, y) 73 | this.strokeBuilder.addPoint(point) 74 | } 75 | 76 | override fun finishRecording() { 77 | val stroke = this.strokeBuilder.build() 78 | 79 | val inkBuilder = Ink.builder() 80 | inkBuilder.addStroke(stroke) 81 | 82 | this.recognizer.recognize(inkBuilder.build()) 83 | .addOnCompleteListener { 84 | this.strokeBuilder = Ink.Stroke.builder() 85 | } 86 | .addOnSuccessListener { result -> this.predictions.trySend(result.candidates.map { it.text }) } 87 | .addOnFailureListener { it.printStackTrace() } 88 | } 89 | 90 | override fun close() { 91 | this.recognizer.close() 92 | } 93 | } 94 | 95 | interface DigitalInkProvider { 96 | 97 | val predictions: Channel> 98 | 99 | fun finishRecording() 100 | fun record(x: Float, y: Float) 101 | 102 | fun downloadModel(): Flow 103 | fun checkIfModelIsDownlaoded(): Flow 104 | 105 | fun close() 106 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/FileProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data 2 | 3 | import `in`.surajsau.jisho.base.getUriForFile 4 | import `in`.surajsau.jisho.base.rotate 5 | import android.content.Context 6 | import android.graphics.Bitmap 7 | import android.graphics.BitmapFactory 8 | import android.media.ExifInterface 9 | import android.os.Environment 10 | import android.util.JsonReader 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.flow 13 | import org.tensorflow.lite.Interpreter 14 | import java.io.* 15 | import java.lang.StringBuilder 16 | import java.nio.channels.FileChannel 17 | import javax.inject.Inject 18 | 19 | class FileProviderImpl @Inject constructor(private val context: Context): FileProvider { 20 | 21 | override suspend fun fetchCachedBitmap(fileName: String): Bitmap { 22 | val cacheDir = context.externalCacheDir ?: throw Exception("external cache not found") 23 | val bitmap = getBitmapForFile(File(cacheDir, fileName)) 24 | return bitmap.getOrElse { throw it } 25 | } 26 | 27 | override suspend fun cacheBitmap(bitmap: Bitmap): String { 28 | val cacheDir = context.externalCacheDir ?: throw Exception("external cache not found") 29 | val fileName = "${System.currentTimeMillis()}.jpg" 30 | 31 | val imageFile = File(cacheDir, fileName) 32 | 33 | runCatching { 34 | val os = imageFile.outputStream() 35 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os) 36 | os.close() 37 | }.onFailure { throw it } 38 | 39 | return fileName 40 | } 41 | 42 | override suspend fun storeBitmap(folderName: String, fileName: String, bitmap: Bitmap) { 43 | val storageFolder = File(context.filesDir, folderName) 44 | if (!storageFolder.exists()) 45 | storageFolder.mkdirs() 46 | 47 | val imageFile = File(storageFolder, fileName) 48 | 49 | val uri = context.getUriForFile(imageFile) 50 | runCatching { 51 | val os = context.contentResolver.openOutputStream(uri) ?: throw Exception("Couldn't open OutpuStream") 52 | bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os) 53 | os.close() 54 | }.onFailure { throw it } 55 | } 56 | 57 | override fun getCacheFilePath(fileName: String): String { 58 | val cacheDir = context.externalCacheDir ?: throw Exception("external cache not found") 59 | return File(cacheDir, fileName).absolutePath 60 | } 61 | 62 | override fun getFilePath(folderName: String, fileName: String): String { 63 | val storageFolder = File(context.filesDir, folderName) 64 | return File(storageFolder, fileName).absolutePath 65 | } 66 | 67 | override fun fetchAssetBitmap(fileName: String): Flow = flow { 68 | val bitmap = runCatching { 69 | with(context.assets.open(fileName)) { BitmapFactory.decodeStream(this) } 70 | } 71 | 72 | emit(bitmap.getOrElse { throw it }) 73 | } 74 | 75 | override fun fetchAssetInputStream(fileName: String) = context.assets.open(fileName) 76 | 77 | override fun fetchInterpreter(modelFileName: String): Interpreter { 78 | val assetFileDescriptor = context.assets.openFd(modelFileName) 79 | 80 | val fileChannel = FileInputStream(assetFileDescriptor.fileDescriptor).channel 81 | val fileByteBuffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, assetFileDescriptor.startOffset, assetFileDescriptor.declaredLength) 82 | 83 | return Interpreter(fileByteBuffer, Interpreter.Options().apply { setNumThreads(4) }) 84 | } 85 | 86 | override fun fetchFiles(folderName: String): Flow> = flow { 87 | val storageFolder = File(context.filesDir, folderName) 88 | if (!storageFolder.exists()) 89 | storageFolder.mkdirs() 90 | 91 | val files = storageFolder.listFiles() 92 | if (files.isNullOrEmpty()) 93 | emit(emptyList()) 94 | else { 95 | emit(files.map { it.absolutePath }) 96 | } 97 | } 98 | 99 | private fun getBitmapForFile(file: File): Result = runCatching { 100 | val exif = ExifInterface(file.absolutePath) 101 | val inputStream = FileInputStream(file) 102 | val fileBitmap = BitmapFactory.decodeStream(inputStream) 103 | 104 | return@runCatching when (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)) { 105 | ExifInterface.ORIENTATION_ROTATE_90 -> fileBitmap.rotate(90) 106 | ExifInterface.ORIENTATION_ROTATE_180 -> fileBitmap.rotate(180) 107 | ExifInterface.ORIENTATION_ROTATE_270 -> fileBitmap.rotate(270) 108 | else -> fileBitmap 109 | } 110 | }.onFailure { it.printStackTrace() } 111 | 112 | override suspend fun writeStringToFile(folderName: String, fileName: String, string: String) { 113 | val storageFolder = File(context.filesDir, folderName) 114 | 115 | if (!storageFolder.exists()) 116 | storageFolder.mkdirs() 117 | 118 | val file = File(storageFolder, fileName) 119 | 120 | val uri = context.getUriForFile(file) 121 | runCatching { 122 | val os = context.contentResolver.openOutputStream(uri) ?: throw Exception("Couldn't open OutpuStream") 123 | val osw = OutputStreamWriter(os) 124 | val bw = BufferedWriter(osw) 125 | bw.newLine() 126 | bw.write(string) 127 | 128 | bw.close() 129 | osw.close() 130 | os.close() 131 | }.onFailure { throw it } 132 | } 133 | 134 | override suspend fun readStringFromFile(folderName: String, fileName: String): List { 135 | val storageFolder = File(context.filesDir, folderName) 136 | 137 | if (!storageFolder.exists()) 138 | storageFolder.mkdirs() 139 | 140 | val file = File(storageFolder, fileName) 141 | val result = mutableListOf() 142 | 143 | runCatching { 144 | val br = BufferedReader(FileReader(file)) 145 | var line = br.readLine() 146 | while(line != null) { 147 | result.add(line) 148 | line = br.readLine() 149 | } 150 | br.close() 151 | }.onFailure { 152 | it.printStackTrace() 153 | return emptyList() 154 | } 155 | 156 | return result 157 | } 158 | } 159 | 160 | interface FileProvider { 161 | 162 | fun fetchFiles(folderName: String): Flow> 163 | 164 | suspend fun fetchCachedBitmap(fileName: String): Bitmap 165 | 166 | fun fetchAssetBitmap(fileName: String): Flow 167 | 168 | fun fetchAssetInputStream(fileName: String): InputStream 169 | 170 | fun fetchInterpreter(modelFileName: String): Interpreter 171 | 172 | suspend fun cacheBitmap(bitmap: Bitmap): String 173 | 174 | suspend fun storeBitmap(folderName: String, fileName: String, bitmap: Bitmap) 175 | 176 | fun getCacheFilePath(fileName: String): String 177 | 178 | fun getFilePath(folderName: String, fileName: String): String 179 | 180 | suspend fun writeStringToFile(folderName: String, fileName: String, string: String) 181 | 182 | suspend fun readStringFromFile(folderName: String, fileName: String): List 183 | 184 | companion object { 185 | const val FACENET_IMAGE_FOLDER = "images/faces/" 186 | } 187 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/ScreensDataProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data 2 | 3 | import `in`.surajsau.jisho.data.model.Screen 4 | import `in`.surajsau.jisho.ui.Destinations 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.flow 7 | import javax.inject.Inject 8 | 9 | class ScreensDataProviderImpl @Inject constructor(): ScreensDataProvider { 10 | 11 | override fun fetchScreen(): Flow> = flow { 12 | emit(listOf( 13 | Screen( 14 | destinations = Destinations.DigitalInk, 15 | previewImage = "https://github.com/surajsau/ML-Android/raw/main/screenshots/translate_app.gif", 16 | title = "On-device Google Translate", 17 | description = "Character Recognition & Translation using Google MLKit", 18 | tags = listOf("MLKit") 19 | ), 20 | 21 | Screen( 22 | destinations = Destinations.StyleTransfer, 23 | previewImage = "", 24 | title = "Style Transfer", 25 | description = "Character Recognition & Translation using Google MLKit", 26 | tags = listOf("TFLite") 27 | ), 28 | 29 | Screen( 30 | destinations = Destinations.SmartChat, 31 | previewImage = "", 32 | title = "Smart Chat", 33 | description = "Using MLKit Smart Replies, MLKit Entity Extraction and Image Classification", 34 | tags = listOf("MLKit", "TorchVision") 35 | ), 36 | 37 | Screen( 38 | destinations = Destinations.Facenet, 39 | previewImage = "https://github.com/surajsau/ML-Android/raw/main/screenshots/face.gif", 40 | title = "On-device Face Recognition & Classification", 41 | description = "Using MLKit's FaceDetection to crop out face and using Sirius-AI's MobileFacenet tflite for recognition", 42 | tags = listOf("MLKit", "TFLite") 43 | ), 44 | 45 | Screen( 46 | destinations = Destinations.CardReader, 47 | previewImage = "https://github.com/surajsau/ML-Android/raw/main/screenshots/card_reader.gif", 48 | title = "Card Reader", 49 | description = "Using MLKit's Text Recognition", 50 | tags = listOf("MLKit") 51 | ) 52 | ) 53 | ) 54 | } 55 | } 56 | 57 | interface ScreensDataProvider { 58 | 59 | fun fetchScreen(): Flow> 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/StyleTransferProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data 2 | 3 | import `in`.surajsau.jisho.ml.MagentaStylePredictor 4 | import `in`.surajsau.jisho.ml.MagentaStyleTransfer 5 | import `in`.surajsau.jisho.ml.StylePredictorModel 6 | import `in`.surajsau.jisho.ml.StyleTransferModel 7 | import android.content.Context 8 | import android.graphics.Bitmap 9 | import kotlinx.coroutines.channels.Channel 10 | import kotlinx.coroutines.flow.Flow 11 | import kotlinx.coroutines.flow.flow 12 | import org.tensorflow.lite.DataType 13 | import org.tensorflow.lite.support.image.TensorImage 14 | import org.tensorflow.lite.support.tensorbuffer.TensorBuffer 15 | import javax.inject.Inject 16 | 17 | class StyleTransferProviderImpl @Inject constructor( 18 | private val context: Context, 19 | ): StyleTransferProvider { 20 | 21 | private val stylePredictorModel = MagentaStylePredictor.newInstance(context) 22 | private val styleTransferModel = MagentaStyleTransfer.newInstance(context) 23 | 24 | override fun process(targetImage: Bitmap, styleImage: Bitmap): Flow = flow { 25 | emit(StyleTransferProvider.StyleTransferState.Started) 26 | 27 | val styleTensor = TensorImage.fromBitmap(styleImage) 28 | val targetTensor = TensorImage.fromBitmap(targetImage) 29 | val styleOutput = stylePredictorModel.process(styleTensor).styleBottleneckAsTensorBuffer 30 | 31 | val styleBottleneck = TensorBuffer.createFixedSize(intArrayOf(1, 1, 1, StyleTransferProvider.BottleNeckSize), DataType.FLOAT32).apply { 32 | loadBuffer(styleOutput.buffer) 33 | } 34 | 35 | val output = styleTransferModel.process(targetTensor, styleBottleneck).styledImageAsTensorImage 36 | emit(StyleTransferProvider.StyleTransferState.Finished(output.bitmap)) 37 | } 38 | } 39 | 40 | interface StyleTransferProvider { 41 | 42 | fun process(targetImage: Bitmap, styleImage: Bitmap): Flow 43 | 44 | sealed class StyleTransferState { 45 | object Idle: StyleTransferState() 46 | object Started: StyleTransferState() 47 | data class Finished(val image: Bitmap): StyleTransferState() 48 | } 49 | 50 | companion object { 51 | const val BottleNeckSize = 100 52 | const val StyleImageSize = 256 53 | const val ContentImageSize = 384 54 | 55 | val Styles = (0..5).map { "style$it.jpg" } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/TranslatorProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data 2 | 3 | import `in`.surajsau.jisho.ui.digitalink.MLKitModelStatus 4 | import com.google.mlkit.common.model.DownloadConditions 5 | import com.google.mlkit.nl.translate.TranslateLanguage 6 | import com.google.mlkit.nl.translate.Translation 7 | import com.google.mlkit.nl.translate.TranslatorOptions 8 | import kotlinx.coroutines.cancel 9 | import kotlinx.coroutines.channels.Channel 10 | import kotlinx.coroutines.channels.awaitClose 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.callbackFlow 13 | import javax.inject.Inject 14 | 15 | class TranslatorProviderImpl @Inject constructor(): TranslatorProvider { 16 | 17 | override val translation = Channel(4) 18 | 19 | private val translatorOptions = TranslatorOptions.Builder() 20 | .setSourceLanguage(TranslateLanguage.JAPANESE) 21 | .setTargetLanguage(TranslateLanguage.ENGLISH) 22 | .build() 23 | 24 | private val translator = Translation.getClient(this.translatorOptions) 25 | 26 | override fun checkIfModelIsDownloaded(): Flow = callbackFlow { 27 | trySend(MLKitModelStatus.CheckingDownload) 28 | val downloadConditions = DownloadConditions.Builder() 29 | .build() 30 | 31 | this@TranslatorProviderImpl.translator.downloadModelIfNeeded(downloadConditions) 32 | .addOnSuccessListener { 33 | trySend(MLKitModelStatus.Downloaded) 34 | } 35 | .addOnCompleteListener { close() } 36 | .addOnFailureListener { 37 | it.printStackTrace() 38 | close(it) 39 | } 40 | 41 | awaitClose { cancel() } 42 | } 43 | 44 | override fun translate(text: String) { 45 | this.translator.translate(text) 46 | .addOnSuccessListener { this.translation.trySend(it) } 47 | .addOnFailureListener { it.printStackTrace() } 48 | } 49 | 50 | override fun close() { 51 | this.translator.close() 52 | } 53 | } 54 | 55 | interface TranslatorProvider { 56 | 57 | val translation: Channel 58 | 59 | fun checkIfModelIsDownloaded(): Flow 60 | fun translate(text: String) 61 | 62 | fun close() 63 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/chat/EmojiLabellingDataProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.chat 2 | 3 | import `in`.surajsau.jisho.data.FileProvider 4 | import `in`.surajsau.jisho.data.model.Emoji 5 | import android.content.Context 6 | import android.graphics.Bitmap 7 | import android.util.Log 8 | import com.google.gson.Gson 9 | import com.google.gson.reflect.TypeToken 10 | import com.google.mlkit.vision.common.InputImage 11 | import com.google.mlkit.vision.label.ImageLabeling 12 | import com.google.mlkit.vision.label.defaults.ImageLabelerOptions 13 | import kotlinx.coroutines.cancel 14 | import kotlinx.coroutines.channels.awaitClose 15 | import kotlinx.coroutines.flow.Flow 16 | import kotlinx.coroutines.flow.callbackFlow 17 | import javax.inject.Inject 18 | 19 | class EmojiLabellingDataProviderImpl @Inject constructor( 20 | private val fileProvider: FileProvider 21 | ): EmojiLabellingDataProvider { 22 | 23 | private var emojis: List = emptyList() 24 | 25 | private val labeller by lazy { 26 | ImageLabeling.getClient(ImageLabelerOptions.Builder().build()) 27 | } 28 | 29 | override suspend fun loadEmojis() { 30 | runCatching { 31 | val gson = Gson() 32 | val inputStream = fileProvider.fetchAssetInputStream("emojis.json") 33 | val jsonString = inputStream.bufferedReader().use { it.readText() } 34 | 35 | val listEmojiType = object: TypeToken>(){}.type 36 | emojis = gson.fromJson(jsonString, listEmojiType) 37 | }.onFailure { it.printStackTrace() } 38 | } 39 | 40 | override fun getEmojis(bitmap: Bitmap): Flow> = callbackFlow { 41 | val inputImage = InputImage.fromBitmap(bitmap, 0) 42 | labeller.process(inputImage) 43 | .addOnSuccessListener { labels -> 44 | val result = mutableListOf() 45 | labels 46 | .filter { label -> label.confidence > 0.4 } 47 | .forEach { label -> 48 | val emoji = emojis.firstOrNull { it.label == label.text && it.emojis.isNotEmpty() } ?: return@forEach 49 | result.add(emoji) 50 | } 51 | 52 | trySend(result) 53 | } 54 | .addOnFailureListener { 55 | it.printStackTrace() 56 | cancel("getEmojis() failed", it) 57 | } 58 | .addOnCompleteListener { close() } 59 | 60 | awaitClose { close() } 61 | } 62 | 63 | } 64 | 65 | interface EmojiLabellingDataProvider { 66 | 67 | suspend fun loadEmojis() 68 | 69 | fun getEmojis(bitmap: Bitmap): Flow> 70 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/chat/EntityExtractionProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.chat 2 | 3 | import `in`.surajsau.jisho.ui.digitalink.MLKitModelStatus 4 | import com.google.android.gms.tasks.Tasks 5 | import com.google.mlkit.common.model.DownloadConditions 6 | import com.google.mlkit.nl.entityextraction.EntityAnnotation 7 | import com.google.mlkit.nl.entityextraction.EntityExtraction 8 | import com.google.mlkit.nl.entityextraction.EntityExtractionParams 9 | import com.google.mlkit.nl.entityextraction.EntityExtractorOptions 10 | import kotlinx.coroutines.channels.awaitClose 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.callbackFlow 13 | import java.util.* 14 | import javax.inject.Inject 15 | 16 | class EntityExtractionpProviderImpl @Inject constructor(): EntityExtractionProvider { 17 | 18 | private val extractor by lazy { 19 | EntityExtraction.getClient(EntityExtractorOptions.Builder(EntityExtractorOptions.ENGLISH).build()) 20 | } 21 | 22 | override fun initModel(): Flow = callbackFlow { 23 | trySend(MLKitModelStatus.CheckingDownload) 24 | 25 | val downloadConditions = DownloadConditions.Builder() 26 | .requireWifi() 27 | .build() 28 | 29 | extractor.downloadModelIfNeeded(downloadConditions) 30 | .addOnSuccessListener { trySend(MLKitModelStatus.Downloaded) } 31 | .addOnFailureListener { 32 | it.printStackTrace() 33 | throw it 34 | } 35 | .addOnCompleteListener { close() } 36 | 37 | awaitClose { close() } 38 | } 39 | 40 | override suspend fun extractEntities(text: String): List { 41 | val extractionParams = EntityExtractionParams.Builder(text) 42 | .build() 43 | 44 | return Tasks.await(extractor.annotate(extractionParams)) 45 | } 46 | 47 | 48 | } 49 | 50 | interface EntityExtractionProvider { 51 | 52 | fun initModel(): Flow 53 | 54 | suspend fun extractEntities(text: String): List 55 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/chat/ReplySuggestionProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.chat 2 | 3 | import `in`.surajsau.jisho.data.model.ChatMessageModel 4 | import com.google.android.gms.tasks.Tasks 5 | import com.google.mlkit.nl.smartreply.* 6 | import kotlinx.coroutines.channels.Channel 7 | import javax.inject.Inject 8 | 9 | class ReplySuggestionProviderImpl @Inject constructor() : ReplySuggestionProvider { 10 | 11 | private val smartReplyGenerator by lazy { 12 | SmartReply.getClient(SmartReplyGeneratorOptions.Builder().build()) 13 | } 14 | 15 | private val conversation = mutableListOf() 16 | 17 | override val suggestions: Channel> = Channel() 18 | 19 | override suspend fun addMessage(message: ChatMessageModel.Message) { 20 | conversation.add( 21 | if (message.isMe) 22 | TextMessage.createForLocalUser(message.text, message.timeStamp) 23 | else 24 | TextMessage.createForRemoteUser(message.text, message.timeStamp, "1") 25 | ) 26 | 27 | if (!message.isMe) { 28 | val task = smartReplyGenerator.suggestReplies(conversation) 29 | val result = Tasks.await(task) 30 | 31 | suggestions.trySend(result.suggestions) 32 | } 33 | } 34 | 35 | override fun clearConversation() { 36 | conversation.clear() 37 | } 38 | } 39 | 40 | interface ReplySuggestionProvider { 41 | 42 | val suggestions: Channel> 43 | 44 | suspend fun addMessage(message: ChatMessageModel.Message) 45 | 46 | fun clearConversation() 47 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/db/AppDb.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.db 2 | 3 | import androidx.room.Database 4 | import androidx.room.RoomDatabase 5 | 6 | @Database(entities = [FaceImage::class], version = AppDb.VERSION) 7 | abstract class AppDb: RoomDatabase() { 8 | 9 | abstract fun faceDao(): FacenetDAO 10 | 11 | companion object { 12 | const val FILE_NAME = "_ml_android_" 13 | const val VERSION = 1 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/db/FaceImage.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.db 2 | 3 | import androidx.room.ColumnInfo 4 | import androidx.room.Entity 5 | import androidx.room.PrimaryKey 6 | 7 | @Entity 8 | data class FaceImage( 9 | @PrimaryKey(autoGenerate = true) 10 | val uid: Int, 11 | 12 | @ColumnInfo(name = "isPrimary") val isPrimary: Boolean, 13 | @ColumnInfo(name = "fileName") val fileName: String, 14 | @ColumnInfo(name = "faceName") val faceName: String 15 | ) { 16 | constructor(isPrimary: Boolean, fileName: String, faceName: String): this(0, isPrimary, fileName, faceName) 17 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/db/FacenetDAO.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.db 2 | 3 | import androidx.room.Dao 4 | import androidx.room.Insert 5 | import androidx.room.OnConflictStrategy 6 | import androidx.room.Query 7 | 8 | @Dao 9 | interface FacenetDAO { 10 | 11 | @Query("SELECT * FROM faceimage WHERE isPrimary = 1") 12 | suspend fun fetchAllFaces(): List 13 | 14 | @Query("SELECT * FROM faceimage WHERE faceName = :name AND isPrimary = :isPrimary") 15 | suspend fun fetchImagesFor(name: String, isPrimary: Int): List 16 | 17 | @Query("SELECT * FROM faceimage WHERE faceName = :name") 18 | suspend fun fetchImagesFor(name: String): List 19 | 20 | @Query("SELECT * FROM faceimage WHERE isPrimary = :isPrimary") 21 | suspend fun fetchAllImages(isPrimary: Int): List 22 | 23 | @Query("SELECT DISTINCT faceName FROM faceimage") 24 | suspend fun getFaceNames(): List 25 | 26 | @Insert(onConflict = OnConflictStrategy.REPLACE) 27 | suspend fun saveFace(face: FaceImage) 28 | } 29 | -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/facenet/FaceDetectionProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.facenet 2 | 3 | import android.graphics.Bitmap 4 | import android.util.Log 5 | import androidx.camera.core.ExperimentalGetImage 6 | import androidx.camera.core.ImageProxy 7 | import com.google.mlkit.vision.common.InputImage 8 | import com.google.mlkit.vision.face.Face 9 | import com.google.mlkit.vision.face.FaceDetection 10 | import kotlinx.coroutines.cancel 11 | import kotlinx.coroutines.channels.awaitClose 12 | import kotlinx.coroutines.flow.Flow 13 | import kotlinx.coroutines.flow.callbackFlow 14 | import kotlinx.coroutines.flow.map 15 | import javax.inject.Inject 16 | 17 | class FaceDetectionProviderImpl @Inject constructor(): FaceDetectionProvider { 18 | 19 | val detector by lazy { FaceDetection.getClient() } 20 | 21 | override fun getFaces(bitmap: Bitmap): Flow> { 22 | val image = InputImage.fromBitmap(bitmap, 0) 23 | return detectFacesFlow(image) 24 | .map { faces -> 25 | faces.map { face -> 26 | Bitmap.createBitmap( 27 | bitmap, 28 | face.boundingBox.left, 29 | face.boundingBox.top, 30 | face.boundingBox.width(), 31 | face.boundingBox.height() 32 | ) 33 | } 34 | } 35 | } 36 | 37 | private fun detectFacesFlow(image: InputImage?): Flow> = callbackFlow { 38 | if (image == null) 39 | close(Exception("Image not found")) 40 | 41 | detector.process(image!!) 42 | .addOnFailureListener { 43 | it.printStackTrace() 44 | throw it 45 | } 46 | .addOnSuccessListener { faces -> trySend(faces) } 47 | .addOnCompleteListener { 48 | Log.e("Facenet", "Image detection completed") 49 | close() 50 | } 51 | 52 | awaitClose { close() } 53 | } 54 | 55 | } 56 | 57 | interface FaceDetectionProvider { 58 | 59 | fun getFaces(bitmap: Bitmap): Flow> 60 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/facenet/FaceRecognitionProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.facenet 2 | 3 | import `in`.surajsau.jisho.base.NormalizeOp 4 | import `in`.surajsau.jisho.data.FileProvider 5 | import `in`.surajsau.jisho.ml.Facenet 6 | import `in`.surajsau.jisho.ui.digitalink.MLKitModelStatus 7 | import android.content.Context 8 | import android.graphics.Bitmap 9 | import android.util.Log 10 | import android.view.ContextMenu 11 | import kotlinx.coroutines.flow.Flow 12 | import kotlinx.coroutines.flow.flow 13 | import org.tensorflow.lite.support.image.ImageProcessor 14 | import org.tensorflow.lite.support.image.TensorImage 15 | import org.tensorflow.lite.support.image.ops.ResizeOp 16 | import javax.inject.Inject 17 | import javax.inject.Singleton 18 | import kotlin.math.pow 19 | import kotlin.math.sqrt 20 | 21 | class FaceRecognitionProviderImpl @Inject constructor( 22 | private val context: Context, 23 | private val fileProvider: FileProvider, 24 | ): FaceRecognitionProvider { 25 | 26 | private lateinit var facenet: Facenet 27 | 28 | val embeddings = mutableMapOf>() 29 | 30 | override suspend fun loadEmbeddingFor(faceName: String) { 31 | val lines = fileProvider.readStringFromFile( 32 | folderName = FaceRecognitionProvider.FACENET_EMBEDDINGS_FOLDER, 33 | fileName = faceName 34 | ) 35 | val embedding = lines 36 | .filter { it.isNotEmpty() } 37 | .map { line -> line.split(":").map { it.toFloat() }.toFloatArray() } 38 | embeddings[faceName] = embedding 39 | } 40 | 41 | private val imageProcessor = ImageProcessor.Builder() 42 | .add(ResizeOp(160, 160, ResizeOp.ResizeMethod.BILINEAR)) 43 | .add(NormalizeOp()) 44 | .build() 45 | 46 | override suspend fun generateEmbedding(bitmap: Bitmap): FloatArray { 47 | val input = imageProcessor.process(TensorImage.fromBitmap(bitmap)) 48 | val output = facenet.process(input.tensorBuffer) 49 | 50 | return output.outputFeature0AsTensorBuffer.floatArray 51 | } 52 | 53 | override suspend fun saveEmbedding(faceName: String, embedding: FloatArray) { 54 | fileProvider.writeStringToFile( 55 | folderName = FaceRecognitionProvider.FACENET_EMBEDDINGS_FOLDER, 56 | fileName = faceName, 57 | string = embedding.joinToString(":") 58 | ) 59 | 60 | if (embeddings.containsKey(faceName)) 61 | embeddings[faceName]!!.toMutableList().add(embedding) 62 | else 63 | embeddings[faceName] = mutableListOf().apply { add(embedding) } 64 | } 65 | 66 | override suspend fun compareEmbedding(embedding: FloatArray): Map> { 67 | return embeddings.mapValues { entry -> entry.value.map { cosineSimilarity(embedding, it) }} 68 | } 69 | 70 | override fun close() { 71 | facenet.close() 72 | embeddings.clear() 73 | } 74 | 75 | override fun initiate() { 76 | facenet = Facenet.newInstance(context) 77 | } 78 | 79 | private fun l2Similarity(embedding1: FloatArray, embedding2: FloatArray): Float { 80 | return sqrt(embedding1.mapIndexed { i, e1 -> (e1 * embedding2[i]).pow(2) }.sum()) 81 | } 82 | 83 | private fun cosineSimilarity(embedding1: FloatArray, embedding2: FloatArray): Float { 84 | val magnitude1 = sqrt(embedding1.map { it * it }.sum()) 85 | val magnitude2 = sqrt(embedding2.map { it * it }.sum()) 86 | 87 | val dot = embedding1.mapIndexed { i, e1 -> e1 * embedding2[i] }.sum() 88 | 89 | return (dot/magnitude1 * magnitude2) 90 | } 91 | 92 | } 93 | 94 | interface FaceRecognitionProvider { 95 | 96 | suspend fun loadEmbeddingFor(faceName: String) 97 | 98 | suspend fun saveEmbedding(faceName: String, embedding: FloatArray) 99 | 100 | suspend fun generateEmbedding(bitmap: Bitmap): FloatArray 101 | 102 | suspend fun compareEmbedding(embedding: FloatArray): Map> 103 | 104 | fun close() 105 | 106 | fun initiate() 107 | 108 | companion object { 109 | const val FACENET_EMBEDDINGS_FOLDER = "embeddings/faces/" 110 | } 111 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/facenet/FacesDataProvider.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.facenet 2 | 3 | import `in`.surajsau.jisho.base.toInt 4 | import `in`.surajsau.jisho.data.db.FaceImage 5 | import `in`.surajsau.jisho.data.db.FacenetDAO 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.flow 8 | import javax.inject.Inject 9 | 10 | class FacesDataProviderImpl @Inject constructor(private val dao: FacenetDAO) : FacesDataProvider { 11 | 12 | override fun getImagesForFace(faceName: String, onlyPrimary: Boolean): Flow> = flow { 13 | val faces = if (onlyPrimary) 14 | dao.fetchImagesFor(name = faceName, isPrimary = true.toInt()) 15 | else 16 | dao.fetchImagesFor(name = faceName) 17 | emit(faces) 18 | } 19 | 20 | override fun getAllImages(isPrimary: Boolean): Flow> = flow { 21 | val faces = dao.fetchAllImages(isPrimary = isPrimary.toInt()) 22 | emit(faces) 23 | } 24 | 25 | override suspend fun saveFace(faceName: String, fileName: String, isPrimary: Boolean) { 26 | val face = FaceImage(isPrimary = isPrimary, faceName = faceName, fileName = fileName) 27 | dao.saveFace(face) 28 | } 29 | 30 | override suspend fun getFaceNames(): List { 31 | return dao.getFaceNames() 32 | } 33 | } 34 | 35 | interface FacesDataProvider { 36 | 37 | suspend fun saveFace(faceName: String, fileName: String, isPrimary: Boolean) 38 | 39 | suspend fun getFaceNames(): List 40 | 41 | fun getAllImages(isPrimary: Boolean): Flow> 42 | 43 | fun getImagesForFace(faceName: String, onlyPrimary: Boolean = false): Flow> 44 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/model/ChatMessageModel.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.model 2 | 3 | sealed class ChatMessageModel(val timeStamp: Long) { 4 | data class Typing(private val ts: Long): ChatMessageModel(timeStamp = ts) 5 | data class Message( 6 | val id: Int, 7 | val text: String, 8 | val isMe: Boolean, 9 | private val ts: Long, 10 | val imageUrl: String?, 11 | ): ChatMessageModel(timeStamp = ts) 12 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/model/Emoji.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.model 2 | 3 | data class Emoji( 4 | val emojis: List, 5 | val label: String, 6 | ) 7 | -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/model/Screen.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.model 2 | 3 | import `in`.surajsau.jisho.ui.Destinations 4 | 5 | data class Screen( 6 | val destinations: Destinations, 7 | val previewImage: String = "", 8 | val title: String, 9 | val description: String, 10 | val tags: List = emptyList() 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/data/model/Suggestion.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.data.model 2 | 3 | sealed class Suggestion { 4 | object Interpreting: Suggestion() 5 | data class Message(val value: String): Suggestion() 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/di/DataModule.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.di 2 | 3 | import `in`.surajsau.jisho.data.* 4 | import `in`.surajsau.jisho.data.chat.* 5 | import `in`.surajsau.jisho.data.db.AppDb 6 | import `in`.surajsau.jisho.data.facenet.* 7 | import android.content.Context 8 | import androidx.room.Room 9 | import dagger.Binds 10 | import dagger.Module 11 | import dagger.Provides 12 | import dagger.hilt.InstallIn 13 | import dagger.hilt.android.qualifiers.ApplicationContext 14 | import dagger.hilt.components.SingletonComponent 15 | import javax.inject.Singleton 16 | 17 | @Module 18 | @InstallIn(SingletonComponent::class) 19 | abstract class DataModule { 20 | 21 | @Binds 22 | @Singleton 23 | abstract fun bindTranslatorProvider(provider: TranslatorProviderImpl): TranslatorProvider 24 | 25 | @Binds 26 | @Singleton 27 | abstract fun bindDigitalInkProvider(provider: DigitalInkProviderImpl): DigitalInkProvider 28 | 29 | @Binds 30 | @Singleton 31 | abstract fun bindScreenDataProvider(provider: ScreensDataProviderImpl): ScreensDataProvider 32 | 33 | @Binds 34 | @Singleton 35 | abstract fun bindChatProvider(provider: ChatDataProviderImpl): ChatDataProvider 36 | 37 | @Binds 38 | @Singleton 39 | abstract fun bindFaceDetectionProvider(provider: FaceDetectionProviderImpl): FaceDetectionProvider 40 | 41 | @Binds 42 | @Singleton 43 | abstract fun bindCardDataProvider(provider: CardDataProviderImpl): CardDataProvider 44 | 45 | @Binds 46 | @Singleton 47 | abstract fun bindEntityExtractionProvider(provider: EntityExtractionpProviderImpl): EntityExtractionProvider 48 | 49 | @Binds 50 | @Singleton 51 | abstract fun bindSmartRepliesProvider(provider: ReplySuggestionProviderImpl): ReplySuggestionProvider 52 | 53 | @Binds 54 | @Singleton 55 | abstract fun bindEmojiLabellingProvider(provider: EmojiLabellingDataProviderImpl): EmojiLabellingDataProvider 56 | 57 | companion object { 58 | 59 | @Provides 60 | @Singleton 61 | fun provideFileProvider(@ApplicationContext context: Context): FileProvider = FileProviderImpl(context) 62 | 63 | @Provides 64 | @Singleton 65 | fun provideStyleTransfer(@ApplicationContext context: Context): StyleTransferProvider 66 | = StyleTransferProviderImpl(context) 67 | 68 | @Provides 69 | @Singleton 70 | fun provideDb(@ApplicationContext context: Context): AppDb = Room 71 | .databaseBuilder(context, AppDb::class.java, AppDb.FILE_NAME) 72 | .build() 73 | 74 | @Provides 75 | @Singleton 76 | fun provideFacesProvider(appDb: AppDb): FacesDataProvider = FacesDataProviderImpl(appDb.faceDao()) 77 | 78 | @Provides 79 | @Singleton 80 | fun proviceFaceRecognitionProvider( 81 | @ApplicationContext context: Context, 82 | fileProvider: FileProvider, 83 | ): FaceRecognitionProvider = FaceRecognitionProviderImpl(context, fileProvider) 84 | } 85 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/di/cardreader/OnboardingScreenModule.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.di.cardreader 2 | 3 | import `in`.surajsau.jisho.data.CardDataProvider 4 | import `in`.surajsau.jisho.data.FileProvider 5 | import `in`.surajsau.jisho.domain.cardreader.GetCardDetails 6 | import `in`.surajsau.jisho.domain.cardreader.processor.CCFrontProcessor 7 | import `in`.surajsau.jisho.domain.cardreader.processor.IDCardBackProcessor 8 | import `in`.surajsau.jisho.domain.cardreader.processor.IDCardFrontProcessor 9 | import `in`.surajsau.jisho.domain.models.CreditCard 10 | import `in`.surajsau.jisho.domain.models.IDCard 11 | import dagger.Module 12 | import dagger.Provides 13 | import dagger.hilt.InstallIn 14 | import dagger.hilt.components.SingletonComponent 15 | 16 | @Module 17 | @InstallIn(SingletonComponent::class) 18 | abstract class OnboardingScreenModule { 19 | 20 | companion object { 21 | 22 | @Provides 23 | fun provideCCFrontDetails( 24 | fileProvider: FileProvider, 25 | cardDataProvider: CardDataProvider 26 | ): GetCardDetails { 27 | return GetCardDetails( 28 | textProcessor = CCFrontProcessor(), 29 | fileProvider = fileProvider, 30 | cardDataProvider = cardDataProvider 31 | ) 32 | } 33 | 34 | @Provides 35 | fun provideGetCardFrontDetails( 36 | fileProvider: FileProvider, 37 | cardDataProvider: CardDataProvider 38 | ): GetCardDetails { 39 | return GetCardDetails( 40 | textProcessor = IDCardFrontProcessor(), 41 | fileProvider = fileProvider, 42 | cardDataProvider = cardDataProvider 43 | ) 44 | } 45 | 46 | @Provides 47 | fun provideGetCardBackDetails( 48 | fileProvider: FileProvider, 49 | cardDataProvider: CardDataProvider 50 | ): GetCardDetails { 51 | return GetCardDetails( 52 | textProcessor = IDCardBackProcessor(), 53 | fileProvider = fileProvider, 54 | cardDataProvider = cardDataProvider 55 | ) 56 | } 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/Cleanup.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain 2 | 3 | import `in`.surajsau.jisho.data.CardDataProvider 4 | import `in`.surajsau.jisho.data.facenet.FaceRecognitionProvider 5 | import javax.inject.Inject 6 | 7 | class Cleanup @Inject constructor( 8 | private val faceRecognitionProvider: FaceRecognitionProvider, 9 | ) { 10 | fun invoke() { 11 | faceRecognitionProvider.close() 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/cardreader/GetCardDetails.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.cardreader 2 | 3 | import `in`.surajsau.jisho.data.CardDataProvider 4 | import `in`.surajsau.jisho.data.FileProvider 5 | import `in`.surajsau.jisho.domain.cardreader.processor.TextProcessor 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.flatMapLatest 8 | import kotlinx.coroutines.flow.flow 9 | import kotlinx.coroutines.flow.map 10 | import javax.inject.Inject 11 | 12 | class GetCardDetails @Inject constructor( 13 | private val textProcessor: TextProcessor, 14 | private val cardDataProvider: CardDataProvider, 15 | private val fileProvider: FileProvider 16 | ) { 17 | 18 | fun invoke(fileName: String): Flow 19 | = flow { emit(fileProvider.fetchCachedBitmap(fileName = fileName)) } 20 | .flatMapLatest { cardDataProvider.identifyTexts(it) } 21 | .map { textResult -> textProcessor.process(textResult) } 22 | 23 | 24 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/cardreader/IdentifyText.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.cardreader 2 | 3 | import `in`.surajsau.jisho.data.CardDataProvider 4 | import `in`.surajsau.jisho.data.FileProvider 5 | import kotlinx.coroutines.flow.Flow 6 | import kotlinx.coroutines.flow.flatMapLatest 7 | import kotlinx.coroutines.flow.flow 8 | import kotlinx.coroutines.flow.map 9 | import javax.inject.Inject 10 | 11 | class IdentifyText @Inject constructor( 12 | private val cardDataProvider: CardDataProvider, 13 | private val fileProvider: FileProvider 14 | ) { 15 | 16 | fun invoke(fileName: String, language: CardDataProvider.Language = CardDataProvider.Language.EN): Flow 17 | = flow { emit(fileProvider.fetchCachedBitmap(fileName)) } 18 | .flatMapLatest { cardDataProvider.identifyTexts(it, language) } 19 | .map { it.text } 20 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/cardreader/processor/CCFrontProcessor.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.cardreader.processor 2 | 3 | import `in`.surajsau.jisho.domain.models.CreditCard 4 | import android.util.Log 5 | import com.google.mlkit.vision.text.Text 6 | import com.soywiz.klock.DateTime 7 | 8 | class CCFrontProcessor : TextProcessor { 9 | 10 | override fun process(textResult: Text): CreditCard.Front { 11 | val lines = textResult.text 12 | Log.e("Card", lines) 13 | return CreditCard.Front("", DateTime.now().date, "") 14 | } 15 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/cardreader/processor/IDCardBackProcessor.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.cardreader.processor 2 | 3 | import `in`.surajsau.jisho.base.Optional 4 | import `in`.surajsau.jisho.domain.models.IDCard 5 | import android.util.Log 6 | import android.util.Patterns 7 | import com.google.mlkit.vision.text.Text 8 | 9 | /* 10 | Card No. : S00150 11 | Date of Issue : June.23, 2015 12 | Address for correspondence: 13 | Qr. No. - 413 C, Sector 4, 14 | Ukkunagaram Township, 15 | VISAKHAPATNAM - 530032 16 | Mob. No. : +91- 7829953236 17 | e-mail: i@surajsau.in 18 | 2015010006 19 | */ 20 | 21 | class IDCardBackProcessor : TextProcessor { 22 | 23 | override fun process(textResult: Text): IDCard.Back { 24 | val lines = textResult.text.replace(":", "").split("\n") 25 | 26 | Log.e("Card", lines.joinToString("\n")) 27 | 28 | val mobileNumber = lines 29 | .map { Optional.of(MobileNumberRegex.find(it)?.value) } 30 | .first { it !is Optional.Empty } 31 | 32 | val email = lines 33 | .map { Optional.of(EmailRegex.find(it)?.value) } 34 | .first { it !is Optional.Empty } 35 | 36 | val address = lines.subList( 37 | fromIndex = lines.indexOfFirst { it.startsWith("Address") } + 1, 38 | toIndex = lines.indexOfFirst { it.startsWith("Mob.") } 39 | ).joinToString(separator = "\n") 40 | 41 | return IDCard.Back( 42 | address = address, 43 | mobileNumber = mobileNumber.getValue(), 44 | email = email.getValue() 45 | ) 46 | } 47 | 48 | companion object { 49 | private val MobileNumberRegex = Regex("(\\+91-\\s+)\\d{10}") 50 | private val EmailRegex = Patterns.EMAIL_ADDRESS.pattern().toRegex() 51 | } 52 | 53 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/cardreader/processor/IDCardFrontProcessor.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.cardreader.processor 2 | 3 | import `in`.surajsau.jisho.domain.models.IDCard 4 | import android.util.Log 5 | import androidx.core.text.isDigitsOnly 6 | import com.google.mlkit.vision.text.Text 7 | import com.soywiz.klock.DateFormat 8 | import com.soywiz.klock.parseDate 9 | 10 | /* 11 | Sample: 12 | ------- 13 | ALUMNI ASSOCIATION 14 | INDIAN INSTITUTE OF TECHNOLOGY ROORKEE 15 | (Formerly University of Roorkee)) 16 | ROORKEE -247667, UTTRAKHAND, INDIA 17 | Membership No. : 2015010006 18 | Name 19 | :Suraj Kumar Sau 20 | Year 21 | :2015 22 | Degree 23 | : B.Tech (ECE) 24 | Date of Birth :26-04-1994 25 | Issuing Au 26 | Hon. Secretary 27 | Signature of Card Holder 28 | 29 | */ 30 | 31 | class IDCardFrontProcessor: TextProcessor { 32 | 33 | override fun process(textResult: Text): IDCard.Front { 34 | val lines = textResult 35 | .text 36 | .replace(":", "") 37 | .split("\n") 38 | .filterNot { 39 | it.startsWith("ALUMNI") or 40 | it.startsWith("INDIAN") or 41 | it.startsWith("(Formerly") or 42 | it.startsWith("ROORKEE") or 43 | it.startsWith("Issuing") or 44 | it.startsWith("Hon.") or 45 | it.startsWith("Signature") or 46 | it.startsWith("Date") or 47 | it.startsWith("Degree") or 48 | it.startsWith("Name") 49 | }.map { it.trim() } 50 | 51 | Log.e("Card", lines.joinToString("\n")) 52 | 53 | val words = lines.flatMap { it.split(" ") } 54 | 55 | val year = words.firstOrNull { it.isDigitsOnly() && it.length == 4 } ?: "" 56 | 57 | val dateOfBirth = words.firstOrNull { it.matches(DateOfBirthRegex) } ?: "" 58 | 59 | val membershipNumber = words.firstOrNull { it.isDigitsOnly() && it.length > 4 } ?: "" 60 | 61 | val name = lines.firstOrNull { it.matches("[a-zA-Z\\s]+".toRegex()) and (it.split(" ").size > 1) } ?: "" 62 | 63 | val degree = lines.firstOrNull { it.matches(DegreeRegex) } ?: "" 64 | 65 | return IDCard.Front( 66 | membershipNumber = membershipNumber, 67 | name = name, 68 | year = year, 69 | dateOfBirth = DateFormat.invoke("dd-MM-yyyy").parseDate(dateOfBirth), 70 | degree = degree 71 | ) 72 | } 73 | 74 | companion object { 75 | private val DateOfBirthRegex = Regex("(0[1-9]|[12][0-9]|3[01])-(0[1-9]|1[0-2])-\\d{4}") 76 | private val DegreeRegex = Regex("([BM])\\.([A-Za-z]+)\\s\\(([A-Z]+)\\)") 77 | } 78 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/cardreader/processor/TextProcessor.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.cardreader.processor 2 | 3 | import com.google.mlkit.vision.text.Text 4 | 5 | interface TextProcessor { 6 | fun process(textResult: Text): T 7 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/chat/CheckEntityExtractorAvailability.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.chat 2 | 3 | import `in`.surajsau.jisho.data.chat.EntityExtractionProvider 4 | import javax.inject.Inject 5 | 6 | class CheckEntityExtractorAvailability @Inject constructor( 7 | private val entityExtractionProvider: EntityExtractionProvider 8 | ) { 9 | 10 | fun invoke() = entityExtractionProvider.initModel() 11 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/chat/FetchChatDetails.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.chat 2 | 3 | import `in`.surajsau.jisho.data.ChatDataProvider 4 | import `in`.surajsau.jisho.domain.models.chat.ChatDetails 5 | import kotlinx.coroutines.flow.Flow 6 | import javax.inject.Inject 7 | 8 | class FetchChatDetails @Inject constructor( 9 | private val chatDataProvider: ChatDataProvider 10 | ) { 11 | 12 | fun invoke(): Flow = chatDataProvider.fetchChatDetails() 13 | } 14 | -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/chat/FetchLatestMessage.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.chat 2 | 3 | import `in`.surajsau.jisho.base.Optional 4 | import `in`.surajsau.jisho.data.ChatDataProvider 5 | import `in`.surajsau.jisho.data.chat.EntityExtractionProvider 6 | import `in`.surajsau.jisho.data.chat.ReplySuggestionProvider 7 | import `in`.surajsau.jisho.data.model.ChatMessageModel 8 | import `in`.surajsau.jisho.domain.models.chat.ChatAnnotation 9 | import `in`.surajsau.jisho.domain.models.chat.ChatRowModel 10 | import androidx.compose.ui.text.SpanStyle 11 | import androidx.compose.ui.text.buildAnnotatedString 12 | import androidx.compose.ui.text.style.TextDecoration 13 | import androidx.compose.ui.text.withStyle 14 | import com.google.mlkit.nl.entityextraction.DateTimeEntity 15 | import com.google.mlkit.nl.entityextraction.Entity 16 | import com.soywiz.klock.DateTime 17 | import kotlinx.coroutines.flow.* 18 | import javax.inject.Inject 19 | 20 | class FetchLatestMessage @Inject constructor( 21 | private val replySuggestionProvider: ReplySuggestionProvider, 22 | private val chatDataProvider: ChatDataProvider, 23 | private val entityExtractionProvider: EntityExtractionProvider, 24 | ) { 25 | 26 | fun invoke(): Flow { 27 | return chatDataProvider.latestMessage 28 | .receiveAsFlow() 29 | .onEach { 30 | if (it is ChatMessageModel.Message) { 31 | replySuggestionProvider.addMessage(it) 32 | } 33 | } 34 | .map { 35 | when (it) { 36 | is ChatMessageModel.Message -> { 37 | val annotations = entityExtractionProvider.extractEntities(it.text) 38 | val mainText = it.text 39 | 40 | val annotationMaps = mutableListOf() 41 | 42 | val text = buildAnnotatedString { 43 | if (annotations.isEmpty()) { 44 | append(mainText) 45 | return@buildAnnotatedString 46 | } 47 | 48 | val firstAnnotation = annotations.first() 49 | 50 | annotations.forEachIndexed { index, annotation -> 51 | // if first annotation, append text before the first annotation 52 | if (index == 0 && annotation.start > 0) { 53 | append(mainText.substring(startIndex = 0, endIndex = firstAnnotation.start)) 54 | } 55 | 56 | val entity = annotation.entities.first() 57 | val annotatedSubString = mainText.substring(startIndex = annotation.start, endIndex = annotation.end) 58 | 59 | val chatAnnotation = when { 60 | entity is DateTimeEntity -> { 61 | Optional.of(ChatAnnotation.Reminder(id = "${it.id}:$index", timeStamp = entity.timestampMillis)) 62 | } 63 | 64 | entity.type == Entity.TYPE_ADDRESS -> { 65 | Optional.of(ChatAnnotation.Address(id = "${it.id}:$index", address = annotation.annotatedText)) 66 | } 67 | 68 | entity.type == Entity.TYPE_EMAIL -> { 69 | Optional.of(ChatAnnotation.Email(id = "${it.id}:$index", email = annotation.annotatedText)) 70 | } 71 | 72 | entity.type == Entity.TYPE_PHONE -> { 73 | Optional.of(ChatAnnotation.Phone(id = "${it.id}:$index", phone = annotation.annotatedText)) 74 | } 75 | 76 | else -> Optional.Empty 77 | } 78 | 79 | if (chatAnnotation is Optional.Some) { 80 | annotationMaps.add(chatAnnotation.data) 81 | 82 | pushStringAnnotation(tag = chatAnnotation.data.tag, annotation = mainText) 83 | withStyle(style = SpanStyle(textDecoration = TextDecoration.Underline)) { 84 | append(annotatedSubString) 85 | } 86 | pop() 87 | 88 | } else { 89 | append(annotatedSubString) 90 | } 91 | 92 | // if last annotation, append text after the last annotation 93 | if (index == annotations.size - 1 && annotation.end < mainText.length - 1) { 94 | append(mainText.substring(startIndex = annotation.end + 1)) 95 | } 96 | } 97 | } 98 | 99 | if (!it.imageUrl.isNullOrEmpty()) { 100 | ChatRowModel.PictureMessage( 101 | imageUrl = it.imageUrl, 102 | message = text, 103 | isLocal = it.isMe, 104 | timestamp = DateTime.invoke(it.timeStamp).toString("HH:mm"), 105 | annotationMaps = annotationMaps 106 | ) 107 | } else { 108 | ChatRowModel.Message( 109 | value = text, 110 | isLocal = it.isMe, 111 | timestamp = DateTime.invoke(it.timeStamp).toString("HH:mm"), 112 | annotationMaps = annotationMaps 113 | ) 114 | } 115 | } 116 | 117 | is ChatMessageModel.Typing -> ChatRowModel.Typing 118 | } 119 | } 120 | } 121 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/chat/FetchSuggestions.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.chat 2 | 3 | import `in`.surajsau.jisho.data.chat.ReplySuggestionProvider 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.map 6 | import kotlinx.coroutines.flow.receiveAsFlow 7 | import javax.inject.Inject 8 | 9 | class FetchSuggestions @Inject constructor( 10 | private val replySuggestionProvider: ReplySuggestionProvider 11 | ) { 12 | 13 | fun invoke(): Flow> { 14 | return replySuggestionProvider.suggestions 15 | .receiveAsFlow() 16 | .map { suggestions -> suggestions.map { it.text } } 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/chat/GetEmojiSuggestions.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.chat 2 | 3 | import `in`.surajsau.jisho.data.chat.EmojiLabellingDataProvider 4 | import `in`.surajsau.jisho.data.model.Emoji 5 | import android.graphics.Bitmap 6 | import kotlinx.coroutines.flow.Flow 7 | import javax.inject.Inject 8 | 9 | class GetEmojiSuggestions @Inject constructor( 10 | private val emojiLabellingDataProvider: EmojiLabellingDataProvider 11 | ) { 12 | 13 | fun invoke(bitmap: Bitmap): Flow> 14 | = emojiLabellingDataProvider.getEmojis(bitmap) 15 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/chat/LoadEmojis.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.chat 2 | 3 | import `in`.surajsau.jisho.data.chat.EmojiLabellingDataProvider 4 | import javax.inject.Inject 5 | 6 | class LoadEmojis @Inject constructor( 7 | private val emojiLabellingDataProvider: EmojiLabellingDataProvider 8 | ) { 9 | 10 | suspend fun invoke() { 11 | emojiLabellingDataProvider.loadEmojis() 12 | } 13 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/chat/SendMessage.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.chat 2 | 3 | import `in`.surajsau.jisho.data.ChatDataProvider 4 | import javax.inject.Inject 5 | 6 | class SendMessage @Inject constructor( 7 | private val chatDataProvider: ChatDataProvider 8 | ) { 9 | 10 | suspend fun invoke(imageUrl: String?, message: String, isLocal: Boolean) { 11 | chatDataProvider.sendMessage( 12 | message = message, 13 | isMe = isLocal, 14 | imageUrl = imageUrl, 15 | ) 16 | } 17 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/facenet/DetectFaces.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.facenet 2 | 3 | import `in`.surajsau.jisho.data.FileProvider 4 | import `in`.surajsau.jisho.data.facenet.FaceDetectionProvider 5 | import `in`.surajsau.jisho.data.facenet.FaceRecognitionProvider 6 | import `in`.surajsau.jisho.domain.models.FaceRecognitionResult 7 | import android.util.Log 8 | import kotlinx.coroutines.ExperimentalCoroutinesApi 9 | import kotlinx.coroutines.flow.Flow 10 | import kotlinx.coroutines.flow.flatMapLatest 11 | import kotlinx.coroutines.flow.flow 12 | import kotlinx.coroutines.flow.map 13 | import javax.inject.Inject 14 | 15 | @ExperimentalCoroutinesApi 16 | class DetectFaces @Inject constructor( 17 | private val faceDetectionProvider: FaceDetectionProvider, 18 | private val faceRecognitionProvider: FaceRecognitionProvider, 19 | private val fileProvider: FileProvider 20 | ) { 21 | 22 | fun invoke(fileName: String): Flow> { 23 | return flow { emit(fileProvider.fetchCachedBitmap(fileName)) } 24 | .flatMapLatest { 25 | faceDetectionProvider.getFaces(it) 26 | } 27 | .map { faceBitmaps -> 28 | faceBitmaps.map result@ { bitmap -> 29 | val faceFileName = fileProvider.cacheBitmap(bitmap) 30 | val faceFilePath = fileProvider.getCacheFilePath(faceFileName) 31 | val embeddings = faceRecognitionProvider.generateEmbedding(bitmap) 32 | 33 | val estimateResults = faceRecognitionProvider.compareEmbedding(embeddings) 34 | 35 | Log.e("Facenet", estimateResults.map { "${it.key}: ${it.value.joinToString()}" }.joinToString("\n")) 36 | 37 | val averageScores = estimateResults.values.map { scores -> scores.average() } 38 | val names = estimateResults.keys.toTypedArray() 39 | 40 | if (averageScores.maxOrNull() == null) 41 | return@result FaceRecognitionResult.NotRecognised(fileName = faceFileName, filePath = faceFilePath) 42 | 43 | if (averageScores.maxOrNull()!! >= 0.4) { 44 | val index = averageScores.indexOf(averageScores.maxOrNull()) 45 | return@result FaceRecognitionResult.Recognised( 46 | fileName = faceFileName, 47 | filePath = faceFilePath, 48 | estimatedName = names[index] 49 | ) 50 | } 51 | 52 | return@result FaceRecognitionResult.NotRecognised(fileName = faceFileName, filePath = faceFilePath) 53 | } 54 | 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/facenet/FetchAllFaces.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.facenet 2 | 3 | import `in`.surajsau.jisho.data.FileProvider 4 | import `in`.surajsau.jisho.data.db.FaceImage 5 | import `in`.surajsau.jisho.data.facenet.FacesDataProvider 6 | import `in`.surajsau.jisho.domain.models.FaceModel 7 | import kotlinx.coroutines.flow.Flow 8 | import kotlinx.coroutines.flow.map 9 | import javax.inject.Inject 10 | 11 | class FetchAllFaces @Inject constructor( 12 | private val facesDataProvider: FacesDataProvider, 13 | private val fileProvider: FileProvider, 14 | ) { 15 | 16 | fun invoke(): Flow> = facesDataProvider.getAllImages(isPrimary = true) 17 | .map { images -> 18 | images.map { 19 | FaceModel( 20 | imageFilePath = fileProvider.getFilePath(folderName = FileProvider.FACENET_IMAGE_FOLDER, fileName = it.fileName), 21 | faceName = it.faceName 22 | ) 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/facenet/FetchAllImages.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.facenet 2 | 3 | import `in`.surajsau.jisho.data.FileProvider 4 | import `in`.surajsau.jisho.data.facenet.FacesDataProvider 5 | import `in`.surajsau.jisho.domain.models.GalleryModel 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.map 8 | import javax.inject.Inject 9 | 10 | class FetchAllImages @Inject constructor( 11 | private val facesDataProvider: FacesDataProvider, 12 | private val fileProvider: FileProvider, 13 | ) { 14 | 15 | fun invoke(): Flow> = facesDataProvider.getAllImages(isPrimary = false) 16 | .map { images -> 17 | images.map { 18 | GalleryModel.Image( 19 | imageFilePath = fileProvider.getFilePath(FileProvider.FACENET_IMAGE_FOLDER, it.fileName), 20 | faceName = it.faceName 21 | ) 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/facenet/FetchAllImagesForFace.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.facenet 2 | 3 | import `in`.surajsau.jisho.data.FileProvider 4 | import `in`.surajsau.jisho.data.facenet.FacesDataProvider 5 | import `in`.surajsau.jisho.domain.models.GalleryModel 6 | import kotlinx.coroutines.flow.Flow 7 | import kotlinx.coroutines.flow.map 8 | import javax.inject.Inject 9 | 10 | class FetchAllImagesForFace @Inject constructor( 11 | private val facesDataProvider: FacesDataProvider, 12 | private val fileProvider: FileProvider, 13 | ) { 14 | 15 | fun invoke(faceName: String): Flow> = facesDataProvider 16 | .getImagesForFace(faceName = faceName) 17 | .map { images -> 18 | images.map { 19 | GalleryModel.Image( 20 | imageFilePath = fileProvider.getFilePath( 21 | folderName = FileProvider.FACENET_IMAGE_FOLDER, 22 | fileName = it.fileName 23 | ), 24 | faceName = it.faceName 25 | ) 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/facenet/FetchFaceNames.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.facenet 2 | 3 | import `in`.surajsau.jisho.data.facenet.FacesDataProvider 4 | import kotlinx.coroutines.flow.Flow 5 | import kotlinx.coroutines.flow.flow 6 | import javax.inject.Inject 7 | 8 | class FetchFaceNames @Inject constructor( 9 | private val facesDataProvider: FacesDataProvider, 10 | ) { 11 | 12 | fun invoke(): Flow> = flow { 13 | val faceNames = facesDataProvider.getFaceNames() 14 | emit(faceNames) 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/facenet/Initiate.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.facenet 2 | 3 | import `in`.surajsau.jisho.data.facenet.FaceRecognitionProvider 4 | import javax.inject.Inject 5 | 6 | class Initiate @Inject constructor(private val faceRecognitionProvider: FaceRecognitionProvider) { 7 | 8 | fun invoke() { 9 | faceRecognitionProvider.initiate() 10 | } 11 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/facenet/LoadEmbeddings.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.facenet 2 | 3 | import `in`.surajsau.jisho.data.facenet.FaceRecognitionProvider 4 | import `in`.surajsau.jisho.data.facenet.FacesDataProvider 5 | import javax.inject.Inject 6 | 7 | class LoadEmbeddings @Inject constructor( 8 | private val faceRecognitionProvider: FaceRecognitionProvider, 9 | private val facesDataProvider: FacesDataProvider, 10 | ) { 11 | 12 | suspend fun invoke() { 13 | val faceNames = facesDataProvider.getFaceNames() 14 | faceNames.forEach { faceRecognitionProvider.loadEmbeddingFor(faceName = it) } 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/facenet/SaveFaceEmbedding.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.facenet 2 | 3 | import `in`.surajsau.jisho.data.FileProvider 4 | import `in`.surajsau.jisho.data.facenet.FaceRecognitionProvider 5 | import javax.inject.Inject 6 | 7 | class SaveFaceEmbedding @Inject constructor( 8 | private val fileProvider: FileProvider, 9 | private val faceRecognitionProvider: FaceRecognitionProvider, 10 | ) { 11 | 12 | suspend fun invoke(faceName: String, faceFileName: String) { 13 | val bitmap = fileProvider.fetchCachedBitmap(fileName = faceFileName) 14 | val embeddings = faceRecognitionProvider.generateEmbedding(bitmap) 15 | faceRecognitionProvider.saveEmbedding(faceName = faceName, embedding = embeddings) 16 | } 17 | 18 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/facenet/SaveImage.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.facenet 2 | 3 | import `in`.surajsau.jisho.data.FileProvider 4 | import `in`.surajsau.jisho.data.facenet.FacesDataProvider 5 | import javax.inject.Inject 6 | 7 | class SaveImage @Inject constructor( 8 | private val fileProvider: FileProvider, 9 | private val facesDataProvider: FacesDataProvider 10 | ) { 11 | 12 | suspend fun invoke(faceName: String, fileName: String) { 13 | val bitmap = fileProvider.fetchCachedBitmap(fileName = fileName) 14 | fileProvider.storeBitmap(folderName = FileProvider.FACENET_IMAGE_FOLDER, fileName = fileName, bitmap = bitmap) 15 | 16 | facesDataProvider.saveFace(faceName = faceName, fileName = fileName, isPrimary = false) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/facenet/SaveNewFace.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.facenet 2 | 3 | import `in`.surajsau.jisho.data.FileProvider 4 | import `in`.surajsau.jisho.data.facenet.FacesDataProvider 5 | import javax.inject.Inject 6 | 7 | class SaveNewFace @Inject constructor( 8 | private val fileProvider: FileProvider, 9 | private val facesDataProvider: FacesDataProvider, 10 | ) { 11 | 12 | suspend fun invoke(faceName: String, fileName: String) { 13 | val bitmap = fileProvider.fetchCachedBitmap(fileName = fileName) 14 | fileProvider.storeBitmap(folderName = FileProvider.FACENET_IMAGE_FOLDER, fileName = fileName, bitmap = bitmap) 15 | 16 | facesDataProvider.saveFace(faceName = faceName, fileName = fileName, isPrimary = true) 17 | } 18 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/models/CreditCard.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.models 2 | 3 | import com.soywiz.klock.Date 4 | 5 | sealed class CreditCard { 6 | 7 | data class Front( 8 | val cardNumber: String, 9 | val expiryDate: Date, 10 | val cardHolderName: String, 11 | ): CreditCard() 12 | 13 | data class Back(val cvv: String): CreditCard() 14 | } 15 | -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/models/FaceModel.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.models 2 | 3 | data class FaceModel(val imageFilePath: String, val faceName: String) -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/models/FaceRecognitionResult.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.models 2 | 3 | sealed class FaceRecognitionResult(val faceFileName: String, val faceFilePath: String) { 4 | data class Recognised( 5 | private val fileName: String, 6 | private val filePath: String, 7 | val estimatedName: String 8 | ): FaceRecognitionResult(faceFileName = fileName, faceFilePath = filePath) 9 | 10 | data class NotRecognised( 11 | private val fileName: String, 12 | private val filePath: String 13 | ): FaceRecognitionResult(faceFileName = fileName, faceFilePath = filePath) 14 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/models/GalleryModel.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.models 2 | 3 | sealed class GalleryModel { 4 | data class Image(val imageFilePath: String, val faceName: String): GalleryModel() 5 | object Empty: GalleryModel() 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/models/IDCard.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.models 2 | 3 | import com.soywiz.klock.Date 4 | 5 | sealed class IDCard { 6 | data class Front( 7 | val membershipNumber: String, 8 | val name: String, 9 | val year: String, 10 | val degree: String, 11 | val dateOfBirth: Date, 12 | ): IDCard() 13 | 14 | data class Back( 15 | val address: String, 16 | val mobileNumber: String, 17 | val email: String, 18 | ): IDCard() 19 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/models/User.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.models 2 | 3 | data class User( 4 | private val firstName: String, 5 | private val lastName: String, 6 | val profileUrl: String 7 | ) { 8 | 9 | val displayName: String 10 | get() = "$firstName $lastName" 11 | } 12 | -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/models/chat/ChatAnnotation.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.models.chat 2 | 3 | sealed class ChatAnnotation(val tag: String) { 4 | 5 | data class Email(private val id: String, val email: String): ChatAnnotation(tag = id) 6 | data class Phone(private val id: String, val phone: String): ChatAnnotation(tag = id) 7 | data class Reminder(private val id: String, val timeStamp: Long): ChatAnnotation(tag = id) 8 | data class Address(private val id: String, val address: String): ChatAnnotation(tag = id) 9 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/models/chat/ChatDetails.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.models.chat 2 | 3 | data class ChatDetails( 4 | val chatName: String, 5 | val chatIconUrl: String, 6 | ) -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/models/chat/ChatRowModel.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.models.chat 2 | 3 | import androidx.compose.ui.text.AnnotatedString 4 | 5 | sealed interface ChatRowModel { 6 | 7 | val isLocal: Boolean 8 | 9 | object Typing: ChatRowModel { 10 | override val isLocal: Boolean = false 11 | } 12 | 13 | data class Message( 14 | val value: AnnotatedString, 15 | val timestamp: String, 16 | override val isLocal: Boolean, 17 | val annotationMaps: List 18 | ): ChatRowModel 19 | 20 | data class PictureMessage( 21 | val imageUrl: String, 22 | val timestamp: String, 23 | override val isLocal: Boolean, 24 | val message: AnnotatedString, 25 | val annotationMaps: List 26 | ): ChatRowModel 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/domain/models/chat/EntityModel.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.domain.models.chat 2 | 3 | import com.google.mlkit.nl.entityextraction.Entity 4 | 5 | data class EntityModel( 6 | val entity: Entity, 7 | val start: Int, 8 | val end: Int, 9 | ) -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/Destinations.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui 2 | 3 | enum class Destinations(val value: String) { 4 | Home("home"), 5 | 6 | DigitalInk("digital_ink"), 7 | StyleTransfer("style_transfer"), 8 | SmartChat("gpt"), 9 | Facenet("facenet"), 10 | CardReader("card_reader") 11 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/MLApp.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui 2 | 3 | import `in`.surajsau.jisho.base.BitmapCache 4 | import `in`.surajsau.jisho.base.LocalBitmapCache 5 | import `in`.surajsau.jisho.ui.cardreader.CardReaderViewModelImpl 6 | import `in`.surajsau.jisho.ui.cardreader.LocalOnboardingViewModel 7 | import `in`.surajsau.jisho.ui.cardreader.OnBoardingScreen 8 | import `in`.surajsau.jisho.ui.chat.LocalSmartChatViewModel 9 | import `in`.surajsau.jisho.ui.chat.SmartChatScreen 10 | import `in`.surajsau.jisho.ui.chat.SmartChatViewModelImpl 11 | import `in`.surajsau.jisho.ui.digitalink.DigitalInkScreen 12 | import `in`.surajsau.jisho.ui.digitalink.DigitalInkViewModelImpl 13 | import `in`.surajsau.jisho.ui.digitalink.LocalDigitalInkViewModel 14 | import `in`.surajsau.jisho.ui.facenet.FacenetScreen 15 | import `in`.surajsau.jisho.ui.facenet.FacenetViewModelImpl 16 | import `in`.surajsau.jisho.ui.facenet.LocalFacenetViewModel 17 | import `in`.surajsau.jisho.ui.home.HomeScreen 18 | import `in`.surajsau.jisho.ui.home.HomeViewModelImpl 19 | import `in`.surajsau.jisho.ui.home.LocalHomeViewModel 20 | import `in`.surajsau.jisho.ui.styletransfer.LocalStyleTransferViewModel 21 | import `in`.surajsau.jisho.ui.styletransfer.StyleTransferScreen 22 | import `in`.surajsau.jisho.ui.styletransfer.StyleTransferViewModelImpl 23 | import `in`.surajsau.jisho.ui.theme.JishoTheme 24 | import androidx.compose.foundation.layout.fillMaxSize 25 | import androidx.compose.material.MaterialTheme 26 | import androidx.compose.material.Surface 27 | import androidx.compose.runtime.Composable 28 | import androidx.compose.runtime.CompositionLocalProvider 29 | import androidx.compose.ui.Modifier 30 | import androidx.hilt.navigation.compose.hiltViewModel 31 | import androidx.navigation.compose.NavHost 32 | import androidx.navigation.compose.composable 33 | import androidx.navigation.compose.rememberNavController 34 | 35 | @Composable 36 | fun MLApp( 37 | navigateToSettings: () -> Unit 38 | ) { 39 | val navController = rememberNavController() 40 | 41 | JishoTheme { 42 | // A surface container using the 'background' color from the theme 43 | Surface( 44 | modifier = Modifier.fillMaxSize(), 45 | color = MaterialTheme.colors.background 46 | ) { 47 | NavHost(navController = navController, startDestination = Destinations.Home.value) { 48 | composable(route = Destinations.Home.value) { 49 | CompositionLocalProvider( 50 | LocalHomeViewModel provides hiltViewModel() 51 | ) { 52 | HomeScreen( 53 | navigateToDestination = { navController.navigate(route = it.value) }, 54 | modifier = Modifier.fillMaxSize() 55 | ) 56 | } 57 | } 58 | composable(route = Destinations.DigitalInk.value) { 59 | CompositionLocalProvider( 60 | LocalDigitalInkViewModel provides hiltViewModel(), 61 | ) { 62 | DigitalInkScreen( 63 | modifier = Modifier.fillMaxSize() 64 | ) 65 | } 66 | } 67 | composable(route = Destinations.StyleTransfer.value) { 68 | CompositionLocalProvider( 69 | LocalStyleTransferViewModel provides hiltViewModel(), 70 | LocalBitmapCache provides BitmapCache() 71 | ) { 72 | StyleTransferScreen( 73 | modifier = Modifier.fillMaxSize(), 74 | navigateBack = { navController.popBackStack() }, 75 | navigateToSettings = navigateToSettings 76 | ) 77 | } 78 | } 79 | composable(route = Destinations.SmartChat.value) { 80 | CompositionLocalProvider( 81 | LocalSmartChatViewModel provides hiltViewModel(), 82 | ) { 83 | SmartChatScreen( 84 | modifier = Modifier.fillMaxSize(), 85 | onDismiss = { navController.popBackStack() } 86 | ) 87 | } 88 | } 89 | 90 | composable(route = Destinations.Facenet.value) { 91 | CompositionLocalProvider( 92 | LocalFacenetViewModel provides hiltViewModel() 93 | ) { 94 | FacenetScreen(modifier = Modifier.fillMaxSize()) 95 | } 96 | } 97 | 98 | composable(route = Destinations.CardReader.value) { 99 | CompositionLocalProvider( 100 | LocalOnboardingViewModel provides hiltViewModel() 101 | ) { 102 | OnBoardingScreen(modifier = Modifier.fillMaxSize()) 103 | } 104 | } 105 | } 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/base/AskPermissionScreen.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.base 2 | 3 | import `in`.surajsau.jisho.ui.theme.Purple500 4 | import androidx.compose.foundation.background 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.material.Button 7 | import androidx.compose.material.ButtonDefaults 8 | import androidx.compose.material.Text 9 | import androidx.compose.runtime.* 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.text.font.FontWeight 13 | import androidx.compose.ui.unit.dp 14 | import androidx.compose.ui.unit.sp 15 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 16 | import com.google.accompanist.permissions.rememberPermissionState 17 | 18 | @OptIn(ExperimentalPermissionsApi::class) 19 | @Composable 20 | fun AskPermissionScreen( 21 | modifier: Modifier = Modifier, 22 | permission: String, 23 | onDismiss: () -> Unit, 24 | onPermissionDeniedFallback: (() -> Unit)? = null, 25 | onPermissionAcceptedContents: @Composable () -> Unit, 26 | ) { 27 | 28 | val permissionState = rememberPermissionState(permission = permission) 29 | 30 | var doNotShowRationale by remember { mutableStateOf(false) } 31 | 32 | Box(modifier = modifier) { 33 | when { 34 | permissionState.hasPermission -> onPermissionAcceptedContents.invoke() 35 | 36 | permissionState.shouldShowRationale 37 | || !permissionState.permissionRequested -> { 38 | 39 | if (doNotShowRationale) { 40 | 41 | PermissionRationale( 42 | title = "Check settings", 43 | onDismissed = { onDismiss.invoke() }, 44 | onAccepted = { onPermissionDeniedFallback?.invoke() } 45 | ) 46 | 47 | } else { 48 | PermissionRationale( 49 | title = "Require Camera permission", 50 | onDismissed = { doNotShowRationale = true }, 51 | onAccepted = { permissionState.launchPermissionRequest() } 52 | ) 53 | } 54 | } 55 | 56 | else -> { 57 | 58 | PermissionRationale( 59 | title = "Check settings", 60 | onDismissed = { onDismiss.invoke() }, 61 | onAccepted = { onPermissionDeniedFallback?.invoke() } 62 | ) 63 | } 64 | 65 | } 66 | } 67 | 68 | 69 | } 70 | 71 | @Composable 72 | fun PermissionRationale( 73 | modifier: Modifier = Modifier, 74 | title: String, 75 | onDismissed: () -> Unit, 76 | onAccepted: () -> Unit 77 | ) { 78 | 79 | Column(modifier = modifier 80 | .fillMaxSize() 81 | .background(Color.White)) { 82 | 83 | Text( 84 | text = title, 85 | color = Color.DarkGray, 86 | modifier = Modifier 87 | .fillMaxWidth() 88 | .padding(16.dp), 89 | fontSize = 36.sp, 90 | fontWeight = FontWeight.Bold 91 | ) 92 | 93 | Spacer(modifier = Modifier.weight(1f)) 94 | 95 | Row(modifier = Modifier 96 | .fillMaxWidth() 97 | .padding(16.dp)) { 98 | 99 | Spacer(modifier = Modifier.weight(1f)) 100 | 101 | Button( 102 | onClick = { onDismissed.invoke() }, 103 | colors = ButtonDefaults.buttonColors( 104 | backgroundColor = Color.Transparent, 105 | contentColor = Purple500 106 | ), 107 | elevation = ButtonDefaults 108 | .elevation( 109 | defaultElevation = 0.dp, 110 | pressedElevation = 0.dp 111 | ) 112 | ) { 113 | Text(text = "Cancel", 114 | fontWeight = FontWeight.Normal, 115 | fontSize = 18.sp, 116 | modifier = Modifier.padding(8.dp) 117 | ) 118 | } 119 | 120 | Button( 121 | onClick = { onAccepted.invoke() }, 122 | colors = ButtonDefaults.buttonColors( 123 | backgroundColor = Color.Transparent, 124 | contentColor = Purple500 125 | ), 126 | elevation = ButtonDefaults 127 | .elevation( 128 | defaultElevation = 0.dp, 129 | pressedElevation = 0.dp 130 | ) 131 | ) { 132 | Text(text = "Allow", 133 | fontWeight = FontWeight.Medium, 134 | fontSize = 18.sp, 135 | modifier = Modifier.padding(8.dp) 136 | ) 137 | } 138 | } 139 | 140 | } 141 | } 142 | 143 | -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/base/ExternalIntentHandler.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.base 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.provider.AlarmClock 7 | import android.util.Log 8 | import androidx.activity.compose.rememberLauncherForActivityResult 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.runtime.mutableStateOf 12 | import androidx.compose.runtime.remember 13 | import androidx.compose.ui.platform.LocalContext 14 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 15 | import com.google.accompanist.permissions.rememberPermissionState 16 | import com.soywiz.klock.DateTime 17 | 18 | @OptIn(ExperimentalPermissionsApi::class) 19 | @Composable 20 | fun rememberExternalIntentHandler(): ExternalIntentHandler { 21 | val context = LocalContext.current 22 | 23 | return remember { ExternalIntentHandler(context = context) } 24 | } 25 | 26 | class ExternalIntentHandler constructor( 27 | private val context: Context, 28 | ) { 29 | 30 | fun openMap(location: String) { 31 | val gmmIntentUri = 32 | Uri.parse("geo:0,0?q=$location") 33 | val intent = Intent(Intent.ACTION_VIEW, gmmIntentUri).apply { 34 | setPackage("com.google.android.apps.maps") 35 | } 36 | 37 | context.startActivity(intent) 38 | } 39 | 40 | fun openAlarm(message: String, timestamp: Long) { 41 | val dateTime = DateTime.invoke(timestamp) 42 | val intent = Intent(AlarmClock.ACTION_SET_ALARM).apply { 43 | putExtra(AlarmClock.EXTRA_MESSAGE, message) 44 | putExtra(AlarmClock.EXTRA_HOUR, dateTime.hours) 45 | putExtra(AlarmClock.EXTRA_MINUTES, dateTime.minutes) 46 | } 47 | 48 | context.startActivity(intent) 49 | } 50 | 51 | fun openPhone(phone: String) { 52 | val intent = Intent(Intent.ACTION_CALL).apply { 53 | data = Uri.parse("tel:$phone") 54 | } 55 | 56 | if (intent.resolveActivity(context.packageManager) != null) 57 | context.startActivity(intent) 58 | } 59 | 60 | fun openEmail(address: String) { 61 | val intent = Intent(Intent.ACTION_SEND).apply { 62 | putExtra(Intent.EXTRA_EMAIL, address) 63 | } 64 | 65 | context.startActivity(Intent.createChooser(intent, "Send Email")) 66 | } 67 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/base/ForEachRow.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.base 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.lazy.LazyListScope 6 | import androidx.compose.material.Text 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Alignment 9 | import androidx.compose.ui.Modifier 10 | import androidx.compose.ui.graphics.Color 11 | import androidx.compose.ui.tooling.preview.Preview 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | inline fun ForEachRow( 16 | items: List, 17 | modifier: Modifier = Modifier, 18 | horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, 19 | verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, 20 | rowItem: @Composable RowScope.(T) -> Unit, 21 | ) { 22 | Row( 23 | modifier = modifier, 24 | horizontalArrangement = horizontalArrangement, 25 | verticalAlignment = verticalAlignment 26 | ) { 27 | items.forEach { rowItem(it) } 28 | } 29 | } 30 | 31 | @Composable 32 | inline fun ForEachRowIndexed( 33 | items: List, 34 | modifier: Modifier = Modifier, 35 | horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, 36 | verticalAlignment: Alignment.Vertical = Alignment.CenterVertically, 37 | rowItem: @Composable RowScope.(Int, T) -> Unit, 38 | ) { 39 | Row( 40 | modifier = modifier, 41 | horizontalArrangement = horizontalArrangement, 42 | verticalAlignment = verticalAlignment 43 | ) { 44 | items.forEachIndexed { index, elem -> rowItem(index, elem) } 45 | } 46 | } 47 | 48 | @Preview 49 | @Composable 50 | private fun PreviewForEachRow() { 51 | 52 | Box(modifier = Modifier.fillMaxWidth().background(Color.White).height(100.dp)) { 53 | ForEachRow(items = (1..3).toList(), modifier = Modifier.padding(4.dp)) { 54 | Box(modifier = Modifier.size(100.dp).padding(end = 4.dp).background(Color.LightGray)) { 55 | Text(text = "$it", modifier = Modifier.align(Alignment.Center)) 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/base/Line.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.base 2 | 3 | import androidx.compose.foundation.background 4 | import androidx.compose.foundation.layout.Box 5 | import androidx.compose.foundation.layout.height 6 | import androidx.compose.foundation.layout.width 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.unit.Dp 11 | import androidx.compose.ui.unit.dp 12 | 13 | @Composable 14 | fun Line( 15 | modifier: Modifier = Modifier, 16 | thickness: Dp = 1.dp, 17 | color: Color = Color.DarkGray, 18 | isVertical: Boolean = false, 19 | 20 | ) { 21 | Box( 22 | modifier = modifier.apply { 23 | if (isVertical) 24 | width(thickness) 25 | else 26 | height(thickness) 27 | }.background(color) 28 | ) 29 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/base/camera/Camera.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.base.camera 2 | 3 | import androidx.camera.core.CameraSelector 4 | import androidx.camera.core.ImageCapture 5 | import androidx.camera.core.ImageCaptureException 6 | import androidx.camera.core.Preview 7 | import androidx.camera.lifecycle.ProcessCameraProvider 8 | import androidx.camera.view.PreviewView 9 | import androidx.compose.runtime.* 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.platform.LocalContext 12 | import androidx.compose.ui.platform.LocalLifecycleOwner 13 | import androidx.compose.ui.viewinterop.AndroidView 14 | import androidx.core.content.ContextCompat 15 | import java.io.File 16 | import kotlin.coroutines.resume 17 | import kotlin.coroutines.suspendCoroutine 18 | 19 | enum class CameraAction { 20 | Click, SwitchCamera, None 21 | } 22 | 23 | private fun CameraSelector.toggle() = when (this) { 24 | CameraSelector.DEFAULT_BACK_CAMERA -> CameraSelector.DEFAULT_FRONT_CAMERA 25 | else -> CameraSelector.DEFAULT_BACK_CAMERA 26 | } 27 | 28 | @Composable 29 | fun Camera( 30 | modifier: Modifier = Modifier, 31 | cameraAction: CameraAction, 32 | onImageCaptured: (String) -> Unit, 33 | ) { 34 | 35 | val context = LocalContext.current 36 | 37 | val imageCapture: ImageCapture = remember { ImageCapture.Builder().build() } 38 | 39 | var cameraSelector by remember { mutableStateOf(CameraSelector.DEFAULT_BACK_CAMERA) } 40 | 41 | val lifecycleOwner = LocalLifecycleOwner.current 42 | 43 | val previewView = remember { 44 | PreviewView(context).apply { this.scaleType = PreviewView.ScaleType.FILL_CENTER } 45 | } 46 | 47 | DisposableEffect(cameraAction) { 48 | when (cameraAction) { 49 | CameraAction.Click -> { 50 | val imageFileName = "${System.currentTimeMillis()}.jpg" 51 | val imageFile = File(context.externalCacheDir, imageFileName) 52 | val outputImageOption = ImageCapture.OutputFileOptions.Builder(imageFile) 53 | .build() 54 | 55 | imageCapture.takePicture( 56 | outputImageOption, 57 | ContextCompat.getMainExecutor(context), 58 | object: ImageCapture.OnImageSavedCallback { 59 | override fun onError(exception: ImageCaptureException) { 60 | exception.printStackTrace() 61 | } 62 | 63 | override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) { 64 | onImageCaptured.invoke(imageFileName) 65 | } 66 | } 67 | ) 68 | } 69 | CameraAction.SwitchCamera -> { 70 | cameraSelector = cameraSelector.toggle() 71 | } 72 | } 73 | 74 | onDispose {} 75 | } 76 | 77 | LaunchedEffect(cameraSelector) { 78 | val cameraProvider = suspendCoroutine { continuation -> 79 | ProcessCameraProvider.getInstance(context).also { future -> 80 | future.addListener({ 81 | continuation.resume(future.get()) 82 | }, ContextCompat.getMainExecutor(context)) 83 | } 84 | } 85 | 86 | val previewUseCase = Preview.Builder() 87 | .build() 88 | .also { it.setSurfaceProvider(previewView.surfaceProvider) } 89 | 90 | runCatching { 91 | cameraProvider.unbindAll() 92 | cameraProvider.bindToLifecycle( 93 | lifecycleOwner, 94 | cameraSelector, 95 | previewUseCase, 96 | imageCapture 97 | ) 98 | } 99 | } 100 | 101 | AndroidView(modifier = modifier, factory = { previewView }) 102 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/base/camera/CameraControls.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.base.camera 2 | 3 | import `in`.surajsau.jisho.R 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.border 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.layout.* 9 | import androidx.compose.foundation.shape.CircleShape 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clipToBounds 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.layout.ContentScale 16 | import androidx.compose.ui.res.painterResource 17 | import androidx.compose.ui.unit.dp 18 | 19 | @Composable 20 | fun CameraControls( 21 | modifier: Modifier = Modifier, 22 | onCameraAction: (CameraAction) -> Unit 23 | ) { 24 | 25 | Row( 26 | modifier = modifier 27 | .padding(16.dp), 28 | horizontalArrangement = Arrangement.SpaceEvenly, 29 | verticalAlignment = Alignment.Bottom 30 | ) { 31 | 32 | Spacer(modifier = Modifier.size(48.dp)) 33 | 34 | Box( 35 | modifier = Modifier 36 | .size(48.dp) 37 | .border(width = 2.dp, color = Color.DarkGray, shape = CircleShape) 38 | .background(Color.White, CircleShape) 39 | .clickable { onCameraAction.invoke(CameraAction.Click) } 40 | ) 41 | 42 | Image( 43 | painter = painterResource(id = R.drawable.ic_switch_camera), 44 | contentDescription = null, 45 | modifier = Modifier 46 | .size(36.dp) 47 | .clickable { onCameraAction.invoke(CameraAction.SwitchCamera) } 48 | .clipToBounds(), 49 | contentScale = ContentScale.Fit 50 | ) 51 | 52 | } 53 | 54 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/cardreader/CameraScreen.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.cardreader 2 | 3 | import `in`.surajsau.jisho.ui.base.camera.Camera 4 | import `in`.surajsau.jisho.ui.base.camera.CameraAction 5 | import `in`.surajsau.jisho.ui.base.camera.CameraControls 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.foundation.layout.padding 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.* 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.unit.dp 13 | 14 | @Composable 15 | fun CameraScreen( 16 | modifier: Modifier = Modifier, 17 | instructionMessage: String, 18 | onImageCaptured: (String) -> Unit, 19 | ) { 20 | 21 | var cameraAction by remember { mutableStateOf(CameraAction.None) } 22 | 23 | Column(modifier = modifier) { 24 | 25 | Camera( 26 | modifier = Modifier 27 | .fillMaxWidth() 28 | .weight(1f), 29 | cameraAction = cameraAction, 30 | onImageCaptured = { 31 | onImageCaptured.invoke(it) 32 | cameraAction = CameraAction.None 33 | }) 34 | 35 | Text( 36 | modifier = Modifier 37 | .fillMaxWidth() 38 | .padding(16.dp), 39 | text = instructionMessage, 40 | ) 41 | 42 | CameraControls( 43 | modifier = Modifier.fillMaxWidth() 44 | .weight(1f), 45 | onCameraAction = { cameraAction = it } 46 | ) 47 | } 48 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/cardreader/CardReaderScreen.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.cardreader 2 | 3 | import `in`.surajsau.jisho.base.use 4 | import `in`.surajsau.jisho.ui.base.AskPermissionScreen 5 | import android.widget.Toast 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.material.* 8 | import androidx.compose.runtime.Composable 9 | import androidx.compose.runtime.LaunchedEffect 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.platform.LocalContext 13 | import androidx.compose.ui.unit.dp 14 | 15 | @OptIn(ExperimentalMaterialApi::class) 16 | @Composable 17 | fun CardReaderScreen( 18 | modifier: Modifier = Modifier, 19 | onDismiss: (() -> Unit)? = null, 20 | ) { 21 | 22 | val context = LocalContext.current 23 | 24 | val (state, event) = use(viewModel = LocalOnboardingViewModel.current, initialStateValue = CardReaderViewModel.State()) 25 | 26 | LaunchedEffect(state.instruction) { 27 | val instruction = state.instruction.takeIf { it.isNotEmpty() } ?: return@LaunchedEffect 28 | Toast.makeText(context, instruction, Toast.LENGTH_SHORT).show() 29 | } 30 | 31 | Box(modifier = modifier) { 32 | AskPermissionScreen( 33 | permission = android.Manifest.permission.CAMERA, 34 | onDismiss = { onDismiss?.invoke() }, 35 | onPermissionDeniedFallback = { event(CardReaderViewModel.Event.OnPermissionDenied) } 36 | ) { 37 | CameraScreen( 38 | modifier = Modifier.fillMaxSize(), 39 | instructionMessage = state.instruction, 40 | onImageCaptured = { event(CardReaderViewModel.Event.CameraResultReceived(it)) } 41 | ) 42 | } 43 | 44 | if (state.showLoader) { 45 | CircularProgressIndicator( 46 | modifier = Modifier 47 | .align(Alignment.Center) 48 | .padding(vertical = 24.dp) 49 | ) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/cardreader/DetailsScreen.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.cardreader 2 | 3 | import `in`.surajsau.jisho.base.use 4 | import androidx.compose.foundation.layout.* 5 | import androidx.compose.foundation.text.KeyboardOptions 6 | import androidx.compose.material.* 7 | import androidx.compose.material.icons.Icons 8 | import androidx.compose.material.icons.filled.ArrowBack 9 | import androidx.compose.runtime.* 10 | import androidx.compose.ui.Alignment 11 | import androidx.compose.ui.Modifier 12 | import androidx.compose.ui.graphics.Color 13 | import androidx.compose.ui.text.font.FontWeight 14 | import androidx.compose.ui.text.input.KeyboardCapitalization 15 | import androidx.compose.ui.text.input.KeyboardType 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.unit.sp 18 | 19 | @Composable 20 | fun DetailsScreen( 21 | modifier: Modifier = Modifier, 22 | screen: CardReaderViewModel.Screen, 23 | navigateBack: () -> Unit, 24 | ) { 25 | 26 | val (_, event) = use( 27 | viewModel = LocalOnboardingViewModel.current, 28 | initialStateValue = CardReaderViewModel.State(), 29 | ) 30 | 31 | var membershipNumber by remember { mutableStateOf("") } 32 | var name by remember { mutableStateOf("") } 33 | var year by remember { mutableStateOf("") } 34 | var degree by remember { mutableStateOf("") } 35 | var address by remember { mutableStateOf("") } 36 | var mobileNumber by remember { mutableStateOf("") } 37 | var email by remember { mutableStateOf("") } 38 | var dob by remember { mutableStateOf("") } 39 | 40 | LaunchedEffect(screen) { 41 | if (screen is CardReaderViewModel.Screen.FilledDetails) { 42 | membershipNumber = screen.front.membershipNumber 43 | name = screen.front.name 44 | year = screen.front.year 45 | degree = screen.front.degree 46 | dob = screen.front.dateOfBirth.toString() 47 | 48 | address = screen.back.address 49 | mobileNumber = screen.back.mobileNumber 50 | email = screen.back.email 51 | } 52 | } 53 | 54 | Column(modifier = modifier) { 55 | 56 | TopAppBar(backgroundColor = Color.White, elevation = 0.dp) { 57 | 58 | Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize()) { 59 | IconButton(onClick = { navigateBack.invoke() }) { 60 | Icon(imageVector = Icons.Filled.ArrowBack, contentDescription = "back") 61 | } 62 | 63 | Text(text = screen.title, fontWeight = FontWeight.Bold, fontSize = 16.sp) 64 | } 65 | } 66 | 67 | Column( 68 | modifier = Modifier 69 | .fillMaxWidth() 70 | .weight(1f) 71 | .padding(16.dp), 72 | horizontalAlignment = Alignment.CenterHorizontally, 73 | ) { 74 | 75 | TextField( 76 | modifier = Modifier.fillMaxWidth(), 77 | value = name, 78 | onValueChange = { name = it }, 79 | label = { Text(text = "Name") }, 80 | keyboardOptions = KeyboardOptions( 81 | capitalization = KeyboardCapitalization.Words, 82 | keyboardType = KeyboardType.Text 83 | ) 84 | ) 85 | 86 | Spacer(modifier = Modifier.height(16.dp)) 87 | 88 | TextField( 89 | modifier = Modifier.fillMaxWidth(), 90 | value = membershipNumber, 91 | onValueChange = { membershipNumber = it }, 92 | label = { Text(text = "Membership No.") }, 93 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number) 94 | ) 95 | 96 | Spacer(modifier = Modifier.height(16.dp)) 97 | 98 | Row(modifier = Modifier.fillMaxWidth()) { 99 | 100 | TextField( 101 | modifier = Modifier.weight(3f), 102 | value = degree, 103 | onValueChange = { degree = it }, 104 | label = { Text(text = "Degree") }, 105 | keyboardOptions = KeyboardOptions( 106 | keyboardType = KeyboardType.Text, 107 | capitalization = KeyboardCapitalization.Characters, 108 | ), 109 | ) 110 | 111 | TextField( 112 | modifier = Modifier 113 | .weight(1f) 114 | .padding(horizontal = 4.dp), 115 | value = year, 116 | onValueChange = { year = it }, 117 | label = { Text(text = "Year") }, 118 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), 119 | ) 120 | } 121 | 122 | Spacer(modifier = Modifier.height(16.dp)) 123 | 124 | TextField( 125 | modifier = Modifier.fillMaxWidth(), 126 | value = address, 127 | label = { Text(text = "Address") }, 128 | onValueChange = { address = it }, 129 | maxLines = 3, 130 | ) 131 | 132 | Spacer(modifier = Modifier.height(16.dp)) 133 | 134 | Row(modifier = Modifier.fillMaxWidth()) { 135 | TextField( 136 | modifier = Modifier.weight(1f), 137 | value = mobileNumber, 138 | onValueChange = { mobileNumber = it }, 139 | label = { Text(text = "Mobile No.") }, 140 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Phone) 141 | ) 142 | 143 | Spacer(modifier = Modifier.width(4.dp)) 144 | 145 | TextField( 146 | modifier = Modifier.weight(1f), 147 | value = email, 148 | onValueChange = { email = it }, 149 | label = { Text(text = "Email") }, 150 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Email), 151 | ) 152 | } 153 | 154 | Spacer(modifier = Modifier.height(16.dp)) 155 | 156 | TextField( 157 | modifier = Modifier.fillMaxWidth(), 158 | value = dob, 159 | onValueChange = { dob = it }, 160 | label = { Text(text = "Date of Birth") }, 161 | keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), 162 | ) 163 | 164 | Spacer(modifier = Modifier.height(16.dp)) 165 | 166 | Button( 167 | modifier = Modifier 168 | .fillMaxWidth() 169 | .padding(horizontal = 24.dp), 170 | onClick = { event(CardReaderViewModel.Event.ConfirmClicked) } 171 | ) { 172 | Text(text = "Confirm") 173 | } 174 | } 175 | } 176 | 177 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/cardreader/OnBoardingScreen.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.cardreader 2 | 3 | import `in`.surajsau.jisho.base.use 4 | import androidx.compose.foundation.BorderStroke 5 | import androidx.compose.foundation.layout.* 6 | import androidx.compose.foundation.shape.RoundedCornerShape 7 | import androidx.compose.material.Button 8 | import androidx.compose.material.ButtonDefaults 9 | import androidx.compose.material.MaterialTheme 10 | import androidx.compose.material.Text 11 | import androidx.compose.runtime.Composable 12 | import androidx.compose.ui.Alignment 13 | import androidx.compose.ui.Modifier 14 | import androidx.compose.ui.graphics.Color 15 | import androidx.compose.ui.text.font.FontWeight 16 | import androidx.compose.ui.unit.dp 17 | import androidx.compose.ui.unit.sp 18 | 19 | @Composable 20 | fun OnBoardingScreen( 21 | modifier: Modifier = Modifier, 22 | ) { 23 | 24 | val (state, event) = use( 25 | viewModel = LocalOnboardingViewModel.current, 26 | initialStateValue = CardReaderViewModel.State() 27 | ) 28 | 29 | Box(modifier = modifier) { 30 | when (val screen = state.screen) { 31 | is CardReaderViewModel.Screen.Intro -> IntroScreen( 32 | modifier = Modifier.fillMaxSize(), 33 | onScreenSelected = { event(CardReaderViewModel.Event.ScreenSelected(it)) } 34 | ) 35 | 36 | is CardReaderViewModel.Screen.CardReader -> CardReaderScreen(modifier = Modifier.fillMaxSize()) 37 | 38 | is CardReaderViewModel.Screen.EmptyDetails, 39 | 40 | is CardReaderViewModel.Screen.FilledDetails -> { 41 | DetailsScreen( 42 | modifier = Modifier.fillMaxSize(), 43 | screen = screen, 44 | navigateBack = {} 45 | ) 46 | } 47 | } 48 | } 49 | } 50 | 51 | @Composable 52 | private fun IntroScreen( 53 | modifier: Modifier = Modifier, 54 | onScreenSelected: (CardReaderViewModel.Screen) -> Unit, 55 | ) { 56 | 57 | Box(modifier = modifier) { 58 | 59 | Column( 60 | modifier = Modifier 61 | .align(Alignment.Center) 62 | .padding(24.dp), 63 | horizontalAlignment = Alignment.CenterHorizontally 64 | ) { 65 | 66 | Text( 67 | text = "Welcome to Alumni App", 68 | fontSize = 24.sp, 69 | fontWeight = FontWeight.Bold 70 | ) 71 | 72 | Spacer(modifier = Modifier.height(16.dp)) 73 | 74 | Button(onClick = { onScreenSelected.invoke(CardReaderViewModel.Screen.CardReader) }) { 75 | Text(text = "Open Card Reader") 76 | } 77 | 78 | Spacer(modifier = Modifier.height(16.dp)) 79 | 80 | Text(text = "- or -") 81 | 82 | Spacer(modifier = Modifier.height(16.dp)) 83 | 84 | Button( 85 | onClick = { onScreenSelected.invoke(CardReaderViewModel.Screen.EmptyDetails) }, 86 | colors = ButtonDefaults.buttonColors( 87 | backgroundColor = Color.Transparent, 88 | contentColor = MaterialTheme.colors.primary, 89 | disabledBackgroundColor = Color.Transparent 90 | ), 91 | elevation = null, 92 | border = BorderStroke(width = 1.dp, color = MaterialTheme.colors.primary), 93 | shape = MaterialTheme.shapes.small 94 | ) { 95 | Text(text = "Fill out manually") 96 | } 97 | 98 | } 99 | } 100 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/cardreader/OnBoardingViewModel.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.cardreader 2 | 3 | import `in`.surajsau.jisho.base.Optional 4 | import `in`.surajsau.jisho.base.SingleFlowViewModel 5 | import `in`.surajsau.jisho.domain.cardreader.GetCardDetails 6 | import `in`.surajsau.jisho.domain.models.IDCard 7 | import androidx.compose.runtime.compositionLocalOf 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.viewModelScope 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.Dispatchers 12 | import kotlinx.coroutines.flow.* 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class CardReaderViewModelImpl @Inject constructor( 18 | private val getFrontDetails: GetCardDetails, 19 | private val getBackDetails: GetCardDetails, 20 | ): ViewModel(), CardReaderViewModel { 21 | 22 | private val _frontDetails = MutableStateFlow>(Optional.Empty) 23 | private val _backDetails = MutableStateFlow>(Optional.Empty) 24 | 25 | private val _cardReaderMode = MutableStateFlow(CardReaderViewModel.CardReaderMode.FrontCapture) 26 | 27 | private val _currentScreen = MutableStateFlow(CardReaderViewModel.Screen.Intro) 28 | 29 | private val _showLoader = MutableStateFlow(false) 30 | 31 | private var instruction = "" 32 | 33 | override val state: StateFlow 34 | get() = combine( 35 | _frontDetails, 36 | _backDetails, 37 | _cardReaderMode, 38 | _currentScreen, 39 | _showLoader, 40 | ) { frontDetails, backDetails, cardReaderMode, currentScreen, showLoader -> 41 | // val instruction = when (cardReaderMode) { 42 | // CardReaderViewModel.CardReaderMode.FrontCapture -> "Show front of the Card" 43 | // CardReaderViewModel.CardReaderMode.BackCapture -> "Show back of the Card" 44 | // 45 | // else -> "" 46 | // } 47 | 48 | val screen = when (cardReaderMode) { 49 | CardReaderViewModel.CardReaderMode.Result -> CardReaderViewModel.Screen.FilledDetails( 50 | front = frontDetails.getValue(), 51 | back = backDetails.getValue(), 52 | ) 53 | 54 | else -> currentScreen 55 | } 56 | 57 | CardReaderViewModel.State( 58 | instruction = instruction, 59 | showLoader = showLoader, 60 | screen = screen, 61 | ) 62 | } 63 | .stateIn(viewModelScope, SharingStarted.WhileSubscribed(), CardReaderViewModel.State()) 64 | 65 | override fun onEvent(event: CardReaderViewModel.Event) { 66 | when (event) { 67 | is CardReaderViewModel.Event.CameraResultReceived -> { 68 | val cardReaderMode = _cardReaderMode.value 69 | 70 | _showLoader.value = true 71 | 72 | viewModelScope.launch { 73 | when (cardReaderMode) { 74 | CardReaderViewModel.CardReaderMode.FrontCapture -> { 75 | getFrontDetails.invoke(fileName = event.fileName) 76 | .flowOn(Dispatchers.IO) 77 | .collect { 78 | _frontDetails.value = Optional.of(it) 79 | _cardReaderMode.value = CardReaderViewModel.CardReaderMode.BackCapture 80 | } 81 | } 82 | 83 | CardReaderViewModel.CardReaderMode.BackCapture -> { 84 | getBackDetails.invoke(fileName = event.fileName) 85 | .flowOn(Dispatchers.IO) 86 | .collect { 87 | _backDetails.value = Optional.of(it) 88 | _cardReaderMode.value = CardReaderViewModel.CardReaderMode.Result 89 | } 90 | } 91 | } 92 | } 93 | } 94 | 95 | is CardReaderViewModel.Event.ScreenSelected -> { 96 | _currentScreen.value = event.screen 97 | } 98 | 99 | is CardReaderViewModel.Event.ConfirmClicked -> { 100 | 101 | } 102 | } 103 | } 104 | 105 | } 106 | 107 | interface CardReaderViewModel : SingleFlowViewModel { 108 | 109 | sealed class Event{ 110 | data class CameraResultReceived(val fileName: String): Event() 111 | object OnPermissionDenied: Event() 112 | data class ScreenSelected(val screen: Screen): Event() 113 | object ConfirmClicked: Event() 114 | } 115 | 116 | data class State( 117 | val showLoader: Boolean = false, 118 | val instruction: String = "", 119 | val screen: Screen = Screen.Intro, 120 | ) 121 | 122 | enum class CardReaderMode { 123 | FrontCapture, 124 | BackCapture, 125 | Result, 126 | } 127 | 128 | sealed class Screen(val title: String) { 129 | object Intro: Screen("") 130 | object CardReader: Screen("") 131 | object EmptyDetails: Screen("Enter details") 132 | data class FilledDetails( 133 | val front: IDCard.Front, 134 | val back: IDCard.Back, 135 | ): Screen("Confirm details") 136 | } 137 | } 138 | 139 | val LocalOnboardingViewModel = compositionLocalOf { 140 | error("Factory for CardReaderViewModel is not implemented") 141 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/chat/ChatClickableText.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.chat 2 | 3 | import `in`.surajsau.jisho.domain.models.chat.ChatAnnotation 4 | import `in`.surajsau.jisho.domain.models.chat.ChatRowModel 5 | import `in`.surajsau.jisho.ui.base.rememberExternalIntentHandler 6 | import android.content.pm.PackageManager 7 | import android.util.Log 8 | import androidx.activity.compose.rememberLauncherForActivityResult 9 | import androidx.activity.result.contract.ActivityResultContracts 10 | import androidx.compose.foundation.text.ClickableText 11 | import androidx.compose.runtime.* 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.graphics.Color 14 | import androidx.compose.ui.platform.LocalContext 15 | import androidx.compose.ui.text.AnnotatedString 16 | import androidx.compose.ui.text.TextStyle 17 | import androidx.core.content.ContextCompat 18 | import com.google.accompanist.permissions.ExperimentalPermissionsApi 19 | import com.google.accompanist.permissions.rememberPermissionState 20 | 21 | @OptIn(ExperimentalPermissionsApi::class) 22 | @Composable 23 | fun ChatClickableText( 24 | annotatedString: AnnotatedString, 25 | annotationMaps: List, 26 | modifier: Modifier = Modifier 27 | ) { 28 | val callPermissionState = rememberPermissionState(permission = android.Manifest.permission.CALL_PHONE) 29 | 30 | val externalIntentHandler = rememberExternalIntentHandler() 31 | 32 | ClickableText( 33 | modifier = modifier, 34 | text = annotatedString, 35 | style = TextStyle(color = Color.White), 36 | onClick = { offset -> 37 | annotationMaps.forEach { annotation -> 38 | val tag = annotation.tag 39 | annotatedString.getStringAnnotations(tag = tag, start = offset, end = offset).firstOrNull()?.let { 40 | when (annotation) { 41 | is ChatAnnotation.Reminder -> externalIntentHandler.openAlarm(message = it.item, timestamp = annotation.timeStamp) 42 | is ChatAnnotation.Address -> externalIntentHandler.openMap(annotation.address) 43 | is ChatAnnotation.Phone -> { 44 | when { 45 | callPermissionState.hasPermission -> externalIntentHandler.openPhone(annotation.phone) 46 | else -> callPermissionState.launchPermissionRequest() 47 | } 48 | 49 | } 50 | is ChatAnnotation.Email -> externalIntentHandler.openEmail(annotation.email) 51 | } 52 | } 53 | } 54 | } 55 | ) 56 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/digitalink/DigitalInkScreen.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.digitalink 2 | 3 | import `in`.surajsau.jisho.base.use 4 | import `in`.surajsau.jisho.ui.theme.DigitalInkColors 5 | import `in`.surajsau.jisho.ui.theme.Purple700 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.clickable 8 | import androidx.compose.foundation.layout.* 9 | import androidx.compose.foundation.lazy.LazyRow 10 | import androidx.compose.foundation.lazy.items 11 | import androidx.compose.foundation.shape.RoundedCornerShape 12 | import androidx.compose.material.* 13 | import androidx.compose.runtime.* 14 | import androidx.compose.ui.Alignment 15 | import androidx.compose.ui.Modifier 16 | import androidx.compose.ui.graphics.Color 17 | import androidx.compose.ui.platform.LocalLifecycleOwner 18 | import androidx.compose.ui.text.TextStyle 19 | import androidx.compose.ui.text.style.TextAlign 20 | import androidx.compose.ui.unit.dp 21 | import androidx.compose.ui.unit.sp 22 | import androidx.lifecycle.Lifecycle 23 | import androidx.lifecycle.LifecycleEventObserver 24 | 25 | @Composable 26 | fun DigitalInkScreen( 27 | modifier: Modifier = Modifier 28 | ) { 29 | 30 | val lifecycleOwner = LocalLifecycleOwner.current 31 | 32 | val (state, event) = use(LocalDigitalInkViewModel.current, DigitalInkViewModel.State()) 33 | 34 | DisposableEffect(Unit) { 35 | val lifecycleObserver = LifecycleEventObserver { _, event -> 36 | if (event == Lifecycle.Event.ON_STOP) 37 | event(DigitalInkViewModel.Event.OnStop) 38 | } 39 | 40 | lifecycleOwner.lifecycle.addObserver(lifecycleObserver) 41 | onDispose { lifecycleOwner.lifecycle.removeObserver(lifecycleObserver) } 42 | } 43 | 44 | Box(modifier = modifier) { 45 | if (state.showModelStatusProgress) { 46 | ModelStatusProgress( 47 | statusText = "Checking models...", 48 | modifier = Modifier 49 | .align(Alignment.Center) 50 | ) 51 | } else { 52 | 53 | Column(modifier = Modifier.fillMaxSize()) { 54 | 55 | Card(elevation = 4.dp) { 56 | Column(modifier = Modifier.fillMaxWidth()) { 57 | TextField( 58 | value = state.finalText, 59 | onValueChange = { event(DigitalInkViewModel.Event.TextChanged(it)) }, 60 | modifier = Modifier.fillMaxWidth(), 61 | maxLines = 1, 62 | textStyle = TextStyle(color = DigitalInkColors.Text, fontSize = 24.sp), 63 | colors = TextFieldDefaults.textFieldColors( 64 | backgroundColor = Color.White, 65 | placeholderColor = DigitalInkColors.PredictionPlaceholder, 66 | focusedIndicatorColor = Color.Transparent, 67 | unfocusedIndicatorColor = Color.Transparent, 68 | cursorColor = DigitalInkColors.PredictionText 69 | ), 70 | placeholder = { 71 | Text( 72 | text = "Enter text(Japanese)", 73 | fontSize = 24.sp, 74 | ) 75 | }, 76 | shape = RoundedCornerShape(0.dp) 77 | ) 78 | 79 | Spacer(modifier = Modifier 80 | .fillMaxWidth() 81 | .height(1.dp) 82 | .background(Color.LightGray)) 83 | 84 | Text( 85 | text = state.translation, 86 | fontSize = 24.sp, 87 | color = Purple700, 88 | modifier = Modifier 89 | .fillMaxWidth() 90 | .padding(16.dp) 91 | ) 92 | } 93 | } 94 | 95 | Spacer(modifier = Modifier.weight(1f, fill = true)) 96 | 97 | LazyRow( 98 | modifier = Modifier 99 | .fillMaxWidth() 100 | .height(48.dp) 101 | .background(DigitalInkColors.PredictionBackground) 102 | ) { 103 | items(state.predictions) { prediction -> 104 | Prediction( 105 | text = prediction, 106 | onClick = { DigitalInkViewModel.Event.PredictionSelected(it) } 107 | ) 108 | } 109 | } 110 | 111 | DrawSpace( 112 | reset = state.resetCanvas, 113 | onDrawEvent = { event(DigitalInkViewModel.Event.Pointer(it)) }, 114 | modifier = Modifier 115 | .fillMaxWidth() 116 | .weight(1f, fill = true) 117 | ) 118 | } 119 | } 120 | } 121 | } 122 | 123 | @Composable 124 | fun ModelStatusProgress( 125 | statusText: String, 126 | modifier: Modifier = Modifier 127 | ) { 128 | 129 | Column(modifier = modifier) { 130 | 131 | CircularProgressIndicator( 132 | modifier = Modifier 133 | .align(Alignment.CenterHorizontally) 134 | .padding(vertical = 24.dp) 135 | ) 136 | 137 | Text( 138 | text = statusText, 139 | textAlign = TextAlign.Center, 140 | fontSize = 18.sp 141 | ) 142 | } 143 | } 144 | 145 | @Composable 146 | fun Prediction( 147 | text: String, 148 | onClick: (String) -> Unit, 149 | modifier: Modifier = Modifier 150 | ) { 151 | 152 | Row(modifier = modifier) { 153 | Text( 154 | text = text, 155 | color = DigitalInkColors.PredictionText, 156 | modifier = modifier 157 | .padding(horizontal = 16.dp, vertical = 8.dp) 158 | .clickable { onClick.invoke(text) }, 159 | ) 160 | 161 | Box(modifier = Modifier 162 | .fillMaxHeight() 163 | .width(1.dp) 164 | .background(DigitalInkColors.PredictionDivider) 165 | ) 166 | } 167 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/digitalink/DigitalInkViewModel.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.digitalink 2 | 3 | import `in`.surajsau.jisho.base.SingleFlowViewModel 4 | import `in`.surajsau.jisho.data.DigitalInkProvider 5 | import `in`.surajsau.jisho.data.TranslatorProvider 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.compositionLocalOf 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.viewModelScope 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.Job 12 | import kotlinx.coroutines.delay 13 | import kotlinx.coroutines.flow.* 14 | import kotlinx.coroutines.launch 15 | import javax.inject.Inject 16 | 17 | @HiltViewModel 18 | class DigitalInkViewModelImpl @Inject constructor( 19 | private val digitalInkProvider: DigitalInkProvider, 20 | private val translatorProvider: TranslatorProvider 21 | ): ViewModel(), DigitalInkViewModel { 22 | 23 | private var finishRecordingJob: Job? = null 24 | 25 | private val _digitalInkModelStatus = digitalInkProvider.checkIfModelIsDownlaoded() 26 | .flatMapLatest { status -> 27 | if (status == MLKitModelStatus.Downloaded) 28 | flowOf(status) 29 | else 30 | digitalInkProvider.downloadModel() 31 | } 32 | .stateIn(viewModelScope, SharingStarted.Lazily, MLKitModelStatus.NotDownloaded) 33 | 34 | private val _translatorModelStatus = translatorProvider.checkIfModelIsDownloaded() 35 | .stateIn(viewModelScope, SharingStarted.Lazily, MLKitModelStatus.NotDownloaded) 36 | 37 | private val _predictions = digitalInkProvider.predictions 38 | .consumeAsFlow() 39 | .onEach { 40 | if (it.isEmpty()) 41 | return@onEach 42 | 43 | setFinalText(text = _finalText.value.plus(it[0])) 44 | } 45 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) 46 | 47 | private val _translation = translatorProvider.translation 48 | .consumeAsFlow() 49 | .stateIn(viewModelScope, SharingStarted.Lazily, "") 50 | 51 | private val _finalText = MutableStateFlow("") 52 | private val _resetCanvas = MutableStateFlow(false) 53 | 54 | override val state: StateFlow 55 | get() = combine( 56 | _digitalInkModelStatus, 57 | _translatorModelStatus, 58 | _resetCanvas, 59 | _predictions, 60 | _translation, 61 | _finalText 62 | ) { result -> 63 | val digitalInkModelStatus = result[0] as MLKitModelStatus 64 | val translatorModelStatus = result[1] as MLKitModelStatus 65 | val resetCanvas = result[2] as Boolean 66 | val predictions = result[3] as List 67 | val translation = result[4] as String 68 | val finalText = result[5] as String 69 | val areModelsDownloaded = digitalInkModelStatus == MLKitModelStatus.Downloaded && translatorModelStatus == MLKitModelStatus.Downloaded 70 | 71 | DigitalInkViewModel.State( 72 | resetCanvas = resetCanvas, 73 | showModelStatusProgress = !areModelsDownloaded, 74 | finalText = finalText, 75 | translation = translation, 76 | predictions = predictions 77 | ) 78 | }.stateIn(viewModelScope, SharingStarted.Eagerly, DigitalInkViewModel.State()) 79 | 80 | override fun onEvent(event: DigitalInkViewModel.Event) { 81 | 82 | when (event) { 83 | is DigitalInkViewModel.Event.Pointer -> { 84 | 85 | when (val drawEvent = event.event) { 86 | is DrawEvent.Down -> { 87 | this.finishRecordingJob?.cancel() 88 | _resetCanvas.value = false 89 | 90 | digitalInkProvider.record(drawEvent.x, drawEvent.y) 91 | } 92 | 93 | is DrawEvent.Move -> { 94 | digitalInkProvider.record(drawEvent.x, drawEvent.y) 95 | } 96 | 97 | is DrawEvent.Up -> { 98 | this.finishRecordingJob = viewModelScope.launch { 99 | delay(DEBOUNCE_INTERVAL) 100 | _resetCanvas.value = true 101 | digitalInkProvider.finishRecording() 102 | } 103 | } 104 | } 105 | } 106 | 107 | is DigitalInkViewModel.Event.OnStop -> { 108 | digitalInkProvider.close() 109 | translatorProvider.close() 110 | } 111 | 112 | is DigitalInkViewModel.Event.TextChanged -> { 113 | setFinalText(event.text) 114 | } 115 | 116 | is DigitalInkViewModel.Event.PredictionSelected -> { 117 | setFinalText(text = _finalText.value.dropLast(1).plus(event.prediction)) 118 | } 119 | } 120 | } 121 | 122 | private fun setFinalText(text: String) { 123 | _finalText.value = text 124 | 125 | if (text.isNotEmpty()) 126 | translatorProvider.translate(text) 127 | } 128 | 129 | companion object { 130 | private const val DEBOUNCE_INTERVAL = 1000L 131 | } 132 | } 133 | 134 | interface DigitalInkViewModel: SingleFlowViewModel { 135 | 136 | data class State( 137 | val resetCanvas: Boolean = false, 138 | val showModelStatusProgress: Boolean = false, 139 | val finalText: String = "", 140 | val translation: String = "", 141 | val predictions: List = emptyList(), 142 | ) 143 | 144 | sealed class Event { 145 | data class TextChanged(val text: String): Event() 146 | data class Pointer(val event: DrawEvent): Event() 147 | data class PredictionSelected(val prediction: String): Event() 148 | 149 | object OnStop: Event() 150 | } 151 | } 152 | 153 | val LocalDigitalInkViewModel = compositionLocalOf { 154 | error("LocalDigitalViewModelFactory not provided") 155 | } 156 | 157 | @Composable 158 | fun provideDigitalInkViewModel(viewModelFactory: @Composable () -> DigitalInkViewModel) 159 | = LocalDigitalInkViewModel provides viewModelFactory.invoke() -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/digitalink/DrawSpace.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.digitalink 2 | 3 | import `in`.surajsau.jisho.ui.theme.DigitalInkColors 4 | import android.view.MotionEvent 5 | import androidx.compose.foundation.Canvas 6 | import androidx.compose.foundation.background 7 | import androidx.compose.foundation.gestures.detectDragGestures 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.ExperimentalComposeUiApi 10 | import androidx.compose.ui.Modifier 11 | import androidx.compose.ui.graphics.Color 12 | import androidx.compose.ui.graphics.Path 13 | import androidx.compose.ui.graphics.StrokeCap 14 | import androidx.compose.ui.graphics.StrokeJoin 15 | import androidx.compose.ui.graphics.drawscope.Stroke 16 | import androidx.compose.ui.input.pointer.pointerInput 17 | import androidx.compose.ui.input.pointer.pointerInteropFilter 18 | import kotlin.math.abs 19 | 20 | sealed class DrawEvent { 21 | data class Down(val x: Float, val y: Float): DrawEvent() 22 | data class Move(val x: Float, val y: Float): DrawEvent() 23 | object Up: DrawEvent() 24 | } 25 | 26 | private sealed class DrawPath { 27 | data class MoveTo(val x: Float, val y: Float): DrawPath() 28 | data class CurveTo(val prevX: Float, val prevY: Float, val x: Float, val y: Float): DrawPath() 29 | } 30 | 31 | @OptIn(ExperimentalComposeUiApi::class) 32 | @Composable 33 | fun DrawSpace( 34 | modifier: Modifier = Modifier, 35 | reset: Boolean = false, 36 | onDrawEvent: (DrawEvent) -> Unit, 37 | ) { 38 | 39 | val path = remember { Path() } 40 | 41 | var drawPath by remember { mutableStateOf(null) } 42 | 43 | if (reset) { 44 | drawPath = null 45 | path.reset() 46 | } 47 | 48 | Canvas( 49 | modifier = modifier 50 | .background(Color.White) 51 | .pointerInteropFilter { event -> 52 | when (event.action) { 53 | MotionEvent.ACTION_DOWN -> { 54 | drawPath = DrawPath.MoveTo(event.x, event.y) 55 | onDrawEvent.invoke(DrawEvent.Down(event.x, event.y)) 56 | } 57 | MotionEvent.ACTION_MOVE -> { 58 | val prevX = when (drawPath) { 59 | is DrawPath.MoveTo -> (drawPath as DrawPath.MoveTo).x 60 | is DrawPath.CurveTo -> (drawPath as DrawPath.CurveTo).x 61 | 62 | else -> 0f 63 | } 64 | 65 | val prevY = when (drawPath) { 66 | is DrawPath.MoveTo -> (drawPath as DrawPath.MoveTo).y 67 | is DrawPath.CurveTo -> (drawPath as DrawPath.CurveTo).y 68 | 69 | else -> 0f 70 | } 71 | drawPath = DrawPath.CurveTo(prevX, prevY, event.x, event.y) 72 | onDrawEvent.invoke(DrawEvent.Move(event.x, event.y)) 73 | } 74 | MotionEvent.ACTION_UP -> { 75 | onDrawEvent.invoke(DrawEvent.Up) 76 | } 77 | 78 | else -> { /* do nothing */ } 79 | } 80 | 81 | return@pointerInteropFilter true 82 | } 83 | ) { 84 | if (drawPath == null) 85 | return@Canvas 86 | 87 | when (drawPath) { 88 | is DrawPath.MoveTo -> { 89 | val (x, y) = drawPath as DrawPath.MoveTo 90 | path.moveTo(x, y) 91 | } 92 | 93 | is DrawPath.CurveTo -> { 94 | val (prevX, prevY, x, y) = drawPath as DrawPath.CurveTo 95 | path.quadraticBezierTo(prevX, prevY, (x + prevX)/2, (y + prevY)/2) 96 | } 97 | } 98 | 99 | drawPath( 100 | path = path, 101 | color = DigitalInkColors.Stroke, 102 | style = Stroke(width = 5f, cap = StrokeCap.Round, join = StrokeJoin.Round) 103 | ) 104 | } 105 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/digitalink/MLKitModelStatus.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.digitalink 2 | 3 | enum class MLKitModelStatus { 4 | NotDownloaded, Downloaded, CheckingDownload, Downloading 5 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/facenet/CameraScreen.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.facenet 2 | 3 | import `in`.surajsau.jisho.ui.base.camera.Camera 4 | import `in`.surajsau.jisho.ui.base.camera.CameraAction 5 | import `in`.surajsau.jisho.ui.base.camera.CameraControls 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Modifier 10 | 11 | @Composable 12 | fun CameraScreen( 13 | modifier: Modifier, 14 | onImageCaptured: (String) -> Unit, 15 | ) { 16 | 17 | var cameraAction by remember { mutableStateOf(CameraAction.None) } 18 | 19 | Column(modifier = modifier) { 20 | 21 | Camera( 22 | modifier = Modifier 23 | .fillMaxWidth() 24 | .weight(1f), 25 | cameraAction = cameraAction, 26 | onImageCaptured = onImageCaptured 27 | ) 28 | 29 | CameraControls( 30 | modifier = Modifier.fillMaxWidth(), 31 | onCameraAction = { cameraAction = it } 32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/facenet/SavedFaceImage.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.facenet 2 | 3 | import `in`.surajsau.jisho.domain.models.FaceModel 4 | import androidx.compose.foundation.Image 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.border 7 | import androidx.compose.foundation.layout.* 8 | import androidx.compose.foundation.shape.CircleShape 9 | import androidx.compose.material.Text 10 | import androidx.compose.runtime.Composable 11 | import androidx.compose.ui.Alignment 12 | import androidx.compose.ui.Modifier 13 | import androidx.compose.ui.draw.clip 14 | import androidx.compose.ui.draw.clipToBounds 15 | import androidx.compose.ui.graphics.Color 16 | import androidx.compose.ui.layout.ContentScale 17 | import androidx.compose.ui.text.style.TextAlign 18 | import androidx.compose.ui.tooling.preview.Preview 19 | import androidx.compose.ui.unit.dp 20 | import androidx.compose.ui.unit.sp 21 | import coil.compose.rememberImagePainter 22 | import java.io.File 23 | 24 | @Composable 25 | fun SavedFace( 26 | modifier: Modifier = Modifier, 27 | isSelected: Boolean = false, 28 | model: FaceModel, 29 | ) { 30 | 31 | Column( 32 | modifier = modifier, 33 | horizontalAlignment = Alignment.CenterHorizontally 34 | ) { 35 | Image( 36 | painter = rememberImagePainter(data = File(model.imageFilePath)), 37 | contentDescription = model.faceName, 38 | modifier = Modifier 39 | .clip(CircleShape) 40 | .background(Color.LightGray) 41 | .border( 42 | width = if (isSelected) 4.dp else 0.dp, 43 | color = Color.DarkGray, 44 | shape = CircleShape 45 | ) 46 | .clipToBounds() 47 | .fillMaxWidth() 48 | .aspectRatio(1f), 49 | contentScale = ContentScale.Fit 50 | ) 51 | 52 | Text( 53 | text = model.faceName, 54 | modifier = Modifier.padding(top = 4.dp), 55 | fontSize = 12.sp, 56 | maxLines = 2, 57 | textAlign = TextAlign.Center 58 | ) 59 | } 60 | } 61 | 62 | @Preview 63 | @Composable 64 | private fun previewSavedFaceImage() { 65 | 66 | SavedFace( 67 | modifier = Modifier.width(56.dp), 68 | model = FaceModel("", "John Doe") 69 | ) 70 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/home/HomeScreen.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.home 2 | 3 | import `in`.surajsau.jisho.base.use 4 | import `in`.surajsau.jisho.data.model.Screen 5 | import `in`.surajsau.jisho.ui.Destinations 6 | import android.os.Build 7 | import androidx.compose.foundation.Image 8 | import androidx.compose.foundation.background 9 | import androidx.compose.foundation.clickable 10 | import androidx.compose.foundation.layout.* 11 | import androidx.compose.foundation.lazy.LazyColumn 12 | import androidx.compose.foundation.lazy.items 13 | import androidx.compose.foundation.lazy.itemsIndexed 14 | import androidx.compose.foundation.shape.RoundedCornerShape 15 | import androidx.compose.material.Text 16 | import androidx.compose.runtime.Composable 17 | import androidx.compose.ui.Modifier 18 | import androidx.compose.ui.draw.clip 19 | import androidx.compose.ui.draw.clipToBounds 20 | import androidx.compose.ui.graphics.Color 21 | import androidx.compose.ui.layout.ContentScale 22 | import androidx.compose.ui.platform.LocalContext 23 | import androidx.compose.ui.text.font.FontWeight 24 | import androidx.compose.ui.unit.dp 25 | import androidx.compose.ui.unit.sp 26 | import coil.ImageLoader 27 | import coil.compose.rememberImagePainter 28 | import coil.decode.GifDecoder 29 | import coil.decode.ImageDecoderDecoder 30 | 31 | @Composable 32 | fun HomeScreen( 33 | modifier: Modifier = Modifier, 34 | navigateToDestination: (Destinations) -> Unit 35 | ) { 36 | 37 | val (state, _) = use(LocalHomeViewModel.current, HomeViewModel.State()) 38 | 39 | Box(modifier = modifier) { 40 | LazyColumn( 41 | modifier = Modifier.fillMaxSize() 42 | .padding(horizontal = 16.dp) 43 | ) { 44 | itemsIndexed(state.screens) { index, screen -> 45 | 46 | ScreenCard( 47 | screen = screen, 48 | onClick = { navigateToDestination.invoke(it) }, 49 | modifier = Modifier.padding( 50 | top = if (index == 0) 16.dp else 0.dp, 51 | bottom = 16.dp 52 | ) 53 | ) 54 | } 55 | } 56 | } 57 | } 58 | 59 | @Composable 60 | fun ScreenCard( 61 | screen: Screen, 62 | modifier: Modifier = Modifier, 63 | onClick: (Destinations) -> Unit 64 | ) { 65 | val context = LocalContext.current 66 | 67 | val painter = rememberImagePainter( 68 | data = screen.previewImage, 69 | imageLoader = ImageLoader.invoke(context).newBuilder() 70 | .componentRegistry { 71 | val decoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { 72 | ImageDecoderDecoder(LocalContext.current) 73 | } else { 74 | GifDecoder() 75 | } 76 | add(decoder) 77 | } 78 | .build() 79 | ) 80 | 81 | Row( 82 | modifier = modifier 83 | .clickable { onClick.invoke(screen.destinations) } 84 | .fillMaxWidth() 85 | .height(200.dp) 86 | .clip(RoundedCornerShape(4.dp)) 87 | .background(Color.White) 88 | ) { 89 | Image( 90 | painter = painter, 91 | contentDescription = null, 92 | modifier = Modifier 93 | .weight(1f) 94 | .fillMaxHeight() 95 | .clipToBounds() 96 | .height(200.dp), 97 | contentScale = ContentScale.Crop 98 | ) 99 | 100 | Column( 101 | modifier = Modifier.weight(2f) 102 | .padding(16.dp) 103 | ) { 104 | Text( 105 | text = screen.title, 106 | fontSize = 18.sp, 107 | fontWeight = FontWeight.Medium, 108 | modifier = Modifier 109 | .fillMaxWidth() 110 | ) 111 | 112 | Text( 113 | text = screen.description, 114 | modifier = Modifier.fillMaxWidth() 115 | .padding(top = 8.dp) 116 | ) 117 | } 118 | } 119 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/home/HomeViewModel.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.home 2 | 3 | import `in`.surajsau.jisho.base.SingleFlowViewModel 4 | import `in`.surajsau.jisho.data.ScreensDataProvider 5 | import `in`.surajsau.jisho.data.model.Screen 6 | import androidx.compose.runtime.Composable 7 | import androidx.compose.runtime.compositionLocalOf 8 | import androidx.lifecycle.ViewModel 9 | import androidx.lifecycle.viewModelScope 10 | import dagger.hilt.android.lifecycle.HiltViewModel 11 | import kotlinx.coroutines.flow.* 12 | import javax.inject.Inject 13 | 14 | @HiltViewModel 15 | class HomeViewModelImpl @Inject constructor( 16 | private val screensDataProvider: ScreensDataProvider, 17 | ) : ViewModel(), HomeViewModel { 18 | 19 | private val _screens = screensDataProvider.fetchScreen() 20 | .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) 21 | 22 | override val state: StateFlow 23 | get() = _screens 24 | .map { HomeViewModel.State(screens = it) } 25 | .stateIn(viewModelScope, SharingStarted.Eagerly, HomeViewModel.State()) 26 | 27 | override fun onEvent(event: HomeViewModel.Event) {} 28 | 29 | } 30 | 31 | interface HomeViewModel: SingleFlowViewModel { 32 | 33 | sealed class Event 34 | 35 | data class State( 36 | val screens: List = emptyList(), 37 | ) 38 | 39 | } 40 | 41 | val LocalHomeViewModel = compositionLocalOf { 42 | error("HomeViewModel not provided") 43 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/styletransfer/CameraScreen.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.styletransfer 2 | 3 | import `in`.surajsau.jisho.ui.base.camera.Camera 4 | import `in`.surajsau.jisho.ui.base.camera.CameraAction 5 | import `in`.surajsau.jisho.ui.base.camera.CameraControls 6 | import androidx.compose.foundation.layout.Column 7 | import androidx.compose.foundation.layout.fillMaxWidth 8 | import androidx.compose.runtime.* 9 | import androidx.compose.ui.Modifier 10 | 11 | @Composable 12 | internal fun CameraScreen( 13 | modifier: Modifier = Modifier, 14 | onImageCaptured: (String) -> Unit, 15 | ) { 16 | 17 | var cameraAction by remember { mutableStateOf(CameraAction.None) } 18 | 19 | Column(modifier = modifier) { 20 | 21 | Camera( 22 | modifier = Modifier.fillMaxWidth() 23 | .weight(1f), 24 | cameraAction = cameraAction, 25 | onImageCaptured = onImageCaptured 26 | ) 27 | 28 | CameraControls( 29 | modifier = Modifier.fillMaxWidth() 30 | .weight(1f), 31 | onCameraAction = { cameraAction = it } 32 | ) 33 | } 34 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/styletransfer/StylePreviewScreen.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.styletransfer 2 | 3 | import `in`.surajsau.jisho.base.LocalBitmapCache 4 | import android.graphics.Bitmap 5 | import android.graphics.BitmapFactory 6 | import androidx.compose.foundation.Image 7 | import androidx.compose.foundation.background 8 | import androidx.compose.foundation.border 9 | import androidx.compose.foundation.clickable 10 | import androidx.compose.foundation.layout.* 11 | import androidx.compose.foundation.lazy.LazyRow 12 | import androidx.compose.foundation.lazy.itemsIndexed 13 | import androidx.compose.foundation.shape.RoundedCornerShape 14 | import androidx.compose.material.CircularProgressIndicator 15 | import androidx.compose.material.Text 16 | import androidx.compose.runtime.* 17 | import androidx.compose.ui.Alignment 18 | import androidx.compose.ui.Modifier 19 | import androidx.compose.ui.draw.clip 20 | import androidx.compose.ui.draw.clipToBounds 21 | import androidx.compose.ui.graphics.Color 22 | import androidx.compose.ui.graphics.asImageBitmap 23 | import androidx.compose.ui.layout.ContentScale 24 | import androidx.compose.ui.platform.LocalContext 25 | import androidx.compose.ui.unit.dp 26 | import kotlinx.coroutines.Dispatchers 27 | import kotlinx.coroutines.launch 28 | 29 | @Composable 30 | fun StylePreviewScreen( 31 | imagePreview: @Composable () -> Unit, 32 | previewGallery: @Composable () -> Unit, 33 | modifier: Modifier = Modifier, 34 | ) { 35 | 36 | Column(modifier = modifier) { 37 | 38 | Box(modifier = Modifier 39 | .fillMaxWidth() 40 | .weight(1f) 41 | ) { 42 | imagePreview() 43 | } 44 | 45 | Box(modifier = Modifier 46 | .fillMaxWidth() 47 | .weight(1f) 48 | ) { 49 | previewGallery() 50 | } 51 | } 52 | 53 | } 54 | 55 | @Composable 56 | fun ImagePreview( 57 | bitmap: Bitmap?, 58 | modifier: Modifier = Modifier, 59 | showLoader: Boolean = false, 60 | ) { 61 | 62 | Box(modifier = modifier) { 63 | if (bitmap != null) { 64 | Image( 65 | bitmap = bitmap.asImageBitmap(), 66 | contentDescription = null, 67 | modifier = Modifier.fillMaxSize() 68 | ) 69 | } 70 | 71 | if (showLoader) { 72 | CircularProgressIndicator(modifier = Modifier.align(Alignment.Center)) 73 | } 74 | } 75 | } 76 | 77 | @Composable 78 | fun PreviewGallery( 79 | previews: List, 80 | modifier: Modifier = Modifier, 81 | onStyleSelected: (String) -> Unit 82 | ) { 83 | 84 | Column(modifier = modifier) { 85 | 86 | Text( 87 | text = "Select a style to apply", 88 | color = Color.DarkGray, 89 | modifier = Modifier.align(Alignment.CenterHorizontally) 90 | .padding(vertical = 24.dp) 91 | ) 92 | 93 | LazyRow( 94 | modifier = modifier 95 | .fillMaxWidth() 96 | .padding(16.dp) 97 | ) { 98 | itemsIndexed(previews) { index, fileName -> 99 | PreviewGalleryItem( 100 | fileName = fileName, 101 | onClick = { onStyleSelected.invoke(it) }, 102 | modifier = Modifier 103 | .width(96.dp) 104 | .height(96.dp) 105 | .padding( 106 | start = if (index == 0) 16.dp else 0.dp, 107 | end = 16.dp 108 | ) 109 | ) 110 | } 111 | } 112 | } 113 | } 114 | 115 | @Composable 116 | fun PreviewGalleryItem( 117 | fileName: String, 118 | onClick: (String) -> Unit, 119 | modifier: Modifier = Modifier 120 | ) { 121 | 122 | val bitmapCache = LocalBitmapCache.current 123 | 124 | val context = LocalContext.current 125 | 126 | var previewBitmap by remember { mutableStateOf(null) } 127 | 128 | LaunchedEffect(fileName) { 129 | if (!bitmapCache.has(fileName)) { 130 | launch(Dispatchers.IO) { 131 | val bitmapStream = context.assets.open(fileName) 132 | val bitmap = BitmapFactory.decodeStream(bitmapStream) 133 | 134 | bitmapCache.save(fileName, bitmap) 135 | bitmapStream.close() 136 | 137 | previewBitmap = bitmap 138 | } 139 | } else { 140 | previewBitmap = bitmapCache.get(fileName) 141 | } 142 | } 143 | 144 | if (previewBitmap != null) { 145 | Image( 146 | bitmap = previewBitmap!!.asImageBitmap(), 147 | contentDescription = null, 148 | modifier = modifier 149 | .clip(RoundedCornerShape(4.dp)) 150 | .clipToBounds() 151 | .clickable { onClick.invoke(fileName) }, 152 | contentScale = ContentScale.Crop 153 | ) 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/styletransfer/StyleTransferScreen.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.styletransfer 2 | 3 | import `in`.surajsau.jisho.base.use 4 | import `in`.surajsau.jisho.ui.base.AskPermissionScreen 5 | import androidx.compose.foundation.background 6 | import androidx.compose.foundation.layout.* 7 | import androidx.compose.runtime.* 8 | import androidx.compose.ui.Modifier 9 | import androidx.compose.ui.graphics.Color 10 | import androidx.compose.ui.platform.LocalLifecycleOwner 11 | import androidx.lifecycle.Lifecycle 12 | import androidx.lifecycle.LifecycleEventObserver 13 | 14 | @Composable 15 | fun StyleTransferScreen( 16 | modifier: Modifier = Modifier, 17 | navigateBack: () -> Unit, 18 | navigateToSettings: () -> Unit, 19 | ) { 20 | 21 | val (state, event) = use(LocalStyleTransferViewModel.current, StyleTransferViewModel.State()) 22 | 23 | val lifecycleOwner = LocalLifecycleOwner.current 24 | 25 | DisposableEffect(Unit) { 26 | val lifecycleObserver = LifecycleEventObserver { _, lifecycleEvent -> 27 | if (lifecycleEvent == Lifecycle.Event.ON_STOP) { 28 | event(StyleTransferViewModel.Event.OnStop) 29 | } 30 | } 31 | lifecycleOwner.lifecycle.addObserver(lifecycleObserver) 32 | 33 | onDispose { 34 | lifecycleOwner.lifecycle.removeObserver(lifecycleObserver) 35 | } 36 | } 37 | 38 | Box(modifier = modifier 39 | .fillMaxSize() 40 | .background(Color.White) 41 | ) { 42 | 43 | AskPermissionScreen( 44 | modifier = Modifier.fillMaxSize(), 45 | permission = android.Manifest.permission.CAMERA, 46 | onDismiss = { navigateBack.invoke() }, 47 | onPermissionDeniedFallback = { navigateToSettings.invoke() }, 48 | ) { 49 | 50 | when (val screenMode = state.mode) { 51 | is StyleTransferViewModel.ScreenMode.Camera -> { 52 | CameraScreen(modifier = Modifier.fillMaxSize(), 53 | onImageCaptured = { fileName -> 54 | event(StyleTransferViewModel.Event.CameraResultReceived(fileName)) 55 | } 56 | ) 57 | } 58 | 59 | is StyleTransferViewModel.ScreenMode.StylePreview -> { 60 | StylePreviewScreen( 61 | modifier = Modifier.fillMaxSize(), 62 | imagePreview = { 63 | ImagePreview( 64 | bitmap = screenMode.image, 65 | showLoader = screenMode.showLoading, 66 | modifier = Modifier.fillMaxSize() 67 | ) 68 | }, 69 | previewGallery = { 70 | PreviewGallery( 71 | previews = screenMode.stylePreviews, 72 | onStyleSelected = { fileName -> 73 | event(StyleTransferViewModel.Event.StyleSelected(fileName)) 74 | }, 75 | modifier = Modifier.fillMaxSize() 76 | ) 77 | } 78 | ) 79 | } 80 | } 81 | 82 | } 83 | } 84 | 85 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/styletransfer/StyleTransferViewModel.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.styletransfer 2 | 3 | import `in`.surajsau.jisho.base.FileName 4 | import `in`.surajsau.jisho.base.SingleFlowViewModel 5 | import `in`.surajsau.jisho.data.FileProvider 6 | import `in`.surajsau.jisho.data.StyleTransferProvider 7 | import android.graphics.Bitmap 8 | import androidx.compose.runtime.compositionLocalOf 9 | import androidx.lifecycle.ViewModel 10 | import androidx.lifecycle.viewModelScope 11 | import dagger.hilt.android.lifecycle.HiltViewModel 12 | import kotlinx.coroutines.flow.* 13 | import kotlinx.coroutines.launch 14 | import javax.inject.Inject 15 | 16 | @HiltViewModel 17 | class StyleTransferViewModelImpl @Inject constructor( 18 | private val styleTransferProvider: StyleTransferProvider, 19 | private val fileProvider: FileProvider, 20 | ) : ViewModel(), StyleTransferViewModel { 21 | 22 | private val _cameraImageFileName = MutableStateFlow(null) 23 | private val _styleImageFileName = MutableStateFlow(null) 24 | 25 | private val _isImageCaptured = MutableStateFlow(false) 26 | 27 | private val _cameraImage = _cameraImageFileName.filterNotNull().map { fileProvider.fetchCachedBitmap(it.value) } 28 | private val _styleImage = _styleImageFileName.filterNotNull().flatMapLatest { fileProvider.fetchAssetBitmap(it.value) } 29 | 30 | private val _styleTransferState = combine( 31 | _cameraImage, 32 | _styleImage 33 | ) { cameraIamge, styleImage -> Pair(cameraIamge, styleImage) } 34 | .flatMapLatest { (targetImage, styleImage) -> styleTransferProvider.process(targetImage, styleImage) } 35 | .stateIn(viewModelScope, SharingStarted.Lazily, StyleTransferProvider.StyleTransferState.Idle) 36 | 37 | override val state: StateFlow 38 | get() = combine( 39 | _styleTransferState, 40 | _isImageCaptured 41 | ){ styleTransferState, isImagedCaptured -> 42 | val mode = if (!isImagedCaptured) { 43 | StyleTransferViewModel.ScreenMode.Camera 44 | } else { 45 | StyleTransferViewModel.ScreenMode.StylePreview( 46 | showLoading = styleTransferState !is StyleTransferProvider.StyleTransferState.Finished, 47 | image = (styleTransferState as? StyleTransferProvider.StyleTransferState.Finished)?.image, 48 | stylePreviews = StyleTransferProvider.Styles 49 | ) 50 | } 51 | 52 | StyleTransferViewModel.State(mode = mode) 53 | } 54 | .stateIn(viewModelScope, SharingStarted.Eagerly, StyleTransferViewModel.State()) 55 | 56 | override fun onEvent(event: StyleTransferViewModel.Event) { 57 | viewModelScope.launch { 58 | when (event) { 59 | is StyleTransferViewModel.Event.CameraResultReceived -> { 60 | _isImageCaptured.value = true 61 | _cameraImageFileName.value = FileName(event.fileName) 62 | } 63 | 64 | is StyleTransferViewModel.Event.StyleSelected -> { _styleImageFileName.value = FileName(event.fileName) } 65 | 66 | is StyleTransferViewModel.Event.OnStop -> {} 67 | } 68 | } 69 | } 70 | 71 | } 72 | 73 | interface StyleTransferViewModel: SingleFlowViewModel { 74 | 75 | sealed class ScreenMode { 76 | object Camera: ScreenMode() 77 | 78 | data class StylePreview( 79 | val showLoading: Boolean, 80 | val image: Bitmap?, 81 | val stylePreviews: List 82 | ): ScreenMode() 83 | } 84 | 85 | sealed class Event { 86 | data class CameraResultReceived(val fileName: String): Event() 87 | data class StyleSelected(val fileName: String): Event() 88 | 89 | object OnStop: Event() 90 | } 91 | 92 | data class State( 93 | val mode: ScreenMode = ScreenMode.Camera 94 | ) 95 | } 96 | 97 | val LocalStyleTransferViewModel = compositionLocalOf { 98 | error("StyleTransferViewModel not provided") 99 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/theme/Color.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.theme 2 | 3 | import androidx.compose.ui.graphics.Color 4 | 5 | val Purple200 = Color(0xFFBB86FC) 6 | val Purple500 = Color(0xFF6200EE) 7 | val Purple700 = Color(0xFF3700B3) 8 | val Teal200 = Color(0xFF03DAC5) 9 | 10 | object DigitalInkColors { 11 | val PredictionDivider = Color(206, 206, 206) 12 | val PredictionBackground = Color(233, 233, 233) 13 | val PredictionText = Color(77, 77, 77) 14 | val PredictionPlaceholder = Color(186, 186, 186) 15 | 16 | val Text = Color(71, 71, 71) 17 | val Stroke = Color(39, 39, 39) 18 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/theme/Shape.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.theme 2 | 3 | import androidx.compose.foundation.shape.RoundedCornerShape 4 | import androidx.compose.material.Shapes 5 | import androidx.compose.ui.unit.dp 6 | 7 | val Shapes = Shapes( 8 | small = RoundedCornerShape(4.dp), 9 | medium = RoundedCornerShape(4.dp), 10 | large = RoundedCornerShape(0.dp) 11 | ) -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/theme/Theme.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.theme 2 | 3 | import androidx.compose.foundation.isSystemInDarkTheme 4 | import androidx.compose.material.MaterialTheme 5 | import androidx.compose.material.darkColors 6 | import androidx.compose.material.lightColors 7 | import androidx.compose.runtime.Composable 8 | import androidx.compose.ui.graphics.Color 9 | 10 | private val DarkColorPalette = darkColors( 11 | primary = Purple200, 12 | primaryVariant = Purple700, 13 | secondary = Teal200 14 | ) 15 | 16 | private val LightColorPalette = lightColors( 17 | primary = Purple500, 18 | primaryVariant = Purple700, 19 | secondary = Teal200, 20 | background = Color(238, 238, 238), 21 | surface = Color(238, 238, 238), 22 | 23 | /* Other default colors to override 24 | background = Color.White, 25 | surface = Color.White, 26 | onPrimary = Color.White, 27 | onSecondary = Color.Black, 28 | onBackground = Color.Black, 29 | onSurface = Color.Black, 30 | */ 31 | ) 32 | 33 | @Composable 34 | fun JishoTheme(darkTheme: Boolean = isSystemInDarkTheme(), content: @Composable () -> Unit) { 35 | val colors = if (darkTheme) { 36 | DarkColorPalette 37 | } else { 38 | LightColorPalette 39 | } 40 | 41 | MaterialTheme( 42 | colors = colors, 43 | typography = Typography, 44 | shapes = Shapes, 45 | content = content 46 | ) 47 | } -------------------------------------------------------------------------------- /app/src/main/java/in/surajsau/jisho/ui/theme/Type.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho.ui.theme 2 | 3 | import androidx.compose.material.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 | body1 = TextStyle( 12 | fontFamily = FontFamily.Default, 13 | fontWeight = FontWeight.Normal, 14 | fontSize = 16.sp 15 | ) 16 | /* Other default text styles to override 17 | button = TextStyle( 18 | fontFamily = FontFamily.Default, 19 | fontWeight = FontWeight.W500, 20 | fontSize = 14.sp 21 | ), 22 | caption = TextStyle( 23 | fontFamily = FontFamily.Default, 24 | fontWeight = FontWeight.Normal, 25 | fontSize = 12.sp 26 | ) 27 | */ 28 | ) -------------------------------------------------------------------------------- /app/src/main/ml/facenet.tflite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/ml/facenet.tflite -------------------------------------------------------------------------------- /app/src/main/ml/magenta_style_predictor.tflite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/ml/magenta_style_predictor.tflite -------------------------------------------------------------------------------- /app/src/main/ml/magenta_style_transfer.tflite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/ml/magenta_style_transfer.tflite -------------------------------------------------------------------------------- /app/src/main/ml/style_predictor_model.tflite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/ml/style_predictor_model.tflite -------------------------------------------------------------------------------- /app/src/main/ml/style_transfer_model.tflite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/ml/style_transfer_model.tflite -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_switch_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/res/drawable/ic_switch_camera.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ML 3 | -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/xml/backup_rules.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/xml/data_extraction_rules.xml: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 12 | 13 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/xml/file_paths.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/test/java/in/surajsau/jisho/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package `in`.surajsau.jisho 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 | } -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | dependencies { 7 | classpath("com.android.tools.build:gradle:7.0.3") 8 | classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31") 9 | classpath("com.google.dagger:hilt-android-gradle-plugin:2.38.1") 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } -------------------------------------------------------------------------------- /buildSrc/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | `kotlin-dsl` 3 | } 4 | 5 | repositories { 6 | jcenter() 7 | } 8 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Nov 01 13:02:41 IST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.3-rc-1-bin.zip 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /screenshots/card_reader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/screenshots/card_reader.gif -------------------------------------------------------------------------------- /screenshots/face.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/screenshots/face.gif -------------------------------------------------------------------------------- /screenshots/gpt2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/screenshots/gpt2.gif -------------------------------------------------------------------------------- /screenshots/translate_app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/surajsau/ML-Android/66ca1373067c57257e7d26f9b922d2d0915b0f4a/screenshots/translate_app.gif -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | gradlePluginPortal() 4 | google() 5 | mavenCentral() 6 | } 7 | } 8 | dependencyResolutionManagement { 9 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 10 | repositories { 11 | google() 12 | mavenCentral() 13 | } 14 | } 15 | rootProject.name = "Jisho" 16 | include ':app' 17 | --------------------------------------------------------------------------------