diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java index 46bdbb6780..1049fc750d 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java @@ -856,7 +856,8 @@ public class MainActivity extends AppCompatActivity implements OnKeyboardEventLi kbInfo.put(KMManager.KMKey_OskFont, kOskFont); if (i == 0) { if (KMManager.addKeyboard(this, kbInfo)) { - KMManager.setKeyboard(packageID, keyboardID, langId, keyboardName, langName, kFont, kOskFont); + if (!KMKeyboardDownloaderActivity.USE_DOWNLOAD_MANAGER) + KMManager.setKeyboard(packageID, keyboardID, langId, keyboardName, langName, kFont, kOskFont); } } else { KMManager.addKeyboard(this, kbInfo); @@ -864,7 +865,8 @@ public class MainActivity extends AppCompatActivity implements OnKeyboardEventLi } } else { if (KMManager.addKeyboard(this, keyboardInfo)) { - KMManager.setKeyboard(packageID, keyboardID, languageID, keyboardName, languageName, kFont, kOskFont); + if (!KMKeyboardDownloaderActivity.USE_DOWNLOAD_MANAGER) + KMManager.setKeyboard(packageID, keyboardID, languageID, keyboardName, languageName, kFont, kOskFont); } } } else { diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java index 766557ce89..d4c2b036ab 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java @@ -8,6 +8,7 @@ import android.content.Context; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; +import android.widget.Toast; import org.json.JSONArray; import org.json.JSONException; @@ -19,6 +20,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import com.tavultesoft.kmea.data.CloudApiTypes; +import com.tavultesoft.kmea.data.CloudDataJsonUtil; +import com.tavultesoft.kmea.data.CloudDownloadMgr; +import com.tavultesoft.kmea.data.CloudKeyboardDataDownloadCallback; +import com.tavultesoft.kmea.data.CloudKeyboardMetaDataDownloadCallback; +import com.tavultesoft.kmea.data.CloudLexicalPackageDownloadCallback; import com.tavultesoft.kmea.packages.LexicalModelPackageProcessor; import com.tavultesoft.kmea.packages.PackageProcessor; import com.tavultesoft.kmea.util.FileUtils; @@ -29,7 +36,11 @@ import static com.tavultesoft.kmea.KMManager.KMDefault_UndefinedPackageID; public class KMKeyboardDownloaderActivity extends AppCompatActivity { // Bundle Keys + // Cloud + //TODO: Should be removed with the old implementation when downloadmanager impl works + public static boolean USE_DOWNLOAD_MANAGER = true; + public static final String ARG_PKG_ID = "KMKeyboardActivity.pkgID"; public static final String ARG_KB_ID = "KMKeyboardActivity.kbID"; public static final String ARG_LANG_ID = "KMKeyboardActivity.langID"; @@ -66,6 +77,7 @@ public class KMKeyboardDownloaderActivity extends AppCompatActivity { public static final String KMKey_KeyboardBaseURI = "keyboardBaseUri"; public static final String KMKey_FontBaseURI = "fontBaseUri"; + //TODO: use keyboard model class, should not be static private static String pkgID; private static String kbID; private static String langID; @@ -89,43 +101,41 @@ public class KMKeyboardDownloaderActivity extends AppCompatActivity { super.onCreate(savedInstanceState); Bundle bundle = getIntent().getExtras(); - if (bundle != null) { - pkgID = bundle.getString(ARG_PKG_ID); - if (pkgID == null || pkgID.isEmpty()) { - pkgID = KMManager.KMDefault_UndefinedPackageID; - } - langID = bundle.getString(ARG_LANG_ID); - langName = bundle.getString(ARG_LANG_NAME); - - downloadOnlyLexicalModel = bundle.containsKey(ARG_MODEL_URL) && - bundle.getString(ARG_MODEL_URL) != null && - !bundle.getString(ARG_MODEL_URL).isEmpty(); - - if (downloadOnlyLexicalModel) { - modelID = bundle.getString(ARG_MODEL_ID); - modelName = bundle.getString(ARG_MODEL_NAME); - isCustom = false; - url = bundle.getString(ARG_MODEL_URL); - } else { - - kbID = bundle.getString(ARG_KB_ID); - kbName = bundle.getString(ARG_KB_NAME); - isCustom = bundle.getBoolean(ARG_IS_CUSTOM); - - // URL parameters for custom keyboard (if they exist) - customKeyboard = bundle.getString(ARG_KEYBOARD); - customLanguage = bundle.getString(ARG_LANGUAGE); - url = bundle.getString(ARG_URL); - filename = bundle.getString(ARG_FILENAME); - if (filename == null || filename.isEmpty()) { - filename = "unknown"; - } - } - } else { + if (bundle == null) return; + + pkgID = bundle.getString(ARG_PKG_ID); + if (pkgID == null || pkgID.isEmpty()) { + pkgID = KMManager.KMDefault_UndefinedPackageID; + } + langID = bundle.getString(ARG_LANG_ID); + langName = bundle.getString(ARG_LANG_NAME); + + downloadOnlyLexicalModel = bundle.containsKey(ARG_MODEL_URL) && + bundle.getString(ARG_MODEL_URL) != null && + !bundle.getString(ARG_MODEL_URL).isEmpty(); + + if (downloadOnlyLexicalModel) { + modelID = bundle.getString(ARG_MODEL_ID); + modelName = bundle.getString(ARG_MODEL_NAME); + isCustom = false; + url = bundle.getString(ARG_MODEL_URL); + } else { + + kbID = bundle.getString(ARG_KB_ID); + kbName = bundle.getString(ARG_KB_NAME); + isCustom = bundle.getBoolean(ARG_IS_CUSTOM); + + // URL parameters for custom keyboard (if they exist) + customKeyboard = bundle.getString(ARG_KEYBOARD); + customLanguage = bundle.getString(ARG_LANGUAGE); + url = bundle.getString(ARG_URL); + filename = bundle.getString(ARG_FILENAME); + if (filename == null || filename.isEmpty()) { + filename = "unknown"; + } } - Bundle args = new Bundle(); String title = ""; if (url != null) { title = String.format("%s: %s", getString(R.string.custom_keyboard), filename); @@ -162,6 +172,7 @@ public class KMKeyboardDownloaderActivity extends AppCompatActivity { * Used by the download method to handle asynchronous downloading and evaluation of * packages and keyboards. */ + @Deprecated static class DownloadTask extends AsyncTask { static class Result { public final Integer kbdResult; @@ -247,6 +258,8 @@ public class KMKeyboardDownloaderActivity extends AppCompatActivity { String remoteUrl = "",remoteLexicalModelUrl = ""; if (isCustom) { + //TODO: Doesn't work, because this case will end up in an exception during download??? + // See downloadNonKMPKeyboard remoteUrl = url; } else { // Keyman cloud @@ -357,10 +370,9 @@ public class KMKeyboardDownloaderActivity extends AppCompatActivity { } String fontBaseUri = options.optString(KMKey_FontBaseURI, ""); - JSONObject language, keyboard = null; // Keyman cloud keyboard distribution via JSON - language = kbData.optJSONObject(KMKey_Language); + JSONObject language = kbData.optJSONObject(KMKey_Language); if (language == null) { throw new Exception(exceptionStr); } @@ -368,20 +380,8 @@ public class KMKeyboardDownloaderActivity extends AppCompatActivity { langName = language.optString(KMManager.KMKey_Name, ""); JSONArray keyboards = language.getJSONArray(KMKey_LanguageKeyboards); - if (keyboards == null) { - throw new Exception(exceptionStr); - } - // In case keyboards array contains multiple keyboards, get the one with matching keyboard ID - for (int index = 0; index < keyboards.length(); index++) { - keyboard = keyboards.getJSONObject(index); - if (keyboard != null && (kbID.equals(keyboard.getString(KMManager.KMKey_ID)))) { - break; - } - } - if (keyboard == null) { - throw new Exception(exceptionStr); - } + JSONObject keyboard = CloudDataJsonUtil.findMatchingKeyboardByID(keyboards,kbID); kbID = keyboard.getString(KMManager.KMKey_ID); pkgID = keyboard.optString(KMManager.KMKey_PackageID, KMManager.KMDefault_UndefinedPackageID); @@ -400,13 +400,13 @@ public class KMKeyboardDownloaderActivity extends AppCompatActivity { JSONObject jsonOskFont = keyboard.optJSONObject(KMManager.KMKey_OskFont); if (jsonFont != null) { - findTTF(jsonFont); + CloudDataJsonUtil.updateFontSourceToTTFFont(jsonFont); } if (jsonOskFont != null) { - findTTF(jsonOskFont); + CloudDataJsonUtil.updateFontSourceToTTFFont(jsonOskFont); } - ArrayList fontUrls = fontUrls(jsonFont, fontBaseUri, true); - ArrayList oskFontUrls = fontUrls(jsonOskFont, fontBaseUri, true); + ArrayList fontUrls = CloudDataJsonUtil.fontUrls(jsonFont, fontBaseUri, false); + ArrayList oskFontUrls = CloudDataJsonUtil.fontUrls(jsonOskFont, fontBaseUri, true); if (fontUrls != null) urls.addAll(fontUrls); if (oskFontUrls != null) { @@ -502,17 +502,9 @@ public class KMKeyboardDownloaderActivity extends AppCompatActivity { */ protected void notifyListeners(KeyboardEventHandler.EventType eventType, int result) { if (kbDownloadEventListeners != null) { - HashMap keyboardInfo = new HashMap(); - keyboardInfo.put(KMManager.KMKey_PackageID, pkgID); - keyboardInfo.put(KMManager.KMKey_KeyboardID, kbID); - keyboardInfo.put(KMManager.KMKey_LanguageID, langID); - keyboardInfo.put(KMManager.KMKey_KeyboardName, kbName); - keyboardInfo.put(KMManager.KMKey_LanguageName, langName); - keyboardInfo.put(KMManager.KMKey_KeyboardVersion, kbVersion); - keyboardInfo.put(KMManager.KMKey_CustomKeyboard, kbIsCustom); - keyboardInfo.put(KMManager.KMKey_Font, font); - if (oskFont != null) - keyboardInfo.put(KMManager.KMKey_OskFont, oskFont); + HashMap keyboardInfo = + CloudDataJsonUtil.createKeyboardInfoMap(pkgID,langID,langName,kbID,kbName,kbVersion,kbIsCustom,font,oskFont); + KeyboardEventHandler.notifyListeners(kbDownloadEventListeners, eventType, keyboardInfo, result); } } @@ -537,14 +529,152 @@ public class KMKeyboardDownloaderActivity extends AppCompatActivity { * @param showProgressDialog */ public static void download(final Context context, final boolean showProgressDialog) { - new DownloadTask(context, showProgressDialog).execute(); + if(USE_DOWNLOAD_MANAGER) + downloadKeyboardUsingDownloadManager(context); + else + new DownloadTask(context, showProgressDialog).execute(); } public static void download(final Context context, final boolean showProgressDialog, - final boolean donwloadOnlyLexicalModel) { - new DownloadTask(context, showProgressDialog, downloadOnlyLexicalModel).execute(); + final boolean aDownloadOnlyLexicalModel) { + if(USE_DOWNLOAD_MANAGER) + downloadUsingDownloadManager(context,aDownloadOnlyLexicalModel); + else + new DownloadTask(context, showProgressDialog, aDownloadOnlyLexicalModel).execute(); } + /** + * Download in keyboards and lexical model using download manager. + * @param context the context + * @param aDownloadOnlyLexicalModel is lexical model download + */ + private static void downloadUsingDownloadManager(final Context context, + final boolean aDownloadOnlyLexicalModel) + { + if(aDownloadOnlyLexicalModel) + { + downloadLexicalModelUsingDownloadManager(context); + } + else + { + downloadKeyboardUsingDownloadManager(context); + } + } + + /** + * prepare and execute keyboard download using downloadmanager. + * @param context the context + */ + private static void downloadKeyboardUsingDownloadManager(Context context) + { + if (pkgID == null || pkgID.trim().isEmpty() || + (!isCustom && (langID == null || langID.trim().isEmpty() || kbID == null || kbID.trim().isEmpty()))) { + throw new IllegalStateException("Invalid keyboard"); + } + + + + List cloudQueries = getPrepareCloudQueriesForKeyboardDownload(context); + + String _downloadid= CloudKeyboardMetaDataDownloadCallback.createDownloadId(langID , kbID); + + if( CloudDownloadMgr.getInstance().alreadyDownloadingData(_downloadid) + || CloudDownloadMgr.getInstance().alreadyDownloadingData( + CloudKeyboardDataDownloadCallback.createDownloadId(kbID))) + { + Toast.makeText(context, + context.getString(R.string.keyboard_download_is_running_in_background), + Toast.LENGTH_SHORT).show(); + } + else + { + CloudKeyboardMetaDataDownloadCallback _callback = new CloudKeyboardMetaDataDownloadCallback(); + _callback.setDownloadEventListeners(kbDownloadEventListeners); + + Toast.makeText(context, + context.getString(R.string.keyboard_download_start_in_background), + Toast.LENGTH_SHORT).show(); + + CloudDownloadMgr.getInstance().executeAsDownload( + context, _downloadid, null, _callback, + cloudQueries.toArray(new CloudApiTypes.CloudApiParam[0])); + } + + ((AppCompatActivity) context).finish(); + } + + /** + * Prepare the cloud queries for keyboard metadata download. + * @param context the context + * @return the result + */ + private static List getPrepareCloudQueriesForKeyboardDownload(Context context) + { + List cloudQueries = new ArrayList<>(); + + String deviceType = CloudDataJsonUtil.getDeviceTypeForCloudQuery(context); + + if (isCustom) { + //TODO: will end up in an exception during download??? + cloudQueries.add(new CloudApiTypes.CloudApiParam( + CloudApiTypes.ApiTarget.KeyboardData, url)); + } + else + { + // Keyman cloud + String _remoteUrl = String.format("%s/%s/%s?version=%s&device=%s&languageidtype=bcp47", + kKeymanApiBaseURL, langID, kbID, BuildConfig.VERSION_NAME, deviceType); + cloudQueries.add( + new CloudApiTypes.CloudApiParam( + CloudApiTypes.ApiTarget.Keyboard, _remoteUrl) + .setType(CloudApiTypes.JSONType.Object) + .setAdditionalProperty(CloudKeyboardMetaDataDownloadCallback.PARAM_IS_CUSTOM,isCustom) + .setAdditionalProperty(CloudKeyboardMetaDataDownloadCallback.PARAM_LANG_ID,langID) + .setAdditionalProperty(CloudKeyboardMetaDataDownloadCallback.PARAM_KB_ID,kbID)); + + String _remoteLexicalModelUrl = String.format("%s?q=bcp47:%s", kKeymanApiModelURL, langID); + cloudQueries.add(new CloudApiTypes.CloudApiParam( + CloudApiTypes.ApiTarget.KeyboardLexicalModels, _remoteLexicalModelUrl) + .setType(CloudApiTypes.JSONType.Array)); + } + return cloudQueries; + } + + /** + * prepare and execute lexical model download using downloadmanager. + * @param context the context + */ + private static void downloadLexicalModelUsingDownloadManager(Context context) { + + + String _downloadid= CloudLexicalPackageDownloadCallback.createDownloadId(modelID); + + if( CloudDownloadMgr.getInstance().alreadyDownloadingData(_downloadid)) + { + Toast.makeText(context, + context.getString(R.string.dictionary_download_is_running_in_background), + Toast.LENGTH_SHORT).show(); + } + else + { + CloudLexicalPackageDownloadCallback _callback = new CloudLexicalPackageDownloadCallback(); + _callback.setDownloadEventListeners(kbDownloadEventListeners); + + CloudApiTypes.CloudApiParam _param = new CloudApiTypes.CloudApiParam( + CloudApiTypes.ApiTarget.LexicalModelPackage, url); + + Toast.makeText(context, + context.getString(R.string.dictionary_download_start_in_background), + Toast.LENGTH_SHORT).show(); + + CloudDownloadMgr.getInstance().executeAsDownload( + context, _downloadid, null, _callback, _param); + } + + ((AppCompatActivity) context).finish(); + } + + public static boolean isCustom(String u) { boolean ret = false; if (u != null && !u.contains(KMKeyboardDownloaderActivity.kKeymanApiBaseURL) && @@ -554,108 +684,7 @@ public class KMKeyboardDownloaderActivity extends AppCompatActivity { return ret; } - // If a font JSONObject contains multiple font font files, only keep the .ttf source - private static void findTTF(JSONObject jsonFont) { - boolean updateJsonFont = false; - try { - JSONArray fontSource = jsonFont.optJSONArray(KMManager.KMKey_FontSource); - if ((fontSource != null) && hasTTFFont(fontSource)) { - for (int i = fontSource.length() - 1; i >= 0; i--) { - String s = fontSource.getString(i); - if (!FileUtils.isTTFFont(s)) { - updateJsonFont = true; - // remove() was added in API 19 - // https://developer.android.com/reference/org/json/JSONArray#remove(int) - if (Build.VERSION.SDK_INT > 19) { - fontSource.remove(i); - } else { - fontSource = removeJsonObjectAtIndex(fontSource, i); - } - } - } - if (updateJsonFont) { - JSONArray copy = fontSource; - jsonFont.remove(KMManager.KMKey_FontSource); - jsonFont.put(KMManager.KMKey_FontSource, copy); - } - } - } catch (JSONException e) { - Log.e(TAG, "findTTF exception" + e); - } - } - - // Parse the fontSource JSONArray to see if it contains a .ttf font - private static boolean hasTTFFont(JSONArray fontSource) { - try { - for (int i = 0; i < fontSource.length(); i++) { - String s = fontSource.getString(i); - if (FileUtils.isTTFFont(s)) { - return true; - } - } - return false; - } catch (JSONException e) { - Log.e(TAG, "hasTTFFont exception" + e); - return false; - } - } - - // From https://stackoverflow.com/questions/27427999/remove-jsonobeject-before-android-api-lvl-19 - private static JSONArray removeJsonObjectAtIndex(JSONArray source, int index) throws JSONException { - if (index < 0 || index > source.length() - 1) { - throw new IndexOutOfBoundsException(); - } - - final JSONArray copy = new JSONArray(); - for (int i=0, count = source.length(); i fontUrls(JSONObject jsonFont, String baseUri, boolean isOskFont) { - if (jsonFont == null) - return null; - - ArrayList urls = new ArrayList(); - JSONArray fontSource = jsonFont.optJSONArray(KMManager.KMKey_FontSource); - if (fontSource != null) { - int fcCount = fontSource.length(); - for (int i = 0; i < fcCount; i++) { - String fontSourceString; - try { - fontSourceString = fontSource.getString(i); - - if (FileUtils.hasFontExtension(fontSourceString)) { - urls.add(baseUri + fontSourceString); - } else if (isOskFont && FileUtils.hasSVGViewBox(fontSourceString)) { - String fontFilename = FileUtils.getSVGFilename(fontSourceString); - urls.add(baseUri + fontFilename); - } - } catch (JSONException e) { - return null; - } - } - } else { - String fontSourceString; - try { - fontSourceString = jsonFont.getString(KMManager.KMKey_FontSource); - if (FileUtils.hasFontExtension(fontSourceString)) { - urls.add(baseUri + fontSourceString); - } else if (isOskFont && FileUtils.hasSVGViewBox(fontSourceString)) { - String fontFilename = FileUtils.getSVGFilename(fontSourceString); - urls.add(baseUri + fontFilename); - } - } catch (JSONException e) { - return null; - } - } - - return urls; - } public static void addKeyboardDownloadEventListener(KeyboardEventHandler.OnKeyboardDownloadEventListener listener) { if (kbDownloadEventListeners == null) { diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardPickerAdapter.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardPickerAdapter.java index 628e4ef966..848af2b8b0 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardPickerAdapter.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardPickerAdapter.java @@ -74,8 +74,12 @@ final class KMKeyboardPickerAdapter extends NestedAdapter> keyboardsList = null; private static ArrayList> lexicalModelsList = null; @@ -176,6 +179,39 @@ public final class KeyboardPickerActivity extends AppCompatActivity { int curKbPos = getCurrentKeyboardIndex(); setSelection(curKbPos); + + KMKeyboard.addOnKeyboardEventListener(new KeyboardEventHandler.OnKeyboardEventListener() { + @Override + public void onKeyboardLoaded(KMManager.KeyboardType keyboardType) { + + } + + @Override + public void onKeyboardChanged(String newKeyboard) { + int _index = getKeyboardIndex(context,newKeyboard); + if(_index>=0) + { + Map _keyboard = keyboardsList.get(_index); + if(_keyboard==null) + return; + if(_keyboard.get(KMKEY_INTERNAL_NEW_KEYBOARD)==null) + return; + _keyboard.remove(KMKEY_INTERNAL_NEW_KEYBOARD); + saveList(context, KMManager.KMFilename_KeyboardsList); + notifyKeyboardsUpdate(context); + } + } + + @Override + public void onKeyboardShown() { + + } + + @Override + public void onKeyboardDismissed() { + + } + }); } @Override @@ -343,9 +379,12 @@ public final class KeyboardPickerActivity extends AppCompatActivity { if (kbKey.length() >= 3) { int x = getKeyboardIndex(context, kbKey); if (x >= 0) { + if(keyboardsList.get(x).get(KMKEY_INTERNAL_NEW_KEYBOARD)!=null) + keyboardInfo.put(KMKEY_INTERNAL_NEW_KEYBOARD,KMKEY_INTERNAL_NEW_KEYBOARD); keyboardsList.set(x, keyboardInfo); result = saveList(context, KMManager.KMFilename_KeyboardsList); } else { + keyboardInfo.put(KMKEY_INTERNAL_NEW_KEYBOARD,KMKEY_INTERNAL_NEW_KEYBOARD); keyboardsList.add(keyboardInfo); result = saveList(context, KMManager.KMFilename_KeyboardsList); if (!result) { diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageListActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageListActivity.java index 37ff95d90e..0cc20e4fc3 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageListActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageListActivity.java @@ -137,7 +137,9 @@ public final class LanguageListActivity extends AppCompatActivity implements OnK if (!pkgID.equals(KMManager.KMDefault_UndefinedPackageID)) { // Custom keyboard already exists in packages/ so just add the language association KeyboardPickerActivity.addKeyboard(context, kbInfo); - KMManager.setKeyboard(pkgID, kbID, langID, kbName, language.name, kFont, kOskFont); + + if (!KMKeyboardDownloaderActivity.USE_DOWNLOAD_MANAGER) + KMManager.setKeyboard(pkgID, kbID, langID, kbName, language.name, kFont, kOskFont); Toast.makeText(context, "Keyboard installed", Toast.LENGTH_SHORT).show(); setResult(RESULT_OK); ((AppCompatActivity) context).finish(); @@ -207,7 +209,8 @@ public final class LanguageListActivity extends AppCompatActivity implements OnK String kOskFont = keyboardInfo.get(KMManager.KMKey_OskFont); KeyboardPickerActivity.addKeyboard(this, keyboardInfo); - KMManager.setKeyboard(packageID, keyboardID, languageID, keyboardName, languageName, kFont, kOskFont); + if (!KMKeyboardDownloaderActivity.USE_DOWNLOAD_MANAGER) + KMManager.setKeyboard(packageID, keyboardID, languageID, keyboardName, languageName, kFont, kOskFont); if (result == 2) { Toast.makeText(context, context.getString(R.string.font_failed_to_download), Toast.LENGTH_LONG).show(); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java index ef757c6e8e..ad9aca38b4 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java @@ -53,6 +53,7 @@ public final class LanguagesSettingsActivity extends AppCompatActivity // ********* ONLY USED BY UPDATE CODE *********** + //TODO: Refactoring to separate update logic from view private static boolean updateCheckFailed = false; private static boolean updateFailed = false; private static Calendar lastUpdateCheck = null; diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudApiTypes.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudApiTypes.java index 7cb8f787bf..ecc758e2eb 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudApiTypes.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudApiTypes.java @@ -9,8 +9,10 @@ import org.json.JSONObject; import java.io.File; import java.util.Collections; +import java.util.HashMap; import java.util.LinkedList; import java.util.List; +import java.util.Map; public class CloudApiTypes { protected static class CloudApiReturns { @@ -33,25 +35,64 @@ public class CloudApiTypes { } - protected enum ApiTarget { + public enum ApiTarget { + /** + * Catalog download: available keyboards including meta data. + */ Keyboards, - LexicalModels + /** + * Catalog download: available lexical models including meta data. + */ + LexicalModels, + /** + * Keyboard download: keyboard meta data for the selected keyboard. + */ + Keyboard, + /** + * Keyboard download: lexical models meta data for the language of the selected keyboard. + */ + KeyboardLexicalModels, + /** + * Keyboard download: download keyboard data package and fonts. + */ + KeyboardData, + /** + * Lexical download: download lexical model package + * Used for single lexical model download and + * automatic lexical model download during keyboard download + */ + LexicalModelPackage, } - protected enum JSONType { + public enum JSONType { Array, Object } - protected static class CloudApiParam { + public static class CloudApiParam { public final ApiTarget target; public final String url; - public final JSONType type; + public JSONType type; + private Map additionalProperties = new HashMap<>(); - CloudApiParam(ApiTarget target, String url, JSONType type) { + public CloudApiParam(ApiTarget target, String url) { this.target = target; this.url = url; + } + + public CloudApiParam setType(JSONType type) { this.type = type; + return this; + } + public CloudApiParam setAdditionalProperty(String aProperty, Object aValue) + { + additionalProperties.put(aProperty,aValue); + return this; + } + + public T getAdditionalProperty(String aProperty,Class aType) + { + return (T)additionalProperties.get(aProperty); } } @@ -61,8 +102,7 @@ public class CloudApiTypes { private boolean downloadFinished =false; private long downloadId; private File destinationFile; - private CloudApiTypes.JSONType type; - private CloudApiTypes.ApiTarget target; + private CloudApiParam cloudParams; public SingleCloudDownload(DownloadManager.Request aRequest,File aDestinationFile) { @@ -74,13 +114,8 @@ public class CloudApiTypes { return this; } - public SingleCloudDownload setJsonType(JSONType type) { - this.type = type; - return this; - } - - public SingleCloudDownload setTarget(ApiTarget target) { - this.target = target; + public SingleCloudDownload setCloudParams(CloudApiParam params) { + this.cloudParams = params; return this; } @@ -96,12 +131,8 @@ public class CloudApiTypes { return destinationFile; } - public JSONType getType() { - return type; - } - - public ApiTarget getTarget() { - return target; + public CloudApiParam getCloudParams() { + return cloudParams; } } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudCatalogDownloadCallback.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudCatalogDownloadCallback.java index 8d3e9a2269..cffbd79627 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudCatalogDownloadCallback.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudCatalogDownloadCallback.java @@ -229,6 +229,11 @@ public class CloudCatalogDownloadCallback implements ICloudDownloadCallback retrievedJSON = new ArrayList<>(aDownload.getSingleDownloads().size()); - for (CloudApiTypes.SingleCloudDownload _d : aDownload.getSingleDownloads()) { - JSONParser jsonParser = new JSONParser(); - JSONArray dataArray = null; - JSONObject dataObject = null; - if (_d.getDestinationFile() != null && _d.getDestinationFile().length() > 0) { - try { + CloudApiTypes.CloudApiReturns _json_result = CloudDataJsonUtil.retrieveJsonFromDownload(_d); - if (_d.getType() == CloudApiTypes.JSONType.Array) { - dataArray = jsonParser.getJSONObjectFromFile(_d.getDestinationFile(),JSONArray.class); - } else { - dataObject = jsonParser.getJSONObjectFromFile(_d.getDestinationFile(),JSONObject.class); - } - } catch (Exception e) { - Log.d(TAG, e.getMessage()); - } finally { - _d.getDestinationFile().delete(); - } - } else { - // Offline trouble! That said, we can't get anything, so we simply shouldn't add anything. - } - - if (_d.getType() == CloudApiTypes.JSONType.Array) { - retrievedJSON.add(new CloudApiTypes.CloudApiReturns(_d.getTarget(), dataArray)); // Null if offline. - } else { - retrievedJSON.add(new CloudApiTypes.CloudApiReturns(_d.getTarget(), dataObject)); // Null if offline. - } + if (_json_result!=null) + retrievedJSON.add(_json_result); // Null if offline. } + return new CloudCatalogDownloadReturns(retrievedJSON); } } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudDataJsonUtil.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudDataJsonUtil.java index aabd98061a..3072627d0d 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudDataJsonUtil.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudDataJsonUtil.java @@ -1,8 +1,10 @@ package com.tavultesoft.kmea.data; import android.content.Context; +import android.os.Build; import android.util.Log; +import com.tavultesoft.kmea.JSONParser; import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; import com.tavultesoft.kmea.R; @@ -29,6 +31,27 @@ public class CloudDataJsonUtil { { //no instances } + + public static HashMap createKeyboardInfoMap(String aPackageId,String aLanguageId, String aLanguageName, String aKeyboardId, + String aKeyboardName, String aKeyboardVersion, String anIsCustomKeyboard, + String aFont, String aOskFont) + { + HashMap keyboardInfo = new HashMap(); + if(aPackageId!=null) + keyboardInfo.put(KMManager.KMKey_PackageID, aPackageId); + keyboardInfo.put(KMManager.KMKey_KeyboardID, aKeyboardId); + keyboardInfo.put(KMManager.KMKey_LanguageID, aLanguageId); + keyboardInfo.put(KMManager.KMKey_KeyboardName, aKeyboardName); + keyboardInfo.put(KMManager.KMKey_LanguageName, aLanguageName); + keyboardInfo.put(KMManager.KMKey_KeyboardVersion, aKeyboardVersion); + keyboardInfo.put(KMManager.KMKey_CustomKeyboard, anIsCustomKeyboard); + keyboardInfo.put(KMManager.KMKey_Font, aFont); + if (aOskFont != null) + keyboardInfo.put(KMManager.KMKey_OskFont, aOskFont); + + return keyboardInfo; + } + static List processKeyboardJSON(JSONObject query, boolean fromKMP) { List keyboardsList = new ArrayList<>(); //keyboardModifiedDates = new HashMap(); @@ -55,14 +78,8 @@ public class CloudDataJsonUtil { String kbFont = keyboardJSON.optString(KMManager.KMKey_Font, ""); //String kbKey = String.format("%s_%s", langID, kbID); - HashMap hashMap = new HashMap(); - hashMap.put(KMManager.KMKey_KeyboardName, kbName); - hashMap.put(KMManager.KMKey_KeyboardID, kbID); - hashMap.put(KMManager.KMKey_LanguageName, langName); - hashMap.put(KMManager.KMKey_LanguageID, langID); - hashMap.put(KMManager.KMKey_KeyboardVersion, kbVersion); - hashMap.put(KMManager.KMKey_CustomKeyboard, isCustom); - hashMap.put(KMManager.KMKey_Font, kbFont); + HashMap hashMap = createKeyboardInfoMap(null,langID,langName,kbID,kbName,kbVersion,isCustom,kbFont,null); + // if (keyboardModifiedDates.get(kbID) == null) { // keyboardModifiedDates.put(kbID, keyboardJSON.getString(KMManager.KMKey_KeyboardModified)); @@ -220,4 +237,187 @@ public class CloudDataJsonUtil { final String jsonLexicalCacheFilename = "jsonLexicalModelsCache.dat"; return new File(context.getCacheDir(), jsonLexicalCacheFilename); } + + /** + * retrieve a json object from a downloaded file. + * @param aDownload the download + * @return the result + */ + public static CloudApiTypes.CloudApiReturns retrieveJsonFromDownload( CloudApiTypes.SingleCloudDownload aDownload) + { + JSONParser jsonParser = new JSONParser(); + JSONArray dataArray = null; + JSONObject dataObject = null; + + if (aDownload.getDestinationFile() != null && aDownload.getDestinationFile().length() > 0) { + try { + + if (aDownload.getCloudParams().type == CloudApiTypes.JSONType.Array) { + dataArray = jsonParser.getJSONObjectFromFile(aDownload.getDestinationFile(),JSONArray.class); + } else { + dataObject = jsonParser.getJSONObjectFromFile(aDownload.getDestinationFile(),JSONObject.class); + } + } catch (Exception e) { + Log.d(TAG, e.getMessage()); + } finally { + aDownload.getDestinationFile().delete(); + } + } else { + // Offline trouble! That said, we can't get anything, so we simply shouldn't add anything. + } + + if (aDownload.getCloudParams().type == CloudApiTypes.JSONType.Array) + { + if(dataArray!=null) + return new CloudApiTypes.CloudApiReturns(aDownload.getCloudParams().target, dataArray); // Null if offline. + return null; + } + if(dataObject!=null) + return new CloudApiTypes.CloudApiReturns(aDownload.getCloudParams().target, dataObject); // Null if offline. + + + return null; + } + + // If a font JSONObject contains multiple font font files, only keep the .ttf source + public static void updateFontSourceToTTFFont(JSONObject jsonFont) { + boolean updateJsonFont = false; + try { + JSONArray fontSource = jsonFont.optJSONArray(KMManager.KMKey_FontSource); + if ((fontSource != null) && hasTTFFont(fontSource)) { + for (int i = fontSource.length() - 1; i >= 0; i--) { + String s = fontSource.getString(i); + if (!FileUtils.isTTFFont(s)) { + updateJsonFont = true; + // remove() was added in API 19 + // https://developer.android.com/reference/org/json/JSONArray#remove(int) + if (Build.VERSION.SDK_INT > 19) { + fontSource.remove(i); + } else { + fontSource = removeJsonObjectAtIndex(fontSource, i); + } + } + } + + if (updateJsonFont) { + JSONArray copy = fontSource; + jsonFont.remove(KMManager.KMKey_FontSource); + jsonFont.put(KMManager.KMKey_FontSource, copy); + } + } + } catch (JSONException e) { + Log.e(TAG, "findTTF exception" + e); + } + } + + // Parse the fontSource JSONArray to see if it contains a .ttf font + private static boolean hasTTFFont(JSONArray fontSource) { + try { + for (int i = 0; i < fontSource.length(); i++) { + String s = fontSource.getString(i); + if (FileUtils.isTTFFont(s)) { + return true; + } + } + return false; + } catch (JSONException e) { + Log.e(TAG, "hasTTFFont exception" + e); + return false; + } + } + + // From https://stackoverflow.com/questions/27427999/remove-jsonobeject-before-android-api-lvl-19 + private static JSONArray removeJsonObjectAtIndex(JSONArray source, int index) throws JSONException { + if (index < 0 || index > source.length() - 1) { + throw new IndexOutOfBoundsException(); + } + + final JSONArray copy = new JSONArray(); + for (int i=0, count = source.length(); i fontUrls(JSONObject jsonFont, String baseUri, boolean isOskFont) { + if (jsonFont == null) + return null; + + ArrayList urls = new ArrayList(); + JSONArray fontSource = jsonFont.optJSONArray(KMManager.KMKey_FontSource); + if (fontSource != null) { + int fcCount = fontSource.length(); + for (int i = 0; i < fcCount; i++) { + String fontSourceString; + try { + fontSourceString = fontSource.getString(i); + + if (FileUtils.hasFontExtension(fontSourceString)) { + urls.add(baseUri + fontSourceString); + } else if (isOskFont && FileUtils.hasSVGViewBox(fontSourceString)) { + String fontFilename = FileUtils.getSVGFilename(fontSourceString); + urls.add(baseUri + fontFilename); + } + } catch (JSONException e) { + return null; + } + } + } else { + String fontSourceString; + try { + fontSourceString = jsonFont.getString(KMManager.KMKey_FontSource); + if (FileUtils.hasFontExtension(fontSourceString)) { + urls.add(baseUri + fontSourceString); + } else if (isOskFont && FileUtils.hasSVGViewBox(fontSourceString)) { + String fontFilename = FileUtils.getSVGFilename(fontSourceString); + urls.add(baseUri + fontFilename); + } + } catch (JSONException e) { + return null; + } + } + + return urls; + } + + public static JSONObject findMatchingKeyboardByID(JSONArray aKeyboards, String aKbId) + throws IllegalStateException, JSONException + { + if (aKeyboards == null) { + throw new IllegalStateException("Keyboard array is empty"); + } + + JSONObject keyboard = null; + // In case keyboards array contains multiple keyboards, get the one with matching keyboard ID + for (int index = 0; index < aKeyboards.length(); index++) { + keyboard = aKeyboards.getJSONObject(index); + if (keyboard != null && (aKbId.equals(keyboard.getString(KMManager.KMKey_ID)))) { + break; + } + } + if (keyboard == null) { + throw new IllegalStateException("could not find matching keyboard"); + } + + return keyboard; + } + + /** + * select device type for api queries. + * @param aContext a context + * @return the result + */ + public static String getDeviceTypeForCloudQuery(Context aContext) + { + String deviceType = aContext.getString(R.string.device_type); + if (deviceType.equals("AndroidTablet")) { + deviceType = "androidtablet"; + } else { + deviceType = "androidphone"; + } + return deviceType; + } + } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudDownloadMgr.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudDownloadMgr.java index 4ddbe25994..321b281131 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudDownloadMgr.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudDownloadMgr.java @@ -189,6 +189,9 @@ public class CloudDownloadMgr{ DownloadManager downloadManager = (DownloadManager) aContext.getSystemService(Context.DOWNLOAD_SERVICE); + + aCallback.initializeContext(aContext); + CloudApiTypes.CloudDownloadSet _downloadSet = new CloudApiTypes.CloudDownloadSet( aDownloadIdentifier,aTargetModel); @@ -232,7 +235,6 @@ public class CloudDownloadMgr{ //.setAllowedOverRoaming(true);// Set if download is allowed on roaming network return new CloudApiTypes.SingleCloudDownload(_request,_file) - .setJsonType(aParam.type) - .setTarget(aParam.target); + .setCloudParams(aParam); } } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudKeyboardDataDownloadCallback.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudKeyboardDataDownloadCallback.java new file mode 100644 index 0000000000..36b144d26a --- /dev/null +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudKeyboardDataDownloadCallback.java @@ -0,0 +1,138 @@ +package com.tavultesoft.kmea.data; + +import android.content.Context; +import android.util.Log; +import android.widget.Toast; + +import com.tavultesoft.kmea.KeyboardEventHandler; +import com.tavultesoft.kmea.R; +import com.tavultesoft.kmea.util.FileUtils; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; + +import static com.tavultesoft.kmea.KMManager.KMDefault_UndefinedPackageID; + +/** + * Install keyboard data, when download is finished. + * Could be keyboard packages or fonts. + */ +public class CloudKeyboardDataDownloadCallback implements ICloudDownloadCallback< + Void, CloudKeyboardDownloadReturns> +{ + + private static final String TAG = "CloudkbDataDldCb"; + + private File dataDir; + + private ArrayList downloadEventListeners = new ArrayList<>(); + private HashMap keyboardInfo; + + /** + * Additional Cloud API parameter: + * Parameter to force a special destination file name during installation. + * Default name is the file name of the url. + */ + public static final String PARAM_DESTINATION_FILE_NAME = "destination_file_name"; + + /** + * Additional Cloud API parameter: + * Parameter to force a special destination file name during installation. + * Default name is the file name of the url. + */ + public static final String PARAM_PACKAGE = "package"; + + /** + * listeners to inform after installation is completed. + * @param aDownloadEventListeners the listeners + */ + public void setDownloadEventListeners(ArrayList aDownloadEventListeners) + { + downloadEventListeners.clear(); + downloadEventListeners.addAll(aDownloadEventListeners); + } + + /** + * Keyboard meta data. + * @param aKeyboardInfo the keyboard + */ + public void setKeyboardInfo(HashMap aKeyboardInfo) { + this.keyboardInfo = aKeyboardInfo; + } + + @Override + public void initializeContext(Context context) + { + dataDir = context.getDir("data", Context.MODE_PRIVATE); + } + + @Override + public CloudKeyboardDownloadReturns extractCloudResultFromDownloadSet( + CloudApiTypes.CloudDownloadSet aDownload) + { + int _result = FileUtils.DOWNLOAD_SUCCESS; + for(CloudApiTypes.SingleCloudDownload _d:aDownload.getSingleDownloads()) + { + if (_d.getDestinationFile() != null && _d.getDestinationFile().length() > 0) + { + + try { + + String _pkg = _d.getCloudParams().getAdditionalProperty(PARAM_PACKAGE,String.class); + String destination = dataDir.toString() + + File.separator + _pkg + File.separator; + + String _filename = _d.getCloudParams().getAdditionalProperty(PARAM_DESTINATION_FILE_NAME,String.class); + if (_filename==null) { + + _filename = FileUtils.getFilename(_d.getCloudParams().url); + } + + File _data_file = new File(destination,_filename); + FileUtils.copy(_d.getDestinationFile(), _data_file); + + } + catch (IOException _e) + { + Log.e(TAG,_e.getLocalizedMessage(),_e); + } + } + else + { + if (FileUtils.hasFontExtension(_d.getCloudParams().url)) + _result = 2; + else + _result = FileUtils.DOWNLOAD_ERROR; + } + } + return new CloudKeyboardDownloadReturns(_result); + } + + + + @Override + public void applyCloudDownloadToModel(Context aContext, Void aModel, CloudKeyboardDownloadReturns aCloudResult) + { + Toast.makeText(aContext, + aContext.getString(R.string.keyboard_download_finished), + Toast.LENGTH_SHORT).show(); + + if(keyboardInfo!=null) + { + KeyboardEventHandler.notifyListeners(downloadEventListeners, KeyboardEventHandler.EventType.KEYBOARD_DOWNLOAD_FINISHED, + keyboardInfo, aCloudResult.kbdResult); + } + } + + /** + * create a download id for the keyboard data. + * @param aKeyboardId the keyboard id + * @return the result + */ + public static String createDownloadId(String aKeyboardId) + { + return "keyboarddata_" + aKeyboardId; + } +} diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudKeyboardDownloadReturns.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudKeyboardDownloadReturns.java new file mode 100644 index 0000000000..8aaaa21d84 --- /dev/null +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudKeyboardDownloadReturns.java @@ -0,0 +1,21 @@ +package com.tavultesoft.kmea.data; + +import java.util.List; +import java.util.Map; + +/** + * key board and lexical model download result. + */ +public class CloudKeyboardDownloadReturns { + public final Integer kbdResult; + public final List> installedLexicalModels; + + public CloudKeyboardDownloadReturns(Integer i) { + this(i, null); + } + + public CloudKeyboardDownloadReturns(Integer i, List> installedLexicalModels) { + this.kbdResult = i; + this.installedLexicalModels = installedLexicalModels; + } +} diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudKeyboardMetaDataDownloadCallback.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudKeyboardMetaDataDownloadCallback.java new file mode 100644 index 0000000000..6971be7bb3 --- /dev/null +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudKeyboardMetaDataDownloadCallback.java @@ -0,0 +1,349 @@ +package com.tavultesoft.kmea.data; + +import android.content.Context; +import android.util.Log; +import android.widget.Toast; + +import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; +import com.tavultesoft.kmea.KMManager; +import com.tavultesoft.kmea.KeyboardEventHandler; +import com.tavultesoft.kmea.R; +import com.tavultesoft.kmea.util.FileUtils; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +/** + * Start the keyboard download when keyboard metadata is downloaded. + */ +public class CloudKeyboardMetaDataDownloadCallback implements ICloudDownloadCallback> +{ + /** + * the metadata result and all necessary downloads which should be started. + */ + public static class MetaDataResult + { + CloudApiTypes.CloudApiReturns returnjson; + CloudApiTypes.CloudApiParam params; + HashMap keyboardInfo; + String additionalDownloadid; + List additionalDownloads; + } + + private static final String TAG = "CloudKeyboardMetaDldCb"; + + private ArrayList downloadEventListeners = new ArrayList<>(); + + /** + * Additional Cloud API parameter: Is custom keyboard. + */ + public static final String PARAM_IS_CUSTOM = "is_custom"; + /** + * Additional Cloud API parameter: language id. + */ + public static final String PARAM_LANG_ID = "lang_id"; + /** + * Additional Cloud API parameter: keyboard id. + */ + public static final String PARAM_KB_ID = "kb_id"; + + /** + * Listeners to notify about starting the data download. + * @param aDownloadEventListeners + */ + public void setDownloadEventListeners(ArrayList aDownloadEventListeners) { + downloadEventListeners.clear(); + downloadEventListeners.addAll(aDownloadEventListeners); + } + + @Override + public void initializeContext(Context context) { + + } + + @Override + public List extractCloudResultFromDownloadSet( + CloudApiTypes.CloudDownloadSet> aDownload) { + + List _result = new ArrayList<>(aDownload.getSingleDownloads().size()); + + for (CloudApiTypes.SingleCloudDownload _d : aDownload.getSingleDownloads()) { + + CloudApiTypes.CloudApiReturns _json_result = CloudDataJsonUtil.retrieveJsonFromDownload(_d); + + if (_json_result!=null) { + + CloudKeyboardMetaDataDownloadCallback.MetaDataResult _data = new CloudKeyboardMetaDataDownloadCallback.MetaDataResult(); + _data.returnjson = _json_result; + _data.params = _d.getCloudParams(); + _result.add(_data); // Null if offline. + } + } + return _result; + } + + + + @Override + public void applyCloudDownloadToModel(Context aContext, Void aModel, List aCloudResult) + { + if(aCloudResult.isEmpty()) { + String msg = aContext.getString(R.string.catalog_unavailable); + Toast.makeText(aContext, msg, Toast.LENGTH_SHORT).show(); + //throw new IllegalStateException("Could not reach server"); + return; + } + + processCloudResults(aCloudResult); + + startDownloads(aContext, aCloudResult); + } + + /** + * Start the keyboard data and lexical model download. + * @param aContext the context + * @param aMetaDataResult the meta data result + */ + private void startDownloads(Context aContext, List aMetaDataResult) { + for(MetaDataResult _r:aMetaDataResult) + { + if(_r.additionalDownloads!=null) + { + if(_r.returnjson.target== CloudApiTypes.ApiTarget.Keyboard) + { + CloudKeyboardDataDownloadCallback _callback = new CloudKeyboardDataDownloadCallback(); + _callback.setDownloadEventListeners(downloadEventListeners); + _callback.setKeyboardInfo(_r.keyboardInfo); + + if( CloudDownloadMgr.getInstance().alreadyDownloadingData(_r.additionalDownloadid)) + { + continue; + } + + KeyboardEventHandler.notifyListeners(downloadEventListeners, + KeyboardEventHandler.EventType.KEYBOARD_DOWNLOAD_STARTED, _r.keyboardInfo, 0); + + CloudDownloadMgr.getInstance().executeAsDownload(aContext, _r.additionalDownloadid, null, _callback, + _r.additionalDownloads.toArray(new CloudApiTypes.CloudApiParam[0])); + } + else if(_r.returnjson.target== CloudApiTypes.ApiTarget.KeyboardLexicalModels) { + + if( CloudDownloadMgr.getInstance().alreadyDownloadingData(_r.additionalDownloadid)) + { + Toast.makeText(aContext, + aContext.getString(R.string.dictionary_download_is_running_in_background), + Toast.LENGTH_SHORT).show(); + continue; + } + CloudLexicalPackageDownloadCallback _callback = new CloudLexicalPackageDownloadCallback(); + _callback.setDownloadEventListeners(downloadEventListeners); + + Toast.makeText(aContext, + aContext.getString(R.string.dictionary_download_start_in_background), + Toast.LENGTH_SHORT).show(); + + CloudDownloadMgr.getInstance().executeAsDownload(aContext, + _r.additionalDownloadid, null, _callback, + _r.additionalDownloads.toArray(new CloudApiTypes.CloudApiParam[0])); + } + } + } + } + + /** + * process the meta data result and prepare the additional downloads. + * @param aMetaDataResult the meta data results + */ + private void processCloudResults(List aMetaDataResult) { + for(MetaDataResult _r:aMetaDataResult) + { + if(_r.returnjson.target== CloudApiTypes.ApiTarget.Keyboard) + { + handleKeyboardMetaData(_r); + } + if(_r.returnjson.target== CloudApiTypes.ApiTarget.KeyboardLexicalModels) + { + JSONArray lmData = _r.returnjson.jsonArray; + if (lmData != null && lmData.length() > 0) { + try + { + JSONObject modelInfo = lmData.getJSONObject(0); + + if (modelInfo.has("packageFilename") && modelInfo.has("id")) + { + String _modelID = modelInfo.getString("id"); + ArrayList urls = new ArrayList<>(); + urls.add(new CloudApiTypes.CloudApiParam( + CloudApiTypes.ApiTarget.LexicalModelPackage, + modelInfo.getString("packageFilename"))); + _r.additionalDownloadid = CloudLexicalPackageDownloadCallback.createDownloadId(_modelID); + _r.additionalDownloads= urls; + } + } catch (JSONException e) { + Log.e(TAG, "Error parsing lexical model from api.keyman.com. " + e); + } + } + } + } + } + + /** + * handle meta data result. + * @param theKbData the data + */ + private void handleKeyboardMetaData(MetaDataResult theKbData) + { + if (theKbData.params.getAdditionalProperty(PARAM_IS_CUSTOM,Boolean.class)) { + throw new IllegalStateException("Cannot download custom non-KMP keyboard"); + } + + String _key_id = theKbData.params.getAdditionalProperty(PARAM_KB_ID,String.class); + String _lang_id = theKbData.params.getAdditionalProperty(PARAM_LANG_ID,String.class); + + String _kbIsCustom = + theKbData.params.getAdditionalProperty(PARAM_IS_CUSTOM,Boolean.class) ? "Y" : "N"; + JSONObject _kb_data = theKbData.returnjson.jsonObject; + + try { + JSONObject options = _kb_data.optJSONObject( + KMKeyboardDownloaderActivity.KMKey_Options); + if (options == null) { + throw new IllegalStateException("JSON file does not contain a valid \"options\" object"); + } + + + // Keyman cloud _keyboard distribution via JSON + JSONObject language = _kb_data.optJSONObject( + KMKeyboardDownloaderActivity.KMKey_Language); + if (language == null) { + throw new IllegalStateException("JSON file does not contain a valid \"language\" object"); + } + + String _langName = language.optString(KMManager.KMKey_Name, ""); + + JSONArray keyboards = language.getJSONArray(KMKeyboardDownloaderActivity.KMKey_LanguageKeyboards); + + JSONObject _keyboard = CloudDataJsonUtil.findMatchingKeyboardByID(keyboards, _key_id); + + _key_id = _keyboard.getString(KMManager.KMKey_ID); + String _pkgID = _keyboard.optString(KMManager.KMKey_PackageID, KMManager.KMDefault_UndefinedPackageID); + + String _kbName = _keyboard.optString(KMManager.KMKey_Name, ""); + String _kbVersion = _keyboard.optString(KMManager.KMKey_KeyboardVersion, "1.0"); + + if (_kbName.isEmpty() || _langName.isEmpty()) + throw new IllegalStateException("JSON file does not contain a valid base values for _keyboard object"); + + ArrayList urls = new ArrayList<>(); + + urls.add(prepareKeyboardPackageDownload(options, _keyboard)); + + JSONObject jsonFont = _keyboard.optJSONObject(KMManager.KMKey_Font); + JSONObject jsonOskFont = _keyboard.optJSONObject(KMManager.KMKey_OskFont); + + if (jsonFont != null) { + CloudDataJsonUtil.updateFontSourceToTTFFont(jsonFont); + } + if (jsonOskFont != null) { + CloudDataJsonUtil.updateFontSourceToTTFFont(jsonOskFont); + } + String fontBaseUri = options.optString(KMKeyboardDownloaderActivity.KMKey_FontBaseURI, ""); + + ArrayList fontUrls = CloudDataJsonUtil.fontUrls(jsonFont, fontBaseUri, false); + ArrayList oskFontUrls = CloudDataJsonUtil.fontUrls(jsonOskFont, fontBaseUri, true); + if (fontUrls != null) { + for (String url : fontUrls) { + urls.add(new CloudApiTypes.CloudApiParam(CloudApiTypes.ApiTarget.KeyboardData, url) + .setAdditionalProperty( + CloudKeyboardDataDownloadCallback.PARAM_PACKAGE, _pkgID)); + } + } + if (oskFontUrls != null) { + for (String url : oskFontUrls) { + if (!urls.contains(url)) + urls.add(new CloudApiTypes.CloudApiParam(CloudApiTypes.ApiTarget.KeyboardData, url) + .setAdditionalProperty( + CloudKeyboardDataDownloadCallback.PARAM_PACKAGE, _pkgID)); + ; + } + } + + String _font = _keyboard.optString(KMManager.KMKey_Font); + String _oskFont = _keyboard.optString(KMManager.KMKey_OskFont); + + + theKbData.additionalDownloadid = CloudKeyboardDataDownloadCallback.createDownloadId(_key_id); + theKbData.keyboardInfo = CloudDataJsonUtil + .createKeyboardInfoMap( + _pkgID, _lang_id, _langName, _key_id, _kbName, _kbVersion, _kbIsCustom, _font, _oskFont); + theKbData.additionalDownloads = urls; + } + catch(JSONException _e) + { + Log.e(TAG,_e.getLocalizedMessage(),_e); + } + } + + /** + * prepare keyboard package download. + * @param aOptions the options from json + * @param aKeyboard the keyboard + * @return the result + */ + private CloudApiTypes.CloudApiParam prepareKeyboardPackageDownload(JSONObject aOptions, JSONObject aKeyboard) + { + String kbBaseUri = aOptions.optString(KMKeyboardDownloaderActivity.KMKey_KeyboardBaseURI, ""); + if (kbBaseUri.isEmpty()) { + throw new IllegalStateException("JSON file does not contain a valid \"keyboardBaseUri\" object"); + } + + String _kbVersion = aKeyboard.optString(KMManager.KMKey_KeyboardVersion, "1.0"); + String _kbFilename = aKeyboard.optString(KMKeyboardDownloaderActivity.KMKey_Filename, ""); + + if (_kbFilename.isEmpty()) + throw new IllegalStateException("JSON file does not contain a valid \"filename\" object"); + + String _js_filename = _kbFilename; + if (FileUtils.hasJavaScriptExtension(_kbFilename)) { + + int start = _kbFilename.lastIndexOf("/"); + if (start < 0) { + start = 0; + } else { + start++; + } + if (!_kbFilename.contains("-")) { + _js_filename = _kbFilename.substring(start, _kbFilename.length() - 3) + "-" + _kbVersion + ".js"; + } else { + _js_filename = _kbFilename.substring(start); + } + } + + String _pkgID = aKeyboard.optString(KMManager.KMKey_PackageID, KMManager.KMDefault_UndefinedPackageID); + String kbUrl = kbBaseUri + _kbFilename; + + return new CloudApiTypes.CloudApiParam(CloudApiTypes.ApiTarget.KeyboardData, kbUrl) + .setAdditionalProperty( + CloudKeyboardDataDownloadCallback.PARAM_PACKAGE, _pkgID) + .setAdditionalProperty( + CloudKeyboardDataDownloadCallback.PARAM_DESTINATION_FILE_NAME, _js_filename); + } + + /** + * create a download id for the keyboard metadata. + * @param aLanguageId the language id + * @param aKeyboardId the keyboard id + * @return the result + */ + public static String createDownloadId(String aLanguageId, String aKeyboardId) + { + return "metadata_" + aLanguageId + "_" + aKeyboardId; + } + +} diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudLexicalPackageDownloadCallback.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudLexicalPackageDownloadCallback.java new file mode 100644 index 0000000000..91106ad61a --- /dev/null +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudLexicalPackageDownloadCallback.java @@ -0,0 +1,125 @@ +package com.tavultesoft.kmea.data; + +import android.content.Context; +import android.util.Log; +import android.widget.Toast; + +import androidx.appcompat.app.AppCompatActivity; + +import com.tavultesoft.kmea.KeyboardEventHandler; +import com.tavultesoft.kmea.R; +import com.tavultesoft.kmea.packages.LexicalModelPackageProcessor; +import com.tavultesoft.kmea.packages.PackageProcessor; +import com.tavultesoft.kmea.util.FileUtils; + +import org.json.JSONException; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import static com.tavultesoft.kmea.KMManager.KMDefault_UndefinedPackageID; + +/** + * Install lexical model. + */ +public class CloudLexicalPackageDownloadCallback implements ICloudDownloadCallback< + Void, CloudKeyboardDownloadReturns> +{ + + private static final String TAG = "CloudLexModelPKGDldCb"; + + private File resourceRoot; + private File cacheDir; + + private ArrayList downloadEventListeners = new ArrayList<>(); + + /** + * listeners to inform after installation is completed. + * @param aDownloadEventListeners the listeners + */ + public void setDownloadEventListeners(ArrayList aDownloadEventListeners) + { + downloadEventListeners.clear(); + downloadEventListeners.addAll(aDownloadEventListeners); + } + + @Override + public void initializeContext(Context context) + { + resourceRoot = new File(context.getDir("data", Context.MODE_PRIVATE).toString() + File.separator); + cacheDir = context.getCacheDir(); + } + + @Override + public CloudKeyboardDownloadReturns extractCloudResultFromDownloadSet( + CloudApiTypes.CloudDownloadSet aDownload) + { + LexicalModelPackageProcessor kmpProcessor = new LexicalModelPackageProcessor(resourceRoot); + List> installedLexicalModels = null; + + int _result = FileUtils.DOWNLOAD_SUCCESS; + for(CloudApiTypes.SingleCloudDownload _d:aDownload.getSingleDownloads()) + { + if (_d.getDestinationFile() != null && _d.getDestinationFile().length() > 0) + { + + try { + + if (_d.getCloudParams().target== CloudApiTypes.ApiTarget.LexicalModelPackage) { + installedLexicalModels = new LinkedList<>(); + // Extract the kmp. Validate it contains only lexical models, and then process the lexical model package + File kmpFile = new File(cacheDir, FileUtils.getFilename(_d.getCloudParams().url)); + + FileUtils.copy(_d.getDestinationFile(), kmpFile); + + String pkgTarget = kmpProcessor.getPackageTarget(kmpFile); + if (pkgTarget.equals(PackageProcessor.PP_TARGET_LEXICAL_MODELS)) { + File unzipPath = kmpProcessor.unzipKMP(kmpFile); + installedLexicalModels.addAll(kmpProcessor.processKMP(kmpFile, unzipPath, PackageProcessor.PP_LEXICAL_MODELS_KEY)); + } + } + } + catch (IOException | JSONException _e) + { + Log.e(TAG,_e.getLocalizedMessage(),_e); + } + } + else + { + _result = FileUtils.DOWNLOAD_ERROR; + } + } + return new CloudKeyboardDownloadReturns(_result,installedLexicalModels); + } + + + + @Override + public void applyCloudDownloadToModel(Context aContext, Void aModel, CloudKeyboardDownloadReturns aCloudResult) + { + Toast.makeText(aContext, + aContext.getString(R.string.dictionary_download_finished), + Toast.LENGTH_SHORT).show(); + + if(aCloudResult.installedLexicalModels != null) + { + KeyboardEventHandler.notifyListeners(downloadEventListeners, KeyboardEventHandler.EventType.LEXICAL_MODEL_INSTALLED, + aCloudResult.installedLexicalModels, aCloudResult.kbdResult); + } + } + + /** + * create a download id for the model. + * @param aModelId the lexical model id + * @return the result + */ + public static String createDownloadId(String aModelId) + { + return "dictionary_" + aModelId; + } +} diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java index 3c225e43ff..dc843a7281 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/CloudRepository.java @@ -28,7 +28,9 @@ public class CloudRepository { static public final CloudRepository shared = new CloudRepository(); private static final String TAG = "CloudRepository"; + //TODO: Should be removed with the old implementation when downloadmanager impl works public static final boolean USE_DOWNLOAD_MANAGER = true; + public static final String DOWNLOAD_IDENTIFIER_CATALOGUE = "catalogue"; private Dataset memCachedDataset; @@ -117,19 +119,15 @@ public class CloudRepository { private CloudApiTypes.CloudApiParam prepareKeyboardUpdateQuery(Context aContext) { - String deviceType = aContext.getString(R.string.device_type); - if (deviceType.equals("AndroidTablet")) { - deviceType = "androidtablet"; - } else { - deviceType = "androidphone"; - } + String deviceType = CloudDataJsonUtil.getDeviceTypeForCloudQuery(aContext); // Retrieves the cloud-based keyboard catalog in Android's preferred format. String keyboardURL = String.format("%s?version=%s&device=%s&languageidtype=bcp47", KMKeyboardDownloaderActivity.kKeymanApiBaseURL, BuildConfig.VERSION_NAME, deviceType); //cloudQueries[cloudQueryEntries++] = new CloudApiParam(ApiTarget.Keyboards, keyboardURL, JSONType.Object); - return new CloudApiTypes.CloudApiParam(CloudApiTypes.ApiTarget.Keyboards, keyboardURL, CloudApiTypes.JSONType.Object); + return new CloudApiTypes.CloudApiParam( + CloudApiTypes.ApiTarget.Keyboards, keyboardURL).setType(CloudApiTypes.JSONType.Object); } private CloudApiTypes.CloudApiParam prepareLexicalModellUpdateQuery(Context aContext) @@ -139,7 +137,8 @@ public class CloudRepository { // query is ready! String lexicalURL = String.format("%s?q", KMKeyboardDownloaderActivity.kKeymanApiModelURL); - return new CloudApiTypes.CloudApiParam(CloudApiTypes.ApiTarget.LexicalModels, lexicalURL, CloudApiTypes.JSONType.Array); + return new CloudApiTypes.CloudApiParam(CloudApiTypes.ApiTarget.LexicalModels, lexicalURL) + .setType(CloudApiTypes.JSONType.Array); // TODO: We want a list of lexical models for every language with an installed resource (kbd, lex model) @@ -167,7 +166,7 @@ public class CloudRepository { preCacheDataSet(context,updateHandler,onSuccess,onFailure); if(USE_DOWNLOAD_MANAGER) - downloadCatalogFromServer(context,updateHandler,onSuccess,onFailure); + downloadMetaDataFromServer(context,updateHandler,onSuccess,onFailure); } /** @@ -191,7 +190,7 @@ public class CloudRepository { preCacheDataSet(context,updateHandler,onSuccess,onFailure); - downloadCatalogFromServer(context,updateHandler,onSuccess,onFailure); + downloadMetaDataFromServer(context,updateHandler,onSuccess,onFailure); } @@ -310,7 +309,7 @@ public class CloudRepository { * @param onFailure A callback to be triggered upon failure of a query. * @return A Dataset object implementing the Adapter interface to be asynchronously filled. */ - private void downloadCatalogFromServer(@NonNull Context context, UpdateHandler updateHandler, Runnable onSuccess, Runnable onFailure) { + private void downloadMetaDataFromServer(@NonNull Context context, UpdateHandler updateHandler, Runnable onSuccess, Runnable onFailure) { boolean loadKeyboardsFromCache = this.shouldUseCache(context, CloudDataJsonUtil.getKeyboardCacheFile(context)); boolean loadLexicalModelsFromCache = this.shouldUseCache(context, CloudDataJsonUtil.getLexicalModelCacheFile(context)); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/ICloudDownloadCallback.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/ICloudDownloadCallback.java index 4fe402c732..5fe73b75c6 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/ICloudDownloadCallback.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/ICloudDownloadCallback.java @@ -9,6 +9,11 @@ import android.content.Context; */ public interface ICloudDownloadCallback { + /** + * Initialize callback using context. + * @param context the context + */ + void initializeContext(Context context); /** * extract download result object from download set diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java index 567d9e0a59..fa088416a8 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/data/Keyboard.java @@ -4,6 +4,7 @@ import android.os.Bundle; import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; +import com.tavultesoft.kmea.KeyboardPickerActivity; import java.io.Serializable; import java.util.Map; @@ -26,6 +27,12 @@ public class Keyboard implements Serializable, LanguageResource { this.map = kbdData; } + + + public boolean isNewKeyboard() { + return map.get(KeyboardPickerActivity.KMKEY_INTERNAL_NEW_KEYBOARD)!=null; + } + public String getResourceId() { return this.map.get(KMManager.KMKey_KeyboardID); } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/packages/PackageProcessor.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/packages/PackageProcessor.java index e6c065a68f..951d05de02 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/packages/PackageProcessor.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/packages/PackageProcessor.java @@ -30,7 +30,7 @@ import org.json.JSONObject; * KMEA engine. This is primarily for installing keyboard packages. */ public class PackageProcessor { - protected static File resourceRoot = null; + protected File resourceRoot = null; public static final String PP_DEFAULT_VERSION = "1.0"; public static final String PP_DEFAULT_METADATA = "kmp.json"; diff --git a/android/KMEA/app/src/main/res/values/strings.xml b/android/KMEA/app/src/main/res/values/strings.xml index df77ff5223..aa8b6bf840 100644 --- a/android/KMEA/app/src/main/res/values/strings.xml +++ b/android/KMEA/app/src/main/res/values/strings.xml @@ -51,6 +51,7 @@ Keyboard version Help link Uninstall keyboard + [new] Getting dictionary catalog.\nThis may take a while… @@ -58,10 +59,18 @@ Checking to download dictionary… Downloading dictionary… The resource catalog is unavailable\n + + Catalog update started in background.\n The catalog is still downloading; please try again in a moment!\n + Downloading keyboard started in Background + The selected keyboard is already downloading; please try again in a moment! + Keyboard download is finished! + Downloading dictionary started in Background + The selected dictionary is already downloading; please try again in a moment! + Dictionary download is finished. - + Failed to access server!\n "All resources are up to date!" diff --git a/android/history.md b/android/history.md index 67d54ed0d4..9d80655bec 100644 --- a/android/history.md +++ b/android/history.md @@ -3,7 +3,7 @@ ## 13.0 alpha * Start version 13.0 * New Features: - * Adding a download manager to execute downloads in background (#2247) + * Adding a download manager to execute downloads in background (#2247,#2275) * Improve custom package installation: Show readme.htm before starting installation process (#2286) * Update target Android SDK version to 29 (#2279) * Add linting to Debug builds and resolve lint errors (#2305)