diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelPickerActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelPickerActivity.java index d2b3e1672f..5933ceb1a4 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelPickerActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ModelPickerActivity.java @@ -22,7 +22,9 @@ import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; +import com.tavultesoft.kmea.data.CloudRepository; import com.tavultesoft.kmea.data.Dataset; +import com.tavultesoft.kmea.data.LexicalModel; import com.tavultesoft.kmea.data.adapters.LexicalModelsAdapter; import com.tavultesoft.kmea.packages.JSONUtils; import com.tavultesoft.kmea.util.FileUtils; @@ -43,6 +45,8 @@ import java.util.ArrayList; import java.util.Calendar; import java.util.Date; import java.util.HashMap; +import java.util.List; +import java.util.Map; /** * Keyman Settings --> Languages Settings --> Language Settings --> Model List @@ -57,6 +61,7 @@ public final class ModelPickerActivity extends AppCompatActivity { // Merged array list of lexical models to display in ListView private static ArrayList> lexicalModelsArrayList = null; + private static Dataset repo; private static Dataset.LexicalModels lexicalModelsAdapter = null; private boolean didExecuteParser = false; @@ -110,12 +115,110 @@ public final class ModelPickerActivity extends AppCompatActivity { listView.setFastScrollEnabled(true); } + @Override + public void finish() { + Log.d(TAG, "Exiting ModelPickerActivity"); + } + @Override protected void onResume() { super.onResume(); if (!didExecuteParser) { didExecuteParser = true; - new JSONParse().execute(); + //new JSONParse().execute(); + repo = CloudRepository.shared.fetchDataset(context, new Runnable() { + public void run() { + + List> mapList = new ArrayList<>(); + for(LexicalModel model: repo.lexicalModels.asList()) { + mapList.add(model.map); + } + + String[] from = new String[]{"leftIcon", KMManager.KMKey_LexicalModelName, KMManager.KMKey_Icon}; + int[] to = new int[]{R.id.image1, R.id.text1, R.id.image2}; + + ListAdapter adapter = new KMListAdapter(context, mapList, + R.layout.models_list_row_layout, from, to); + listView.setAdapter(adapter); + listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { + + @Override + public void onItemClick(AdapterView parent, View view, final int position, long id) { + selectedIndex = position; + HashMap modelInfo = lexicalModelsArrayList.get(position); + String packageID = modelInfo.get(KMManager.KMKey_PackageID); + String languageID = modelInfo.get(KMManager.KMKey_LanguageID); + String modelID = modelInfo.get(KMManager.KMKey_LexicalModelID); + String modelName = modelInfo.get(KMManager.KMKey_LexicalModelName); + String langName = modelInfo.get(KMManager.KMKey_LanguageName); + + // File check to see if lexical model already exists locally + File modelCheck = new File(KMManager.getLexicalModelsDir() + packageID + File.separator + modelID + ".model.js"); + + // Using the presence of the left icon "check" to determine if the model is installed + boolean modelInstalled = modelInfo.containsKey("leftIcon"); + if (modelInstalled) { + // Show Model Info + listView.setItemChecked(position, true); + listView.setSelection(position); + + // Start intent for selected Predictive Text Model screen + if (!languageID.equalsIgnoreCase(modelInfo.get(KMManager.KMKey_LanguageID))) { + Log.d(TAG, "Language ID " + languageID + " doesn't match model language ID: " + + modelInfo.get(KMManager.KMKey_LanguageID)); + } + Bundle bundle = new Bundle(); + // Note: package ID of a model is different from package ID for a keyboard. + // Language ID can be re-used + bundle.putString(KMManager.KMKey_PackageID, packageID); + bundle.putString(KMManager.KMKey_LanguageID, languageID); + bundle.putString(KMManager.KMKey_LexicalModelID, modelID); + bundle.putString(KMManager.KMKey_LexicalModelName, modelName); + bundle.putString(KMManager.KMKey_LexicalModelVersion, + modelInfo.get(KMManager.KMKey_LexicalModelVersion)); + bundle.putString(KMManager.KMKey_CustomModel, + MapCompat.getOrDefault(modelInfo, KMManager.KMKey_CustomModel, "N")); + Intent i = new Intent(context, ModelInfoActivity.class); + i.putExtras(bundle); + startActivityForResult(i, 1); + } else if (modelCheck.exists()) { + // Handle scenario where previously installed kmp already exists so + // we only need to add the model to the list of installed models + // Add help link + modelInfo.put(KMManager.KMKey_CustomHelpLink, ""); + boolean result = KMManager.addLexicalModel(context, modelInfo); + if (result) { + Toast.makeText(context, "Model installed", Toast.LENGTH_SHORT).show(); + } + + // Don't register associated lexical model because this is not a UI view + } else { + // Model isn't installed so prompt to download it + Bundle args = new Bundle(); + args.putString(KMKeyboardDownloaderActivity.ARG_PKG_ID, packageID); + args.putString(KMKeyboardDownloaderActivity.ARG_LANG_ID, languageID); + args.putString(KMKeyboardDownloaderActivity.ARG_MODEL_ID, modelID); + args.putString(KMKeyboardDownloaderActivity.ARG_MODEL_NAME, modelName); + args.putString(KMKeyboardDownloaderActivity.ARG_LANG_NAME, langName); + args.putBoolean(KMKeyboardDownloaderActivity.ARG_IS_CUSTOM, false); + args.putString(KMKeyboardDownloaderActivity.ARG_MODEL_URL, + modelInfo.get(KMManager.KMKey_LexicalModelPackageFilename)); + Intent i = new Intent(getApplicationContext(), KMKeyboardDownloaderActivity.class); + i.putExtras(args); + startActivity(i); + } + } + }); + + Intent i = getIntent(); + listView.setSelectionFromTop(i.getIntExtra("listPosition", 0), + i.getIntExtra("offsetY", 0)); + } + }, new Runnable() { + public void run() { + finish(); + } + }); } } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/adapters/CloudRepository.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java similarity index 57% rename from android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/adapters/CloudRepository.java rename to android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java index 2d76ba4f70..ee0b5e814a 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/adapters/CloudRepository.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java @@ -1,4 +1,4 @@ -package com.tavultesoft.kmea.data.adapters; +package com.tavultesoft.kmea.data; import android.app.ProgressDialog; import android.content.Context; @@ -16,8 +16,6 @@ import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; import com.tavultesoft.kmea.KeyboardPickerActivity; import com.tavultesoft.kmea.R; -import com.tavultesoft.kmea.data.Dataset; -import com.tavultesoft.kmea.data.LexicalModel; import com.tavultesoft.kmea.packages.JSONUtils; import com.tavultesoft.kmea.util.FileUtils; @@ -67,20 +65,33 @@ public class CloudRepository { * @param onFinish A callback to run immediately upon successful completion of the download. * @return A Dataset object implementing the Adapter interface to be asynchronously filled. */ - public Dataset fetchDataset(@NonNull Context context, Runnable onFinish) { + public Dataset fetchDataset(@NonNull Context context, Runnable onSuccess, Runnable onFailure) { + Dataset dataset = new Dataset(context); + + // Get kmp.json info from installed models. + JSONArray kmpLexicalModelsArray = JSONUtils.getLexicalModels(); + + if (kmpLexicalModelsArray.length() == 0) { + // May need to note this for handling a 'failure' check. + } else { + dataset.lexicalModels.addAll(processLexicalModelJSON(kmpLexicalModelsArray, context)); + } + boolean loadFromCache = this.shouldUseCache(context, getLexicalModelCacheFile(context)); if(!loadFromCache) { - CloudDownloadTask downloadTask = new CloudDownloadTask(context, onFinish); + CloudDownloadTask downloadTask = new CloudDownloadTask(context, dataset, onSuccess, onFailure); + + String lexicalURL = String.format("%s?q", KMKeyboardDownloaderActivity.kKeymanApiModelURL); /* do what's possible here, rather than in the Task */ - downloadTask.execute("" /* use actual URLs here */); + downloadTask.execute(lexicalURL); } else { // just load from the cache. } - throw new UnsupportedOperationException(); + return dataset; } protected File getLexicalModelCacheFile(Context context) { @@ -105,6 +116,78 @@ public class CloudRepository { return lmData; } + protected List processLexicalModelJSON(JSONArray models, Context context) { + List modelList = new ArrayList<>(models.length()); + + try { + // Parse the model JSON Object from the merged list of api.keyman.com query and available kmp's. + // Known assumption: + // 2. query is built on a single language ID so the "languages" array will only have one language + int modelsLength = models.length(); + for (int i = 0; i < modelsLength; i++) { + JSONObject model = models.getJSONObject(i); + String packageID = "", modelURL = ""; + if (model.has(KMManager.KMKey_PackageID)) { + packageID = model.getString(KMManager.KMKey_PackageID); + } else { + // Determine package ID from packageFilename + modelURL = model.optString("packageFilename", ""); + packageID = FileUtils.getFilename(modelURL); + packageID = packageID.replace(".model.kmp", ""); + } + + // api.keyman.com query returns an array of language IDs Strings while + // kmp.json "languages" is an array of JSONObject + String languageID = "", langName = ""; + Object obj = model.getJSONArray("languages"); + if (((JSONArray) obj).get(0) instanceof String) { + // language name not provided, so re-use language ID + languageID = model.getJSONArray("languages").getString(0); + langName = languageID; + } else if (((JSONArray) obj).get(0) instanceof JSONObject) { + JSONObject languageObj = model.getJSONArray("languages").getJSONObject(0); + languageID = languageObj.getString("id"); + langName = languageObj.getString("name"); + } + + String modelID = model.getString("id"); + String modelName = model.getString("name"); + String modelVersion = model.getString("version"); + + String isCustom = model.optString("CustomModel", "N"); + String icon = "0"; + + HashMap hashMap = new HashMap(); + hashMap.put(KMManager.KMKey_PackageID, packageID); + hashMap.put(KMManager.KMKey_LanguageID, languageID); + hashMap.put(KMManager.KMKey_LexicalModelID, modelID); + hashMap.put(KMManager.KMKey_LexicalModelName, modelName); + hashMap.put(KMManager.KMKey_LanguageName, langName); + hashMap.put(KMManager.KMKey_LexicalModelVersion, modelVersion); + hashMap.put(KMManager.KMKey_CustomModel, isCustom); + hashMap.put("isEnabled", "true"); + hashMap.put(KMManager.KMKey_Icon, String.valueOf(R.drawable.ic_arrow_forward)); + + // TODO: (Move to PickerActivity) Display check for installed models + String modelKey = String.format("%s_%s_%s", packageID, languageID, modelID); + if (KeyboardPickerActivity.containsLexicalModel(context, modelKey)) { // FIXME: + hashMap.put("leftIcon", String.valueOf(R.drawable.ic_check)); + } else { + // Otherwise, include link to .kmp file + hashMap.put(KMManager.KMKey_LexicalModelPackageFilename, modelURL); + } + // TODO: (Move section) END. + + modelList.add(new LexicalModel(hashMap)); + } + } catch (JSONException e) { + Log.e("JSONParse", "Error: " + e); + return new ArrayList<>(); // Is this ideal? + } + + return modelList; + } + private class CloudApiReturns { public final JSONArray keyboardJSON; public final JSONArray lexicalModelJSON; @@ -124,21 +207,26 @@ public class CloudRepository { private final String iconKey = "icon"; private final Context context; - private final Runnable finish; + private final Runnable success; + private final Runnable failure; - public CloudDownloadTask(Context context, Runnable finish) { + private final Dataset dataset; + + public CloudDownloadTask(Context context, Dataset dataset, Runnable success, Runnable failure) { this.context = context; + this.dataset = dataset; this.hasConnection = KMManager.hasConnection(context); - if(finish == null) { - finish = new Runnable() { + if(failure == null) { + failure = new Runnable() { @Override public void run() { // Do nothing. } }; } - this.finish = finish; + this.success = success; + this.failure = failure; } protected void showProgressDialog(final Runnable finishCallback) { @@ -179,7 +267,7 @@ public class CloudRepository { showProgressDialog(new Runnable() { @Override public void run() { // runs on 'cancel' selection. - finish.run(); + failure.run(); } }); } @@ -191,6 +279,11 @@ public class CloudRepository { return null; } + // Facilitates debugging the AsyncTask. + if(android.os.Debug.isDebuggerConnected()) { + android.os.Debug.waitForDebugger(); + } + int count = urls.length; JSONParser jsonParser = new JSONParser(); @@ -199,9 +292,12 @@ public class CloudRepository { lmData = getCachedJSONArray(context, getLexicalModelCacheFile(context)); } else if (hasConnection) { try { - String remoteUrl = String.format("%s?q=bcp47:%s", KMKeyboardDownloaderActivity.kKeymanApiModelURL); + String remoteUrl = urls[0]; lmData = jsonParser.getJSONObjectFromUrl(remoteUrl, JSONArray.class); + Log.d(TAG, "JSON retrieved!"); + Log.d(TAG, lmData.toString()); } catch (Exception e) { + Log.d(TAG, e.getMessage()); lmData = null; } } else { @@ -215,112 +311,25 @@ public class CloudRepository { // Clear pre-existing list List lexicalModelsArrayList = new ArrayList<>(); - // TODO: This part should be utilized to init the lexical model array before the Cloud returns. - // Consolidate kmp.json info from models. - JSONArray kmpLexicalModelsArray = JSONUtils.getLexicalModels(); - - // TODO: Should this be handled here? Probably split instead - the lexicalJSON check is valid, - // but the kmpLexicalModelsArray check goes with pre-init. - if (lexicalJSON == null && kmpLexicalModelsArray.length() == 0) { + if (lexicalJSON == null && dataset.isEmpty()) { Toast.makeText(context, "Failed to access Keyman server!", Toast.LENGTH_SHORT).show(); - finish.run(); + failure.run(); return null; } - // TODO: End "should this be handled?" section. - try { - JSONArray models; - if (!hasConnection) { - // When offline, only use keyboards available from kmp.json - models = kmpLexicalModelsArray; - } else { - // Otherwise, merge kmpLexicalModelsArray with cloud jsonArray - models = (lexicalJSON != null) ? lexicalJSON : new JSONArray(); // Start with cloud model list. - for (int i = 0; i < kmpLexicalModelsArray.length(); i++) { // Iterate over local KMPs. - JSONObject kmpLexicalModel = kmpLexicalModelsArray.getJSONObject(i); - String kmpModelID = kmpLexicalModel.getString("id"); - - int modelIndex = JSONUtils.findID(models, kmpModelID); - if (modelIndex == -1) { - // Lexical model from KMP didn't exist in cloud so add new entry - models.put(kmpLexicalModel); - } else { - // Lexical model already installed from local kmp so replace models entry - models.put(modelIndex, kmpLexicalModel); - } - } - } - - if (models == null) { - return null; - } - - // Parse the model JSON Object from the merged list of api.keyman.com query and available kmp's. - // Known assumption: - // 2. query is built on a single language ID so the "languages" array will only have one language - int modelsLength = models.length(); - for (int i = 0; i < modelsLength; i++) { - JSONObject model = models.getJSONObject(i); - String packageID = "", modelURL = ""; - if (model.has(KMManager.KMKey_PackageID)) { - packageID = model.getString(KMManager.KMKey_PackageID); - } else { - // Determine package ID from packageFilename - modelURL = model.optString("packageFilename", ""); - packageID = FileUtils.getFilename(modelURL); - packageID = packageID.replace(".model.kmp", ""); - } - - // api.keyman.com query returns an array of language IDs Strings while - // kmp.json "languages" is an array of JSONObject - String languageID = "", langName = ""; - Object obj = model.getJSONArray("languages"); - if (((JSONArray) obj).get(0) instanceof String) { - // language name not provided, so re-use language ID - languageID = model.getJSONArray("languages").getString(0); - langName = languageID; - } else if (((JSONArray) obj).get(0) instanceof JSONObject) { - JSONObject languageObj = model.getJSONArray("languages").getJSONObject(0); - languageID = languageObj.getString("id"); - langName = languageObj.getString("name"); - } - - String modelID = model.getString("id"); - String modelName = model.getString("name"); - String modelVersion = model.getString("version"); - - String isCustom = model.optString("CustomModel", "N"); - String icon = "0"; - - HashMap hashMap = new HashMap(); - hashMap.put(KMManager.KMKey_PackageID, packageID); - hashMap.put(KMManager.KMKey_LanguageID, languageID); - hashMap.put(KMManager.KMKey_LexicalModelID, modelID); - hashMap.put(KMManager.KMKey_LexicalModelName, modelName); - hashMap.put(KMManager.KMKey_LanguageName, langName); - hashMap.put(KMManager.KMKey_LexicalModelVersion, modelVersion); - hashMap.put(KMManager.KMKey_CustomModel, isCustom); - hashMap.put("isEnabled", "true"); - hashMap.put(KMManager.KMKey_Icon, String.valueOf(R.drawable.ic_arrow_forward)); - - // TODO: (Move to PickerActivity) Display check for installed models - String modelKey = String.format("%s_%s_%s", packageID, languageID, modelID); - if (KeyboardPickerActivity.containsLexicalModel(context, modelKey)) { // FIXME: - hashMap.put("leftIcon", String.valueOf(R.drawable.ic_check)); - } else { - // Otherwise, include link to .kmp file - hashMap.put(KMManager.KMKey_LexicalModelPackageFilename, modelURL); - } - // TODO: (Move section) END. - - lexicalModelsArrayList.add(new LexicalModel(hashMap)); - } - - return lexicalModelsArrayList; - } catch (JSONException e) { - Log.e("JSONParse", "Error: " + e); - return null; // Is this ideal? + if (!hasConnection) { + // When offline, only use keyboards available from kmp.json + return new ArrayList<>(); } + + // Otherwise, merge kmpLexicalModelsArray with cloud jsonArray + JSONArray models = (lexicalJSON != null) ? lexicalJSON : new JSONArray(); // Start with cloud model list. + + if (models == null) { + return new ArrayList<>(); + } + + return processLexicalModelJSON(models, context); } @Override @@ -335,8 +344,16 @@ public class CloudRepository { } List lexicalModelsArrayList = processLexicalModels(jsonArray.lexicalModelJSON); + // TODO: Filter out any duplicates from already-installed models! + for(LexicalModel model: lexicalModelsArrayList) { + // TODO: Check for duplicates / possible updates! + //if(dataset.lexicalModels.____) // if the model's already in the list, do a thing. + } // TODO: Set the CloudRepository's LexicalModelsAdapter to have the same members. + + // And finish. + success.run(); } } } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Dataset.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Dataset.java index f592e4a233..887e762a1f 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Dataset.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Dataset.java @@ -137,6 +137,7 @@ public class Dataset extends ArrayAdapter { public Dataset(@NonNull Context context) { super(context, 0, new ArrayList()); + this.context = context.getApplicationContext(); keyboards = new Keyboards(context); lexicalModels = new LexicalModels(context); }