diff --git a/app/build.gradle b/app/build.gradle
index 25dd77d..1e4afe3 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -30,13 +30,13 @@ android {
buildFeatures {
viewBinding true
}
+ namespace 'org.vosk.ime'
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation 'net.java.dev.jna:jna:5.9.0@aar'
implementation group: 'com.alphacephei', name: 'vosk-android', version: '0.3.32'
- implementation project(':models')
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.6.1'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.5.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 10721d9..fbfcf5c 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -1,18 +1,19 @@
+ xmlns:tools="http://schemas.android.com/tools">
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/java/org/vosk/ime/Constants.java b/app/src/main/java/org/vosk/ime/Constants.java
new file mode 100644
index 0000000..ac7c09a
--- /dev/null
+++ b/app/src/main/java/org/vosk/ime/Constants.java
@@ -0,0 +1,26 @@
+package org.vosk.ime;
+
+import android.content.Context;
+
+import java.io.File;
+import java.util.Locale;
+
+public class Constants {
+ public static File getTemporaryDownloadLocation(Context context){
+ return new File(context.getCacheDir().getAbsolutePath(), "ModelZips");
+ }
+
+ public static File getTemporaryUnzipLocation(Context context) {
+ return new File(context.getCacheDir(), "TempUnzip");
+ }
+
+ public static File getModelsDirectory(Context context){
+ return new File(context.getFilesDir().getAbsolutePath(), "Models");
+ }
+
+ public static File getDirectoryForModel(Context context, Locale locale){
+ File dataFolder = Constants.getModelsDirectory(context);
+ String folderName = locale.toLanguageTag();
+ return new File(dataFolder, folderName);
+ }
+}
diff --git a/app/src/main/java/org/vosk/ime/FileDownloadService.java b/app/src/main/java/org/vosk/ime/FileDownloadService.java
index eebada3..9bdee67 100644
--- a/app/src/main/java/org/vosk/ime/FileDownloadService.java
+++ b/app/src/main/java/org/vosk/ime/FileDownloadService.java
@@ -113,16 +113,22 @@ protected void onHandleIntent(Intent intent) {
if (downloadDetails.isRequiresUnzip()) {
- String unzipDestination = downloadDetails.getUnzipAtFilePath();
-
- if (unzipDestination == null) {
+ File unzipDestination;
+ if (downloadDetails.getUnzipAtFilePath() == null) {
File file = new File(localPath);
- unzipDestination = file.getParentFile().getAbsolutePath();
+ unzipDestination = file.getParentFile();
+ } else {
+
+ unzipDestination = new File(downloadDetails.getUnzipAtFilePath());
}
- unzip(localPath, unzipDestination);
+
+ File unzipFolder = Constants.getTemporaryUnzipLocation(this);
+ File currentUnzipFolder = new File(unzipFolder, unzipDestination.getName());
+
+ unzip(localPath, currentUnzipFolder, unzipDestination);
}
downloadCompleted(resultReceiver);
@@ -170,24 +176,35 @@ public void downloadFailed(ResultReceiver resultReceiver) {
resultReceiver.send(STATUS_FAILED, progressBundle);
}
- private void unzip(String zipFilePath, String unzipAtLocation) throws Exception {
-
+ private void unzip(String zipFilePath, File tempUnzipLocation, File unzipFinalDestination) throws IOException {
File archive = new File(zipFilePath);
- try {
+ if (tempUnzipLocation.exists()) {
+ Tools.deleteRecursive(tempUnzipLocation);
+ }
- ZipFile zipfile = new ZipFile(archive);
+ ZipFile zipfile = new ZipFile(archive);
- for (Enumeration e = zipfile.entries(); e.hasMoreElements(); ) {
+ for (Enumeration extends ZipEntry> e = zipfile.entries(); e.hasMoreElements(); ) {
- ZipEntry entry = (ZipEntry) e.nextElement();
+ ZipEntry entry = (ZipEntry) e.nextElement();
- unzipEntry(zipfile, entry, unzipAtLocation);
+ unzipEntry(zipfile, entry, tempUnzipLocation.getAbsolutePath());
+ }
+ boolean moveSuccess;
+ if (unzipFinalDestination.exists()) {
+ moveSuccess = true;
+ for (File f : tempUnzipLocation.listFiles()) {
+ moveSuccess = f.renameTo(new File(unzipFinalDestination, f.getName()));
+ if (!moveSuccess) break;
}
+ tempUnzipLocation.delete();
+ } else {
+ moveSuccess = tempUnzipLocation.renameTo(unzipFinalDestination);
+ }
- } catch (Exception e) {
-
- Log.e("Unzip zip", "Unzip exception", e);
+ if (!moveSuccess) {
+ throw new IOException("Renaming temporary unzip directory failed");
}
}
@@ -206,27 +223,13 @@ private void unzipEntry(ZipFile zipfile, ZipEntry entry, String outputDir) throw
Log.v("ZIP E", "Extracting: " + entry);
InputStream zin = zipfile.getInputStream(entry);
- BufferedInputStream inputStream = new BufferedInputStream(zin);
- BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(outputFile));
-
- try {
-
- //IOUtils.copy(inputStream, outputStream);
-
- try {
- for (int c = inputStream.read(); c != -1; c = inputStream.read()) {
- outputStream.write(c);
- }
-
- } finally {
-
- outputStream.close();
+ try (BufferedInputStream inputStream = new BufferedInputStream(zin); BufferedOutputStream outputStream = new BufferedOutputStream(new FileOutputStream(outputFile))) {
+ byte[] b = new byte[1024];
+ int n;
+ while ((n = inputStream.read(b, 0, 1024)) >= 0) {
+ outputStream.write(b, 0, n);
}
-
- } finally {
- outputStream.close();
- inputStream.close();
}
}
diff --git a/app/src/main/java/org/vosk/ime/Model.java b/app/src/main/java/org/vosk/ime/Model.java
index 1cc8df2..ea11b19 100644
--- a/app/src/main/java/org/vosk/ime/Model.java
+++ b/app/src/main/java/org/vosk/ime/Model.java
@@ -1,19 +1,17 @@
package org.vosk.ime;
-import android.text.TextUtils;
-
import java.io.Serializable;
import java.util.Locale;
public class Model implements Serializable {
public final String path;
public final Locale locale;
- public final String name;
+ public final String filename;
- public Model(String path, Locale locale, String name) {
+ public Model(String path, Locale locale, String filename) {
this.path = path;
this.locale = locale;
- this.name = name;
+ this.filename = filename;
}
public String serialize() {
@@ -23,7 +21,7 @@ public String serialize() {
public static String serialize(Model model) {
return "[path:\"" + encode(model.path) +
"\", locale:\"" + model.locale +
- "\", name:\"" + encode(model.name) + "\"]";
+ "\", name:\"" + encode(model.filename) + "\"]";
}
public static Model deserialize(String serialized) {
diff --git a/app/src/main/java/org/vosk/ime/ModelLinks.java b/app/src/main/java/org/vosk/ime/ModelLink.java
similarity index 89%
rename from app/src/main/java/org/vosk/ime/ModelLinks.java
rename to app/src/main/java/org/vosk/ime/ModelLink.java
index 7681614..ee82846 100644
--- a/app/src/main/java/org/vosk/ime/ModelLinks.java
+++ b/app/src/main/java/org/vosk/ime/ModelLink.java
@@ -1,17 +1,20 @@
package org.vosk.ime;
-import android.os.LocaleList;
+import android.content.Context;
import androidx.annotation.StringRes;
+import java.util.ArrayList;
+import java.util.List;
import java.util.Locale;
+import java.util.Map;
// Locale list available at: https://stackoverflow.com/questions/7973023/what-is-the-list-of-supported-languages-locales-on-android
/**
- *
+ *
*/
-public enum ModelLinks {
+public enum ModelLink {
ENGLISH_US("https://alphacephei.com/vosk/models/vosk-model-small-en-us-0.15.zip", Locale.US, R.string.model_en_us),
ENGLISH_IN("https://alphacephei.com/vosk/models/vosk-model-small-en-in-0.4.zip", new Locale("en", "IN"), R.string.model_en_in),
CHINESE("https://alphacephei.com/vosk/models/vosk-model-small-cn-0.22.zip", Locale.CHINESE, R.string.model_cn),
@@ -38,9 +41,13 @@ public enum ModelLinks {
public final Locale locale;
public final int name;
- ModelLinks(String link, Locale locale, @StringRes int name) {
+ ModelLink(String link, Locale locale, @StringRes int name) {
this.link = link;
this.locale = locale;
this.name = name;
}
+
+ public String getFilename() {
+ return link.substring(link.lastIndexOf('/') + 1, link.lastIndexOf('.'));
+ }
}
diff --git a/app/src/main/java/org/vosk/ime/Tools.java b/app/src/main/java/org/vosk/ime/Tools.java
index 97c1ac6..2322c85 100644
--- a/app/src/main/java/org/vosk/ime/Tools.java
+++ b/app/src/main/java/org/vosk/ime/Tools.java
@@ -9,6 +9,15 @@
import androidx.core.content.ContextCompat;
+import org.vosk.ime.settingsfragments.ModelsAdapter;
+
+import java.io.File;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
public class Tools {
public static boolean isMicrophonePermissionGranted(Activity activity) {
@@ -26,4 +35,123 @@ public static boolean isIMEEnabled(Activity activity) {
}
return false;
}
+
+ public static void downloadModelFromLink(ModelLink model, FileDownloadService.OnDownloadStatusListener listener, Context context) {
+ String serverFilePath = model.link;
+
+ File tempFolder = Constants.getTemporaryDownloadLocation(context);
+ if (!tempFolder.exists()) {
+ tempFolder.mkdirs();
+ }
+
+ String fileName = model.link.substring(model.link.lastIndexOf('/') + 1); // file name
+ File tempFile = new File(tempFolder, fileName);
+
+ String localPath = tempFile.getAbsolutePath();
+
+ File modelFolder = Constants.getDirectoryForModel(context, model.locale);
+
+ if (!modelFolder.exists()) {
+ modelFolder.mkdirs();
+ }
+
+ String unzipPath = modelFolder.getAbsolutePath();
+
+ FileDownloadService.DownloadRequest downloadRequest = new FileDownloadService.DownloadRequest(serverFilePath, localPath, true);
+ downloadRequest.setRequiresUnzip(true);
+ downloadRequest.setDeleteZipAfterExtract(true);
+ downloadRequest.setUnzipAtFilePath(unzipPath);
+
+ FileDownloadService.FileDownloader downloader = FileDownloadService.FileDownloader.getInstance(downloadRequest, listener);
+ downloader.download(context);
+ }
+
+ public static void deleteRecursive(File fileOrDirectory) {
+ if (fileOrDirectory.isDirectory())
+ for (File child : fileOrDirectory.listFiles())
+ deleteRecursive(child);
+
+ fileOrDirectory.delete();
+ }
+
+ public static Map> getInstalledModelsMap(Context context) {
+ Map> localeMap = new HashMap<>();
+
+ File modelsDir = Constants.getModelsDirectory(context);
+
+ if (!modelsDir.exists()) return localeMap;
+
+ for (File localeFolder : modelsDir.listFiles()) {
+ if (!localeFolder.isDirectory()) continue;
+ Locale locale = Locale.forLanguageTag(localeFolder.getName());
+ List models = new ArrayList<>();
+ for (File modelFolder : localeFolder.listFiles()) {
+ if (!modelFolder.isDirectory()) continue;
+ String name = modelFolder.getName();
+ Model model = new Model(modelFolder.getAbsolutePath(), locale, name);
+ models.add(model);
+ }
+ localeMap.put(locale, models);
+ }
+ return localeMap;
+ }
+
+ public static List getInstalledModelsList(Context context){
+ List models = new ArrayList<>();
+
+ File modelsDir = Constants.getModelsDirectory(context);
+
+ if (!modelsDir.exists()) return models;
+
+ for (File localeFolder : modelsDir.listFiles()) {
+ if (!localeFolder.isDirectory()) continue;
+ Locale locale = Locale.forLanguageTag(localeFolder.getName());
+ for (File modelFolder : localeFolder.listFiles()) {
+ if (!modelFolder.isDirectory()) continue;
+ String name = modelFolder.getName();
+ Model model = new Model(modelFolder.getAbsolutePath(), locale, name);
+ models.add(model);
+ }
+ }
+ return models;
+ }
+
+ public static List getModelsData(Context context) {
+ List data = new ArrayList<>();
+ Map> installedModels = getInstalledModelsMap(context);
+ for (ModelLink link : ModelLink.values()) {
+ boolean found = false;
+ if (installedModels.containsKey(link.locale)) {
+ List localeModels = installedModels.get(link.locale);
+ for (int i = 0; i < localeModels.size(); i++) {
+ Model model = localeModels.get(i);
+ if (model.filename.equals(link.getFilename())) {
+ data.add(new ModelsAdapter.Data(link, model));
+ localeModels.remove(i);
+ found = true;
+ break;
+ }
+ }
+ }
+ if (!found)
+ data.add(new ModelsAdapter.Data(link));
+ }
+ for (List models : installedModels.values()) {
+ for (Model model : models) {
+ data.add(new ModelsAdapter.Data(model));
+ }
+ }
+
+ return data;
+ }
+
+ public static Model getModelForLink(ModelLink modelLink, Context context) {
+ File modelsDir = Constants.getModelsDirectory(context);
+ File localeDir = new File(modelsDir, modelLink.locale.toLanguageTag());
+ File modelDir = new File(localeDir, modelLink.getFilename());
+ if (!localeDir.exists() || modelDir.exists() || !modelDir.isDirectory()) {
+ return null;
+ }
+ return new Model(modelDir.getAbsolutePath(), modelLink.locale, modelLink.getFilename());
+ }
}
diff --git a/app/src/main/java/org/vosk/ime/VoskIME.java b/app/src/main/java/org/vosk/ime/VoskIME.java
index 1a383f0..ebb2a30 100644
--- a/app/src/main/java/org/vosk/ime/VoskIME.java
+++ b/app/src/main/java/org/vosk/ime/VoskIME.java
@@ -16,24 +16,28 @@
import android.Manifest;
import android.app.Dialog;
+import android.content.DialogInterface;
import android.content.pm.PackageManager;
import android.inputmethodservice.InputMethodService;
import android.os.Build;
+import android.os.Handler;
import android.os.IBinder;
+import android.os.Looper;
import android.text.method.ScrollingMovementMethod;
-import android.util.DisplayMetrics;
import android.util.Log;
+import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
-import android.view.ViewGroup;
import android.view.Window;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
+import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
+import android.widget.Spinner;
import android.widget.TextView;
import android.widget.Toast;
@@ -45,13 +49,18 @@
import org.vosk.android.RecognitionListener;
import org.vosk.android.SpeechService;
import org.vosk.android.SpeechStreamService;
-import org.vosk.android.StorageService;
import java.io.IOException;
-
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.constraintlayout.widget.ConstraintLayout;
-import androidx.constraintlayout.widget.Constraints;
import androidx.core.content.ContextCompat;
public class VoskIME extends InputMethodService implements
@@ -69,19 +78,39 @@ public class VoskIME extends InputMethodService implements
private Model model;
private SpeechService speechService;
private SpeechStreamService speechStreamService;
+ private EditorInfo editorInfo;
+
+ private int enterAction = EditorInfo.IME_ACTION_UNSPECIFIED;
+
private TextView resultView;
+ private int currentState = STATE_START;
+ private String currentErrorMessage = "";
+ private String loadedModel = "";
+
private ConstraintLayout overlayView;
private ImageButton micButton;
private ImageView fabAnimation;
+ private Button modelButton;
private InputMethodManager mInputMethodManager;
+ private List models;
+ private int currentModelIndex = 0;
+
@Override
public void onCreate() {
super.onCreate();
- initModel();
+
+ models = Tools.getInstalledModelsList(this);
+
+ if (models.size() == 0) {
+ setErrorState("No Model installed!");
+ } else {
+ currentModelIndex = 0;
+ initModel(models.get(0));
+ }
mInputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
}
@@ -109,13 +138,37 @@ public void onInitializeInterface() {
}
}
- private void initModel() {
- StorageService.unpack(this, "model-en-us", "model",
- (model) -> {
- this.model = model;
- setUiState(STATE_READY);
- },
- (exception) -> setErrorState("Failed to unpack the model" + exception.getMessage()));
+ private final Executor executor = Executors.newSingleThreadExecutor();
+
+ private void initModel(org.vosk.ime.Model myModel) {
+ loadedModel = myModel.locale.getDisplayName();
+ if (modelButton != null)
+ modelButton.setText(loadedModel);
+ setUiState(STATE_START);
+
+ Handler handler = new Handler(Looper.getMainLooper());
+ executor.execute(() -> {
+ Model model = new Model(myModel.path);
+ handler.post(() -> {
+ this.model = model;
+ setUiState(STATE_READY);
+ });
+ });
+// StorageService.unpack(this, "model-en-us", "model",
+// (model) -> {
+// this.model = model;
+// setUiState(STATE_READY);
+// },
+// (exception) -> setErrorState("Failed to unpack the model" + exception.getMessage()));
+ }
+
+ private void loadNextModel() {
+ if (models.size() == 0) return;
+
+ currentModelIndex++;
+ if (currentModelIndex >= models.size())
+ currentModelIndex = 0;
+ initModel(models.get(currentModelIndex));
}
@Override
@@ -123,9 +176,26 @@ public void onBindInput() {
// when user first clicks e.g. in text field
}
+ private static final int[] editorActions = new int[]
+ {
+ EditorInfo.IME_ACTION_UNSPECIFIED,
+ EditorInfo.IME_ACTION_NONE, EditorInfo.IME_ACTION_GO, EditorInfo.IME_ACTION_SEARCH,
+ EditorInfo.IME_ACTION_SEND, EditorInfo.IME_ACTION_NEXT, EditorInfo.IME_ACTION_DONE,
+ EditorInfo.IME_ACTION_PREVIOUS,
+ };
+
@Override
public void onStartInputView(EditorInfo info, boolean restarting) {
// text input has started
+ this.editorInfo = info;
+
+ int action = editorInfo.imeOptions & EditorInfo.IME_MASK_ACTION;
+ for (int a : editorActions) {
+ if (action == a) {
+ enterAction = action;
+ break;
+ }
+ }
}
@Override
@@ -140,6 +210,7 @@ public View onCreateInputView() {
resultView = overlayView.findViewById(R.id.result_text);
micButton = overlayView.findViewById(R.id.mic_button);
+ modelButton = overlayView.findViewById(R.id.model_button);
overlayView.setMinHeight(convertDpToPixel(300));
@@ -147,7 +218,10 @@ public View onCreateInputView() {
resultView.setMovementMethod(new ScrollingMovementMethod());
// Setup layout
- setUiState(STATE_START);
+ setUiState(currentState);
+ if (!currentErrorMessage.isEmpty()) {
+ setErrorState(currentErrorMessage);
+ }
// overlayView.findViewById(R.id.recognize_file).setOnClickListener(new View.OnClickListener() {
// @Override
@@ -159,7 +233,8 @@ public View onCreateInputView() {
micButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
- recognizeMicrophone();
+ if (model != null)
+ recognizeMicrophone();
}
});
@@ -268,29 +343,61 @@ public boolean onTouch(View v, MotionEvent event) {
// appendSpecial(".");
// }
// });
- overlayView.findViewById(R.id.return_button).
+ modelButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+// showModelPicker();
+ loadNextModel();
+ }
+ });
+ modelButton.setText(loadedModel);
- setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- appendSpecial("\n");
- }
- });
+ overlayView.findViewById(R.id.return_button).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ sendEnter();
+ }
+ });
InputConnection ic = getCurrentInputConnection();
- ExtractedText et = ic.getExtractedText(new ExtractedTextRequest(), 0);
- if (et != null) {
- selectionStart = et.selectionStart;
- selectionEnd = et.selectionEnd;
- } else {
- selectionStart = 0;
- selectionEnd = 0;
+ if (ic != null) {
+ ExtractedText et = ic.getExtractedText(new ExtractedTextRequest(), 0);
+ if (et != null) {
+ selectionStart = et.selectionStart;
+ selectionEnd = et.selectionEnd;
+ } else {
+ selectionStart = 0;
+ selectionEnd = 0;
+ }
}
return overlayView;
}
+ private void showModelPicker() {
+ // setup the alert builder
+ AlertDialog.Builder builder = new AlertDialog.Builder(this);
+ builder.setTitle(R.string.model_dialog_title);
+
+ models = Tools.getInstalledModelsList(this);
+
+ String[] names = new String[models.size()];
+ for (int i = 0; i < models.size(); i++) {
+ names[i] = models.get(i).locale.getDisplayLanguage();
+ }
+
+ builder.setItems(names, new DialogInterface.OnClickListener() {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ initModel(models.get(which));
+ }
+ });
+
+// create and show the alert dialog
+ AlertDialog dialog = builder.show();
+ }
+
@Override
public void onUpdateSelection(int oldSelStart, int oldSelEnd, int newSelStart, int newSelEnd, int candidatesStart, int candidatesEnd) {
super.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd, candidatesStart, candidatesEnd);
@@ -331,10 +438,21 @@ private void deleteLastChar() {
}
}
+ private void sendEnter() {
+ InputConnection ic = getCurrentInputConnection();
+ if (ic == null) return;
+
+ if (enterAction == EditorInfo.IME_ACTION_UNSPECIFIED) {
+ sendDownUpKeyEvents(KeyEvent.KEYCODE_ENTER);
+ } else {
+ ic.performEditorAction(enterAction);
+ }
+ }
+
private void appendSpecial(String text) {
InputConnection ic = getCurrentInputConnection();
- if (ic != null)
- ic.commitText(text, 1);
+ if (ic == null) return;
+ ic.commitText(text, 1);
}
/**
@@ -494,14 +612,20 @@ private void setUiState(int state) {
default:
return;
}
- resultView.setText(text);
- micButton.setImageDrawable(AppCompatResources.getDrawable(this, icon));
- micButton.setEnabled(enabled);
+ if (resultView != null)
+ resultView.setText(text);
+ if (micButton != null) {
+ micButton.setImageDrawable(AppCompatResources.getDrawable(this, icon));
+ micButton.setEnabled(enabled);
+ }
+ currentState = state;
}
private void setErrorState(String message) {
setUiState(STATE_ERROR);
- resultView.setText(message);
+ if (resultView != null)
+ resultView.setText(message);
+ currentErrorMessage = message;
}
private void recognizeMicrophone() {
diff --git a/app/src/main/java/org/vosk/ime/settingsfragments/ModelsAdapter.java b/app/src/main/java/org/vosk/ime/settingsfragments/ModelsAdapter.java
new file mode 100644
index 0000000..e442034
--- /dev/null
+++ b/app/src/main/java/org/vosk/ime/settingsfragments/ModelsAdapter.java
@@ -0,0 +1,158 @@
+package org.vosk.ime.settingsfragments;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageButton;
+import android.widget.TextView;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+import org.vosk.ime.Model;
+import org.vosk.ime.ModelLink;
+import org.vosk.ime.R;
+
+import java.util.List;
+import java.util.Locale;
+
+public class ModelsAdapter extends RecyclerView.Adapter {
+
+ public static class Data {
+ private ModelLink modelLink;
+ private Model model;
+ private boolean installed;
+
+ public Data(ModelLink modelLink) {
+ this.modelLink = modelLink;
+ this.model = null;
+ this.installed = false;
+ }
+
+ public Data(Model model) {
+ this.modelLink = null;
+ this.model = model;
+ this.installed = true;
+ }
+
+ public Data(ModelLink modelLink, Model model) {
+ this.modelLink = modelLink;
+ this.model = model;
+ this.installed = true;
+ }
+
+ public String getFilename() {
+ if (modelLink != null) {
+ return modelLink.getFilename();
+ } else {
+ return model.filename;
+ }
+ }
+
+ public Locale getLocale() {
+ if (model != null) {
+ return model.locale;
+ } else {
+ return modelLink.locale;
+ }
+ }
+
+ public boolean isInstalled() {
+ return installed;
+ }
+
+ public void wasInstalled(Model model) {
+ this.model = model;
+ this.installed = true;
+ }
+
+ public ModelLink getModelLink() {
+ return modelLink;
+ }
+
+ public Model getModel() {
+ return model;
+ }
+ }
+
+ private final Context context;
+ private final List mData;
+ private final LayoutInflater mInflater;
+ private ItemClickListener mClickListener;
+
+ // data is passed into the constructor
+ ModelsAdapter(Context context, List data) {
+ this.mInflater = LayoutInflater.from(context);
+ this.context = context;
+ this.mData = data;
+ }
+
+ // inflates the row layout from xml when needed
+ @Override
+ public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ View view = mInflater.inflate(R.layout.fragment_models_entry, parent, false);
+ return new ViewHolder(view);
+ }
+
+ // binds the data to the TextView in each row
+ @Override
+ public void onBindViewHolder(ViewHolder holder, int position) {
+ Data data = mData.get(position);
+ holder.titleTextView.setText(data.getLocale().getDisplayName());
+ holder.subtitleTextView.setText(data.getFilename());
+ holder.downloadButton.setVisibility(data.isInstalled() ? View.INVISIBLE : View.VISIBLE);
+ holder.data = data;
+ }
+
+ // total number of rows
+ @Override
+ public int getItemCount() {
+ return mData.size();
+ }
+
+
+ // stores and recycles views as they are scrolled off screen
+ public class ViewHolder extends RecyclerView.ViewHolder {
+ TextView titleTextView;
+ TextView subtitleTextView;
+ ImageButton downloadButton;
+ Data data;
+
+ ViewHolder(View itemView) {
+ super(itemView);
+ titleTextView = itemView.findViewById(R.id.titleTextView);
+ subtitleTextView = itemView.findViewById(R.id.subtitleTextView);
+ downloadButton = itemView.findViewById(R.id.downloadButton);
+
+ downloadButton.setOnClickListener(this::onButtonClick);
+ itemView.setOnClickListener(this::onClick);
+ }
+
+ private void onClick(View view) {
+ if (mClickListener != null)
+ mClickListener.onItemClick(view, getAdapterPosition(), data);
+ }
+
+ private void onButtonClick(View view) {
+ if (mClickListener != null)
+ mClickListener.onDownloadButtonClicked(view, getAdapterPosition(), data);
+ }
+ }
+
+ // convenience method for getting data at click position
+ Data getItem(int id) {
+ return mData.get(id);
+ }
+
+ // allows clicks events to be caught
+ void setClickListener(ItemClickListener itemClickListener) {
+ this.mClickListener = itemClickListener;
+ }
+
+ // parent activity will implement this method to respond to click events
+ public interface ItemClickListener {
+ void onItemClick(View view, int position, Data data);
+
+ void onDownloadButtonClicked(View view, int position, Data data);
+ }
+}
diff --git a/app/src/main/java/org/vosk/ime/settingsfragments/ModelsFragment.java b/app/src/main/java/org/vosk/ime/settingsfragments/ModelsFragment.java
index 02fd010..7f98305 100644
--- a/app/src/main/java/org/vosk/ime/settingsfragments/ModelsFragment.java
+++ b/app/src/main/java/org/vosk/ime/settingsfragments/ModelsFragment.java
@@ -4,28 +4,48 @@
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import android.widget.ProgressBar;
import android.widget.TextView;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import org.vosk.ime.FileDownloadService;
+import org.vosk.ime.Model;
+import org.vosk.ime.ModelLink;
+import org.vosk.ime.Tools;
import org.vosk.ime.databinding.FragmentModelsBinding;
-public class ModelsFragment extends Fragment {
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+
+public class ModelsFragment extends Fragment implements ModelsAdapter.ItemClickListener {
+ private static final String TAG = "ModelsFragment";
private FragmentModelsBinding binding;
+ private ModelsAdapter adapter;
+ private boolean isDownloading = false;
+ private ProgressBar progressBar;
+ private RecyclerView recyclerView;
- public View onCreateView(@NonNull LayoutInflater inflater,
- ViewGroup container, Bundle savedInstanceState) {
-// DashboardViewModel dashboardViewModel =
-// new ViewModelProvider(this).get(DashboardViewModel.class);
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
binding = FragmentModelsBinding.inflate(inflater, container, false);
View root = binding.getRoot();
-// final TextView textView = binding.textDashboard;
-// dashboardViewModel.getText().observe(getViewLifecycleOwner(), textView::setText);
+ recyclerView = binding.recyclerView;
+ progressBar = binding.progressBar;
+
+ recyclerView.setLayoutManager(new LinearLayoutManager(getContext()));
+ adapter = new ModelsAdapter(getContext(), Tools.getModelsData(getContext()));
+ adapter.setClickListener(this);
+ recyclerView.setAdapter(adapter);
+
return root;
}
@@ -34,4 +54,45 @@ public void onDestroyView() {
super.onDestroyView();
binding = null;
}
+
+ @Override
+ public void onItemClick(View view, int position, ModelsAdapter.Data data) {
+ }
+
+ @Override
+ public void onDownloadButtonClicked(View view, int position, ModelsAdapter.Data data) {
+ if (data.isInstalled() || isDownloading) return;
+ isDownloading = true;
+ FileDownloadService.OnDownloadStatusListener listener = new FileDownloadService.OnDownloadStatusListener() {
+ @Override
+ public void onDownloadStarted() {
+ progressBar.setVisibility(View.VISIBLE);
+// Toast.makeText(getContext(), "onDownloadStarted", Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onDownloadCompleted() {
+ isDownloading = false;
+ progressBar.setVisibility(View.GONE);
+ Model model = Tools.getModelForLink(data.getModelLink(), getContext());
+ if (model != null) data.wasInstalled(model);
+ adapter.notifyItemChanged(position);
+// Toast.makeText(getContext(), "onDownloadCompleted", Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onDownloadFailed() {
+ isDownloading = false;
+ progressBar.setVisibility(View.GONE);
+// Toast.makeText(getContext(), "onDownloadFailed", Toast.LENGTH_SHORT).show();
+ }
+
+ @Override
+ public void onDownloadProgress(int progress) {
+ progressBar.setProgress(progress);
+// Toast.makeText(getContext(), "download started", Toast.LENGTH_SHORT).show();
+ }
+ };
+ Tools.downloadModelFromLink(data.getModelLink(), listener, requireContext());
+ }
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_cloud.xml b/app/src/main/res/drawable/ic_cloud.xml
new file mode 100644
index 0000000..a860632
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cloud.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_download.xml b/app/src/main/res/drawable/ic_download.xml
new file mode 100644
index 0000000..1fc8106
--- /dev/null
+++ b/app/src/main/res/drawable/ic_download.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_download_done.xml b/app/src/main/res/drawable/ic_download_done.xml
new file mode 100644
index 0000000..0e3a229
--- /dev/null
+++ b/app/src/main/res/drawable/ic_download_done.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_folder.xml b/app/src/main/res/drawable/ic_folder.xml
new file mode 100644
index 0000000..5e930a2
--- /dev/null
+++ b/app/src/main/res/drawable/ic_folder.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_language.xml b/app/src/main/res/drawable/ic_language.xml
new file mode 100644
index 0000000..4522f8f
--- /dev/null
+++ b/app/src/main/res/drawable/ic_language.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_mic_none.xml b/app/src/main/res/drawable/ic_mic_none.xml
index 1d99d21..8029ea3 100644
--- a/app/src/main/res/drawable/ic_mic_none.xml
+++ b/app/src/main/res/drawable/ic_mic_none.xml
@@ -1,5 +1,10 @@
-
-
+
+
diff --git a/app/src/main/res/layout/fragment_models.xml b/app/src/main/res/layout/fragment_models.xml
index 9be878c..4b024b4 100644
--- a/app/src/main/res/layout/fragment_models.xml
+++ b/app/src/main/res/layout/fragment_models.xml
@@ -6,18 +6,22 @@
android:layout_height="match_parent"
tools:context=".settingsfragments.ModelsFragment">
-
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/fragment_models_entry.xml b/app/src/main/res/layout/fragment_models_entry.xml
new file mode 100644
index 0000000..9c437eb
--- /dev/null
+++ b/app/src/main/res/layout/fragment_models_entry.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/ime.xml b/app/src/main/res/layout/ime.xml
index 429339b..f163459 100644
--- a/app/src/main/res/layout/ime.xml
+++ b/app/src/main/res/layout/ime.xml
@@ -4,7 +4,7 @@
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
- android:layout_height="0dp"
+ android:layout_height="match_parent"
android:layout_alignParentBottom="true"
android:background="@android:color/white"
android:theme="@style/KeyboardTheme">
@@ -96,11 +96,23 @@
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 001106c..6d53245 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -19,6 +19,8 @@
\?
+ Select Model
+
Grant Microphone permission
Not Granted
diff --git a/app/src/main/res/xml/method.xml b/app/src/main/res/xml/method.xml
index 9438e23..8cbcb3b 100644
--- a/app/src/main/res/xml/method.xml
+++ b/app/src/main/res/xml/method.xml
@@ -1,10 +1,8 @@
diff --git a/build.gradle b/build.gradle
index 576599a..1aeb62e 100644
--- a/build.gradle
+++ b/build.gradle
@@ -10,7 +10,7 @@ buildscript {
}
}
dependencies {
- classpath 'com.android.tools.build:gradle:7.2.2'
+ classpath 'com.android.tools.build:gradle:7.3.1'
}
}
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 2e6e589..41dfb87 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/settings.gradle b/settings.gradle
index 2752cdf..e7b4def 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -1 +1 @@
-include ':models', ':app'
+include ':app'