Merge pull request #2275 from keymanapp/feature/keyboard-download-manager

feat(android): Feature/keyboard download manager
This commit is contained in:
safolit 2019-11-08 13:40:22 +07:00 committed by GitHub
commit 799d1b653b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1223 additions and 274 deletions

View file

@ -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 {

View file

@ -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 <code>download</code> method to handle asynchronous downloading and evaluation of
* packages and keyboards.
*/
@Deprecated
static class DownloadTask extends AsyncTask<Void, Integer, DownloadTask.Result> {
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<String> fontUrls = fontUrls(jsonFont, fontBaseUri, true);
ArrayList<String> oskFontUrls = fontUrls(jsonOskFont, fontBaseUri, true);
ArrayList<String> fontUrls = CloudDataJsonUtil.fontUrls(jsonFont, fontBaseUri, false);
ArrayList<String> 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<String, String> keyboardInfo = new HashMap<String, String>();
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<String, String> 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<CloudApiTypes.CloudApiParam> 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<CloudApiTypes.CloudApiParam> getPrepareCloudQueriesForKeyboardDownload(Context context)
{
List<CloudApiTypes.CloudApiParam> 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<count; i++) {
if (i != index) {
copy.put(source.get(i));
}
}
return copy;
}
private static ArrayList<String> fontUrls(JSONObject jsonFont, String baseUri, boolean isOskFont) {
if (jsonFont == null)
return null;
ArrayList<String> urls = new ArrayList<String>();
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) {

View file

@ -74,8 +74,12 @@ final class KMKeyboardPickerAdapter extends NestedAdapter<Keyboard, Dataset.Keyb
holder = (ViewHolder) convertView.getTag();
}
holder.textLang.setText(kbd.map.get(KMManager.KMKey_LanguageName));
holder.textKbd.setText(kbd.map.get(KMManager.KMKey_KeyboardName));
if(kbd.isNewKeyboard())
holder.textLang.setText(getContext().getText(R.string.keyboard_picker_new_keyboard_prefix)
+ " " + kbd.getLanguageName());
else
holder.textLang.setText(kbd.getLanguageName());
holder.textKbd.setText(kbd.getResourceName());
if (listFont != null) {
holder.textLang.setTypeface(listFont, Typeface.BOLD);

View file

@ -145,7 +145,8 @@ public final class KeyboardListActivity 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);
}
finish();
}

View file

@ -4,6 +4,34 @@
package com.tavultesoft.kmea;
import android.content.Context;
import android.content.Intent;
import android.graphics.Typeface;
import android.os.Bundle;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.tavultesoft.kmea.data.CloudDataJsonUtil;
import com.tavultesoft.kmea.data.CloudRepository;
import com.tavultesoft.kmea.data.Dataset;
import com.tavultesoft.kmea.data.Keyboard;
import com.tavultesoft.kmea.data.LexicalModel;
import com.tavultesoft.kmea.util.MapCompat;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
@ -14,42 +42,17 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.tavultesoft.kmea.data.CloudDataJsonUtil;
import com.tavultesoft.kmea.data.CloudRepository;
import com.tavultesoft.kmea.data.Dataset;
import com.tavultesoft.kmea.data.Keyboard;
import com.tavultesoft.kmea.data.LexicalModel;
import com.tavultesoft.kmea.util.MapCompat;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.graphics.Typeface;
import android.os.Bundle;
import androidx.appcompat.widget.Toolbar;
import android.util.Log;
import android.view.MenuItem;
import android.view.View;
import android.view.Window;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ListView;
import android.widget.PopupMenu;
import android.widget.TextView;
import android.widget.Toast;
public final class KeyboardPickerActivity extends AppCompatActivity {
//TODO: view instances should not be static
private static Toolbar toolbar = null;
private static ListView listView = null;
private static Button closeButton = null;
private static KMKeyboardPickerAdapter listAdapter = null;
public static final String KMKEY_INTERNAL_NEW_KEYBOARD = "_internal_new_keyboard_";
// TODO: Refactoring to remove keyboard selection into own keyboard manager class (MVC)
// Lists of installed keyboards and installed lexical models
private static ArrayList<HashMap<String, String>> keyboardsList = null;
private static ArrayList<HashMap<String, String>> 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<String,String> _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) {

View file

@ -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();

View file

@ -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;

View file

@ -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<String,Object> 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> T getAdditionalProperty(String aProperty,Class<T> 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;
}
}

