├── app ├── .gitignore ├── src │ ├── main │ │ ├── assets │ │ │ └── sync │ │ │ │ ├── models │ │ │ │ ├── en-us-ptm │ │ │ │ │ ├── mdef.md5 │ │ │ │ │ ├── means.md5 │ │ │ │ │ ├── noisedict.md5 │ │ │ │ │ ├── sendump.md5 │ │ │ │ │ ├── variances.md5 │ │ │ │ │ ├── en-phone.dmp.md5 │ │ │ │ │ ├── feat.params.md5 │ │ │ │ │ ├── transition_matrices.md5 │ │ │ │ │ ├── noisedict │ │ │ │ │ ├── mdef │ │ │ │ │ ├── means │ │ │ │ │ ├── sendump │ │ │ │ │ ├── variances │ │ │ │ │ ├── en-phone.dmp │ │ │ │ │ ├── transition_matrices │ │ │ │ │ └── feat.params │ │ │ │ └── lm │ │ │ │ │ └── words.dic.md5 │ │ │ │ └── assets.lst │ │ ├── res │ │ │ ├── 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 │ │ │ ├── drawable-hdpi │ │ │ │ └── ic_stat_hearing.png │ │ │ ├── drawable-mdpi │ │ │ │ └── ic_stat_hearing.png │ │ │ ├── drawable-xhdpi │ │ │ │ └── ic_stat_hearing.png │ │ │ ├── drawable-xxhdpi │ │ │ │ └── ic_stat_hearing.png │ │ │ ├── values │ │ │ │ ├── ic_launcher_background.xml │ │ │ │ ├── colors.xml │ │ │ │ ├── arrays.xml │ │ │ │ ├── styles.xml │ │ │ │ └── strings.xml │ │ │ ├── layout │ │ │ │ ├── activity_hotword.xml │ │ │ │ ├── fragment_hotword_detected.xml │ │ │ │ └── listener_fragment.xml │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ └── ic_launcher.xml │ │ │ ├── drawable │ │ │ │ ├── ic_mic_gray_128dp.xml │ │ │ │ ├── ic_mic_green_128dp.xml │ │ │ │ ├── ic_mic_light_gray_128dp.xml │ │ │ │ ├── ic_mic_off_red_128dp.xml │ │ │ │ ├── ic_happy_36.xml │ │ │ │ └── ic_launcher_foreground.xml │ │ │ └── drawable-anydpi-v24 │ │ │ │ └── ic_stat_hearing.xml │ │ ├── java │ │ │ └── com │ │ │ │ └── tuckercr │ │ │ │ └── zamzam │ │ │ │ ├── HotWordDetectedFragment.kt │ │ │ │ ├── prefs │ │ │ │ └── PrefsManager.kt │ │ │ │ ├── ListenerService.kt │ │ │ │ ├── BindingAdapters.kt │ │ │ │ ├── DictionaryRepository.kt │ │ │ │ ├── ListenerFragment.kt │ │ │ │ ├── NotificationUtils.kt │ │ │ │ ├── ListenerViewModel.kt │ │ │ │ └── HotWordActivity.kt │ │ └── AndroidManifest.xml │ └── androidTest │ │ └── java │ │ └── com │ │ └── tuckercr │ │ └── zamzam │ │ └── ExampleInstrumentedTest.java ├── proguard-rules.pro └── build.gradle ├── settings.gradle ├── screenshots ├── main.png ├── fg_service.png └── triggered.png ├── .idea ├── copyright │ └── profiles_settings.xml ├── markdown-navigator │ └── profiles_settings.xml ├── inspectionProfiles │ └── Project_Default.xml ├── codeStyles │ └── Project.xml └── markdown-navigator.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── README.md ├── .gitignore ├── gradlew.bat └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/mdef.md5: -------------------------------------------------------------------------------- 1 | ba13d30c2fee63e039e119ab449bd618 2 | -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/means.md5: -------------------------------------------------------------------------------- 1 | d0ee21e7d0e03575f27497b2833c6f02 2 | -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/lm/words.dic.md5: -------------------------------------------------------------------------------- 1 | 7966c7291fc744969ea90a961178e924 2 | -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/noisedict.md5: -------------------------------------------------------------------------------- 1 | 05034ffef21f4810d10d3c76a6f5e921 2 | -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/sendump.md5: -------------------------------------------------------------------------------- 1 | 75328625279cdbb72f800b315365ff45 2 | -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/variances.md5: -------------------------------------------------------------------------------- 1 | d4d6ba74707952aa7e00c3bc1e7d0fb4 2 | -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/en-phone.dmp.md5: -------------------------------------------------------------------------------- 1 | 912236bae5e072d02ab1ec5b0c202a68 2 | -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/feat.params.md5: -------------------------------------------------------------------------------- 1 | 1124afb8cc8f875fd2daa47683e68530 2 | -------------------------------------------------------------------------------- /screenshots/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/screenshots/main.png -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/transition_matrices.md5: -------------------------------------------------------------------------------- 1 | 7a63d8971f81eef2154ea38b8bdfe520 2 | -------------------------------------------------------------------------------- /screenshots/fg_service.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/screenshots/fg_service.png -------------------------------------------------------------------------------- /screenshots/triggered.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/screenshots/triggered.png -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/noisedict: -------------------------------------------------------------------------------- 1 | SIL 2 | SIL 3 | SIL 4 | [NOISE] +NSN+ 5 | [SPEECH] +SPN+ 6 | -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/mdef: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/assets/sync/models/en-us-ptm/mdef -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/means: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/assets/sync/models/en-us-ptm/means -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /.idea/markdown-navigator/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/sendump: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/assets/sync/models/en-us-ptm/sendump -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/variances: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/assets/sync/models/en-us-ptm/variances -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_stat_hearing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/res/drawable-hdpi/ic_stat_hearing.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_stat_hearing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/res/drawable-mdpi/ic_stat_hearing.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_stat_hearing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/res/drawable-xhdpi/ic_stat_hearing.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_stat_hearing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/res/drawable-xxhdpi/ic_stat_hearing.png -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/en-phone.dmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/assets/sync/models/en-us-ptm/en-phone.dmp -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/transition_matrices: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tuckercr/wakewordapp/HEAD/app/src/main/assets/sync/models/en-us-ptm/transition_matrices -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #B71C1C 4 | -------------------------------------------------------------------------------- /app/src/main/assets/sync/assets.lst: -------------------------------------------------------------------------------- 1 | models/lm/words.dic 2 | models/en-us-ptm/feat.params 3 | models/en-us-ptm/mdef 4 | models/en-us-ptm/means 5 | models/en-us-ptm/noisedict 6 | models/en-us-ptm/sendump 7 | models/en-us-ptm/transition_matrices 8 | models/en-us-ptm/variances 9 | -------------------------------------------------------------------------------- /app/src/main/assets/sync/models/en-us-ptm/feat.params: -------------------------------------------------------------------------------- 1 | -lowerf 130 2 | -upperf 6800 3 | -nfilt 25 4 | -transform dct 5 | -lifter 22 6 | -feat 1s_c_d_dd 7 | -svspec 0-12/13-25/26-38 8 | -agc none 9 | -cmn batch 10 | -varnorm no 11 | -model ptm 12 | -cmninit 40,10,10 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_hotword.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat Mar 28 14:06:48 EDT 2020 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #b71c1c 4 | #7f0000 5 | #f05545 6 | #e8e6d9 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/values/arrays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mic_gray_128dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mic_green_128dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mic_light_gray_128dp.xml: -------------------------------------------------------------------------------- 1 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_mic_off_red_128dp.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_happy_36.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | android.enableJetifier=true 13 | android.useAndroidX=true 14 | org.gradle.jvmargs=-Xmx1536m 15 | 16 | # When configured, Gradle will run in incubating parallel mode. 17 | # This option should only be used with decoupled projects. More details, visit 18 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 19 | # org.gradle.parallel=true 20 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/tuckercr/zamzam/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package com.tuckercr.zamzam; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.InstrumentationRegistry; 6 | import androidx.test.runner.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.assertEquals; 12 | 13 | /** 14 | * Instrumentation test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getTargetContext(); 24 | 25 | assertEquals("com.tuckercr.zamzam", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/wolf/Library/Android/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | 19 | # Uncomment this to preserve the line number information for 20 | # debugging stack traces. 21 | #-keepattributes SourceFile,LineNumberTable 22 | 23 | # If you keep the line number information, uncomment this to 24 | # hide the original source file name. 25 | #-renamesourcefileattribute SourceFile 26 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 12 | 16 | 17 | 18 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-anydpi-v24/ic_stat_hearing.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | ZamZam 3 | 4 | Sensitivity: 5 | Go Back 6 | Listening for hotword 7 | Background Service Notifications 8 | Alerts you when the app is listening 9 | Hot Word Notifications 10 | Alerts you when the app hears a hot word 11 | Hotword Detected! 12 | Click here to return to app. 13 | ZamZam is listening in the background 14 | Click the button to test again. 15 | Yes 16 | No 17 | Do you want to stop the recognizer? 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/java/com/tuckercr/zamzam/HotWordDetectedFragment.kt: -------------------------------------------------------------------------------- 1 | package com.tuckercr.zamzam 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | 9 | /** 10 | * A simple Fragment for when the hotword is detected 11 | */ 12 | class HotWordDetectedFragment : Fragment() { 13 | override fun onCreateView( 14 | inflater: LayoutInflater, container: ViewGroup?, 15 | savedInstanceState: Bundle? 16 | ): View? { 17 | return inflater.inflate(R.layout.fragment_hotword_detected, container, false) 18 | } 19 | 20 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 21 | super.onViewCreated(view, savedInstanceState) 22 | view.findViewById(R.id.button).setOnClickListener { v: View? -> buttonPressed() } 23 | } 24 | 25 | private fun buttonPressed() { 26 | // Go back 27 | requireActivity().onBackPressed() 28 | } 29 | 30 | companion object { 31 | const val FTAG = "HotWordDetectedFragment" 32 | fun newInstance(): HotWordDetectedFragment { 33 | return HotWordDetectedFragment() 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZamZam 2 | 3 | An POC app demonstrating on-device wake word voice recognition using the PocketSphinx engine 4 | 5 | The app lets you select a wake word, and then - even with the app in the background or the phone in your pocket - it notifies you noisily and with vibration when that word is head. 6 | 7 | The sensitivity of the voice reco engine is tunable to balance between false positives and not hearing the word. 8 | 9 | The idea of this app is that it could be useful for someone that is hearing impaired to help them when someone says their names. The app does not use or require any kind of network connection for the voice recognition. 10 | 11 | ## Technologies Used: 12 | 13 | - [PocketSphinx](http://www.speech.cs.cmu.edu/pocketsphinx/) 14 | - [Databinding](https://developer.android.com/topic/libraries/data-binding) 15 | - [View Binding](https://developer.android.com/topic/libraries/view-binding) 16 | - [Smart Material Spinner](https://developer.android.com/guide/components/loaders) 17 | - [Foreground Services](https://developer.android.com/guide/components/foreground-services) 18 | - [Notifications](https://developer.android.com/guide/topics/ui/notifiers/notifications) 19 | 20 | ## Screenshots 21 | 22 | ### Main Page 23 | ![main](screenshots/main.png) 24 | 25 | ### Foreground Service 26 | ![service](screenshots/fg_service.png) 27 | 28 | ### Wake Word Triggered 29 | ![wake word](screenshots/triggered.png) 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.aar 4 | *.ap_ 5 | *.aab 6 | 7 | # Files for the ART/Dalvik VM 8 | *.dex 9 | 10 | # Java class files 11 | *.class 12 | 13 | # Generated files 14 | bin/ 15 | gen/ 16 | out/ 17 | # Uncomment the following line in case you need and you don't have the release build type files in your app 18 | # release/ 19 | 20 | # Gradle files 21 | .gradle/ 22 | build/ 23 | 24 | # Local configuration file (sdk path, etc) 25 | local.properties 26 | 27 | # Proguard folder generated by Eclipse 28 | proguard/ 29 | 30 | # Log Files 31 | *.log 32 | 33 | # Android Studio Navigation editor temp files 34 | .navigation/ 35 | 36 | # Android Studio captures folder 37 | captures/ 38 | 39 | # IntelliJ 40 | *.iml 41 | .idea/workspace.xml 42 | .idea/tasks.xml 43 | .idea/gradle.xml 44 | .idea/assetWizardSettings.xml 45 | .idea/dictionaries 46 | .idea/libraries 47 | .idea/jarRepositories.xml 48 | # Android Studio 3 in .gitignore file. 49 | .idea/caches 50 | .idea/modules.xml 51 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 52 | .idea/navEditor.xml 53 | 54 | .idea/compiler.xml 55 | .idea/misc.xml 56 | 57 | # Keystore files 58 | # Uncomment the following lines if you do not want to check your keystore files in. 59 | *.jks 60 | *.keystore 61 | 62 | # External native build folder generated in Android Studio 2.2 and later 63 | .externalNativeBuild 64 | .cxx/ 65 | 66 | # Google Services (e.g. APIs or Firebase) 67 | # google-services.json 68 | 69 | # Freeline 70 | freeline.py 71 | freeline/ 72 | freeline_project_description.json 73 | 74 | # fastlane 75 | fastlane/report.xml 76 | fastlane/Preview.html 77 | fastlane/screenshots 78 | fastlane/test_output 79 | fastlane/readme.md 80 | 81 | # Version control 82 | vcs.xml 83 | 84 | # lint 85 | lint/intermediates/ 86 | lint/generated/ 87 | lint/outputs/ 88 | lint/tmp/ 89 | # lint/reports/ 90 | 91 | # Android Profiling 92 | *.hprof 93 | .DS_Store -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 25 | 26 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /app/src/main/java/com/tuckercr/zamzam/prefs/PrefsManager.kt: -------------------------------------------------------------------------------- 1 | package com.tuckercr.zamzam.prefs 2 | 3 | import android.content.Context 4 | import android.content.SharedPreferences 5 | import android.content.SharedPreferences.OnSharedPreferenceChangeListener 6 | 7 | class PrefsManager private constructor(context: Context) { 8 | private val mPref: SharedPreferences 9 | fun getInt(key: String, defValue: Int): Int { 10 | return mPref.getInt(key, defValue) 11 | } 12 | 13 | fun putInt(key: String, value: Int) { 14 | mPref.edit().putInt(key, value).apply() 15 | } 16 | 17 | fun getString(key: String, defValue: String): String? { 18 | return mPref.getString(key, defValue) 19 | } 20 | 21 | fun putString(key: String, value: String) { 22 | mPref.edit().putString(key, value).apply() 23 | } 24 | 25 | fun getBoolean(key: String): Boolean { 26 | return mPref.getBoolean(key, false) 27 | } 28 | 29 | fun putBoolean(key: String, value: Boolean) { 30 | mPref.edit().putBoolean(key, value).apply() 31 | } 32 | 33 | fun remove(key: String) { 34 | mPref.edit().remove(key).apply() 35 | } 36 | 37 | fun clear(): Boolean { 38 | return mPref.edit().clear().commit() 39 | } 40 | 41 | fun registerListener(listener: OnSharedPreferenceChangeListener?) { 42 | mPref.registerOnSharedPreferenceChangeListener(listener) 43 | } 44 | 45 | fun unregisterListener(listener: OnSharedPreferenceChangeListener?) { 46 | mPref.unregisterOnSharedPreferenceChangeListener(listener) 47 | } 48 | 49 | companion object { 50 | const val KEY_WAKE_WORD = "wake_word" 51 | private const val PREF_NAME = "com.tuckercr.zamzam.prefs" 52 | private var sInstance: PrefsManager? = null 53 | @Synchronized 54 | fun initialize(context: Context) { 55 | if (sInstance == null) { 56 | sInstance = PrefsManager(context) 57 | } 58 | } 59 | 60 | @JvmStatic 61 | @get:Synchronized 62 | val instance: PrefsManager? 63 | get() { 64 | checkNotNull(sInstance) { "Not initialized" } 65 | return sInstance 66 | } 67 | } 68 | 69 | init { 70 | mPref = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) 71 | } 72 | } -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | apply plugin: 'kotlin-android' 3 | apply plugin: 'kotlin-kapt' 4 | 5 | android { 6 | compileSdkVersion 31 7 | defaultConfig { 8 | applicationId 'com.tuckercr.zamzam' 9 | minSdkVersion 21 10 | //noinspection ExpiredTargetSdkVersion 11 | targetSdkVersion 29 12 | versionCode 2 13 | versionName "1.0" 14 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 15 | } 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | compileOptions { 23 | sourceCompatibility = 1.8 24 | targetCompatibility = 1.8 25 | } 26 | buildFeatures { 27 | dataBinding true 28 | } 29 | } 30 | 31 | dependencies { 32 | def lifecycle_version = "2.4.0" 33 | def paging_version = "3.1.0" 34 | 35 | implementation 'androidx.core:core-ktx:1.7.0' 36 | implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" 37 | 38 | // Test stuff 39 | testImplementation 'junit:junit:4.13.2' 40 | androidTestImplementation('androidx.test.espresso:espresso-core:3.4.0', { 41 | exclude group: 'com.android.support', module: 'support-annotations' 42 | }) 43 | 44 | // Pocket Sphinx 45 | implementation fileTree(dir: 'libs', include: ['*.jar']) 46 | implementation(name: 'pocketsphinx-android-5prealpha-release', ext: 'aar') 47 | 48 | // AndroidX Libs 49 | implementation 'androidx.legacy:legacy-support-v4:1.0.0' 50 | implementation 'androidx.appcompat:appcompat:1.4.1' 51 | implementation 'androidx.constraintlayout:constraintlayout:2.1.3' 52 | 53 | 54 | // ViewModel 55 | implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" 56 | // LiveData 57 | implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" 58 | 59 | // Searchable Spinner library 60 | implementation 'com.github.chivorns:smartmaterialspinner:1.5.0' 61 | 62 | // Saved state module for ViewModel 63 | implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version" 64 | 65 | // Annotation processor 66 | implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" 67 | 68 | implementation "androidx.paging:paging-runtime-ktx:$paging_version" 69 | 70 | } 71 | -------------------------------------------------------------------------------- /app/src/main/java/com/tuckercr/zamzam/ListenerService.kt: -------------------------------------------------------------------------------- 1 | package com.tuckercr.zamzam 2 | 3 | import android.app.Activity 4 | import android.app.Service 5 | import android.content.Intent 6 | import android.os.IBinder 7 | import android.util.Log 8 | 9 | /** 10 | * Foreground Service for sticky notifications 11 | * 12 | * @author colintucker 13 | */ 14 | class ListenerService : Service() { 15 | override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { 16 | if (ACTION_START_FOREGROUND == intent.action) { 17 | Log.i(TAG, "onStartCommand: Received Start Foreground Intent") 18 | val wakeWord = intent.getStringExtra(EXTRA_WAKE_WORD)!! 19 | val notification = NotificationUtils.createServiceNotification(this, wakeWord) 20 | startForeground(NotificationUtils.NOTIFICATION_ID_SERVICE, notification) 21 | } else if (ACTION_STOP_FOREGROUND == intent.action) { 22 | Log.i(TAG, "onStartCommand: Received Stop Foreground Intent") 23 | stopForeground(true) 24 | stopSelf() 25 | } 26 | 27 | // This was START_STICKY but when service is recreated it causes the intent to be null 28 | return START_REDELIVER_INTENT 29 | } 30 | 31 | override fun onCreate() { 32 | super.onCreate() 33 | Log.d(TAG, "onCreate() called") 34 | } 35 | 36 | override fun onDestroy() { 37 | super.onDestroy() 38 | Log.d(TAG, "onDestroy() called") 39 | } 40 | 41 | override fun onBind(intent: Intent): IBinder? { 42 | // Used only in case of bound services. 43 | return null 44 | } 45 | 46 | companion object { 47 | private const val TAG = "ListeningService" 48 | private const val ACTION_START_FOREGROUND = "action_start_foreground" 49 | private const val ACTION_STOP_FOREGROUND = "action_stop_foreground" 50 | private const val EXTRA_WAKE_WORD = "wake_word" 51 | fun createStartForegroundIntent(activity: Activity, wakeWord: String): Intent { 52 | val intent = Intent(activity, ListenerService::class.java) 53 | intent.action = ACTION_START_FOREGROUND 54 | intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) 55 | intent.putExtra(EXTRA_WAKE_WORD, wakeWord) 56 | return intent 57 | } 58 | 59 | fun createStopForegroundIntent(activity: Activity): Intent { 60 | val stopForegroundIntent = Intent(activity, ListenerService::class.java) 61 | stopForegroundIntent.action = ACTION_STOP_FOREGROUND 62 | return stopForegroundIntent 63 | } 64 | } 65 | } -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/tuckercr/zamzam/BindingAdapters.kt: -------------------------------------------------------------------------------- 1 | package com.tuckercr.zamzam 2 | 3 | import android.view.View 4 | import android.widget.AdapterView 5 | import android.widget.AdapterView.OnItemSelectedListener 6 | import android.widget.ArrayAdapter 7 | import androidx.appcompat.widget.AppCompatImageView 8 | import androidx.appcompat.widget.AppCompatSpinner 9 | import androidx.databinding.BindingAdapter 10 | import androidx.databinding.InverseBindingAdapter 11 | import androidx.databinding.InverseBindingListener 12 | import com.tuckercr.zamzam.ListenerFragment.MicState 13 | import com.tuckercr.zamzam.prefs.PrefsManager 14 | import com.tuckercr.zamzam.prefs.PrefsManager.Companion.instance 15 | 16 | object BindingAdapters { 17 | @JvmStatic 18 | @BindingAdapter("micState") 19 | fun setMicStateImage(view: View?, @MicState micState: Int) { 20 | if (view !is AppCompatImageView) { 21 | return 22 | } 23 | when (micState) { 24 | MicState.DISABLED_NO_PERMISSION -> view.setImageResource(R.drawable.ic_mic_off_red_128dp) 25 | MicState.OFF -> view.setImageResource(R.drawable.ic_mic_light_gray_128dp) 26 | MicState.LISTENING -> view.setImageResource(R.drawable.ic_mic_gray_128dp) 27 | MicState.SPEAKING -> view.setImageResource(R.drawable.ic_mic_green_128dp) 28 | } 29 | } 30 | 31 | @JvmStatic 32 | @BindingAdapter(value = ["selectedValue", "selectedValueAttrChanged"], requireAll = false) 33 | fun bindSpinnerData( 34 | pAppCompatSpinner: AppCompatSpinner, 35 | newSelectedValue: String?, 36 | newTextAttrChanged: InverseBindingListener 37 | ) { 38 | pAppCompatSpinner.onItemSelectedListener = object : OnItemSelectedListener { 39 | override fun onItemSelected( 40 | parent: AdapterView<*>?, 41 | view: View, 42 | position: Int, 43 | id: Long 44 | ) { 45 | newTextAttrChanged.onChange() 46 | } 47 | 48 | override fun onNothingSelected(parent: AdapterView<*>?) {} 49 | } 50 | if (newSelectedValue != null) { 51 | val pos = 52 | (pAppCompatSpinner.adapter as ArrayAdapter).getPosition(newSelectedValue) 53 | pAppCompatSpinner.setSelection(pos, true) 54 | } 55 | } 56 | 57 | @JvmStatic 58 | @InverseBindingAdapter(attribute = "selectedValue", event = "selectedValueAttrChanged") 59 | fun captureSelectedValue(pAppCompatSpinner: AppCompatSpinner): String? { 60 | 61 | // Log.d(TAG, "captureSelectedValue: selectedItem = [" + pAppCompatSpinner.getSelectedItem() + "]"); 62 | if (pAppCompatSpinner.selectedItem == null) { 63 | return null 64 | } 65 | 66 | // There's a shared preference listener in the ViewModel that'll notice this changed 67 | instance!!.putString(PrefsManager.KEY_WAKE_WORD, pAppCompatSpinner.selectedItem.toString()) 68 | return pAppCompatSpinner.selectedItem as String 69 | } 70 | } -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_hotword_detected.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 26 | 27 | 40 | 41 |