├── .gitignore ├── COPYING ├── README.md ├── app ├── build.gradle.kts ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── org │ │ └── vosk │ │ └── service │ │ ├── VoskRecognitionService.java │ │ ├── download │ │ ├── Download.java │ │ ├── DownloadModelService.java │ │ ├── DownloadProgressInterceptor.java │ │ ├── DownloadProgressListener.java │ │ ├── DownloadProgressResponseBody.java │ │ ├── Error.java │ │ ├── EventBus.java │ │ ├── FileHelper.java │ │ ├── VoskModelStorage.java │ │ └── VoskModelStorageClient.java │ │ ├── ui │ │ ├── SpeechRecognizerActivity.java │ │ └── selector │ │ │ ├── DiffCallback.java │ │ │ ├── ModelItem.java │ │ │ ├── ModelListActivity.java │ │ │ └── ModelListAdapter.java │ │ └── utils │ │ ├── PreferenceConstants.java │ │ └── Tools.java │ └── res │ ├── drawable-hdpi │ ├── ic_service_trigger.xml │ ├── icon.png │ └── rounded.xml │ ├── drawable-ldpi │ └── icon.png │ ├── drawable-mdpi │ └── icon.png │ ├── drawable │ ├── circle.xml │ ├── gradient_progress_color.xml │ ├── ic_baseline_check_circle_24.xml │ ├── ic_baseline_cloud_download_24.xml │ └── ic_baseline_mic_24.xml │ ├── layout │ ├── activity_model_list.xml │ ├── main.xml │ ├── model_list_item.xml │ └── speech_recognizer_activity.xml │ ├── raw │ └── start_speech_effect.mp3 │ ├── values │ ├── colors.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── recognition_service.xml ├── build.gradle.kts ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /release 7 | /captures 8 | /app/ontest* 9 | /app/production* 10 | /app/develop* 11 | /app/google-services.json 12 | /app/build 13 | .externalNativeBuild 14 | hs_err_* 15 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. this License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vosk android service 2 | 3 | This is a service module for android, 4 | allowing other applications to call vosk to perform speech to text. -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | } 4 | 5 | repositories { 6 | google() 7 | maven("https://alphacephei.com/maven/") 8 | } 9 | 10 | android { 11 | compileSdk = 33 12 | defaultConfig { 13 | applicationId = "org.vosk.service" 14 | minSdk = 24 15 | targetSdk = 33 16 | versionCode = 1 17 | versionName = "1.1" 18 | ndk { 19 | abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64", "x86") 20 | } 21 | splits { 22 | abi { 23 | isEnable = true 24 | 25 | isUniversalApk = true 26 | } 27 | } 28 | } 29 | buildTypes { 30 | release { 31 | isMinifyEnabled = true 32 | proguardFiles( 33 | getDefaultProguardFile("proguard-android-optimize.txt"), 34 | "proguard-rules.pro" 35 | ) 36 | } 37 | } 38 | compileOptions { 39 | sourceCompatibility = JavaVersion.VERSION_1_8 40 | targetCompatibility = JavaVersion.VERSION_1_8 41 | } 42 | } 43 | 44 | dependencies { 45 | implementation("com.alphacephei:vosk-android:0.3.46@aar") 46 | implementation("net.java.dev.jna:jna:5.13.0@aar") 47 | implementation("androidx.appcompat:appcompat:1.5.1") 48 | implementation("com.google.code.gson:gson:2.9.0") 49 | implementation("com.google.android.material:material:1.6.1") 50 | implementation("io.reactivex.rxjava2:rxandroid:2.1.1") 51 | implementation("io.reactivex.rxjava2:rxjava:2.2.9") 52 | implementation("com.squareup.retrofit2:retrofit:2.9.0") 53 | implementation("com.squareup.retrofit2:converter-gson:2.9.0") 54 | implementation("com.squareup.retrofit2:adapter-rxjava2:2.3.0") 55 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") 56 | implementation("com.github.pwittchen:reactivenetwork-rx2:0.12.3") 57 | implementation("commons-io:commons-io:2.11.0") 58 | } 59 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | -keep class com.sun.jna.* { *; } 2 | -keepclassmembers class * extends com.sun.jna.* { public *; } 3 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 21 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 97 | 98 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/VoskRecognitionService.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Ciaran O'Reilly 2 | // Copyright 2019 Alpha Cephei Inc. 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | 17 | package org.vosk.service; 18 | 19 | import android.content.Intent; 20 | import android.content.SharedPreferences; 21 | import android.os.Bundle; 22 | import android.os.RemoteException; 23 | import android.preference.PreferenceManager; 24 | import android.speech.RecognitionService; 25 | import android.util.Log; 26 | 27 | import com.google.gson.Gson; 28 | import com.google.gson.reflect.TypeToken; 29 | 30 | import org.vosk.Model; 31 | import org.vosk.Recognizer; 32 | import org.vosk.android.RecognitionListener; 33 | import org.vosk.android.SpeechService; 34 | import org.vosk.service.utils.PreferenceConstants; 35 | import org.vosk.service.utils.Tools; 36 | 37 | import java.io.File; 38 | import java.io.IOException; 39 | import java.lang.reflect.Type; 40 | import java.util.ArrayList; 41 | import java.util.Map; 42 | import java.util.concurrent.TimeUnit; 43 | 44 | import io.reactivex.Single; 45 | import io.reactivex.android.schedulers.AndroidSchedulers; 46 | import io.reactivex.disposables.CompositeDisposable; 47 | import io.reactivex.schedulers.Schedulers; 48 | 49 | public class VoskRecognitionService extends RecognitionService implements RecognitionListener { 50 | private final static String TAG = VoskRecognitionService.class.getSimpleName(); 51 | private Recognizer recognizer; 52 | private SpeechService speechService; 53 | private Model model; 54 | 55 | private RecognitionService.Callback mCallback; 56 | 57 | private final CompositeDisposable compositeDisposable = new CompositeDisposable(); 58 | 59 | @Override 60 | protected void onStartListening(Intent intent, Callback callback) { 61 | Log.v(TAG, "onStartListening"); 62 | mCallback = callback; 63 | runRecognizerSetup(); 64 | } 65 | 66 | @Override 67 | protected void onCancel(Callback callback) { 68 | Log.v(TAG, "onCancel"); 69 | results(new Bundle(), true); 70 | } 71 | 72 | @Override 73 | protected void onStopListening(Callback callback) { 74 | Log.v(TAG, "onStopListening"); 75 | results(new Bundle(), true); 76 | } 77 | 78 | private void runRecognizerSetup() { 79 | Log.v(TAG, "runRecognizerSetup"); 80 | if (this.model == null) { 81 | SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 82 | 83 | if (sharedPreferences.contains(PreferenceConstants.ACTIVE_MODEL)) { 84 | final File MODEL_FILE_ROOT_PATH = Tools.getModelFileRootPath(this); 85 | final String ACTIVE_MODEL = sharedPreferences.getString(PreferenceConstants.ACTIVE_MODEL, ""); 86 | 87 | File outputFile = new File( 88 | MODEL_FILE_ROOT_PATH, 89 | ACTIVE_MODEL + "/" + ACTIVE_MODEL 90 | ); 91 | 92 | final String outputPath = outputFile.getAbsolutePath(); 93 | Log.d(TAG, outputPath); 94 | 95 | compositeDisposable.add(Single.fromCallable(() -> new Model(outputPath)) 96 | .doOnSuccess(model_ -> this.model = model_) 97 | .delay(1, TimeUnit.MICROSECONDS) 98 | .subscribeOn(Schedulers.io()) 99 | .observeOn(AndroidSchedulers.mainThread()) 100 | .subscribe(model_ -> startSpeech(), Throwable::printStackTrace)); 101 | } 102 | } else { 103 | startSpeech(); 104 | } 105 | } 106 | 107 | private void startSpeech() { 108 | Log.v(TAG, "startSpeech"); 109 | setupRecognizer(); 110 | this.readyForSpeech(new Bundle()); 111 | beginningOfSpeech(); 112 | } 113 | 114 | @Override 115 | public void onDestroy() { 116 | Log.v(TAG, "onDestroy"); 117 | super.onDestroy(); 118 | 119 | if (speechService != null) { 120 | speechService.cancel(); 121 | speechService.shutdown(); 122 | } 123 | } 124 | 125 | private void setupRecognizer() { 126 | Log.v(TAG, "setupRecognizer"); 127 | try { 128 | if (recognizer == null) { 129 | Log.i(TAG, "Creating recognizer"); 130 | 131 | recognizer = new Recognizer(model, 16000.0f); 132 | } 133 | 134 | if (speechService == null) { 135 | Log.i(TAG, "Creating speechService"); 136 | 137 | speechService = new SpeechService(recognizer, 16000.0f); 138 | } else { 139 | speechService.cancel(); 140 | } 141 | speechService.startListening(this); 142 | } catch (IOException e) { 143 | Log.e(TAG, e.getMessage()); 144 | } 145 | } 146 | 147 | private void readyForSpeech(Bundle bundle) { 148 | Log.v(TAG, "readyForSpeech"); 149 | try { 150 | mCallback.readyForSpeech(bundle); 151 | } catch (RemoteException e) { 152 | // empty 153 | } 154 | } 155 | 156 | private void results(Bundle bundle, boolean isFinal) { 157 | Log.v(TAG, "results"); 158 | try { 159 | if (isFinal) { 160 | speechService.cancel(); 161 | mCallback.results(bundle); 162 | } else { 163 | mCallback.partialResults(bundle); 164 | } 165 | } catch (RemoteException e) { 166 | // empty 167 | } 168 | } 169 | 170 | private Bundle createResultsBundle(String hypothesis) { 171 | Log.v(TAG, "createResultsBundle"); 172 | ArrayList hypotheses = new ArrayList<>(); 173 | hypotheses.add(hypothesis); 174 | Bundle bundle = new Bundle(); 175 | bundle.putStringArrayList(android.speech.SpeechRecognizer.RESULTS_RECOGNITION, hypotheses); 176 | return bundle; 177 | } 178 | 179 | private void beginningOfSpeech() { 180 | Log.v(TAG, "beginningOfSpeech"); 181 | try { 182 | mCallback.beginningOfSpeech(); 183 | } catch (RemoteException e) { 184 | // empty 185 | } 186 | } 187 | 188 | private void error(int errorCode) { 189 | Log.v(TAG, "error"); 190 | if (speechService != null) { 191 | speechService.cancel(); 192 | } 193 | try { 194 | mCallback.error(errorCode); 195 | } catch (RemoteException e) { 196 | // empty 197 | } 198 | } 199 | 200 | Type mapType = new TypeToken>() { 201 | }.getType(); 202 | 203 | @Override 204 | public void onResult(String hypothesis) { 205 | Log.v(TAG, "onResult"); 206 | if (hypothesis != null) { 207 | Log.i(TAG, hypothesis); 208 | Gson gson = new Gson(); 209 | Map map = gson.fromJson(hypothesis, mapType); 210 | String text = map.get("text"); 211 | results(createResultsBundle(text), true); 212 | } 213 | } 214 | 215 | @Override 216 | public void onFinalResult(String hypothesis) { 217 | Log.v(TAG, "onFinalResult"); 218 | if (hypothesis != null) { 219 | Log.i(TAG, hypothesis); 220 | Gson gson = new Gson(); 221 | Map map = gson.fromJson(hypothesis, mapType); 222 | String text = map.get("text"); 223 | results(createResultsBundle(text), true); 224 | } 225 | } 226 | 227 | @Override 228 | public void onPartialResult(String hypothesis) { 229 | Log.v(TAG, "onPartialResult"); 230 | if (hypothesis != null) { 231 | Log.i(TAG, hypothesis); 232 | Gson gson = new Gson(); 233 | Map map = gson.fromJson(hypothesis, mapType); 234 | String text = map.get("partial"); 235 | results(createResultsBundle(text), false); 236 | } 237 | } 238 | 239 | @Override 240 | public void onError(Exception e) { 241 | Log.v(TAG, "onError"); 242 | Log.e(TAG, e.getMessage()); 243 | error(android.speech.SpeechRecognizer.ERROR_CLIENT); 244 | } 245 | 246 | @Override 247 | public void onTimeout() { 248 | Log.v(TAG, "onTimeout"); 249 | speechService.cancel(); 250 | speechService.startListening(this); 251 | } 252 | } 253 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/download/Download.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.download; 2 | 3 | public class Download { 4 | 5 | public final static int CLEAR = 200; 6 | public final static int STARTING = 0; 7 | public final static int UNZIPPING = 202; 8 | public final static int COMPLETE = 203; 9 | public final static int RESTARTING = 204; 10 | 11 | private int progress; 12 | private long currentFileSize; 13 | private long totalFileSize; 14 | String modelName; 15 | 16 | public Download() { 17 | 18 | } 19 | 20 | public Download(int progress, String modelName) { 21 | this.progress = progress; 22 | this.modelName = modelName; 23 | } 24 | 25 | public Download(int progress, long currentFileSize, long totalFileSize) { 26 | this.progress = progress; 27 | this.currentFileSize = currentFileSize; 28 | this.totalFileSize = totalFileSize; 29 | } 30 | 31 | public Download(int progress) { 32 | this.progress = progress; 33 | } 34 | 35 | public int getProgress() { 36 | return progress; 37 | } 38 | 39 | public void setProgress(int progress) { 40 | this.progress = progress; 41 | } 42 | 43 | public long getCurrentFileSize() { 44 | return currentFileSize; 45 | } 46 | 47 | public void setCurrentFileSize(long currentFileSize) { 48 | this.currentFileSize = currentFileSize; 49 | } 50 | 51 | public long getTotalFileSize() { 52 | return totalFileSize; 53 | } 54 | 55 | public void setTotalFileSize(long totalFileSize) { 56 | this.totalFileSize = totalFileSize; 57 | } 58 | 59 | public String getModelName() { 60 | return modelName; 61 | } 62 | 63 | public void setModelName(String modelName) { 64 | this.modelName = modelName; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/download/DownloadModelService.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.download; 2 | 3 | import static org.vosk.service.download.Download.CLEAR; 4 | import static org.vosk.service.download.Download.COMPLETE; 5 | import static org.vosk.service.download.Download.UNZIPPING; 6 | import static org.vosk.service.download.FileHelper.writeFile; 7 | import static org.vosk.service.download.VoskModelStorageClient.ServiceType.DOWNLOAD_MODEL; 8 | 9 | import android.app.NotificationChannel; 10 | import android.app.NotificationManager; 11 | import android.app.PendingIntent; 12 | import android.app.Service; 13 | import android.content.Context; 14 | import android.content.Intent; 15 | import android.content.SharedPreferences; 16 | import android.media.AudioAttributes; 17 | import android.net.Uri; 18 | import android.os.Build; 19 | import android.os.Environment; 20 | import android.os.IBinder; 21 | import android.preference.PreferenceManager; 22 | import android.util.Log; 23 | 24 | import androidx.annotation.Nullable; 25 | import androidx.annotation.RequiresApi; 26 | import androidx.core.app.NotificationCompat; 27 | 28 | import org.vosk.service.R; 29 | import org.vosk.service.ui.selector.ModelListActivity; 30 | import org.vosk.service.utils.PreferenceConstants; 31 | import org.vosk.service.utils.Tools; 32 | 33 | import java.io.File; 34 | 35 | import io.reactivex.android.schedulers.AndroidSchedulers; 36 | import io.reactivex.disposables.CompositeDisposable; 37 | import io.reactivex.schedulers.Schedulers; 38 | import okhttp3.ResponseBody; 39 | 40 | public class DownloadModelService extends Service { 41 | 42 | public static final String DOWNLOAD_MODEL_CHANNEL_ID_VALUE = "download_model_channel_id"; 43 | public static final String DOWNLOAD_MODEL_CHANNEL_NAME = "Vosk model downloader"; 44 | public static final int DOWNLOAD_MODEL_NOTIFICATION_ID = 1; 45 | public static final int DOWNLOAD_MODEL_MAX_PROGRESS = 100; 46 | 47 | private static File MODEL_FILE_ROOT_PATH ; 48 | private final CompositeDisposable compositeDisposable = new CompositeDisposable(); 49 | private final VoskModelStorage service = VoskModelStorageClient.getClient(getListener(), DOWNLOAD_MODEL); 50 | private SharedPreferences sharedPreferences; 51 | private final EventBus eventBus = EventBus.getInstance(); 52 | private NotificationManager notificationManager; 53 | private NotificationCompat.Builder notificationBuilder; 54 | 55 | private int actualProgress = 0; 56 | private String modelName; 57 | 58 | @Nullable 59 | @Override 60 | public IBinder onBind(Intent intent) { 61 | return null; 62 | } 63 | 64 | @Override 65 | public void onCreate() { 66 | super.onCreate(); 67 | sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 68 | MODEL_FILE_ROOT_PATH = Tools.getModelFileRootPath(this); 69 | modelName = sharedPreferences.getString(PreferenceConstants.DOWNLOADING_FILE, ""); 70 | downloadModel(modelName); 71 | observeEvents(); 72 | } 73 | 74 | private void observeEvents() { 75 | compositeDisposable.add(eventBus.getDownloadStatusObservable() 76 | .subscribeOn(Schedulers.io()) 77 | .observeOn(AndroidSchedulers.mainThread()) 78 | .subscribe(download -> { 79 | if (download.getProgress() == UNZIPPING) { 80 | 81 | File outputFile = new File(MODEL_FILE_ROOT_PATH, modelName + ".zip"); 82 | File destinationFile = new File(MODEL_FILE_ROOT_PATH, modelName); 83 | 84 | FileHelper.unzipFIle(this, outputFile, destinationFile); 85 | actualProgress = CLEAR; 86 | } else if (download.getProgress() == COMPLETE) { 87 | sharedPreferences.edit() 88 | .remove(PreferenceConstants.DOWNLOADING_FILE) 89 | .apply(); 90 | if (!sharedPreferences.contains(PreferenceConstants.ACTIVE_MODEL)) 91 | sharedPreferences.edit().putString(PreferenceConstants.ACTIVE_MODEL, modelName).apply(); 92 | stopSelf(); 93 | } else { 94 | if (actualProgress != download.getProgress()) { 95 | actualProgress = download.getProgress(); 96 | updateNotificationProgress(); 97 | } 98 | } 99 | })); 100 | compositeDisposable.add(EventBus.getInstance().geErrorObservable().subscribeOn(Schedulers.io()).subscribe(error -> stopSelf())); 101 | } 102 | 103 | private void updateNotificationProgress() { 104 | notificationBuilder.setProgress(DOWNLOAD_MODEL_MAX_PROGRESS, actualProgress, false); 105 | notificationBuilder.setSilent(true); 106 | notificationManager.notify(DOWNLOAD_MODEL_NOTIFICATION_ID, notificationBuilder.build()); 107 | } 108 | 109 | private DownloadProgressListener getListener() { 110 | return (bytesRead, contentLength, done) -> { 111 | Download download = new Download(); 112 | download.setTotalFileSize(contentLength); 113 | download.setCurrentFileSize(bytesRead); 114 | int progress = (int) ((bytesRead * 100) / contentLength); 115 | download.setProgress(progress); 116 | Log.d("DOWNLOAD", "Progress: " + progress); 117 | EventBus.getInstance().postDownloadStatus(download); 118 | }; 119 | } 120 | 121 | private void downloadModel(String modelName) { 122 | File outputFile = new File(MODEL_FILE_ROOT_PATH, modelName + ".zip"); 123 | FileHelper.createDir(MODEL_FILE_ROOT_PATH); 124 | 125 | compositeDisposable.add(service.downloadFile(outputFile.getName()) 126 | .subscribeOn(Schedulers.io()) 127 | .map(ResponseBody::byteStream) 128 | .doOnNext(inputStream -> writeFile(inputStream, outputFile)) 129 | .subscribe(inputStream -> EventBus.getInstance().postDownloadStatus(new Download(UNZIPPING, modelName)), 130 | error -> EventBus.getInstance().postErrorStatus(Error.CONNECTION))); 131 | 132 | } 133 | 134 | @Override 135 | public int onStartCommand(Intent intent, int flags, int startId) { 136 | registerNotification(); 137 | return START_NOT_STICKY; 138 | } 139 | 140 | private void registerNotification() { 141 | notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); 142 | Intent notificationIntent = new Intent(this, ModelListActivity.class); 143 | PendingIntent pendingIntent; 144 | int flags = 0; 145 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { 146 | flags = PendingIntent.FLAG_MUTABLE; 147 | } 148 | pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, flags); 149 | notificationBuilder = getNotification(notificationManager, pendingIntent); 150 | 151 | startForeground(DOWNLOAD_MODEL_NOTIFICATION_ID, notificationBuilder.build()); 152 | } 153 | 154 | private NotificationCompat.Builder getNotification(NotificationManager notificationManager, PendingIntent pendingIntent) { 155 | 156 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { 157 | createNotificationChannel(notificationManager, DOWNLOAD_MODEL_CHANNEL_ID_VALUE, DOWNLOAD_MODEL_CHANNEL_ID_VALUE, null); 158 | } 159 | return new NotificationCompat.Builder(this, DOWNLOAD_MODEL_CHANNEL_ID_VALUE) 160 | .setContentTitle(getString(R.string.download_model_service_notification_title)) 161 | .setSmallIcon(R.drawable.icon) 162 | .setAutoCancel(false) 163 | .setProgress(DOWNLOAD_MODEL_MAX_PROGRESS, 0, false) 164 | .setContentIntent(pendingIntent); 165 | } 166 | 167 | @RequiresApi(api = Build.VERSION_CODES.O) 168 | public static void createNotificationChannel(NotificationManager notificationManager, String channelId, String channelName, Uri notificationSoundUri) { 169 | NotificationChannel notificationChannel = new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH); 170 | notificationChannel.setVibrationPattern(new long[]{1000, 1000, 1000, 1000, 1000}); 171 | 172 | if (notificationSoundUri != null) { 173 | AudioAttributes audioAttributes = new AudioAttributes.Builder() 174 | .setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) 175 | .setUsage(AudioAttributes.USAGE_NOTIFICATION) 176 | .build(); 177 | notificationChannel.setSound(notificationSoundUri, audioAttributes); 178 | } 179 | 180 | notificationManager.createNotificationChannel(notificationChannel); 181 | } 182 | 183 | @Override 184 | public void onDestroy() { 185 | super.onDestroy(); 186 | compositeDisposable.clear(); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/download/DownloadProgressInterceptor.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.download; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.io.IOException; 6 | 7 | import okhttp3.Interceptor; 8 | import okhttp3.Response; 9 | 10 | public class DownloadProgressInterceptor implements Interceptor { 11 | 12 | private DownloadProgressListener listener; 13 | 14 | public DownloadProgressInterceptor(DownloadProgressListener listener) { 15 | this.listener = listener; 16 | } 17 | 18 | @NonNull 19 | @Override 20 | public Response intercept(Chain chain) throws IOException { 21 | Response originalResponse = chain.proceed(chain.request()); 22 | 23 | return originalResponse.newBuilder() 24 | .body(new DownloadProgressResponseBody(originalResponse.body(), listener)) 25 | .build(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/download/DownloadProgressListener.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.download; 2 | 3 | public interface DownloadProgressListener { 4 | void update(long bytesRead, long contentLength, boolean done); 5 | } 6 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/download/DownloadProgressResponseBody.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.download; 2 | 3 | import androidx.annotation.NonNull; 4 | 5 | import java.io.IOException; 6 | 7 | import okhttp3.MediaType; 8 | import okhttp3.ResponseBody; 9 | import okio.Buffer; 10 | import okio.BufferedSource; 11 | import okio.ForwardingSource; 12 | import okio.Okio; 13 | import okio.Source; 14 | 15 | public class DownloadProgressResponseBody extends ResponseBody { 16 | 17 | private ResponseBody responseBody; 18 | DownloadProgressListener progressListener; 19 | private BufferedSource bufferedSource; 20 | 21 | public DownloadProgressResponseBody(ResponseBody responseBody, 22 | DownloadProgressListener progressListener) { 23 | this.responseBody = responseBody; 24 | this.progressListener = progressListener; 25 | } 26 | 27 | @Override 28 | public MediaType contentType() { 29 | return responseBody.contentType(); 30 | } 31 | 32 | @Override 33 | public long contentLength() { 34 | return responseBody.contentLength(); 35 | } 36 | 37 | @NonNull 38 | @Override 39 | public BufferedSource source() { 40 | if (bufferedSource == null) { 41 | bufferedSource = Okio.buffer(source(responseBody.source())); 42 | } 43 | return bufferedSource; 44 | } 45 | 46 | private Source source(Source source) { 47 | return new ForwardingSource(source) { 48 | long totalBytesRead = 0L; 49 | 50 | @Override 51 | public long read(Buffer sink, long byteCount) throws IOException { 52 | long bytesRead = super.read(sink, byteCount); 53 | // read() returns the number of bytes read, or -1 if this source is exhausted. 54 | totalBytesRead += bytesRead != -1 ? bytesRead : 0; 55 | 56 | if (null != progressListener) { 57 | progressListener.update(totalBytesRead, responseBody.contentLength(), bytesRead == -1); 58 | } 59 | return bytesRead; 60 | } 61 | }; 62 | 63 | } 64 | } -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/download/Error.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.download; 2 | 3 | public enum Error { 4 | CONNECTION, 5 | WRITE_STORAGE 6 | } 7 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/download/EventBus.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.download; 2 | 3 | import android.net.NetworkInfo; 4 | 5 | import org.vosk.service.ui.selector.ModelItem; 6 | 7 | import io.reactivex.Observable; 8 | import io.reactivex.subjects.PublishSubject; 9 | 10 | public class EventBus { 11 | private static EventBus _instance; 12 | 13 | private final PublishSubject downloadModelProgressEventSubject = PublishSubject.create(); 14 | private final PublishSubject startDownloadEventSubject = PublishSubject.create(); 15 | private final PublishSubject modelSelectedEventSubject = PublishSubject.create(); 16 | private final PublishSubject errorEventSubject = PublishSubject.create(); 17 | private final PublishSubject connectionEventSubject = PublishSubject.create(); 18 | private final PublishSubject deleteDownloadedModel = PublishSubject.create(); 19 | 20 | public EventBus() { 21 | } 22 | 23 | public static EventBus getInstance() { 24 | if (_instance == null) { 25 | _instance = new EventBus(); 26 | } 27 | return _instance; 28 | } 29 | 30 | public Observable getDownloadStatusObservable() { 31 | return downloadModelProgressEventSubject; 32 | } 33 | 34 | public void postDownloadStatus(Download stateId) { 35 | downloadModelProgressEventSubject.onNext(stateId); 36 | } 37 | 38 | 39 | public Observable getDownloadStartObservable() { 40 | return startDownloadEventSubject; 41 | } 42 | 43 | public void postDownloadStart(ModelItem modelItem) { 44 | startDownloadEventSubject.onNext(modelItem); 45 | } 46 | 47 | public Observable getModelSelectedObservable() { 48 | return modelSelectedEventSubject; 49 | } 50 | 51 | public void postModelSelectedObservable(ModelItem modelItem) { 52 | modelSelectedEventSubject.onNext(modelItem); 53 | } 54 | 55 | public Observable geErrorObservable() { 56 | return errorEventSubject; 57 | } 58 | 59 | public void postErrorStatus(Error error) { 60 | errorEventSubject.onNext(error); 61 | } 62 | 63 | public void postConnectionEvent(NetworkInfo.State connection) { 64 | connectionEventSubject.onNext(connection); 65 | } 66 | 67 | public Observable getConnectionEvent() { 68 | return connectionEventSubject; 69 | } 70 | 71 | public Observable getDeleteDownloadedModelObservable() { 72 | return deleteDownloadedModel; 73 | } 74 | 75 | public void postDeleteDownloadedModel(ModelItem modelItem) { 76 | deleteDownloadedModel.onNext(modelItem); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/download/FileHelper.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.download; 2 | 3 | import static org.vosk.service.download.Download.CLEAR; 4 | import static org.vosk.service.download.Download.COMPLETE; 5 | 6 | import android.content.Context; 7 | 8 | import org.apache.commons.io.IOUtils; 9 | import org.vosk.service.ui.selector.ModelListActivity; 10 | import org.vosk.service.utils.Tools; 11 | 12 | import java.io.BufferedInputStream; 13 | import java.io.BufferedOutputStream; 14 | import java.io.File; 15 | import java.io.FileOutputStream; 16 | import java.io.IOException; 17 | import java.io.InputStream; 18 | import java.io.OutputStream; 19 | import java.util.Enumeration; 20 | import java.util.zip.ZipEntry; 21 | import java.util.zip.ZipFile; 22 | 23 | public class FileHelper { 24 | 25 | public static void unzipFIle(Context context,File zipFilePath, File unzipAtLocation) { 26 | 27 | //noinspection ResultOfMethodCallIgnored 28 | unzipAtLocation.mkdir(); 29 | 30 | try (ZipFile zipfile = new ZipFile(zipFilePath)) { 31 | for (Enumeration e = zipfile.entries(); e.hasMoreElements(); ) { 32 | ZipEntry entry = e.nextElement(); 33 | unzipEntry(zipfile, entry, unzipAtLocation); 34 | } 35 | EventBus.getInstance().postDownloadStatus(new Download(COMPLETE)); 36 | FileHelper.deleteFileOrDirectory(new File(Tools.getModelFileRootPath(context), unzipAtLocation.getName() + ".zip")); 37 | } catch (IOException e) { 38 | ModelListActivity.progress = CLEAR; 39 | EventBus.getInstance().postErrorStatus(Error.CONNECTION); 40 | e.printStackTrace(); 41 | } 42 | } 43 | 44 | private static void unzipEntry(ZipFile zipfile, ZipEntry entry, File outputDir) throws IOException { 45 | if (entry.isDirectory()) { 46 | createDir(new File(outputDir, entry.getName())); 47 | return; 48 | } 49 | 50 | File outputFile = new File(outputDir, entry.getName()); 51 | if (outputFile.getParentFile() != null && !outputFile.getParentFile().exists()) { 52 | createDir(outputFile.getParentFile()); 53 | } 54 | 55 | String message = "unzipEntry(" + entry + ")[" + entry.getSize() + "] "; 56 | 57 | InputStream zin = zipfile.getInputStream(entry); 58 | 59 | try (BufferedInputStream input = new BufferedInputStream(zin); 60 | BufferedOutputStream output = new BufferedOutputStream(new FileOutputStream(outputFile))) { 61 | copy(input, output); 62 | } catch (IOException e) { 63 | throw new IOException(message, e); 64 | } 65 | } 66 | 67 | public static void createDir(File dir) { 68 | if (dir.exists()) { 69 | return; 70 | } 71 | //noinspection ResultOfMethodCallIgnored 72 | dir.mkdir(); 73 | } 74 | 75 | public static void copy(InputStream input, OutputStream output) throws IOException { 76 | byte[] data = new byte[10240]; 77 | int count; 78 | 79 | while ((count = input.read(data)) != -1) { 80 | output.write(data, 0, count); 81 | } 82 | output.flush(); 83 | } 84 | 85 | public static void writeFile(InputStream inputStream, File file) { 86 | try (OutputStream outputStream = new FileOutputStream(file)) { 87 | IOUtils.copy(inputStream, outputStream); 88 | } catch (IOException e) { 89 | EventBus.getInstance().postErrorStatus(Error.WRITE_STORAGE); 90 | } 91 | } 92 | 93 | public static void deleteFileOrDirectory(File fileOrDirectory) { 94 | 95 | if (fileOrDirectory.isDirectory()) { 96 | for (File child : fileOrDirectory.listFiles()) { 97 | deleteFileOrDirectory(child); 98 | } 99 | } 100 | 101 | fileOrDirectory.delete(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/download/VoskModelStorage.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.download; 2 | 3 | import org.vosk.service.ui.selector.ModelItem; 4 | 5 | import java.util.List; 6 | 7 | import io.reactivex.Observable; 8 | import okhttp3.ResponseBody; 9 | import retrofit2.http.GET; 10 | import retrofit2.http.Streaming; 11 | import retrofit2.http.Url; 12 | 13 | public interface VoskModelStorage { 14 | @Streaming 15 | @GET 16 | Observable downloadFile(@Url String url); 17 | 18 | @GET("model-list.json") 19 | Observable> getModelList(); 20 | } -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/download/VoskModelStorageClient.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.download; 2 | 3 | import java.util.concurrent.TimeUnit; 4 | 5 | import okhttp3.OkHttpClient; 6 | import retrofit2.Retrofit; 7 | import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory; 8 | import retrofit2.converter.gson.GsonConverterFactory; 9 | 10 | public class VoskModelStorageClient { 11 | 12 | private static final String TAG = "DownloadAPI"; 13 | private static final int DEFAULT_TIMEOUT = 15; 14 | public static Retrofit retrofit; 15 | private static final String BASE_URL = "https://alphacephei.com/vosk/models/"; 16 | 17 | public static VoskModelStorage getClient(DownloadProgressListener listener, ServiceType serviceType) { 18 | 19 | DownloadProgressInterceptor interceptor = new DownloadProgressInterceptor(listener); 20 | 21 | OkHttpClient client = serviceType == ServiceType.DOWNLOAD_MODEL ? new OkHttpClient.Builder() 22 | .retryOnConnectionFailure(true) 23 | .addNetworkInterceptor(interceptor) 24 | .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS) 25 | .build() 26 | : new OkHttpClient.Builder() 27 | .retryOnConnectionFailure(true) 28 | .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS) 29 | .build(); 30 | 31 | Retrofit retrofit = new Retrofit.Builder() 32 | .baseUrl(BASE_URL) 33 | .addConverterFactory(GsonConverterFactory.create()) 34 | .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) 35 | .client(client) 36 | .build(); 37 | return retrofit.create(VoskModelStorage.class); 38 | } 39 | 40 | public enum ServiceType { 41 | DOWNLOAD_MODEL, 42 | DOWNLOAD_MODEL_LIST 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/ui/SpeechRecognizerActivity.java: -------------------------------------------------------------------------------- 1 | // Copyright 2020 Ciaran O'Reilly 2 | // Copyright 2011-2020, Institute of Cybernetics at Tallinn University of Technology 3 | // 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU General Public License for more details. 13 | 14 | // You should have received a copy of the GNU General Public License 15 | // along with this program. If not, see . 16 | package org.vosk.service.ui; 17 | 18 | import android.Manifest; 19 | import android.app.Activity; 20 | import android.app.PendingIntent; 21 | import android.content.Context; 22 | import android.content.Intent; 23 | import android.content.pm.PackageManager; 24 | import android.media.MediaPlayer; 25 | import android.os.Bundle; 26 | import android.os.Handler; 27 | import android.os.Looper; 28 | import android.os.Message; 29 | import android.os.Parcelable; 30 | import android.speech.RecognitionListener; 31 | import android.speech.RecognizerIntent; 32 | import android.speech.SpeechRecognizer; 33 | import android.util.Log; 34 | import android.widget.Toast; 35 | 36 | import org.vosk.service.R; 37 | 38 | import androidx.annotation.NonNull; 39 | import androidx.appcompat.app.AppCompatActivity; 40 | import androidx.core.app.ActivityCompat; 41 | 42 | import java.lang.ref.WeakReference; 43 | import java.util.ArrayList; 44 | import java.util.List; 45 | import java.util.Locale; 46 | 47 | public class SpeechRecognizerActivity extends AppCompatActivity { 48 | protected static final String TAG = SpeechRecognizerActivity.class.getSimpleName(); 49 | 50 | private SpeechRecognizer speechRecognizer; 51 | 52 | private static final String MSG = "MSG"; 53 | private static final int MSG_TOAST = 1; 54 | private static final int MSG_RESULT_ERROR = 2; 55 | 56 | int PERMISSION_ALL = 1; 57 | 58 | String[] PERMISSIONS = { 59 | Manifest.permission.RECORD_AUDIO, 60 | Manifest.permission.WRITE_EXTERNAL_STORAGE 61 | }; 62 | 63 | protected static class SimpleMessageHandler extends Handler { 64 | private final WeakReference mRef; 65 | 66 | private SimpleMessageHandler(Looper looper, SpeechRecognizerActivity activity) { 67 | super(looper); 68 | mRef = new WeakReference<>(activity); 69 | } 70 | 71 | public void handleMessage(Message msg) { 72 | SpeechRecognizerActivity outerClass = mRef.get(); 73 | if (outerClass != null) { 74 | Bundle b = msg.getData(); 75 | String msgAsString = b.getString(MSG); 76 | switch (msg.what) { 77 | case MSG_TOAST: 78 | outerClass.toast(msgAsString); 79 | break; 80 | case MSG_RESULT_ERROR: 81 | outerClass.showError(msgAsString); 82 | break; 83 | default: 84 | break; 85 | } 86 | } 87 | } 88 | } 89 | 90 | protected static Message createMessage(String str) { 91 | Bundle b = new Bundle(); 92 | b.putString(MSG, str); 93 | Message msg = Message.obtain(); 94 | msg.what = SpeechRecognizerActivity.MSG_TOAST; 95 | msg.setData(b); 96 | return msg; 97 | } 98 | 99 | protected void toast(String message) { 100 | Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show(); 101 | } 102 | 103 | void showError(String msg) { 104 | Log.d(TAG, msg); 105 | } 106 | 107 | void setupRecognizer() { 108 | final Intent speechRecognizerIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH); 109 | speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, 110 | RecognizerIntent.LANGUAGE_MODEL_FREE_FORM); 111 | speechRecognizerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, Locale.getDefault().toString().replace("_","-")); 112 | speechRecognizer.startListening(speechRecognizerIntent); 113 | } 114 | 115 | @Override 116 | protected void onCreate(final Bundle savedInstanceState) { 117 | super.onCreate(savedInstanceState); 118 | setContentView(R.layout.speech_recognizer_activity); 119 | 120 | speechRecognizer = SpeechRecognizer.createSpeechRecognizer(this); 121 | 122 | speechRecognizer.setRecognitionListener(new RecognitionListener() { 123 | @Override 124 | public void onReadyForSpeech(Bundle bundle) { 125 | 126 | } 127 | 128 | @Override 129 | public void onBeginningOfSpeech() { 130 | } 131 | 132 | @Override 133 | public void onRmsChanged(float v) { 134 | 135 | } 136 | 137 | @Override 138 | public void onBufferReceived(byte[] bytes) { 139 | 140 | } 141 | 142 | @Override 143 | public void onEndOfSpeech() { 144 | speechRecognizer.stopListening(); 145 | } 146 | 147 | @Override 148 | public void onError(int i) { 149 | showError(); 150 | } 151 | 152 | @Override 153 | public void onResults(Bundle bundle) { 154 | Log.i(TAG, "onResults"); 155 | ArrayList results = bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); 156 | Log.i(TAG, results.get(0)); 157 | returnResults(results); 158 | } 159 | 160 | @Override 161 | public void onPartialResults(Bundle bundle) { 162 | Log.i(TAG, "onPartialResults"); 163 | ArrayList data = bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION); 164 | Log.i(TAG, data.get(0)); 165 | } 166 | 167 | @Override 168 | public void onEvent(int i, Bundle bundle) { 169 | Log.d(TAG, bundle.toString()); 170 | } 171 | }); 172 | } 173 | 174 | @Override 175 | public void onStart() { 176 | super.onStart(); 177 | Log.i(TAG, "onStart"); 178 | 179 | if (!hasPermissions(this, PERMISSIONS)) { 180 | ActivityCompat.requestPermissions(this, PERMISSIONS, PERMISSION_ALL); 181 | } 182 | else { 183 | setupRecognizer(); 184 | } 185 | } 186 | 187 | @Override 188 | protected void onDestroy() { 189 | super.onDestroy(); 190 | Log.i(TAG, "onDestroy"); 191 | speechRecognizer.destroy(); 192 | } 193 | 194 | 195 | public void startSpeechSound() { 196 | MediaPlayer mp = MediaPlayer.create(this, R.raw.start_speech_effect); 197 | mp.start(); 198 | } 199 | 200 | public static boolean hasPermissions(Context context, String... permissions) { 201 | if (context != null && permissions != null) { 202 | for (String permission : permissions) { 203 | if (ActivityCompat.checkSelfPermission(context, permission) != PackageManager.PERMISSION_GRANTED) { 204 | return false; 205 | } 206 | } 207 | } 208 | return true; 209 | } 210 | 211 | @Override 212 | public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, 213 | @NonNull int[] grantResults) { 214 | super.onRequestPermissionsResult(requestCode, permissions, grantResults); 215 | for (int i = 0; i < permissions.length - 1; i++) { 216 | if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { 217 | finish(); 218 | } 219 | } 220 | setupRecognizer(); 221 | } 222 | 223 | private void returnResults(List results) { 224 | Handler handler = new SimpleMessageHandler(Looper.getMainLooper(), this); 225 | 226 | Intent incomingIntent = getIntent(); 227 | Log.d(TAG, incomingIntent.toString()); 228 | Bundle extras = incomingIntent.getExtras(); 229 | if (extras == null) { 230 | return; 231 | } 232 | Log.d(TAG, extras.toString()); 233 | PendingIntent pendingIntent = getPendingIntent(extras); 234 | if (pendingIntent == null) { 235 | Log.d(TAG, "No pending intent, setting result intent."); 236 | setResultIntent(results); 237 | } else { 238 | Log.d(TAG, pendingIntent.toString()); 239 | 240 | Bundle bundle = extras.getBundle(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE); 241 | if (bundle == null) { 242 | bundle = new Bundle(); 243 | } 244 | 245 | Intent intent = new Intent(); 246 | intent.putExtras(bundle); 247 | handler.sendMessage( 248 | createMessage(String.format(getString(R.string.recognized), results.get(0)))); 249 | try { 250 | Log.d(TAG, "Sending result via pendingIntent"); 251 | pendingIntent.send(this, AppCompatActivity.RESULT_OK, intent); 252 | } catch (PendingIntent.CanceledException e) { 253 | Log.e(TAG, e.getMessage()); 254 | handler.sendMessage(createMessage(e.getMessage())); 255 | } 256 | } 257 | finish(); 258 | } 259 | 260 | private void showError() { 261 | toast("Error loading recognizer"); 262 | } 263 | 264 | private void setResultIntent(List matches) { 265 | Intent intent = new Intent(); 266 | intent.putStringArrayListExtra(RecognizerIntent.EXTRA_RESULTS, new ArrayList<>(matches)); 267 | setResult(Activity.RESULT_OK, intent); 268 | } 269 | 270 | private PendingIntent getPendingIntent(Bundle extras) { 271 | Parcelable extraResultsPendingIntentAsParceable = extras 272 | .getParcelable(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT); 273 | if (extraResultsPendingIntentAsParceable != null) { 274 | if (extraResultsPendingIntentAsParceable instanceof PendingIntent) { 275 | return (PendingIntent) extraResultsPendingIntentAsParceable; 276 | } 277 | } 278 | return null; 279 | } 280 | } -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/ui/selector/DiffCallback.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.ui.selector; 2 | 3 | import androidx.annotation.NonNull; 4 | import androidx.recyclerview.widget.DiffUtil; 5 | 6 | public class DiffCallback extends DiffUtil.ItemCallback { 7 | 8 | @Override 9 | public boolean areItemsTheSame(@NonNull ModelItem oldItem, @NonNull ModelItem newItem) { 10 | return oldItem.getName().equals(newItem.getName()); 11 | } 12 | 13 | @Override 14 | public boolean areContentsTheSame(@NonNull ModelItem oldItem, @NonNull ModelItem newItem) { 15 | return oldItem.getName().equals(newItem.getName()); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/ui/selector/ModelItem.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.ui.selector; 2 | 3 | public class ModelItem { 4 | private String lang; 5 | private String lang_text; 6 | private String md5; 7 | private String name; 8 | private boolean obsolete; 9 | private long size; 10 | private String size_text; 11 | private String type; 12 | private String url; 13 | private String version; 14 | 15 | public ModelItem() { 16 | } 17 | 18 | public ModelItem(String lang_text, String name, String size_text) { 19 | this.lang_text = lang_text; 20 | this.name = name; 21 | this.size_text = size_text; 22 | } 23 | 24 | public ModelItem(String lang, String lang_text, String md5, String name, boolean obsolete, long size, String size_text, String type, String url, String version) { 25 | this.lang = lang; 26 | this.lang_text = lang_text; 27 | this.md5 = md5; 28 | this.name = name; 29 | this.obsolete = obsolete; 30 | this.size = size; 31 | this.size_text = size_text; 32 | this.type = type; 33 | this.url = url; 34 | this.version = version; 35 | } 36 | 37 | public String getLang() { 38 | return lang; 39 | } 40 | 41 | public void setLang(String lang) { 42 | this.lang = lang; 43 | } 44 | 45 | public String getLang_text() { 46 | return lang_text; 47 | } 48 | 49 | public void setLang_text(String lang_text) { 50 | this.lang_text = lang_text; 51 | } 52 | 53 | public String getMd5() { 54 | return md5; 55 | } 56 | 57 | public void setMd5(String md5) { 58 | this.md5 = md5; 59 | } 60 | 61 | public String getName() { 62 | return name; 63 | } 64 | 65 | public void setName(String name) { 66 | this.name = name; 67 | } 68 | 69 | public boolean getObsolete() { 70 | return obsolete; 71 | } 72 | 73 | public void setObsolete(boolean obsolete) { 74 | this.obsolete = obsolete; 75 | } 76 | 77 | public long getSize() { 78 | return size; 79 | } 80 | 81 | public void setSize(long size) { 82 | this.size = size; 83 | } 84 | 85 | public String getSize_text() { 86 | return size_text; 87 | } 88 | 89 | public void setSize_text(String size_text) { 90 | this.size_text = size_text; 91 | } 92 | 93 | public String getType() { 94 | return type; 95 | } 96 | 97 | public void setType(String type) { 98 | this.type = type; 99 | } 100 | 101 | public String getUrl() { 102 | return url; 103 | } 104 | 105 | public void setUrl(String url) { 106 | this.url = url; 107 | } 108 | 109 | public String getVersion() { 110 | return version; 111 | } 112 | 113 | public void setVersion(String version) { 114 | this.version = version; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/ui/selector/ModelListActivity.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.ui.selector; 2 | 3 | import static org.vosk.service.download.Download.CLEAR; 4 | import static org.vosk.service.download.Download.COMPLETE; 5 | import static org.vosk.service.download.Download.RESTARTING; 6 | import static org.vosk.service.download.Download.STARTING; 7 | import static org.vosk.service.download.VoskModelStorageClient.ServiceType.DOWNLOAD_MODEL_LIST; 8 | import static org.vosk.service.utils.Tools.isServiceRunning; 9 | 10 | import android.app.Dialog; 11 | import android.content.DialogInterface; 12 | import android.content.Intent; 13 | import android.content.SharedPreferences; 14 | import android.net.NetworkInfo; 15 | import android.os.Bundle; 16 | import android.preference.PreferenceManager; 17 | import android.view.View; 18 | import android.widget.ProgressBar; 19 | import android.widget.Toast; 20 | 21 | import androidx.appcompat.app.AlertDialog; 22 | import androidx.appcompat.app.AppCompatActivity; 23 | import androidx.core.content.ContextCompat; 24 | import androidx.recyclerview.widget.LinearLayoutManager; 25 | import androidx.recyclerview.widget.RecyclerView; 26 | import androidx.recyclerview.widget.SimpleItemAnimator; 27 | 28 | import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork; 29 | import com.google.gson.Gson; 30 | import com.google.gson.reflect.TypeToken; 31 | 32 | import org.vosk.service.download.DownloadModelService; 33 | import org.vosk.service.R; 34 | import org.vosk.service.download.VoskModelStorageClient; 35 | import org.vosk.service.download.VoskModelStorage; 36 | import org.vosk.service.download.Error; 37 | import org.vosk.service.download.EventBus; 38 | import org.vosk.service.utils.PreferenceConstants; 39 | import org.vosk.service.download.FileHelper; 40 | import org.vosk.service.utils.Tools; 41 | 42 | import java.io.File; 43 | import java.util.List; 44 | import java.util.stream.Collectors; 45 | 46 | import io.reactivex.android.schedulers.AndroidSchedulers; 47 | import io.reactivex.disposables.CompositeDisposable; 48 | import io.reactivex.schedulers.Schedulers; 49 | 50 | public class ModelListActivity extends AppCompatActivity { 51 | 52 | private final EventBus eventBus = EventBus.getInstance(); 53 | private final CompositeDisposable compositeDisposable = new CompositeDisposable(); 54 | private final VoskModelStorage service = VoskModelStorageClient.getClient(null, DOWNLOAD_MODEL_LIST); 55 | private final Gson gson = new Gson(); 56 | 57 | private ModelListAdapter modelListAdapter; 58 | private SharedPreferences sharedPreferences; 59 | 60 | private AlertDialog alertDialog; 61 | private RecyclerView recyclerView; 62 | private ProgressBar progressBar; 63 | 64 | private List offlineModelList; 65 | public String modelDownloadingName = ""; 66 | public static int progress = CLEAR; 67 | private boolean isOnline; 68 | 69 | 70 | @Override 71 | protected void onCreate(Bundle savedInstanceState) { 72 | super.onCreate(savedInstanceState); 73 | setContentView(R.layout.activity_model_list); 74 | 75 | //Init fields 76 | sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this); 77 | modelListAdapter = new ModelListAdapter(sharedPreferences); 78 | checkIfIsDownloading(); 79 | loadOfflineModels(); 80 | initViews(); 81 | observeEvents(); 82 | loadModels(); 83 | } 84 | 85 | private void checkIfIsDownloading() { 86 | modelDownloadingName = sharedPreferences.getString(PreferenceConstants.DOWNLOADING_FILE, ""); 87 | if (isOnline && !modelDownloadingName.equals("") && !isServiceRunning(this)) { 88 | progress = RESTARTING; 89 | startDownloadModelService(); 90 | } 91 | } 92 | 93 | private void initViews() { 94 | progressBar = findViewById(R.id.progress_circular); 95 | 96 | recyclerView = findViewById(R.id.model_recycler_view); 97 | recyclerView.setLayoutManager(new LinearLayoutManager(this)); 98 | recyclerView.setAdapter(modelListAdapter); 99 | if (recyclerView.getItemAnimator() != null) 100 | ((SimpleItemAnimator) recyclerView.getItemAnimator()).setSupportsChangeAnimations(false); 101 | } 102 | 103 | private void loadModels() { 104 | compositeDisposable.add(service.getModelList() 105 | .subscribeOn(Schedulers.io()) 106 | .observeOn(AndroidSchedulers.mainThread()) 107 | .subscribe( 108 | newDataset -> { 109 | showList(); 110 | modelListAdapter.updateDataset(newDataset.stream().filter(it -> it.getType().equals("small") && !it.getObsolete()).collect(Collectors.toList()), offlineModelList.stream().filter(it -> !it.getName().equals(modelDownloadingName)).collect(Collectors.toList())); 111 | }, 112 | error -> { 113 | showList(); 114 | modelListAdapter.updateDataset(offlineModelList.stream().filter(it -> !it.getName().equals(modelDownloadingName)).collect(Collectors.toList())); 115 | })); 116 | } 117 | 118 | private void showList() { 119 | progressBar.setVisibility(View.GONE); 120 | recyclerView.setVisibility(View.VISIBLE); 121 | } 122 | 123 | private void observeEvents() { 124 | compositeDisposable.add(eventBus.getDownloadStatusObservable() 125 | .subscribeOn(Schedulers.io()) 126 | .observeOn(AndroidSchedulers.mainThread()) 127 | .subscribe(download -> { 128 | if (download.getProgress() == COMPLETE) { 129 | Toast.makeText(this, R.string.download_complete, Toast.LENGTH_SHORT).show(); 130 | loadOfflineModels(); 131 | progress = CLEAR; 132 | sharedPreferences.edit() 133 | .remove(PreferenceConstants.DOWNLOADING_FILE) 134 | .apply(); 135 | modelListAdapter.updateOfflineModels(offlineModelList); 136 | } else if (progress != download.getProgress()) { 137 | progress = download.getProgress(); 138 | if (modelDownloadingName != null && modelListAdapter.getDataset().stream().anyMatch(it -> it.getName().equals(modelDownloadingName))) 139 | modelListAdapter.getDataset().stream() 140 | .filter(it -> it.getName().equals(modelDownloadingName)).findFirst() 141 | .ifPresent(modelItem -> modelListAdapter.notifyItemChanged(modelListAdapter.getDataset().indexOf(modelItem))); 142 | } 143 | })); 144 | 145 | compositeDisposable.add(eventBus.getDownloadStartObservable() 146 | .subscribeOn(Schedulers.io()) 147 | .observeOn(AndroidSchedulers.mainThread()) 148 | .subscribe(modelItem -> { 149 | progress = STARTING; 150 | //start service 151 | startDownloadModelService(); 152 | modelDownloadingName = modelItem.getName(); 153 | sharedPreferences.edit().putString(PreferenceConstants.DOWNLOADING_FILE, modelItem.getName()).apply(); 154 | addOfflineModel(modelItem); 155 | })); 156 | 157 | compositeDisposable.add(eventBus.getModelSelectedObservable() 158 | .subscribeOn(Schedulers.io()) 159 | .observeOn(AndroidSchedulers.mainThread()) 160 | .subscribe(this::manageModelSelected)); 161 | 162 | compositeDisposable.add(eventBus.geErrorObservable() 163 | .subscribeOn(Schedulers.io()) 164 | .observeOn(AndroidSchedulers.mainThread()) 165 | .subscribe(this::handleError)); 166 | 167 | compositeDisposable.add(EventBus.getInstance().getConnectionEvent().subscribe(state -> { 168 | if (state == NetworkInfo.State.CONNECTED) { 169 | isOnline = true; 170 | checkIfIsDownloading(); 171 | } else { 172 | isOnline = false; 173 | } 174 | }) 175 | ); 176 | 177 | compositeDisposable.add(ReactiveNetwork.observeNetworkConnectivity(getApplicationContext()) 178 | .subscribeOn(Schedulers.io()) 179 | .observeOn(AndroidSchedulers.mainThread()) 180 | .subscribe(connectivity -> { 181 | if (connectivity.getState() == NetworkInfo.State.CONNECTED || connectivity.getState() == NetworkInfo.State.DISCONNECTED) 182 | EventBus.getInstance().postConnectionEvent(connectivity.getState()); 183 | }) 184 | ); 185 | 186 | compositeDisposable.add(eventBus.getDeleteDownloadedModelObservable() 187 | .subscribeOn(Schedulers.io()) 188 | .observeOn(AndroidSchedulers.mainThread()) 189 | .subscribe(this::showDeleteModelDialog)); 190 | } 191 | 192 | private void showDeleteModelDialog(ModelItem modelItem) { 193 | DialogInterface.OnClickListener clickListener = (dialog, which) -> { 194 | deleteOfflineModel(modelItem); 195 | modelListAdapter.updateOfflineModels(offlineModelList); 196 | Toast.makeText(this, getString(R.string.model_delete), Toast.LENGTH_LONG).show(); 197 | dialog.dismiss(); 198 | 199 | }; 200 | showDialog(R.string.delete_model_dialog_title, R.string.delete_model_dialog_message, clickListener); 201 | } 202 | 203 | private void loadOfflineModels() { 204 | String offlineListJson = sharedPreferences.getString(PreferenceConstants.OFFLINE_LIST, "[]"); 205 | offlineModelList = gson.fromJson(offlineListJson, new TypeToken>() { 206 | }.getType()); 207 | } 208 | 209 | private void addOfflineModel(ModelItem modelItem) { 210 | offlineModelList.add(modelItem); 211 | saveOfflineModelList(); 212 | } 213 | 214 | private void deleteOfflineModel(ModelItem modelItem) { 215 | FileHelper.deleteFileOrDirectory(new File(Tools.getModelFileRootPath(this), modelItem.getName())); 216 | offlineModelList.stream() 217 | .filter(it -> it.getName().equals(modelItem.getName())).findFirst() 218 | .ifPresent(item -> offlineModelList.remove(item)); 219 | saveOfflineModelList(); 220 | } 221 | 222 | private void saveOfflineModelList() { 223 | Gson gson = new Gson(); 224 | String offlineModelsJson = gson.toJson(offlineModelList); 225 | sharedPreferences.edit().putString(PreferenceConstants.OFFLINE_LIST, offlineModelsJson).apply(); 226 | } 227 | 228 | private void handleError(Error error) { 229 | switch (error) { 230 | case CONNECTION: { 231 | Toast.makeText(this, getString(R.string.connection_error), Toast.LENGTH_LONG).show(); 232 | if (!modelDownloadingName.equals("")) { 233 | modelListAdapter.notifyDataSetChanged(); 234 | } 235 | } 236 | break; 237 | case WRITE_STORAGE: 238 | Toast.makeText(this, getString(R.string.write_storage_error), Toast.LENGTH_LONG).show(); 239 | break; 240 | } 241 | } 242 | 243 | private void startDownloadModelService() { 244 | if (!isServiceRunning(this)) { 245 | Intent service = new Intent(this, DownloadModelService.class); 246 | ContextCompat.startForegroundService(this, service); 247 | } 248 | } 249 | 250 | public void manageModelSelected(ModelItem modelItem) { 251 | if (isDownloaded(modelItem)) { 252 | selectDefaultModel(modelItem); 253 | modelListAdapter.notifyDataSetChanged(); 254 | } else if (!sharedPreferences.contains(PreferenceConstants.DOWNLOADING_FILE)) { 255 | EventBus.getInstance().postDownloadStart(modelItem); 256 | modelListAdapter.notifyDataSetChanged(); 257 | } else { 258 | showDialog(R.string.warning, R.string.wait_for_download, ((dialog, which) -> dialog.dismiss())); 259 | } 260 | } 261 | 262 | private void showDialog(int title, int message, Dialog.OnClickListener onClickListener) { 263 | if (alertDialog != null && alertDialog.isShowing()) 264 | alertDialog.dismiss(); 265 | alertDialog = new AlertDialog.Builder(this) 266 | .setTitle(getString(title)) 267 | .setMessage(message) 268 | .setPositiveButton("Accept", onClickListener) 269 | .setNegativeButton("Cancel", (dialog, which) -> dialog.dismiss()) 270 | .show(); 271 | } 272 | 273 | private boolean isDownloaded(ModelItem modelItem) { 274 | if (!modelItem.getName().equals(sharedPreferences.getString(PreferenceConstants.DOWNLOADING_FILE, ""))) 275 | return offlineModelList.stream().anyMatch(it -> it.getName().equals(modelItem.getName())); 276 | return false; 277 | } 278 | 279 | private void selectDefaultModel(ModelItem modelItem) { 280 | sharedPreferences.edit().putString(PreferenceConstants.ACTIVE_MODEL, modelItem.getName()).apply(); 281 | } 282 | 283 | @Override 284 | protected void onStop() { 285 | super.onStop(); 286 | if (alertDialog != null && alertDialog.isShowing()) 287 | alertDialog.dismiss(); 288 | } 289 | 290 | @Override 291 | protected void onDestroy() { 292 | super.onDestroy(); 293 | compositeDisposable.clear(); 294 | } 295 | } -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/ui/selector/ModelListAdapter.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.ui.selector; 2 | 3 | import static org.vosk.service.ui.selector.ModelListActivity.progress; 4 | 5 | import android.content.SharedPreferences; 6 | import android.content.res.ColorStateList; 7 | import android.view.LayoutInflater; 8 | import android.view.View; 9 | import android.view.ViewGroup; 10 | import android.widget.ImageView; 11 | import android.widget.ProgressBar; 12 | import android.widget.TextView; 13 | 14 | import androidx.annotation.NonNull; 15 | import androidx.appcompat.content.res.AppCompatResources; 16 | import androidx.constraintlayout.widget.Group; 17 | import androidx.recyclerview.widget.ListAdapter; 18 | import androidx.recyclerview.widget.RecyclerView; 19 | 20 | import org.vosk.service.R; 21 | import org.vosk.service.download.EventBus; 22 | import org.vosk.service.utils.PreferenceConstants; 23 | 24 | import java.util.ArrayList; 25 | import java.util.List; 26 | 27 | public class ModelListAdapter extends ListAdapter { 28 | 29 | List dataset = new ArrayList<>(); 30 | List offlineModelItems = new ArrayList<>(); 31 | SharedPreferences sharedPreferences; 32 | 33 | public ModelListAdapter(SharedPreferences sharedPreferences) { 34 | super(new DiffCallback()); 35 | this.sharedPreferences = sharedPreferences; 36 | } 37 | 38 | @NonNull 39 | @Override 40 | public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { 41 | return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.model_list_item, parent, false)); 42 | } 43 | 44 | @Override 45 | public void onBindViewHolder(@NonNull ViewHolder holder, int position) { 46 | holder.bind(dataset.get(position)); 47 | } 48 | 49 | public void updateDataset(List newDataset) { 50 | 51 | dataset.clear(); 52 | dataset.addAll(newDataset); 53 | 54 | this.offlineModelItems.clear(); 55 | this.offlineModelItems.addAll(newDataset); 56 | 57 | submitList(newDataset); 58 | } 59 | 60 | public void updateDataset(List newDataset, List offlineModelItems) { 61 | 62 | dataset.clear(); 63 | dataset.addAll(newDataset); 64 | 65 | this.offlineModelItems.clear(); 66 | this.offlineModelItems.addAll(offlineModelItems); 67 | 68 | submitList(newDataset); 69 | } 70 | 71 | @Override 72 | public int getItemCount() { 73 | return dataset.size(); 74 | } 75 | 76 | public void updateOfflineModels(List offlineModels) { 77 | this.offlineModelItems.clear(); 78 | this.offlineModelItems.addAll(offlineModels); 79 | notifyDataSetChanged(); 80 | } 81 | 82 | class ViewHolder extends RecyclerView.ViewHolder { 83 | TextView modelLangText; 84 | TextView modelSize; 85 | TextView modelName; 86 | TextView downloadProgressText; 87 | ProgressBar downloadProgressBar; 88 | ImageView modelIndicator; 89 | Group downloadProgressGroup; 90 | 91 | ViewHolder(View v) { 92 | super(v); 93 | modelLangText = v.findViewById(R.id.model_lang_text); 94 | modelSize = v.findViewById(R.id.model_size); 95 | modelName = v.findViewById(R.id.model_name); 96 | downloadProgressText = v.findViewById(R.id.model_download_progress_text); 97 | downloadProgressBar = v.findViewById(R.id.model_download_progress_bar); 98 | modelIndicator = v.findViewById(R.id.model_indicator); 99 | downloadProgressGroup = v.findViewById(R.id.model_download_progress_group); 100 | } 101 | 102 | public void bind(ModelItem modelItem) { 103 | ModelListState modelListState = getModelListState(); 104 | 105 | modelLangText.setText(modelItem.getLang_text()); 106 | modelSize.setText(itemView.getContext().getString(R.string.model_size, modelItem.getSize_text())); 107 | modelName.setText(itemView.getContext().getString(R.string.model_name, modelItem.getName())); 108 | View.OnClickListener sendSelectedEvent = v -> EventBus.getInstance().postModelSelectedObservable(getCurrentList().get(getAdapterPosition())); 109 | itemView.setOnClickListener(sendSelectedEvent); 110 | modelIndicator.setOnClickListener(sendSelectedEvent); 111 | itemView.setOnLongClickListener(v -> { 112 | if (modelListState == ModelListState.DOWNLOADED) { 113 | EventBus.getInstance().postDeleteDownloadedModel(getCurrentList().get(getAdapterPosition())); 114 | return true; 115 | } 116 | return false; 117 | }); 118 | 119 | switch (modelListState) { 120 | 121 | case NOT_DOWNLOADED: 122 | downloadProgressGroup.setVisibility(View.GONE); 123 | setDownloadProgress(0); 124 | modelIndicator.setVisibility(View.VISIBLE); 125 | setIndicator(R.drawable.ic_baseline_cloud_download_24); 126 | break; 127 | case DOWNLOADED: 128 | downloadProgressGroup.setVisibility(View.GONE); 129 | setDownloadProgress(0); 130 | modelIndicator.setVisibility(View.GONE); 131 | break; 132 | case DOWNLOADING: 133 | downloadProgressGroup.setVisibility(View.VISIBLE); 134 | setDownloadProgress(progress); 135 | modelIndicator.setVisibility(View.GONE); 136 | break; 137 | case SELECTED: 138 | downloadProgressGroup.setVisibility(View.GONE); 139 | modelIndicator.setVisibility(View.VISIBLE); 140 | setDownloadProgress(0); 141 | setIndicator(R.drawable.ic_baseline_check_circle_24); 142 | break; 143 | } 144 | } 145 | 146 | private void setDownloadProgress(int progress) { 147 | if (progress <= 100) { 148 | downloadProgressBar.setProgress(progress); 149 | downloadProgressText.setText(itemView.getContext().getString(R.string.model_downloading_progress, progress)); 150 | } 151 | } 152 | 153 | private void setIndicator(int indicatorIcon) { 154 | ColorStateList csl; 155 | if (indicatorIcon == R.drawable.ic_baseline_check_circle_24) 156 | csl = AppCompatResources.getColorStateList(itemView.getContext(), R.color.indicator_green); 157 | else 158 | csl = AppCompatResources.getColorStateList(itemView.getContext(), R.color.indicator_gray); 159 | 160 | modelIndicator.setBackgroundTintList(csl); 161 | modelIndicator.setBackground(AppCompatResources.getDrawable(itemView.getContext(), indicatorIcon)); 162 | } 163 | 164 | public ModelListState getModelListState() { 165 | if (getAdapterPosition() != -1 && sharedPreferences.contains(PreferenceConstants.DOWNLOADING_FILE) && sharedPreferences.getString(PreferenceConstants.DOWNLOADING_FILE, "").equals(dataset.get(getAdapterPosition()).getName())) 166 | return ModelListState.DOWNLOADING; 167 | if (sharedPreferences.getString(PreferenceConstants.ACTIVE_MODEL, "").equals(dataset.get(getAdapterPosition()).getName())) 168 | return ModelListState.SELECTED; 169 | if (offlineModelItems.stream().anyMatch(it -> it.getName().equals(dataset.get(getAdapterPosition()).getName()))) { 170 | return ModelListState.DOWNLOADED; 171 | } 172 | return ModelListState.NOT_DOWNLOADED; 173 | } 174 | } 175 | 176 | 177 | public List getDataset() { 178 | return dataset; 179 | } 180 | 181 | enum ModelListState { 182 | NOT_DOWNLOADED, 183 | DOWNLOADED, 184 | DOWNLOADING, 185 | SELECTED 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/utils/PreferenceConstants.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.utils; 2 | 3 | public class PreferenceConstants { 4 | public static final String DOWNLOADING_FILE = "downloading_file"; 5 | public static final String ACTIVE_MODEL = "active_model"; 6 | public static final String OFFLINE_LIST = "offline_list"; 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/org/vosk/service/utils/Tools.java: -------------------------------------------------------------------------------- 1 | package org.vosk.service.utils; 2 | 3 | import android.app.ActivityManager; 4 | import android.content.Context; 5 | 6 | import org.jetbrains.annotations.NotNull; 7 | import org.vosk.service.download.DownloadModelService; 8 | 9 | import java.io.File; 10 | 11 | public class Tools { 12 | 13 | public static File getModelFileRootPath(@NotNull Context context) { 14 | return new File(context.getFilesDir(), "models"); 15 | } 16 | 17 | public static boolean isServiceRunning(Context context) { 18 | ActivityManager manager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); 19 | for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) { 20 | if (DownloadModelService.class.getName().equals(service.service.getClassName())) { 21 | return true; 22 | } 23 | } 24 | return false; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_service_trigger.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 23 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphacep/vosk-android-service/5e02806198d67fea7ad31a4310fff3c932ed0b09/app/src/main/res/drawable-hdpi/icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/rounded.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-ldpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphacep/vosk-android-service/5e02806198d67fea7ad31a4310fff3c932ed0b09/app/src/main/res/drawable-ldpi/icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alphacep/vosk-android-service/5e02806198d67fea7ad31a4310fff3c932ed0b09/app/src/main/res/drawable-mdpi/icon.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/circle.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/gradient_progress_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_check_circle_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_cloud_download_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_baseline_mic_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_model_list.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 17 | 18 | 30 | 31 | -------------------------------------------------------------------------------- /app/src/main/res/layout/main.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 |