View file

@ -229,6 +229,11 @@ public class CloudCatalogDownloadCallback implements ICloudDownloadCallback<Data
}
}
@Override
public void initializeContext(Context context) {
}
@Override
public void applyCloudDownloadToModel(Context aContext, Dataset aDataSet, CloudCatalogDownloadReturns aCloudResult)
{
@ -245,35 +250,14 @@ public class CloudCatalogDownloadCallback implements ICloudDownloadCallback<Data
{
List<CloudApiTypes.CloudApiReturns> 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);
}
}

View file

@ -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<String,String> createKeyboardInfoMap(String aPackageId,String aLanguageId, String aLanguageName, String aKeyboardId,
String aKeyboardName, String aKeyboardVersion, String anIsCustomKeyboard,
String aFont, String aOskFont)
{
HashMap<String, String> keyboardInfo = new HashMap<String, String>();
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<Keyboard> processKeyboardJSON(JSONObject query, boolean fromKMP) {
List<Keyboard> keyboardsList = new ArrayList<>();
//keyboardModifiedDates = new HashMap<String, String>();
@ -55,14 +78,8 @@ public class CloudDataJsonUtil {
String kbFont = keyboardJSON.optString(KMManager.KMKey_Font, "");
//String kbKey = String.format("%s_%s", langID, kbID);
HashMap<String, String> hashMap = new HashMap<String, String>();
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<String, String> 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<count; i++) {
if (i != index) {
copy.put(source.get(i));
}
}
return copy;
}
public static ArrayList<String> fontUrls(JSONObject jsonFont, String baseUri, boolean isOskFont) {
if (jsonFont == null)
return null;
ArrayList<String> urls = new ArrayList<String>();
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;
}
}

View file

@ -189,6 +189,9 @@ public class CloudDownloadMgr{
DownloadManager downloadManager = (DownloadManager) aContext.getSystemService(Context.DOWNLOAD_SERVICE);
aCallback.initializeContext(aContext);
CloudApiTypes.CloudDownloadSet<ModelType,ResultType> _downloadSet =
new CloudApiTypes.CloudDownloadSet<ModelType,ResultType>(
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);
}
}

View file

@ -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<KeyboardEventHandler.OnKeyboardDownloadEventListener> downloadEventListeners = new ArrayList<>();
private HashMap<String,String> 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<KeyboardEventHandler.OnKeyboardDownloadEventListener> aDownloadEventListeners)
{
downloadEventListeners.clear();
downloadEventListeners.addAll(aDownloadEventListeners);
}
/**
* Keyboard meta data.
* @param aKeyboardInfo the keyboard
*/
public void setKeyboardInfo(HashMap<String, String> aKeyboardInfo) {
this.keyboardInfo = aKeyboardInfo;
}
@Override
public void initializeContext(Context context)
{
dataDir = context.getDir("data", Context.MODE_PRIVATE);
}
@Override
public CloudKeyboardDownloadReturns extractCloudResultFromDownloadSet(
CloudApiTypes.CloudDownloadSet<Void, CloudKeyboardDownloadReturns> 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;
}
}

View file

@ -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<Map<String, String>> installedLexicalModels;
public CloudKeyboardDownloadReturns(Integer i) {
this(i, null);
}
public CloudKeyboardDownloadReturns(Integer i, List<Map<String, String>> installedLexicalModels) {
this.kbdResult = i;
this.installedLexicalModels = installedLexicalModels;
}
}

View file

@ -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<Void,
List<CloudKeyboardMetaDataDownloadCallback.MetaDataResult>>
{
/**
* the metadata result and all necessary downloads which should be started.
*/
public static class MetaDataResult
{
CloudApiTypes.CloudApiReturns returnjson;
CloudApiTypes.CloudApiParam params;
HashMap<String,String> keyboardInfo;
String additionalDownloadid;
List<CloudApiTypes.CloudApiParam> additionalDownloads;
}
private static final String TAG = "CloudKeyboardMetaDldCb";
private ArrayList<KeyboardEventHandler.OnKeyboardDownloadEventListener> 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<KeyboardEventHandler.OnKeyboardDownloadEventListener> aDownloadEventListeners) {
downloadEventListeners.clear();
downloadEventListeners.addAll(aDownloadEventListeners);
}
@Override
public void initializeContext(Context context) {
}
@Override
public List<CloudKeyboardMetaDataDownloadCallback.MetaDataResult> extractCloudResultFromDownloadSet(
CloudApiTypes.CloudDownloadSet<Void, List<CloudKeyboardMetaDataDownloadCallback.MetaDataResult>> aDownload) {
List<CloudKeyboardMetaDataDownloadCallback.MetaDataResult> _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<CloudKeyboardMetaDataDownloadCallback.MetaDataResult> 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<MetaDataResult> 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<MetaDataResult> 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<CloudApiTypes.CloudApiParam> 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<CloudApiTypes.CloudApiParam> 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<String> fontUrls = CloudDataJsonUtil.fontUrls(jsonFont, fontBaseUri, false);
ArrayList<String> 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;
}
}

View file

@ -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<KeyboardEventHandler.OnKeyboardDownloadEventListener> downloadEventListeners = new ArrayList<>();
/**
* listeners to inform after installation is completed.
* @param aDownloadEventListeners the listeners
*/
public void setDownloadEventListeners(ArrayList<KeyboardEventHandler.OnKeyboardDownloadEventListener> 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<Void, CloudKeyboardDownloadReturns> aDownload)
{
LexicalModelPackageProcessor kmpProcessor = new LexicalModelPackageProcessor(resourceRoot);
List<Map<String, String>> 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;
}
}

View file

@ -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));

View file

@ -9,6 +9,11 @@ import android.content.Context;
*/
public interface ICloudDownloadCallback<ModelType,ResultType> {
/**
* Initialize callback using context.
* @param context the context
*/
void initializeContext(Context context);
/**
* extract download result object from download set

View file

@ -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);
}

View file

@ -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";

View file

@ -51,6 +51,7 @@
<string name="keyboard_version" translatable="false">Keyboard version</string>
<string name="help_link" translatable="false">Help link</string>
<string name="uninstall_keyboard" translatable="false">Uninstall keyboard</string>
<string name="keyboard_picker_new_keyboard_prefix" translatable="false">[new]</string>
<!-- Model Updates -->
<string name="getting_model_catalog" translatable="false">Getting dictionary catalog.\nThis may take a while&#8230;</string>
@ -58,10 +59,18 @@
<string name="checking_model_download" translatable="false">Checking to download dictionary&#8230;</string>
<string name="downloading_model" translatable="false">Downloading dictionary&#8230;</string>
<string name="catalog_unavailable" translatable="false">The resource catalog is unavailable\n</string>
<!-- Background download messages-->
<string name="catalog_download_start_in_background" translatable="false">Catalog update started in background.\n</string>
<string name="catalog_download_is_running_in_background" translatable="false">The catalog is still downloading; please try again in a moment!\n</string>
<string name="keyboard_download_start_in_background" translatable="false">Downloading keyboard started in Background</string>
<string name="keyboard_download_is_running_in_background" translatable="false">The selected keyboard is already downloading; please try again in a moment!</string>
<string name="keyboard_download_finished" translatable="false">Keyboard download is finished!</string>
<string name="dictionary_download_start_in_background" translatable="false">Downloading dictionary started in Background</string>
<string name="dictionary_download_is_running_in_background" translatable="false">The selected dictionary is already downloading; please try again in a moment!</string>
<string name="dictionary_download_finished" translatable="false">Dictionary download is finished.</string>
<!-- General Updates -->
<!-- General Updates -->
<string name="update_check_unavailable" translatable="false">Failed to access server!\n</string>
<string name="update_check_current" translatable="false">"All resources are up to date!"</string>

View file

@ -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)