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 a4710604dd..5fb78313ed 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 @@ -14,13 +14,14 @@ import java.util.Arrays; import java.util.HashMap; import java.util.List; +import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; - import com.tavultesoft.kmea.KMManager.KeyboardType; import com.tavultesoft.kmea.KMTextView; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardDownloadEventListener; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardEventListener; +import android.app.FragmentManager; import android.net.Uri; import android.os.Build; import android.os.Bundle; @@ -179,10 +180,43 @@ public class MainActivity extends Activity implements OnKeyboardEventListener, O } KMManager.addKeyboardEventListener(this); - KMManager.addKeyboardDownloadEventListener(this); + KMKeyboardDownloaderActivity.addKeyboardDownloadEventListener(this); - checkUrl(); - getIntent().setData(null); + // If URL provided, start KMKeyboardDownloaderActivity + Intent intent = getIntent(); + Uri data = intent.getData(); + if (data != null) { + String directStr = data.getQueryParameter(KMKeyboardDownloaderActivity.KMKey_Direct); + boolean isDirect = false; + if (directStr != null && directStr.equals("true")) { + isDirect = true; + } + + String url = data.getQueryParameter(KMKeyboardDownloaderActivity.KMKey_URL); + if (url != null) { + boolean isCustom = KMKeyboardDownloaderActivity.isCustom(url); + + int index = url.lastIndexOf("/") + 1; + String filename = "unknown:"; + if (index >= 0 && index <= url.length()) { + filename = url.substring(index); + } + + Bundle bundle = new Bundle(); + bundle.putString(KMKeyboardDownloaderActivity.ARG_KEYBOARD, + data.getQueryParameter(KMKeyboardDownloaderActivity.KMKey_Keyboard)); + bundle.putString(KMKeyboardDownloaderActivity.ARG_LANGUAGE, + data.getQueryParameter(KMKeyboardDownloaderActivity.KMKey_Language)); + bundle.putBoolean(KMKeyboardDownloaderActivity.ARG_IS_CUSTOM, isCustom); + bundle.putBoolean(KMKeyboardDownloaderActivity.ARG_IS_DIRECT, isDirect); + bundle.putString(KMKeyboardDownloaderActivity.ARG_URL, url); + bundle.putString(KMKeyboardDownloaderActivity.ARG_FILENAME, filename); + Intent i = new Intent(getApplicationContext(), KMKeyboardDownloaderActivity.class); + i.putExtras(bundle); + startActivity(i); + } + } + intent.setData(null); } @Override @@ -190,7 +224,10 @@ public class MainActivity extends Activity implements OnKeyboardEventListener, O super.onPause(); KMManager.onPause(); KMManager.removeKeyboardEventListener(this); - KMManager.removeKeyboardDownloadEventListener(this); + + // Intentionally not removing KeyboardDownloadEventListener to + // ensure onKeyboardDownloadFinished() gets called + SharedPreferences prefs = getSharedPreferences(getString(R.string.kma_prefs_name), Context.MODE_PRIVATE); SharedPreferences.Editor editor = prefs.edit(); editor.putString(userTextKey, textView.getText().toString()); @@ -470,102 +507,6 @@ public class MainActivity extends Activity implements OnKeyboardEventListener, O startActivity(getStartedIntent); } - private boolean checkUrl() { - // return true if requires user action (e.g. Download dialog) - Intent i = getIntent(); - Uri data = i.getData(); - if (data == null) - return false; - - final Context context = this; - final String keyboard = data.getQueryParameter("keyboard"); - final String language = data.getQueryParameter("language"); - final String url = data.getQueryParameter("url"); - - if (url != null) { - /* - int kbIndex = -1; - if (keyboard != null && language != null) - kbIndex = KMManager.getKeyboardIndex(this, keyboard, language); - - if (kbIndex >= 0) { - KMManager.setKeyboard(context, kbIndex); - return false; - }*/ - - String direct = data.getQueryParameter("direct"); - final boolean isDirect; - if (direct != null && direct.equals("true")) { - isDirect = true; - } else { - isDirect = false; - } - - int index = url.lastIndexOf("/") + 1; - String jsonFilename = "unknown"; - if (index >= 0 && index <= url.length()) { - jsonFilename = url.substring(index); - } - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context); - dialogBuilder.setTitle("Custom Keyboard: " + jsonFilename); - dialogBuilder.setMessage("Would you like to download this keyboard?"); - dialogBuilder.setPositiveButton("Download", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Download custom keyboard - if (KMManager.hasConnection(context)) { - KMManager.KMCustomKeyboardDownloader.download(context, url, isDirect, true); - } else { - Toast.makeText(context, "No internet connection", Toast.LENGTH_SHORT).show(); - } - } - }); - dialogBuilder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Cancel - } - }); - - AlertDialog dialog = dialogBuilder.create(); - dialog.show(); - - return true; - } else if (keyboard != null && language != null && !keyboard.trim().isEmpty() && !language.trim().isEmpty()) { - int kbIndex = KMManager.getKeyboardIndex(this, keyboard, language); - - if (kbIndex >= 0) { - KMManager.setKeyboard(context, kbIndex); - return false; - } - - String kbKey = String.format("%s_%s", language, keyboard); - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context); - dialogBuilder.setTitle("Keyboard: " + kbKey); - dialogBuilder.setMessage("Would you like to download this keyboard?"); - dialogBuilder.setPositiveButton("Download", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Download keyboard - if (KMManager.hasConnection(context)) { - KMManager.KMKeyboardDownloader.download(context, KMManager.KMDefault_LegacyPackageID, keyboard, language, true); - } else { - Toast.makeText(context, "No internet connection", Toast.LENGTH_SHORT).show(); - } - } - }); - dialogBuilder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Cancel - } - }); - - AlertDialog dialog = dialogBuilder.create(); - dialog.show(); - - return true; - } - - return false; - } - public static Drawable getActionBarDrawable(Context context) { Point size = new Point(); WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); diff --git a/android/KMEA/app/src/main/AndroidManifest.xml b/android/KMEA/app/src/main/AndroidManifest.xml index decccf06cb..b94832507d 100644 --- a/android/KMEA/app/src/main/AndroidManifest.xml +++ b/android/KMEA/app/src/main/AndroidManifest.xml @@ -28,6 +28,11 @@ android:label="@string/app_name" android:theme="@android:style/Theme.Holo.Light.Dialog" > + + \ No newline at end of file diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ConfirmDialogFragment.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ConfirmDialogFragment.java new file mode 100644 index 0000000000..4871bc5aed --- /dev/null +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/ConfirmDialogFragment.java @@ -0,0 +1,42 @@ +package com.tavultesoft.kmea; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.app.DialogFragment; +import android.content.DialogInterface; +import android.os.Bundle; +import android.widget.Toast; + +/** + * Confirmation dialog for downloading a Keyman keyboard + */ +public class ConfirmDialogFragment extends DialogFragment { + public static String ARG_TITLE = "ConfirmDialogFragment.title"; + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Bundle args = getArguments(); + final String title = args.getString(ARG_TITLE); + + return new AlertDialog.Builder(getActivity()) + .setTitle(title) + .setMessage("Would you like to download this keyboard?") + .setPositiveButton("Download", new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + // Download keyboard + if (KMManager.hasConnection(getActivity())) { + KMKeyboardDownloaderActivity.download(getActivity(), true); + } else { + Toast.makeText(getActivity(), "No internet connection", Toast.LENGTH_SHORT).show(); + } + } + }) + .setNegativeButton("Cancel", new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + // Cancel + } + }) + .create(); + } +} 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 new file mode 100644 index 0000000000..cc110691ed --- /dev/null +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMKeyboardDownloaderActivity.java @@ -0,0 +1,447 @@ +package com.tavultesoft.kmea; + +import android.app.Activity; +import android.app.DialogFragment; +import android.app.ProgressDialog; +import android.content.Context; +import android.os.AsyncTask; +import android.os.Bundle; +import android.util.Log; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.File; +import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.HashMap; + +import com.tavultesoft.kmea.util.FileDownloader; + +import static com.tavultesoft.kmea.KMManager.KMDefault_AssetPackages; + +public class KMKeyboardDownloaderActivity extends Activity { + // Bundle Keys + // Cloud + 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"; + public static final String ARG_KB_NAME = "KMKeyboardActivity.kbName"; + public static final String ARG_LANG_NAME = "KMKeyboardActivity.langName"; + public static final String ARG_IS_CUSTOM = "KMKeyboardActivity.isCustom"; + + // custom keyboard + public static final String ARG_KEYBOARD = "KMKeyboardActivity.keyboard"; + public static final String ARG_LANGUAGE = "KMKeyboardActivity.language"; + public static final String ARG_IS_DIRECT = "KMKeyboardActivity.isDirect"; + public static final String ARG_URL = "KMKeyboardActivity.url"; + public static final String ARG_FILENAME = "KMKeyboardActivity.filename"; + + public static final String kKeymanApiBaseURL = "https://r.keymanweb.com/api/3.0/"; + public static final String kKeymanApiRemoteURL = "https://r.keymanweb.com/api/2.0/remote?url="; + + // Keyman public keys + public static final String KMKey_Direct = "direct"; + public static final String KMKey_URL = "url"; + public static final String KMKey_Keyboard = "keyboard"; + public static final String KMKey_LanguageKeyboards = "keyboards"; + public static final String KMKey_Options = "options"; + public static final String KMKey_Language = "language"; + public static final String KMKey_Languages = "languages"; + public static final String KMKey_Filename = "filename"; + + // Keyman internal keys + public static final String KMKey_KeyboardBaseURI = "keyboardBaseUri"; + public static final String KMKey_FontBaseURI = "fontBaseUri"; + + private static String pkgID; + private static String kbID; + private static String langID; + private static String kbName; + private static String langName; + private static Boolean isCustom; + + private static String customKeyboard; + private static String customLanguage; + private static Boolean isDirect; + private static String url; + private static String filename; + + private static ArrayList kbDownloadEventListeners = null; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + Bundle bundle = getIntent().getExtras(); + if (bundle != null) { + pkgID = bundle.getString(ARG_PKG_ID); + if (pkgID == null || pkgID.isEmpty()) { + pkgID = KMManager.KMDefault_LegacyPackageID; + } + kbID = bundle.getString(ARG_KB_ID); + langID = bundle.getString(ARG_LANG_ID); + kbName = bundle.getString(ARG_KB_NAME); + langName = bundle.getString(ARG_LANG_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); + isDirect = bundle.getBoolean(ARG_IS_DIRECT); + url = bundle.getString(ARG_URL); + filename = bundle.getString(ARG_FILENAME); + if (filename == null || filename.isEmpty()) { + filename = "unknown"; + } + } else { + return; + } + + Bundle args = new Bundle(); + String title = ""; + if (url != null) { + title = "Custom Keyboard: " + filename; + } else if (customKeyboard != null && customLanguage != null && + !customKeyboard.trim().isEmpty() && !customLanguage.trim().isEmpty()) { + int kbIndex = KMManager.getKeyboardIndex(getApplicationContext(), customKeyboard, customLanguage); + + if (kbIndex >= 0) { + KMManager.setKeyboard(getApplicationContext(), kbIndex); + // No interaction needed + return; // false + } + + title = customLanguage + "_" + customKeyboard; + } else { + // Download keyboard from cloud server + title = langName + ": " + kbName; + } + + DialogFragment dialog = new ConfirmDialogFragment(); + args.putString(ConfirmDialogFragment.ARG_TITLE, title); + dialog.setArguments(args); + dialog.show(getFragmentManager(), "dialog"); +} + + /** + * Async task to download a Keyman keyboard from either Keyman cloud server or custom url + * @param context + * @param showProgressDialog + */ + public static void download(final Context context, final boolean showProgressDialog) { + + new AsyncTask() { + private ProgressDialog progressDialog; + //private String languageName = ""; + //private String keyboardName = ""; + private String kbVersion = "1.0"; + private String kbIsCustom = isCustom ? "Y" : "N"; + private String font = ""; + private String oskFont = ""; + + @Override + protected void onPreExecute() { + super.onPreExecute(); + if (showProgressDialog) { + progressDialog = new ProgressDialog(context); + progressDialog.setMessage("Downloading keyboard..."); + progressDialog.setCancelable(false); + if (!((Activity) context).isFinishing()) { + progressDialog.show(); + } else { + cancel(true); + progressDialog = null; + } + } + } + + @Override + protected Integer doInBackground(Void... voids) { + int ret = -1; + ArrayList urls = new ArrayList(); + + if (isCancelled()) + return ret; + + try { + String exceptionStr = "Invalid keyboard"; + if (pkgID == null || pkgID.trim().isEmpty() || + (!isCustom && (langID == null || langID.trim().isEmpty() || kbID == null || kbID.trim().isEmpty()))) { + throw new Exception(exceptionStr); + } + + String deviceType = context.getResources().getString(R.string.device_type); + if (deviceType.equals("AndroidTablet")) { + deviceType = "androidtablet"; + } else { + deviceType = "androidphone"; + } + + // Placeholder here to attempt to process .kmp keyboard first: + + //// + + JSONParser jsonParser = new JSONParser(); + JSONObject kbData = null; + String remoteUrl = ""; + if (isCustom) { + if (isDirect) { + remoteUrl = url; + } else { + String encodedUrl = URLEncoder.encode(filename, "utf-8"); + remoteUrl = String.format("%s%s&device=%s", kKeymanApiRemoteURL, encodedUrl, deviceType); + } + } else { + // Keyman cloud + remoteUrl = String.format("%slanguages/%s/%s?device=%s", kKeymanApiBaseURL, langID, kbID, deviceType); + } + + kbData = jsonParser.getJSONObjectFromUrl(remoteUrl); + + exceptionStr = "Could not reach server"; + if (kbData == null) { + throw new Exception(exceptionStr); + } + + exceptionStr = "The keyboard could not be installed"; + JSONObject options = kbData.optJSONObject(KMKey_Options); + if (options == null) { + throw new Exception(exceptionStr); + } + String kbBaseUri = options.optString(KMKey_KeyboardBaseURI, ""); + if (kbBaseUri.isEmpty()) { + throw new Exception(exceptionStr); + } + + String fontBaseUri = options.optString(KMKey_FontBaseURI, ""); + JSONObject language, keyboard; + if (!isCustom) { + // JSON from Keyman cloud server + language = kbData.optJSONObject(KMKey_Language); + if (language == null) { + throw new Exception(exceptionStr); + } + + langName = language.optString(KMManager.KMKey_Name, ""); + + JSONArray keyboards = language.getJSONArray(KMKey_LanguageKeyboards); + if (keyboards == null ) { + throw new Exception(exceptionStr); + } + + keyboard = keyboards.getJSONObject(0); + if (keyboard == null) { + throw new Exception(exceptionStr); + } + + + } else { + // JSON from custom URL + keyboard = kbData.optJSONObject(KMKey_Keyboard); + if (keyboard == null) { + throw new Exception(exceptionStr); + } + + JSONArray languages = keyboard.optJSONArray(KMKey_Languages); + if (languages == null) { + throw new Exception(exceptionStr); + } + + // Concatenate langID and langName + langID = ""; + langName = ""; + int langCount = languages.length(); + for (int i=0; i < langCount; i++) { + langID += languages.getJSONObject(i).getString(KMManager.KMKey_ID); + langName += languages.getJSONObject(i).getString(KMManager.KMKey_Name); + if (i < langCount -1) { + langID += ";"; + langName += ";"; + } + } + pkgID = keyboard.optString(KMManager.KMKey_PackageID, KMManager.KMDefault_LegacyPackageID); + kbID = keyboard.optString(KMManager.KMKey_ID, ""); + + } + + kbName = keyboard.optString(KMManager.KMKey_Name, ""); + kbVersion = keyboard.optString(KMManager.KMKey_KeyboardVersion, "1.0"); + font = keyboard.optString(KMManager.KMKey_Font, "").replace( + "\"" + KMKeyboardDownloaderActivity.KMKey_Filename + "\"", + "\"" + KMManager.KMKey_FontSource + "\""); + oskFont = keyboard.optString(KMManager.KMKey_OskFont, "").replace( + "\"" + KMKeyboardDownloaderActivity.KMKey_Filename + "\"", + "\"" + KMManager.KMKey_FontSource + "\""); + String kbFilename = keyboard.optString(KMKey_Filename, ""); + + if (kbName.isEmpty() || langName.isEmpty() || kbFilename.isEmpty()) + throw new Exception(exceptionStr); + + String kbUrl = kbBaseUri + kbFilename; + urls.add(kbUrl); + JSONObject jsonFont = keyboard.optJSONObject(KMManager.KMKey_Font); + JSONObject jsonOskFont = keyboard.optJSONObject(KMManager.KMKey_OskFont); + ArrayList fontUrls = fontUrls(jsonFont, fontBaseUri, true); + ArrayList oskFontUrls = fontUrls(jsonOskFont, fontBaseUri, true); + if (fontUrls != null) + urls.addAll(fontUrls); + if (oskFontUrls != null) { + for (String url : oskFontUrls) { + if (!urls.contains(url)) + urls.add(url); + } + } + + notifyListeners(KeyboardEventHandler.EventType.KEYBOARD_DOWNLOAD_STARTED, 0); + + ret = 1; + int result = 0; + for (String url : urls) { + String directory = KMDefault_AssetPackages + File.separator + pkgID; + File dir = new File(directory); + String filename = ""; + if (url.endsWith(".js")) { + + int start = kbFilename.lastIndexOf("/"); + if (start < 0) { + start = 0; + } else { + start++; + } + if (!kbFilename.contains("-")) { + filename = kbFilename.substring(start, kbFilename.length() - 3) + "-" + kbVersion + ".js"; + } else { + filename = kbFilename.substring(start); + } + } + + result = FileDownloader.download(context, url, directory, filename); + if (result < 0) { + ret = -1; + break; + } + } + } catch (Exception e) { + ret = -1; + Log.e("Keyboard download", "Error: " + e); + } + + return ret; + } + + @Override + protected void onProgressUpdate(Integer... progress) { + // Do nothing + } + + @Override + protected void onPostExecute(Integer result) { + if (showProgressDialog) { + if (progressDialog != null && progressDialog.isShowing()) { + try { + progressDialog.dismiss(); + progressDialog = null; + } catch (Exception e) { + progressDialog = null; + } + } + } + + notifyListeners(KeyboardEventHandler.EventType.KEYBOARD_DOWNLOAD_FINISHED, result); + super.onPostExecute(result); + } + + /** + * Notify listeners when an event happens + * @param eventType + * @param result + */ + 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); + KeyboardEventHandler.notifyListeners(kbDownloadEventListeners, eventType, keyboardInfo, result); + } + } + }.execute(); + } + + public static boolean isCustom(String u) { + boolean ret = false; + if (u != null && !u.contains(KMKeyboardDownloaderActivity.kKeymanApiBaseURL) && + !u.contains(KMKeyboardDownloaderActivity.kKeymanApiRemoteURL)) { + ret = true; + } + return ret; + } + + private static ArrayList 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 (fontSourceString.endsWith(".ttf") || fontSourceString.endsWith(".otf")) { + urls.add(baseUri + fontSourceString); + } else if (isOskFont && (fontSourceString.endsWith(".svg") || fontSourceString.endsWith(".woff"))) { + urls.add(baseUri + fontSourceString); + } else if (isOskFont && fontSourceString.contains(".svg#")) { + String fontFilename = fontSourceString.substring(0, fontSourceString.indexOf(".svg#") + 5); + urls.add(baseUri + fontFilename); + } + } catch (JSONException e) { + return null; + } + } + } else { + String fontSourceString; + try { + fontSourceString = jsonFont.getString(KMManager.KMKey_FontSource); + if (fontSourceString.endsWith(".ttf") || fontSourceString.endsWith(".otf")) { + urls.add(baseUri + fontSourceString); + } else if (isOskFont && (fontSourceString.endsWith(".svg") || fontSourceString.endsWith(".woff"))) { + urls.add(baseUri + fontSourceString); + } else if (isOskFont && fontSourceString.contains(".svg#")) { + String fontFilename = fontSourceString.substring(0, fontSourceString.indexOf(".svg#") + 5); + urls.add(baseUri + fontFilename); + } + } catch (JSONException e) { + return null; + } + } + + return urls; + } + + public static void addKeyboardDownloadEventListener(KeyboardEventHandler.OnKeyboardDownloadEventListener listener) { + if (kbDownloadEventListeners == null) { + kbDownloadEventListeners = new ArrayList(); + } + + if (listener != null && !kbDownloadEventListeners.contains(listener)) { + kbDownloadEventListeners.add(listener); + } + } + + public static void removeKeyboardDownloadEventListener(KeyboardEventHandler.OnKeyboardDownloadEventListener listener) { + if (kbDownloadEventListeners != null) { + kbDownloadEventListeners.remove(listener); + } + } +} diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java index 953d4dc488..ff3082d58e 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMManager.java @@ -4,24 +4,15 @@ package com.tavultesoft.kmea; -import java.io.BufferedInputStream; import java.io.File; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.net.URL; -import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - import android.annotation.SuppressLint; -import android.app.Activity; -import android.app.ProgressDialog; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; @@ -33,7 +24,6 @@ import android.graphics.Typeface; import android.inputmethodservice.InputMethodService; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.os.AsyncTask; import android.os.Handler; import android.os.Looper; import android.text.InputType; @@ -53,9 +43,7 @@ import android.widget.FrameLayout; import android.widget.RelativeLayout; import com.tavultesoft.kmea.KeyboardEventHandler.EventType; -import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardDownloadEventListener; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardEventListener; -import com.tavultesoft.kmea.util.Connection; public final class KMManager { @@ -86,7 +74,6 @@ public final class KMManager { private static InputMethodService IMService; private static boolean debugMode = false; private static boolean shouldAllowSetKeyboard = true; - private static ArrayList kbDownloadEventListeners = null; private static boolean didCopyAssets = false; private static GlobeKeyAction inappKbGlobeKeyAction = GlobeKeyAction.GLOBE_KEY_ACTION_SHOW_MENU; private static GlobeKeyAction sysKbGlobeKeyAction = GlobeKeyAction.GLOBE_KEY_ACTION_SHOW_MENU; @@ -100,9 +87,6 @@ public final class KMManager { protected static KMKeyboard InAppKeyboard = null; protected static KMKeyboard SystemKeyboard = null; - protected static final String kKeymanApiBaseURL = "https://r.keymanweb.com/api/3.0/"; - private static final String kKeymanApiRemoteURL = "https://r.keymanweb.com/api/2.0/remote?url="; - // Keyman public keys public static final String KMKey_ID = "id"; public static final String KMKey_Name = "name"; @@ -112,18 +96,12 @@ public final class KMManager { public static final String KMKey_KeyboardID = "kbId"; public static final String KMKey_KeyboardName = "kbName"; public static final String KMKey_KeyboardVersion = "version"; - public static final String KMKey_Keyboard = "keyboard"; - public static final String KMKey_LanguageKeyboards = "keyboards"; public static final String KMKey_KeyboardFileSize = "fileSize"; public static final String KMKey_Font = "font"; public static final String KMKey_OskFont = "oskFont"; public static final String KMKey_FontFamily = "family"; public static final String KMKey_FontSource = "source"; public static final String KMKey_FontFiles = "files"; - public static final String KMKey_Options = "options"; - public static final String KMKey_Language = "language"; - public static final String KMKey_Languages = "languages"; - public static final String KMKey_Filename = "filename"; public static final String KMKey_KeyboardModified = "lastModified"; public static final String KMKey_KeyboardRTL = "rtl"; public static final String KMKey_CustomKeyboard = "CustomKeyboard"; @@ -131,8 +109,6 @@ public final class KMManager { public static final String KMKey_UserKeyboardIndex = "UserKeyboardIndex"; // Keyman internal keys - protected static final String KMKey_KeyboardBaseURI = "keyboardBaseUri"; - protected static final String KMKey_FontBaseURI = "fontBaseUri"; protected static final String KMKey_ShouldShowHelpBubble = "ShouldShowHelpBubble"; // Default Asset Paths @@ -465,47 +441,53 @@ public final class KMManager { } String[] files = legacyLanguagesDir.list(); - for (String filename : files) { - // Handle keyboards missing version number - if (filename.lastIndexOf("-") < 0) { - if (filename.equals("us.js")) { - File kbFile = new File(legacyLanguagesPath, filename); - kbFile.delete(); - } else if (filename.endsWith(".js")) { - // Append default version number to keyboard and move - String newFilename = filename.substring(0, filename.lastIndexOf(".js")) + "-1.0.js"; - File kbFile = new File(legacyLanguagesPath, filename); - kbFile.renameTo(new File(migratedDir, newFilename)); - } - // Handle keyboards with version number - } else { - // Remove legacy default keyboards - if (filename.startsWith(KMDefault_KeyboardID + "-")) { - File legacyKbFile = new File(legacyLanguagesDir, filename); - if (legacyKbFile.exists()) { - legacyKbFile.delete(); - continue; + if (files != null) { + for (String filename : files) { + // Handle keyboards missing version number + if (filename.lastIndexOf("-") < 0) { + if (filename.equals("us.js")) { + File kbFile = new File(legacyLanguagesPath, filename); + kbFile.delete(); + } else if (filename.endsWith(".js")) { + // Append default version number to keyboard and move + String newFilename = filename.substring(0, filename.lastIndexOf(".js")) + "-1.0.js"; + File kbFile = new File(legacyLanguagesPath, filename); + kbFile.renameTo(new File(migratedDir, newFilename)); + } + // Handle keyboards with version number + } else { + // Remove legacy default keyboards + if (filename.startsWith(KMDefault_KeyboardID + "-")) { + File legacyKbFile = new File(legacyLanguagesDir, filename); + if (legacyKbFile.exists()) { + legacyKbFile.delete(); + continue; + } } - } - // Migrate /languages/*.js keyboards - File kbFile = new File(legacyLanguagesPath, filename); - kbFile.renameTo(new File(migratedDir, filename)); + // Migrate /languages/*.js keyboards + File kbFile = new File(legacyLanguagesPath, filename); + kbFile.renameTo(new File(migratedDir, filename)); + } + } + + // Cleanup empty directory + if (legacyLanguagesDir.list().length == 0) { + legacyLanguagesDir.delete(); } } files = legacyFontsDir.list(); - for (String filename : files) { - File fontFile = new File(legacyFontsPath, filename); - fontFile.renameTo(new File(migratedDir, filename)); - } + if (files != null) { + for (String filename : files) { + File fontFile = new File(legacyFontsPath, filename); + fontFile.renameTo(new File(migratedDir, filename)); + } - // Cleanup empty directories - if (legacyLanguagesDir.list().length == 0) { - legacyLanguagesDir.delete(); - } - if (legacyFontsDir.list().length == 0) { - legacyFontsDir.delete(); + // Cleanup empty directory + if (legacyFontsDir.list().length == 0) { + legacyFontsDir.delete(); + } } } @@ -636,783 +618,6 @@ public final class KMManager { return KeyboardPickerActivity.getKeyboardsList(context); } - public static final class KMKeyboardDownloader { - - public static void download(final Context context, final int languageIndex, final int keyboardIndex, final boolean showProgressDialog) { - new AsyncTask() { - private ProgressDialog progressDialog; - private String packageID = ""; - private String languageID = ""; - private String keyboardID = ""; - private String languageName = ""; - private String keyboardName = ""; - private String kbVersion = "1.0"; - private String isCustom = "N"; - private String font = ""; - private String oskFont = null; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - if (showProgressDialog) { - progressDialog = new ProgressDialog(context); - progressDialog.setMessage("Downloading keyboard..."); - progressDialog.setCancelable(false); - if (!((Activity) context).isFinishing()) { - progressDialog.show(); - } else { - cancel(true); - progressDialog = null; - } - } - } - - @Override - protected Integer doInBackground(Void... voids) { - int ret = -1; - ArrayList urls = new ArrayList(); - - if (isCancelled()) - return ret; - - try { - JSONArray languages = LanguageListActivity.languages(); - JSONObject options = LanguageListActivity.options(); - - if (languages == null || options == null) { - throw new Exception("Language list is empty"); - } - - JSONObject language = languages.getJSONObject(languageIndex); - JSONArray keyboards = language.getJSONArray(KMKey_LanguageKeyboards); - JSONObject keyboard = keyboards.getJSONObject(keyboardIndex); - - if (keyboard.has(KMKey_PackageID)) { - packageID = keyboard.getString(KMKey_PackageID); - } else { - packageID = KMDefault_LegacyPackageID; - } - keyboardID = keyboard.getString(KMKey_ID); - languageID = language.getString(KMKey_ID); - keyboardName = keyboard.getString(KMKey_Name); - languageName = language.getString(KMKey_Name); - kbVersion = keyboard.optString(KMKey_KeyboardVersion, "1.0"); - font = keyboard.optString(KMKey_Font, ""); - oskFont = keyboard.optString(KMKey_OskFont, null); - String kbFilename = keyboard.optString(KMKey_Filename, ""); - - String kbUrl = options.getString(KMKey_KeyboardBaseURI) + kbFilename; - urls.add(kbUrl); - JSONObject jsonFont = keyboard.optJSONObject(KMKey_Font); - JSONObject jsonOskFont = keyboard.optJSONObject(KMKey_OskFont); - String fontBaseUri = options.getString(KMKey_FontBaseURI); - ArrayList fontUrls = fontUrls(jsonFont, fontBaseUri, true); - ArrayList oskFontUrls = fontUrls(jsonOskFont, fontBaseUri, true); - if (fontUrls != null) - urls.addAll(fontUrls); - if (oskFontUrls != null) { - for (String url : oskFontUrls) { - if (!urls.contains(url)) - urls.add(url); - } - } - - // Notify listeners: onDownloadStarted - if (kbDownloadEventListeners != null) { - HashMap keyboardInfo = new HashMap(); - keyboardInfo.put(KMKey_PackageID, packageID); - keyboardInfo.put(KMKey_KeyboardID, keyboardID); - keyboardInfo.put(KMKey_LanguageID, languageID); - keyboardInfo.put(KMKey_KeyboardName, keyboardName); - keyboardInfo.put(KMKey_LanguageName, languageName); - keyboardInfo.put(KMKey_KeyboardVersion, kbVersion); - keyboardInfo.put(KMKey_CustomKeyboard, isCustom); - keyboardInfo.put(KMKey_Font, font); - if (oskFont != null) - keyboardInfo.put(KMKey_OskFont, oskFont); - KeyboardEventHandler.notifyListeners(kbDownloadEventListeners, EventType.KEYBOARD_DOWNLOAD_STARTED, keyboardInfo, 0); - } - - ret = 1; - int result = 0; - for (String url : urls) { - String directory = KMDefault_AssetPackages + File.separator + packageID; - File dir = new File(directory); - String filename = ""; - if (url.endsWith(".js")) { - int start = kbFilename.lastIndexOf("/"); - if (start < 0) { - start = 0; - } else { - start++; - } - if (!kbFilename.contains("-")) { - filename = kbFilename.substring(start, kbFilename.length() - 3) + "-" + kbVersion + ".js"; - } else { - filename = kbFilename.substring(start); - } - } - - result = FileDownloader.download(context, url, directory, filename); - if (result < 0) { - ret = -1; - break; - } - } - } catch (Exception e) { - ret = -1; - Log.e("Keyboard download", "Error: " + e); - } - - return ret; - } - - @Override - protected void onProgressUpdate(Integer... progress) { - // Do nothing - } - - @Override - protected void onPostExecute(Integer result) { - if (showProgressDialog) { - if (progressDialog != null && progressDialog.isShowing()) { - try { - progressDialog.dismiss(); - progressDialog = null; - } catch (Exception e) { - progressDialog = null; - } - } - } - - // Notify listeners: onDownloadFinished - if (kbDownloadEventListeners != null) { - HashMap keyboardInfo = new HashMap(); - keyboardInfo.put(KMKey_PackageID, packageID); - keyboardInfo.put(KMKey_KeyboardID, keyboardID); - keyboardInfo.put(KMKey_LanguageID, languageID); - keyboardInfo.put(KMKey_KeyboardName, keyboardName); - keyboardInfo.put(KMKey_LanguageName, languageName); - keyboardInfo.put(KMKey_KeyboardVersion, kbVersion); - keyboardInfo.put(KMKey_CustomKeyboard, isCustom); - keyboardInfo.put(KMKey_Font, font); - if (oskFont != null) - keyboardInfo.put(KMKey_OskFont, oskFont); - KeyboardEventHandler.notifyListeners(kbDownloadEventListeners, EventType.KEYBOARD_DOWNLOAD_FINISHED, keyboardInfo, result); - } - } - }.execute(); - } - - public static void download(final Context context, final String packageID, final String keyboardID, final String languageID, final boolean showProgressDialog) { - new AsyncTask() { - private ProgressDialog progressDialog; - private String languageName = ""; - private String keyboardName = ""; - private String kbVersion = "1.0"; - private String isCustom = "N"; - private String font = ""; - private String oskFont = ""; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - if (showProgressDialog) { - progressDialog = new ProgressDialog(context); - progressDialog.setMessage("Downloading keyboard..."); - progressDialog.setCancelable(false); - if (!((Activity) context).isFinishing()) { - progressDialog.show(); - } else { - cancel(true); - progressDialog = null; - } - } - } - - @Override - protected Integer doInBackground(Void... voids) { - int ret = -1; - ArrayList urls = new ArrayList(); - - if (isCancelled()) - return ret; - - try { - String exceptionStr = "Invalid keyboard"; - if (packageID == null || packageID.trim().isEmpty() || - languageID == null || languageID.trim().isEmpty() || - keyboardID == null || keyboardID.trim().isEmpty()) - throw new Exception(exceptionStr); - - String deviceType = context.getResources().getString(R.string.device_type); - if (deviceType.equals("AndroidTablet")) { - deviceType = "androidtablet"; - } else { - deviceType = "androidphone"; - } - - String jsonUrl = String.format("%slanguages/%s/%s?device=%s", kKeymanApiBaseURL, languageID, keyboardID, deviceType); - JSONParser jsonParser = new JSONParser(); - JSONObject kbData = jsonParser.getJSONObjectFromUrl(jsonUrl); - - exceptionStr = "Could not reach Keyman server"; - if (kbData == null) { - throw new Exception(exceptionStr); - } - - JSONObject options = kbData.optJSONObject(KMKey_Options); - JSONObject language = kbData.optJSONObject(KMKey_Language); - exceptionStr = "The keyboard could not be installed"; - if (options == null || language == null) { - throw new Exception(exceptionStr); - } - - JSONArray keyboards = language.getJSONArray(KMKey_LanguageKeyboards); - String kbBaseUri = options.optString(KMKey_KeyboardBaseURI, ""); - String fontBaseUri = options.optString(KMKey_FontBaseURI, ""); - - if (keyboards == null || kbBaseUri.isEmpty()) - throw new Exception(exceptionStr); - - JSONObject keyboard = keyboards.getJSONObject(0); - if (keyboard == null) - throw new Exception(exceptionStr); - - languageName = language.optString(KMKey_Name, ""); - keyboardName = keyboard.optString(KMKey_Name, ""); - kbVersion = keyboard.optString(KMKey_KeyboardVersion, "1.0"); - font = keyboard.optString(KMKey_Font, ""); - oskFont = keyboard.optString(KMKey_OskFont, null); - String kbFilename = keyboard.optString(KMKey_Filename, ""); - - if (keyboardName.isEmpty() || languageName.isEmpty() || kbFilename.isEmpty()) - throw new Exception(exceptionStr); - - String kbUrl = kbBaseUri + kbFilename; - urls.add(kbUrl); - JSONObject jsonFont = keyboard.optJSONObject(KMKey_Font); - JSONObject jsonOskFont = keyboard.optJSONObject(KMKey_OskFont); - ArrayList fontUrls = fontUrls(jsonFont, fontBaseUri, true); - ArrayList oskFontUrls = fontUrls(jsonOskFont, fontBaseUri, true); - if (fontUrls != null) - urls.addAll(fontUrls); - if (oskFontUrls != null) { - for (String url : oskFontUrls) { - if (!urls.contains(url)) - urls.add(url); - } - } - - // Notify listeners: onDownloadStarted - if (kbDownloadEventListeners != null) { - HashMap keyboardInfo = new HashMap(); - keyboardInfo.put(KMKey_PackageID, packageID); - keyboardInfo.put(KMKey_KeyboardID, keyboardID); - keyboardInfo.put(KMKey_LanguageID, languageID); - keyboardInfo.put(KMKey_KeyboardName, keyboardName); - keyboardInfo.put(KMKey_LanguageName, languageName); - keyboardInfo.put(KMKey_KeyboardVersion, kbVersion); - keyboardInfo.put(KMKey_CustomKeyboard, isCustom); - keyboardInfo.put(KMKey_Font, font); - if (oskFont != null) - keyboardInfo.put(KMKey_OskFont, oskFont); - KeyboardEventHandler.notifyListeners(kbDownloadEventListeners, EventType.KEYBOARD_DOWNLOAD_STARTED, keyboardInfo, 0); - } - - ret = 1; - int result = 0; - for (String url : urls) { - String directory = KMDefault_AssetPackages + File.separator + packageID; - File dir = new File(directory); - String filename = ""; - if (url.endsWith(".js")) { - - int start = kbFilename.lastIndexOf("/"); - if (start < 0) { - start = 0; - } else { - start++; - } - if (!kbFilename.contains("-")) { - filename = kbFilename.substring(start, kbFilename.length() - 3) + "-" + kbVersion + ".js"; - } else { - filename = kbFilename.substring(start); - } - } - - result = FileDownloader.download(context, url, directory, filename); - if (result < 0) { - ret = -1; - break; - } - } - } catch (Exception e) { - ret = -1; - Log.e("Keyboard download", "Error: " + e); - } - - return ret; - } - - @Override - protected void onProgressUpdate(Integer... progress) { - // Do nothing - } - - @Override - protected void onPostExecute(Integer result) { - if (showProgressDialog) { - if (progressDialog != null && progressDialog.isShowing()) { - try { - progressDialog.dismiss(); - progressDialog = null; - } catch (Exception e) { - progressDialog = null; - } - } - } - - // Notify listeners: onDownloadFinished - if (kbDownloadEventListeners != null) { - HashMap keyboardInfo = new HashMap(); - keyboardInfo.put(KMKey_KeyboardID, keyboardID); - keyboardInfo.put(KMKey_LanguageID, languageID); - keyboardInfo.put(KMKey_KeyboardName, keyboardName); - keyboardInfo.put(KMKey_LanguageName, languageName); - keyboardInfo.put(KMKey_KeyboardVersion, kbVersion); - keyboardInfo.put(KMKey_CustomKeyboard, isCustom); - keyboardInfo.put(KMKey_Font, font); - if (oskFont != null) - keyboardInfo.put(KMKey_OskFont, oskFont); - KeyboardEventHandler.notifyListeners(kbDownloadEventListeners, EventType.KEYBOARD_DOWNLOAD_FINISHED, keyboardInfo, result); - } - } - }.execute(); - } - - private static ArrayList fontUrls(JSONObject jsonFont, String baseUri, boolean isOskFont) { - if (jsonFont == null) - return null; - - ArrayList urls = new ArrayList(); - JSONArray fontSource = jsonFont.optJSONArray(KMKey_FontSource); - if (fontSource != null) { - int fcCount = fontSource.length(); - for (int i = 0; i < fcCount; i++) { - String fontSourceString; - try { - fontSourceString = fontSource.getString(i); - if (fontSourceString.endsWith(".ttf") || fontSourceString.endsWith(".otf")) { - urls.add(baseUri + fontSourceString); - } else if (isOskFont && (fontSourceString.endsWith(".svg") || fontSourceString.endsWith(".woff"))) { - urls.add(baseUri + fontSourceString); - } else if (isOskFont && fontSourceString.contains(".svg#")) { - String fontFilename = fontSourceString.substring(0, fontSourceString.indexOf(".svg#") + 5); - urls.add(baseUri + fontFilename); - } - } catch (JSONException e) { - return null; - } - } - } else { - String fontSourceString; - try { - fontSourceString = jsonFont.getString(KMKey_FontSource); - if (fontSourceString.endsWith(".ttf") || fontSourceString.endsWith(".otf")) { - urls.add(baseUri + fontSourceString); - } else if (isOskFont && (fontSourceString.endsWith(".svg") || fontSourceString.endsWith(".woff"))) { - urls.add(baseUri + fontSourceString); - } else if (isOskFont && fontSourceString.contains(".svg#")) { - String fontFilename = fontSourceString.substring(0, fontSourceString.indexOf(".svg#") + 5); - urls.add(baseUri + fontFilename); - } - } catch (JSONException e) { - return null; - } - } - - return urls; - } - } - - public static final class KMCustomKeyboardDownloader { - - public static void download(final Context context, final String jsonUrl, final boolean isDirect, final boolean showProgressDialog) { - new AsyncTask() { - private ProgressDialog progressDialog; - private String packageID = ""; - private String keyboardID = ""; - private String languageID = ""; - private String keyboardName = ""; - private String languageName = ""; - private String kbVersion = "1.0"; - private String isCustom = "Y"; - private String font = ""; - private String oskFont = ""; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - if (showProgressDialog) { - progressDialog = new ProgressDialog(context); - progressDialog.setMessage("Downloading keyboard..."); - progressDialog.setCancelable(false); - if (!((Activity) context).isFinishing()) { - progressDialog.show(); - } else { - cancel(true); - progressDialog = null; - } - } - } - - @Override - protected Integer doInBackground(Void... voids) { - int ret = -1; - ArrayList urls = new ArrayList(); - - if (isCancelled()) - return ret; - - try { - JSONParser jsonParser = new JSONParser(); - JSONObject customKb = null; - if (isDirect) { - customKb = jsonParser.getJSONObjectFromUrl(jsonUrl); - } else { - String deviceType = context.getResources().getString(R.string.device_type); - if (deviceType.equals("AndroidTablet")) { - deviceType = "androidtablet"; - } else { - deviceType = "androidphone"; - } - - String encodedUrl = URLEncoder.encode(jsonUrl, "utf-8"); - String remoteUrl = String.format("%s%s&device=%s", kKeymanApiRemoteURL, encodedUrl, deviceType); - customKb = jsonParser.getJSONObjectFromUrl(remoteUrl); - } - - String exceptionStr = "Failed to fetch JSON object from the URL"; - if (customKb == null) { - throw new Exception(exceptionStr); - } - - JSONObject options = customKb.optJSONObject(KMKey_Options); - exceptionStr = "The keyboard could not be installed"; - if (options == null) { - throw new Exception(exceptionStr); - } - - String kbBaseUri = options.optString(KMKey_KeyboardBaseURI, ""); - String fontBaseUri = options.optString(KMKey_FontBaseURI, ""); - - JSONObject keyboard = customKb.getJSONObject(KMKey_Keyboard); - - if (keyboard == null || kbBaseUri.isEmpty()) { - throw new Exception(exceptionStr); - } - - JSONArray languages = keyboard.optJSONArray(KMKey_Languages); - - packageID = keyboard.optString(KMKey_PackageID, ""); - if (packageID == null || packageID.isEmpty()) { - packageID = KMDefault_LegacyPackageID; - } - keyboardID = keyboard.optString(KMKey_ID, ""); - keyboardName = keyboard.optString(KMKey_Name, ""); - kbVersion = keyboard.optString(KMKey_KeyboardVersion, "1.0"); - font = keyboard.optString(KMKey_Font, "").replace("\"" + KMManager.KMKey_Filename + "\"", "\"" + KMManager.KMKey_FontSource + "\""); - oskFont = keyboard.optString(KMKey_OskFont, "").replace("\"" + KMManager.KMKey_Filename + "\"", "\"" + KMManager.KMKey_FontSource + "\""); - if (oskFont.isEmpty()) { - oskFont = null; - } - String kbFilename = keyboard.optString(KMKey_Filename, ""); - - if (packageID == null || packageID.isEmpty() || - languages == null || keyboardID.isEmpty() || - keyboardName.isEmpty() || kbFilename.isEmpty()) { - throw new Exception(exceptionStr); - } - - String kbUrl = kbBaseUri + kbFilename; - urls.add(kbUrl); - JSONObject jsonFont = keyboard.optJSONObject(KMKey_Font); - JSONObject jsonOskFont = keyboard.optJSONObject(KMKey_OskFont); - ArrayList fontUrls = fontUrls(jsonFont, fontBaseUri, true); - ArrayList oskFontUrls = fontUrls(jsonOskFont, fontBaseUri, true); - if (fontUrls != null) { - urls.addAll(fontUrls); - } - if (oskFontUrls != null) { - for (String url : oskFontUrls) { - if (!urls.contains(url)) { - urls.add(url); - } - } - } - - languageID = ""; - languageName = ""; - int langCount = languages.length(); - for (int i = 0; i < langCount; i++) { - languageID += languages.getJSONObject(i).getString(KMKey_ID); - languageName += languages.getJSONObject(i).getString(KMKey_Name); - if (i < langCount - 1) { - languageID += ";"; - languageName += ";"; - } - } - - // Notify listeners: onDownloadStarted - if (kbDownloadEventListeners != null) { - HashMap keyboardInfo = new HashMap(); - keyboardInfo.put(KMKey_PackageID, packageID); - keyboardInfo.put(KMKey_KeyboardID, keyboardID); - keyboardInfo.put(KMKey_LanguageID, languageID); - keyboardInfo.put(KMKey_KeyboardName, keyboardName); - keyboardInfo.put(KMKey_LanguageName, languageName); - keyboardInfo.put(KMKey_KeyboardVersion, kbVersion); - keyboardInfo.put(KMKey_CustomKeyboard, isCustom); - keyboardInfo.put(KMKey_Font, font); - if (oskFont != null) { - keyboardInfo.put(KMKey_OskFont, oskFont); - } - KeyboardEventHandler.notifyListeners(kbDownloadEventListeners, EventType.KEYBOARD_DOWNLOAD_STARTED, keyboardInfo, 0); - } - - ret = 1; - int result = 0; - for (String url : urls) { - String directory = KMDefault_AssetPackages + File.separator + packageID; - File dir = new File(directory); - String filename = ""; - if (url.endsWith(".js")) { - int start = kbFilename.lastIndexOf("/"); - if (start < 0) { - start = 0; - } else { - start++; - } - if (!kbFilename.contains("-")) { - filename = kbFilename.substring(start, kbFilename.length() - 3) + "-" + kbVersion + ".js"; - } else { - filename = kbFilename.substring(start); - } - } - - result = FileDownloader.download(context, url, directory, filename); - if (result < 0) { - ret = -1; - break; - } - } - } catch (Exception e) { - ret = -1; - Log.e("Keyboard download", "Error: " + e); - e.printStackTrace(); - } - - return ret; - } - - @Override - protected void onProgressUpdate(Integer... progress) { - // Do nothing - } - - @Override - protected void onPostExecute(Integer result) { - if (showProgressDialog) { - if (progressDialog != null && progressDialog.isShowing()) { - try { - progressDialog.dismiss(); - progressDialog = null; - } catch (Exception e) { - progressDialog = null; - } - } - } - - // Notify listeners: onDownloadFinished - if (kbDownloadEventListeners != null) { - HashMap keyboardInfo = new HashMap(); - keyboardInfo.put(KMKey_PackageID, packageID); - keyboardInfo.put(KMKey_KeyboardID, keyboardID); - keyboardInfo.put(KMKey_LanguageID, languageID); - keyboardInfo.put(KMKey_KeyboardName, keyboardName); - keyboardInfo.put(KMKey_LanguageName, languageName); - keyboardInfo.put(KMKey_KeyboardVersion, kbVersion); - keyboardInfo.put(KMKey_CustomKeyboard, isCustom); - keyboardInfo.put(KMKey_Font, font); - if (oskFont != null) - keyboardInfo.put(KMKey_OskFont, oskFont); - KeyboardEventHandler.notifyListeners(kbDownloadEventListeners, EventType.KEYBOARD_DOWNLOAD_FINISHED, keyboardInfo, result); - } - - if (result > 0) { - if (KMManager.InAppKeyboard != null) { - InAppKeyboard.loadKeyboard(); - } - if ( KMManager.SystemKeyboard != null) { - SystemKeyboard.loadKeyboard(); - } - } - } - }.execute(); - } - - private static ArrayList fontUrls(JSONObject jsonFont, String baseUri, boolean isOskFont) { - if (jsonFont == null) - return null; - - ArrayList urls = new ArrayList(); - JSONArray fontSource = jsonFont.optJSONArray(KMKey_FontSource); - if (fontSource == null) - fontSource = jsonFont.optJSONArray(KMKey_Filename); // Font filename is deprecated - - if (fontSource != null) { - int fcCount = fontSource.length(); - for (int i = 0; i < fcCount; i++) { - String fontSourceString; - try { - fontSourceString = fontSource.getString(i); - if (fontSourceString.endsWith(".ttf") || fontSourceString.endsWith(".otf")) { - urls.add(baseUri + fontSourceString); - } else if (isOskFont && (fontSourceString.endsWith(".svg") || fontSourceString.endsWith(".woff"))) { - urls.add(baseUri + fontSourceString); - } else if (isOskFont && fontSourceString.contains(".svg#")) { - String fontFilename = fontSourceString.substring(0, fontSourceString.indexOf(".svg#") + 5); - urls.add(baseUri + fontFilename); - } - } catch (JSONException e) { - return null; - } - } - } else { - String fontSourceString; - try { - fontSourceString = jsonFont.optString(KMKey_FontSource, null); - if (fontSourceString == null) - fontSourceString = jsonFont.getString(KMKey_Filename); // Font filename is deprecated - - if (fontSourceString.endsWith(".ttf") || fontSourceString.endsWith(".otf")) { - urls.add(baseUri + fontSourceString); - } else if (isOskFont && (fontSourceString.endsWith(".svg") || fontSourceString.endsWith(".woff"))) { - urls.add(baseUri + fontSourceString); - } else if (isOskFont && fontSourceString.contains(".svg#")) { - String fontFilename = fontSourceString.substring(0, fontSourceString.indexOf(".svg#") + 5); - urls.add(baseUri + fontFilename); - } - } catch (JSONException e) { - return null; - } - } - - return urls; - } - } - - private static final class FileDownloader { - - /** - * Utility to download a file from urlStr and store it at directory/filename - * @param context - * @param urlStr URL of the file to download - * @param directory Path to store the file. If directory does not exist, it will be created - * @param filename Destination filename. If blank, it will use the filename from the URL - * @return - */ - public static int download(Context context, String urlStr, String directory, String filename) { - int ret = -1; - String fileName = ""; - String tmpFileName = ""; - File tmpFile = null; - File file = null; - - try { - if (directory == null) { - directory = ""; - } - directory = directory.trim(); - - String dirPath; - if (directory.length() != 0) { - directory = directory + "/"; - dirPath = context.getDir("data", Context.MODE_PRIVATE) + "/" + directory; - } else { - dirPath = context.getDir("data", Context.MODE_PRIVATE).toString(); - } - File dir = new File(dirPath); - if (!dir.exists()) { - dir.mkdir(); - } - - if (Connection.initialize(urlStr)) { - InputStream binStream = new BufferedInputStream(Connection.getInputStream(), 4096); - byte[] buff = new byte[4096]; - - filename = filename.trim(); - if (filename == null || filename.isEmpty()) { - fileName = Connection.getFile().substring(Connection.getFile().lastIndexOf('/') + 1); - if (fileName.lastIndexOf(".js") > 0 && !fileName.contains("-")) { - fileName = fileName.substring(0, filename.lastIndexOf(".js")) + "-1.0.js"; - } - } else { - fileName = filename; - } - tmpFileName = String.format("%s.tmp", fileName); - file = new File(dirPath, fileName); - tmpFile = new File(dirPath, tmpFileName); - FileOutputStream fos = new FileOutputStream(tmpFile); - - int len; - while ((len = binStream.read(buff)) != -1) { - fos.write(buff, 0, len); - } - - fos.flush(); - fos.close(); - binStream.close(); - - ret = 1; - } - } catch (Exception e) { - ret = -1; - Log.e("FD: Download failed!", "Error: " + e); - } finally { - if (ret > 0) { - if (tmpFile.exists() && tmpFile.length() > 0) { - if (file.exists()) { - file.delete(); - } - if (!tmpFile.renameTo(file)) { - ret = -1; - } else if (isDebugMode()) { - Log.d("FD: Download finished", "Filename = " + file.toString()); - } - } else { - ret = -1; - } - } else { - if (file.exists()) { - file.delete(); - } - if (tmpFile.exists()) { - tmpFile.delete(); - } - if (isDebugMode()) { - Log.d("FD: Could not download", "Filename = " + file.toString()); - } - } - - Connection.disconnect(); - } - - return ret; - } - } - public static boolean addKeyboard(Context context, HashMap keyboardInfo) { return KeyboardPickerActivity.addKeyboard(context, keyboardInfo); } @@ -1731,22 +936,6 @@ public final class KMManager { KMKeyboard.removeOnKeyboardEventListener(listener); } - public static void addKeyboardDownloadEventListener(OnKeyboardDownloadEventListener listener) { - if (kbDownloadEventListeners == null) { - kbDownloadEventListeners = new ArrayList(); - } - - if (listener != null && !kbDownloadEventListeners.contains(listener)) { - kbDownloadEventListeners.add(listener); - } - } - - public static void removeKeyboardDownloadEventListener(OnKeyboardDownloadEventListener listener) { - if (kbDownloadEventListeners != null) { - kbDownloadEventListeners.remove(listener); - } - } - public static int getKeyboardHeight(Context context) { return (int) context.getResources().getDimension(R.dimen.keyboard_height); } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardListActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardListActivity.java index 90a0685c99..1540f7adaa 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardListActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardListActivity.java @@ -14,9 +14,8 @@ import org.json.JSONObject; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardDownloadEventListener; import android.app.Activity; -import android.app.AlertDialog; +import android.app.DialogFragment; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.util.Log; @@ -73,7 +72,7 @@ public final class KeyboardListActivity extends Activity implements OnKeyboardDo final String langName = language.getString(KMManager.KMKey_Name); textView.setText(langName); - keyboards = language.getJSONArray(KMManager.KMKey_LanguageKeyboards); + keyboards = language.getJSONArray(KMKeyboardDownloaderActivity.KMKey_LanguageKeyboards); keyboardsArrayList = new ArrayList>(); int length = keyboards.length(); @@ -103,45 +102,23 @@ public final class KeyboardListActivity extends Activity implements OnKeyboardDo @Override public void onItemClick(AdapterView parent, View view, final int position, long id) { HashMap kbInfo = LanguageListActivity.getKeyboardInfo(langIndex, position); - String pkgID = kbInfo.get(KMManager.KMKey_PackageID); - String kbID = kbInfo.get(KMManager.KMKey_KeyboardID); - String langID = kbInfo.get(KMManager.KMKey_LanguageID); + final String pkgID = kbInfo.get(KMManager.KMKey_PackageID); + final String kbID = kbInfo.get(KMManager.KMKey_KeyboardID); + final String langID = kbInfo.get(KMManager.KMKey_LanguageID); String kbName = kbInfo.get(KMManager.KMKey_KeyboardName); String langName = kbInfo.get(KMManager.KMKey_LanguageName); - String kFont = kbInfo.get(KMManager.KMKey_Font); - String kOskFont = kbInfo.get(KMManager.KMKey_OskFont); - KMManager.KeyboardState kbState = KMManager.getKeyboardState(context, pkgID, kbID, langID); - //if (kbState == KMManager.KeyboardState.KEYBOARD_STATE_NEEDS_DOWNLOAD) { - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context); - dialogBuilder.setTitle(langName + ": " + kbName); - dialogBuilder.setMessage("Would you like to download this keyboard?"); - dialogBuilder.setPositiveButton("Download", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Download keyboard - if (KMManager.hasConnection(context)) { - KMManager.KMKeyboardDownloader.download(context, langIndex, position, true); - } else { - Toast.makeText(context, "No internet connection", Toast.LENGTH_SHORT).show(); - } - } - }); - dialogBuilder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Cancel - } - }); + Bundle args = new Bundle(); + args.putString(KMKeyboardDownloaderActivity.ARG_PKG_ID, pkgID); + args.putString(KMKeyboardDownloaderActivity.ARG_KB_ID, kbID); + args.putString(KMKeyboardDownloaderActivity.ARG_LANG_ID, langID); + args.putString(KMKeyboardDownloaderActivity.ARG_KB_NAME, kbName); + args.putString(KMKeyboardDownloaderActivity.ARG_LANG_NAME, langName); + args.putBoolean(KMKeyboardDownloaderActivity.ARG_IS_CUSTOM, false); + Intent i = new Intent(getApplicationContext(), KMKeyboardDownloaderActivity.class); + i.putExtras(args); + startActivity(i); - AlertDialog dialog = dialogBuilder.create(); - dialog.show(); - /*} else { - KeyboardPickerActivity.addKeyboard(context, kbInfo); - if (KMManager.InAppKeyboard != null) - KMManager.InAppKeyboard.setKeyboard(kbID, langID, kbName, langName, kFont, kOskFont); - if (KMManager.SystemKeyboard != null) - KMManager.SystemKeyboard.setKeyboard(kbID, langID, kbName, langName, kFont, kOskFont); - finish(); - }*/ } }); } catch (JSONException e) { @@ -152,13 +129,15 @@ public final class KeyboardListActivity extends Activity implements OnKeyboardDo @Override protected void onResume() { super.onResume(); - KMManager.addKeyboardDownloadEventListener(this); + KMKeyboardDownloaderActivity.addKeyboardDownloadEventListener(this); } @Override protected void onPause() { super.onPause(); - KMManager.removeKeyboardDownloadEventListener(this); + + // Intentionally not removing KeyboardDownloadEventListener to + // ensure onKeyboardDownloadFinished() gets called } @Override diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardPickerActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardPickerActivity.java index 164f46c8e5..28eb27ad09 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardPickerActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KeyboardPickerActivity.java @@ -180,7 +180,7 @@ public final class KeyboardPickerActivity extends Activity implements OnKeyboard @Override protected void onResume() { super.onResume(); - KMManager.addKeyboardDownloadEventListener(this); + KMKeyboardDownloaderActivity.addKeyboardDownloadEventListener(this); BaseAdapter adapter = (BaseAdapter) listAdapter; adapter.notifyDataSetChanged(); int curKbPos = getCurrentKeyboardIndex(); @@ -234,7 +234,10 @@ public final class KeyboardPickerActivity extends Activity implements OnKeyboard @Override protected void onPause() { super.onPause(); - KMManager.removeKeyboardDownloadEventListener(this); + + // Intentionally not removing KeyboardDownloadEventListener to + // ensure onKeyboardDownloadFinished() gets called + if (didUpdate) { if (KMManager.InAppKeyboard != null) { KMManager.InAppKeyboard.loadKeyboard(); @@ -400,7 +403,7 @@ public final class KeyboardPickerActivity extends Activity implements OnKeyboard list = (ArrayList>) inputStream.readObject(); inputStream.close(); } catch (Exception e) { - Log.e("Failed to read keyboards list", "Error: " + e); + Log.e("KeyboardPickerActivity", "Failed to read keyboards list. Error: " + e); list = null; } } @@ -563,10 +566,10 @@ public final class KeyboardPickerActivity extends Activity implements OnKeyboard String languageID = keyboardsList.get(i).get(KMManager.KMKey_LanguageID); String keyboardID = keyboardsList.get(i).get(KMManager.KMKey_KeyboardID); String kbVersion = keyboardsList.get(i).get(KMManager.KMKey_KeyboardVersion); - String url = String.format("%slanguages/%s/%s?device=%s", KMManager.kKeymanApiBaseURL, languageID, keyboardID, deviceType); + String url = String.format("%slanguages/%s/%s?device=%s", KMKeyboardDownloaderActivity.kKeymanApiBaseURL, languageID, keyboardID, deviceType); JSONObject kbData = jsonParser.getJSONObjectFromUrl(url); - JSONObject language = kbData.optJSONObject(KMManager.KMKey_Language); - JSONArray keyboards = language.getJSONArray(KMManager.KMKey_LanguageKeyboards); + JSONObject language = kbData.optJSONObject(KMKeyboardDownloaderActivity.KMKey_Language); + JSONArray keyboards = language.getJSONArray(KMKeyboardDownloaderActivity.KMKey_LanguageKeyboards); JSONObject keyboard = keyboards.getJSONObject(0); String newKbVersion = keyboard.optString(KMManager.KMKey_KeyboardVersion, "1.0"); String kbKey = String.format("%s_%s", languageID, keyboardID); @@ -615,10 +618,12 @@ public final class KeyboardPickerActivity extends Activity implements OnKeyboard if (KMManager.hasConnection(context)) { int len = keyboardsList.size(); for (int i = 0; i < len; i++) { - String packageID = keyboardsList.get(i).get(KMManager.KMKey_PackageID); - String languageID = keyboardsList.get(i).get(KMManager.KMKey_LanguageID); - String keyboardID = keyboardsList.get(i).get(KMManager.KMKey_KeyboardID); - String kbKey = String.format("%s_%s", languageID, keyboardID); + String pkgID = keyboardsList.get(i).get(KMManager.KMKey_PackageID); + String kbID = keyboardsList.get(i).get(KMManager.KMKey_KeyboardID); + String langID = keyboardsList.get(i).get(KMManager.KMKey_LanguageID); + String kbKey = String.format("%s_%s", langID, kbID); + String langName = keyboardsList.get(i).get(KMManager.KMKey_LanguageName); + String kbName = keyboardsList.get(i).get(KMManager.KMKey_KeyboardName); String kbVersion = keyboardsList.get(i).get(KMManager.KMKey_KeyboardVersion); String newKbVersion = keyboardVersions.get(kbKey); if (newKbVersion != null) { @@ -631,7 +636,16 @@ public final class KeyboardPickerActivity extends Activity implements OnKeyboard updateProgress.show(); } - KMManager.KMKeyboardDownloader.download(context, packageID, keyboardID, languageID, false); + Bundle bundle = new Bundle(); + bundle.putString(KMKeyboardDownloaderActivity.ARG_PKG_ID, pkgID); + bundle.putString(KMKeyboardDownloaderActivity.ARG_KB_ID, kbID); + bundle.putString(KMKeyboardDownloaderActivity.ARG_LANG_ID, langID); + bundle.putString(KMKeyboardDownloaderActivity.ARG_KB_NAME, kbName); + bundle.putString(KMKeyboardDownloaderActivity.ARG_LANG_NAME, langName); + bundle.putBoolean(KMKeyboardDownloaderActivity.ARG_IS_CUSTOM, false); + Intent intent = new Intent(context, KMKeyboardDownloaderActivity.class); + intent.putExtras(bundle); + context.startActivity(intent); } } } @@ -702,6 +716,11 @@ public final class KeyboardPickerActivity extends Activity implements OnKeyboard String languageID = keyboardInfo.get(KMManager.KMKey_LanguageID); String kbKey = String.format("%s_%s", languageID, keyboardID); int index = getKeyboardIndex(this, kbKey); + if (index == -1) { + // Add the downloaded keyboard if not found + addKeyboard(this, keyboardInfo); + index = getKeyboardIndex(this, kbKey); + } keyboardsList.set(index, keyboardInfo); saveKeyboardsList(this); } else if (result < 0) { @@ -738,5 +757,8 @@ public final class KeyboardPickerActivity extends Activity implements OnKeyboard checkingUpdates = false; } } + if (updateProgress != null && updateProgress.isShowing()) { + updateProgress.dismiss(); + } } } \ No newline at end of file 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 076da058b8..1d7c15c588 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 @@ -24,10 +24,8 @@ import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardDownloadEventListener import android.annotation.SuppressLint; import android.app.Activity; -import android.app.AlertDialog; import android.app.ProgressDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; @@ -97,7 +95,7 @@ public final class LanguageListActivity extends Activity implements OnKeyboardDo @Override protected void onResume() { super.onResume(); - KMManager.addKeyboardDownloadEventListener(this); + KMKeyboardDownloaderActivity.addKeyboardDownloadEventListener(this); if (!didExecuteParser) { didExecuteParser = true; new JSONParse().execute(); @@ -107,7 +105,9 @@ public final class LanguageListActivity extends Activity implements OnKeyboardDo @Override protected void onPause() { super.onPause(); - KMManager.removeKeyboardDownloadEventListener(this); + + // Intentionally not removing KeyboardDownloadEventListener to + // ensure onKeyboardDownloadFinished() gets called } @Override @@ -148,7 +148,7 @@ public final class LanguageListActivity extends Activity implements OnKeyboardDo String langName = language.getString(KMManager.KMKey_Name); String pkgID; - JSONArray keyboards = language.getJSONArray(KMManager.KMKey_LanguageKeyboards); + JSONArray keyboards = language.getJSONArray(KMKeyboardDownloaderActivity.KMKey_LanguageKeyboards); if (keyboards.getJSONObject(keyboardIndex).has(KMManager.KMKey_PackageID)) { pkgID = keyboards.getJSONObject(keyboardIndex).getString(KMManager.KMKey_PackageID); } else { @@ -187,8 +187,8 @@ public final class LanguageListActivity extends Activity implements OnKeyboardDo return null; } - languages = jsonObj.getJSONObject(KMManager.KMKey_Languages).getJSONArray(KMManager.KMKey_Languages); - options = jsonObj.getJSONObject(KMManager.KMKey_Options); + languages = jsonObj.getJSONObject(KMKeyboardDownloaderActivity.KMKey_Languages).getJSONArray(KMKeyboardDownloaderActivity.KMKey_Languages); + options = jsonObj.getJSONObject(KMKeyboardDownloaderActivity.KMKey_Options); keyboardsInfo = new HashMap>(); keyboardModifiedDates = new HashMap(); @@ -204,7 +204,7 @@ public final class LanguageListActivity extends Activity implements OnKeyboardDo String kbVersion = "1.0"; String isCustom = "N"; String kbFont = ""; - JSONArray langKeyboards = language.getJSONArray(KMManager.KMKey_LanguageKeyboards); + JSONArray langKeyboards = language.getJSONArray(KMKeyboardDownloaderActivity.KMKey_LanguageKeyboards); JSONObject keyboard = null; int kbLength = langKeyboards.length(); @@ -272,7 +272,7 @@ public final class LanguageListActivity extends Activity implements OnKeyboardDo jsonObj = new JSONObject(objInput.readObject().toString()); objInput.close(); } catch (Exception e) { - Log.e("Failed to read from cache file", "Error: " + e); + Log.e("LanguageListActivity", "Failed to read from cache file. Error: " + e); jsonObj = null; } @@ -287,7 +287,7 @@ public final class LanguageListActivity extends Activity implements OnKeyboardDo objOutput.writeObject(jsonObj.toString()); objOutput.close(); } catch (Exception e) { - Log.e("Failed to save to cache file", "Error: " + e); + Log.e("LanguageListActivity", "Failed to save to cache file. Error: " + e); } } @@ -361,7 +361,7 @@ public final class LanguageListActivity extends Activity implements OnKeyboardDo deviceType = "androidphone"; } - jsonObj = jsonParser.getJSONObjectFromUrl(KMManager.kKeymanApiBaseURL + "languages?device=" + deviceType); + jsonObj = jsonParser.getJSONObjectFromUrl(KMKeyboardDownloaderActivity.kKeymanApiBaseURL + "languages?device=" + deviceType); } catch (Exception e) { jsonObj = null; } @@ -392,8 +392,8 @@ public final class LanguageListActivity extends Activity implements OnKeyboardDo } try { - languages = jsonObj.getJSONObject(KMManager.KMKey_Languages).getJSONArray(KMManager.KMKey_Languages); - options = jsonObj.getJSONObject(KMManager.KMKey_Options); + languages = jsonObj.getJSONObject(KMKeyboardDownloaderActivity.KMKey_Languages).getJSONArray(KMKeyboardDownloaderActivity.KMKey_Languages); + options = jsonObj.getJSONObject(KMKeyboardDownloaderActivity.KMKey_Options); keyboardsInfo = new HashMap>(); keyboardModifiedDates = new HashMap(); @@ -411,7 +411,7 @@ public final class LanguageListActivity extends Activity implements OnKeyboardDo String kbFont = ""; String icon = "0"; String isEnabled = "true"; - JSONArray langKeyboards = language.getJSONArray(KMManager.KMKey_LanguageKeyboards); + JSONArray langKeyboards = language.getJSONArray(KMKeyboardDownloaderActivity.KMKey_LanguageKeyboards); JSONObject keyboard = null; int kbLength = langKeyboards.length(); @@ -494,44 +494,20 @@ public final class LanguageListActivity extends Activity implements OnKeyboardDo startActivity(i); } else { HashMap kbInfo = getKeyboardInfo(selectedIndex, 0); - String pkgID = kbInfo.get(KMManager.KMKey_PackageID); - String kbID = kbInfo.get(KMManager.KMKey_KeyboardID); - String langID = kbInfo.get(KMManager.KMKey_LanguageID); - String kFont = kbInfo.get(KMManager.KMKey_Font); - String kOskFont = kbInfo.get(KMManager.KMKey_OskFont); - KMManager.KeyboardState kbState = KMManager.getKeyboardState(context, pkgID, kbID, langID); - //if (kbState == KMManager.KeyboardState.KEYBOARD_STATE_NEEDS_DOWNLOAD) { - AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context); - dialogBuilder.setTitle(langName + ": " + kbName); - dialogBuilder.setMessage("Would you like to download this keyboard?"); - dialogBuilder.setPositiveButton("Download", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Download keyboard - if (KMManager.hasConnection(context)) { - KMManager.KMKeyboardDownloader.download(context, position, 0, true); - } else { - Toast.makeText(context, "No internet connection", Toast.LENGTH_SHORT).show(); - } - } - }); + final String pkgID = kbInfo.get(KMManager.KMKey_PackageID); + final String kbID = kbInfo.get(KMManager.KMKey_KeyboardID); + final String langID = kbInfo.get(KMManager.KMKey_LanguageID); - dialogBuilder.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int which) { - // Cancel - } - }); - - AlertDialog dialog = dialogBuilder.create(); - dialog.show(); - /*} - else { - KeyboardPickerActivity.addKeyboard(context, kbInfo); - if (KMManager.InAppKeyboard != null) - KMManager.InAppKeyboard.setKeyboard(kbID, langID, kbName, langName, kFont, kOskFont); - if (KMManager.SystemKeyboard != null) - KMManager.SystemKeyboard.setKeyboard(kbID, langID, kbName, langName, kFont, kOskFont); - finish(); - }*/ + Bundle bundle = new Bundle(); + bundle.putString(KMKeyboardDownloaderActivity.ARG_PKG_ID, pkgID); + bundle.putString(KMKeyboardDownloaderActivity.ARG_KB_ID, kbID); + bundle.putString(KMKeyboardDownloaderActivity.ARG_LANG_ID, langID); + bundle.putString(KMKeyboardDownloaderActivity.ARG_KB_NAME, kbName); + bundle.putString(KMKeyboardDownloaderActivity.ARG_LANG_NAME, langName); + bundle.putBoolean(KMKeyboardDownloaderActivity.ARG_IS_CUSTOM, false); + Intent i = new Intent(getApplicationContext(), KMKeyboardDownloaderActivity.class); + i.putExtras(bundle); + startActivity(i); } } }); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/FileDownloader.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/FileDownloader.java new file mode 100644 index 0000000000..a2769cbc3f --- /dev/null +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/FileDownloader.java @@ -0,0 +1,113 @@ +package com.tavultesoft.kmea.util; + +import android.content.Context; +import android.util.Log; + +import com.tavultesoft.kmea.KMManager; + +import java.io.BufferedInputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; + +public final class FileDownloader { + + /** + * Utility to download a file from urlStr and store it at directory/filename. + * If the directory does not exist, it will be created. + * @param context + * @param urlStr Source + * @param directory Destination path. + * @param filename Destination filename. If blank, it will use the filename from the URL + * @return + */ + public static int download(Context context, String urlStr, String directory, String filename) { + final int BUFFER_SIZE = 4096; + int ret = -1; + String fileName = ""; + String tmpFileName = ""; + File tmpFile = null; + File file = null; + + try { + if (directory == null) { + directory = ""; + } + directory = directory.trim(); + + String dirPath; + if (directory.length() != 0) { + directory = directory + "/"; + dirPath = context.getDir("data", Context.MODE_PRIVATE) + "/" + directory; + } else { + dirPath = context.getDir("data", Context.MODE_PRIVATE).toString(); + } + File dir = new File(dirPath); + if (!dir.exists()) { + dir.mkdir(); + } + + if (Connection.initialize(urlStr)) { + InputStream binStream = new BufferedInputStream(Connection.getInputStream(), BUFFER_SIZE); + byte[] buff = new byte[BUFFER_SIZE]; + + filename = filename.trim(); + if (filename == null || filename.isEmpty()) { + fileName = Connection.getFile().substring(Connection.getFile().lastIndexOf('/') + 1); + if (fileName.lastIndexOf(".js") > 0 && !fileName.contains("-")) { + fileName = fileName.substring(0, filename.lastIndexOf(".js")) + "-1.0.js"; + } + } else { + fileName = filename; + } + tmpFileName = String.format("%s.tmp", fileName); + file = new File(dirPath, fileName); + tmpFile = new File(dirPath, tmpFileName); + FileOutputStream fos = new FileOutputStream(tmpFile); + + int len; + while ((len = binStream.read(buff)) != -1) { + fos.write(buff, 0, len); + } + + fos.flush(); + fos.close(); + binStream.close(); + + ret = 1; + } + } catch (Exception e) { + ret = -1; + Log.e("FileDownloader", "Download failed! Error: " + e); + } finally { + if (ret > 0) { + if (tmpFile.exists() && tmpFile.length() > 0) { + if (file.exists()) { + file.delete(); + } + if (!tmpFile.renameTo(file)) { + ret = -1; + } else if (KMManager.isDebugMode()) { + Log.d("FileDownloader", "Download finished for filename " + file.toString()); + } + } else { + ret = -1; + } + } else { + if (file.exists()) { + file.delete(); + } + if (tmpFile.exists()) { + tmpFile.delete(); + } + if (KMManager.isDebugMode()) { + Log.d("FileDownloader", "Could not download filename " + file.toString()); + } + } + + Connection.disconnect(); + } + + return ret; + } +} diff --git a/android/Samples/KMSample1/app/src/main/java/com/keyman/kmsample1/MainActivity.java b/android/Samples/KMSample1/app/src/main/java/com/keyman/kmsample1/MainActivity.java index e7d89850f5..ed77cc9ac2 100644 --- a/android/Samples/KMSample1/app/src/main/java/com/keyman/kmsample1/MainActivity.java +++ b/android/Samples/KMSample1/app/src/main/java/com/keyman/kmsample1/MainActivity.java @@ -7,6 +7,7 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; import com.tavultesoft.kmea.KMTextView; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardEventListener; @@ -38,6 +39,7 @@ public class MainActivity extends Activity implements OnKeyboardEventListener, O kbInfo.put(KMManager.KMKey_LanguageName, "Tamil"); kbInfo.put(KMManager.KMKey_KeyboardVersion, "1.1"); kbInfo.put(KMManager.KMKey_Font, "aava1.ttf"); + //kbInfo.put(KMManager.KMKey_Font, KMManager.KMDefault_KeyboardFont); // Use the default font KMManager.addKeyboard(this, kbInfo); } @@ -68,7 +70,7 @@ public class MainActivity extends Activity implements OnKeyboardEventListener, O super.onResume(); KMManager.onResume(); KMManager.addKeyboardEventListener(this); - KMManager.addKeyboardDownloadEventListener(this); + KMKeyboardDownloaderActivity.addKeyboardDownloadEventListener(this); } @Override @@ -76,7 +78,9 @@ public class MainActivity extends Activity implements OnKeyboardEventListener, O super.onPause(); KMManager.onPause(); KMManager.removeKeyboardEventListener(this); - KMManager.removeKeyboardDownloadEventListener(this); + + // Intentionally not removing KeyboardDownloadEventListener to + // ensure onKeyboardDownloadFinished() gets called } @Override diff --git a/android/Samples/KMSample2/app/src/main/java/com/keyman/kmsample2/SystemKeyboard.java b/android/Samples/KMSample2/app/src/main/java/com/keyman/kmsample2/SystemKeyboard.java index 9e38d1f01b..2f245c6750 100644 --- a/android/Samples/KMSample2/app/src/main/java/com/keyman/kmsample2/SystemKeyboard.java +++ b/android/Samples/KMSample2/app/src/main/java/com/keyman/kmsample2/SystemKeyboard.java @@ -45,6 +45,7 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven kbInfo.put(KMManager.KMKey_LanguageName, "Tamil"); kbInfo.put(KMManager.KMKey_KeyboardVersion, "1.1"); kbInfo.put(KMManager.KMKey_Font, "aava1.ttf"); + //kbInfo.put(KMManager.KMKey_Font, KMManager.KMDefault_KeyboardFont); // Use the default font KMManager.addKeyboard(this, kbInfo); } diff --git a/android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java b/android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java index f79e7835b4..bb0da07e7b 100644 --- a/android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java +++ b/android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java @@ -7,6 +7,7 @@ import android.util.Log; import android.view.Menu; import android.view.MenuItem; +import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; import com.tavultesoft.kmea.KMTextView; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardEventListener; @@ -82,7 +83,7 @@ public class MainActivity extends Activity implements OnKeyboardEventListener, O super.onResume(); KMManager.onResume(); KMManager.addKeyboardEventListener(this); - KMManager.addKeyboardDownloadEventListener(this); + KMKeyboardDownloaderActivity.addKeyboardDownloadEventListener(this); } @Override @@ -90,7 +91,9 @@ public class MainActivity extends Activity implements OnKeyboardEventListener, O super.onPause(); KMManager.onPause(); KMManager.removeKeyboardEventListener(this); - KMManager.removeKeyboardDownloadEventListener(this); + + // Intentionally not removing KeyboardDownloadEventListener to + // ensure onKeyboardDownloadFinished() gets called } @Override diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index 9bccbdf568..1f77fa2f1a 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ C040E5101F8606E300901EE4 /* TextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = C040E50F1F8606E300901EE4 /* TextField.swift */; }; C040E5121F86107E00901EE4 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C040E5111F86107E00901EE4 /* AppDelegate.swift */; }; C040E5141F86108900901EE4 /* MainViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C040E5131F86108900901EE4 /* MainViewController.swift */; }; + C042ED5D1FC6A65A001D82F4 /* Version.swift in Sources */ = {isa = PBXBuildFile; fileRef = C042ED5C1FC6A65A001D82F4 /* Version.swift */; }; C04514881F85D7F500D88416 /* KeyboardViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04514871F85D7F500D88416 /* KeyboardViewController.swift */; }; C045148A1F85DF9100D88416 /* InputViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C04514891F85DF9000D88416 /* InputViewController.swift */; }; C0452BAB1F9F1FE10064431A /* Language.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0452BAA1F9F1FE10064431A /* Language.swift */; }; @@ -87,6 +88,8 @@ C07A9D8C1FD176AB00828ADD /* KeyboardRepositoryDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A9D8B1FD176AB00828ADD /* KeyboardRepositoryDelegate.swift */; }; C07A9D8E1FD1798900828ADD /* APIKeyboardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = C07A9D8D1FD1798900828ADD /* APIKeyboardRepository.swift */; }; C082CE151F90AFD400860F02 /* Collection+SafeAccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = C082CE141F90AFD400860F02 /* Collection+SafeAccess.swift */; }; + C08E698D1FDA392D0026056B /* Migrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08E698C1FDA392D0026056B /* Migrations.swift */; }; + C08E69911FDA6F6F0026056B /* FullKeyboardID.swift in Sources */ = {isa = PBXBuildFile; fileRef = C08E69901FDA6F6F0026056B /* FullKeyboardID.swift */; }; C0959CD41F99C44E00B616BC /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0959CD31F99C44E00B616BC /* Constants.swift */; }; C0A93A541F8B21240079948B /* Manager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0A93A531F8B21240079948B /* Manager.swift */; }; C0B09EAE1FCFD10F002F39AF /* FontManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C0B09EAD1FCFD10F002F39AF /* FontManager.swift */; }; @@ -196,6 +199,7 @@ C040E50F1F8606E300901EE4 /* TextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextField.swift; sourceTree = ""; }; C040E5111F86107E00901EE4 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; C040E5131F86108900901EE4 /* MainViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainViewController.swift; sourceTree = ""; }; + C042ED5C1FC6A65A001D82F4 /* Version.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Version.swift; sourceTree = ""; }; C04514861F85D7F500D88416 /* SystemKeyboard-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SystemKeyboard-Bridging-Header.h"; sourceTree = ""; }; C04514871F85D7F500D88416 /* KeyboardViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardViewController.swift; sourceTree = ""; }; C04514891F85DF9000D88416 /* InputViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InputViewController.swift; sourceTree = ""; }; @@ -222,6 +226,8 @@ C08C62121F67C31100268D03 /* KeyboardNameTableViewCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardNameTableViewCell.swift; sourceTree = ""; }; C08C62141F67C8D500268D03 /* KeyboardInfoViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardInfoViewController.swift; sourceTree = ""; }; C08C62161F67CFB800268D03 /* KeyboardPickerViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeyboardPickerViewController.swift; sourceTree = ""; }; + C08E698C1FDA392D0026056B /* Migrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migrations.swift; sourceTree = ""; }; + C08E69901FDA6F6F0026056B /* FullKeyboardID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FullKeyboardID.swift; sourceTree = ""; }; C092D8381F6A70C8005C5485 /* LanguageDetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LanguageDetailViewController.swift; sourceTree = ""; }; C0959CD31F99C44E00B616BC /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = ""; }; C0A5FF361F6682EB00BE740C /* PopoverView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PopoverView.swift; sourceTree = ""; }; @@ -442,6 +448,8 @@ C055E6EA1F99ED090035C2DD /* RegisteredFont.swift */, C007C4641F9F52D8006461B9 /* LanguagesAPICall.swift */, C05F432C1FBD5A4C0058CBD4 /* KeyboardAPICall.swift */, + C042ED5C1FC6A65A001D82F4 /* Version.swift */, + C08E69901FDA6F6F0026056B /* FullKeyboardID.swift */, ); path = Model; sourceTree = ""; @@ -588,6 +596,7 @@ C0E30C8B1FC40D0400C80416 /* Storage.swift */, C0EF3E7A1F95B65300CE9BD4 /* KeymanWebDelegate.swift */, C05B14321FD914870082A316 /* Log.swift */, + C08E698C1FDA392D0026056B /* Migrations.swift */, ); path = Classes; sourceTree = ""; @@ -833,9 +842,11 @@ C06D37341F81F5C300F61AE0 /* HTTPDownloader.swift in Sources */, C0452BAF1F9F22A80064431A /* Font.swift in Sources */, C0D3F3601F9F3AD80055C7CF /* InstallableKeyboard.swift in Sources */, + C042ED5D1FC6A65A001D82F4 /* Version.swift in Sources */, C0D3F35E1F9F33490055C7CF /* Options.swift in Sources */, C06D37351F81F5C300F61AE0 /* HTTPDownloadRequest.swift in Sources */, C0324B8F1F8750B700AF3785 /* TextView.swift in Sources */, + C08E698D1FDA392D0026056B /* Migrations.swift in Sources */, C07A9D8C1FD176AB00828ADD /* KeyboardRepositoryDelegate.swift in Sources */, C024C9961FA6EC650060583B /* NotificationCenter+Typed.swift in Sources */, C06D37361F81F5C300F61AE0 /* KeyboardMenuView.swift in Sources */, @@ -862,6 +873,7 @@ C082CE151F90AFD400860F02 /* Collection+SafeAccess.swift in Sources */, C06D37441F81F5C400F61AE0 /* KeyboardNameTableViewCell.swift in Sources */, C07A9D8A1FD1762700828ADD /* KeyboardRepository.swift in Sources */, + C08E69911FDA6F6F0026056B /* FullKeyboardID.swift in Sources */, C040E5101F8606E300901EE4 /* TextField.swift in Sources */, C0324B8D1F87480700AF3785 /* TextFieldDelegateProxy.swift in Sources */, C075EB061F8EFF870041F4BD /* String+Helpers.swift in Sources */, diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Constants.swift b/ios/engine/KMEI/KeymanEngine/Classes/Constants.swift index 6445343ff4..ff25d5357b 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Constants.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Constants.swift @@ -19,6 +19,7 @@ public enum Key { static let engineVersion = "KeymanEngineVersion" static let keyboardPickerDisplayed = "KeyboardPickerDisplayed" static let synchronizeSWKeyboard = "KeymanSynchronizeSWKeyboard" + static let migrationLevel = "KeymanEngineMigrationLevel" // JSON keys for language REST calls static let options = "options" diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift b/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift index 21682a9de2..7f50b4df9d 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Extension/UserDefaults+Types.swift @@ -17,7 +17,7 @@ public extension UserDefaults { do { return try array.map { try decoder.decode(InstallableKeyboard.self, from: $0) } } catch { - log.error("UserDefaults: Error decoding keyboards: \(error)") + log.error("Error decoding keyboards: \(error)") return nil } } @@ -32,32 +32,32 @@ public extension UserDefaults { let array = try keyboards.map { try encoder.encode($0) } set(array, forKey: key) } catch { - log.error("UserDefaults: Error encoding keyboards: \(error)") + log.error("Error encoding keyboards: \(error)") } } - public func installableKeyboard(forKey key: String) -> InstallableKeyboard? { + public func fullKeyboardID(forKey key: String) -> FullKeyboardID? { guard let data = data(forKey: key) else { return nil } do { - return try PropertyListDecoder().decode(InstallableKeyboard.self, from: data) + return try PropertyListDecoder().decode(FullKeyboardID.self, from: data) } catch { - log.error("UserDefaults: Error decoding keyboard: \(error)") + log.error("Error decoding FullKeyboardID: \(error)") return nil } } - public func set(_ keyboard: InstallableKeyboard?, forKey key: String) { - guard let keyboard = keyboard else { + public func set(_ fullKeyboardID: FullKeyboardID?, forKey key: String) { + guard let id = fullKeyboardID else { removeObject(forKey: key) return } do { - let data = try PropertyListEncoder().encode(keyboard) + let data = try PropertyListEncoder().encode(id) set(data, forKey: key) } catch { - log.error("UserDefaults: Error encoding keyboard: \(error)") + log.error("Error encoding FullKeyboardID: \(error)") } } @@ -71,17 +71,27 @@ public extension UserDefaults { } } - public var currentKeyboard: InstallableKeyboard? { + public var currentKeyboardID: FullKeyboardID? { get { - return installableKeyboard(forKey: Key.userCurrentKeyboard) + return fullKeyboardID(forKey: Key.userCurrentKeyboard) } - set(keyboard) { - set(keyboard, forKey: Key.userCurrentKeyboard) + set(fullKeyboardID) { + set(fullKeyboardID, forKey: Key.userCurrentKeyboard) } } - public func userKeyboard(withID keyboardID: String, languageID: String) -> InstallableKeyboard? { - return userKeyboards?.first { $0.id == keyboardID && $0.languageID == languageID } + public func userKeyboard(withFullID fullID: FullKeyboardID) -> InstallableKeyboard? { + return userKeyboards?.first { $0.fullID == fullID } + } + + var migrationLevel: Int { + get { + return integer(forKey: Key.migrationLevel) + } + + set(level) { + set(level, forKey: Key.migrationLevel) + } } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/FontManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/FontManager.swift index 399bbb7b82..ceaca51a23 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/FontManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/FontManager.swift @@ -28,38 +28,24 @@ public class FontManager { /// Registers all new fonts found in the font path. Call this after you have preloaded all your font files /// with `preloadFontFile(atPath:shouldOverwrite:)` public func registerCustomFonts() { - guard let customFonts = customFonts() else { + guard let keyboardDirs = Storage.active.keyboardDirs else { return } - - for fontURL in customFonts { - _ = registerFont(at: fontURL) + for dir in keyboardDirs { + registerFonts(in: dir) } } /// Unregisters all registered fonts in the font path. public func unregisterCustomFonts() { - guard let customFonts = customFonts() else { + guard let keyboardDirs = Storage.active.keyboardDirs else { return } - - for fontURL in customFonts { - _ = unregisterFont(at: fontURL) + for dir in keyboardDirs { + unregisterFonts(in: dir) } } - private func customFonts() -> [URL]? { - let urls: [URL] - do { - urls = try FileManager.default.contentsOfDirectory(at: Storage.active.fontDir, - includingPropertiesForKeys: nil) - } catch { - log.error("Failed to list font dir contents: \(error)") - return nil - } - return urls.filter { $0.lastPathComponent.hasFontExtension } - } - private func readFontName(at url: URL) -> String? { guard let provider = CGDataProvider(url: url as CFURL) else { log.error("Failed to open \(url)") @@ -139,6 +125,26 @@ public class FontManager { return font.isRegistered } + public func registerFonts(in directory: URL) { + guard let urls = try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) else { + log.error("Could not list contents of directory \(directory)") + return + } + for url in urls where url.lastPathComponent.hasFontExtension { + _ = registerFont(at: url) + } + } + + public func unregisterFonts(in directory: URL, fromSystemOnly: Bool = true) { + guard let urls = try? FileManager.default.contentsOfDirectory(at: directory, includingPropertiesForKeys: nil) else { + log.error("Could not list contents of directory \(directory)") + return + } + for url in urls where url.lastPathComponent.hasFontExtension { + _ = unregisterFont(at: url, fromSystemOnly: fromSystemOnly) + } + } + private func fontExists(_ fontName: String) -> Bool { return UIFont.familyNames.contains { familyName in UIFont.fontNames(forFamilyName: familyName).contains(fontName) diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardMenuView.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardMenuView.swift index caa0def4f0..8e51c0f74a 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardMenuView.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardMenuView.swift @@ -49,11 +49,7 @@ class KeyboardMenuView: UIView, UITableViewDelegate, UITableViewDataSource, UIGe _tableList = keyboardList _tableList!.append(titleCloseButton) } else { - var keyboard = Defaults.keyboard - if let version = Manager.shared.latestKeyboardFileVersion(withID: Defaults.keyboard.id) { - keyboard.version = version - } - _tableList = [keyboard] + _tableList = [Defaults.keyboard] _tableList!.append(titleCloseButton) } return _tableList! @@ -284,7 +280,7 @@ class KeyboardMenuView: UIView, UITableViewDelegate, UITableViewDataSource, UIGe let keyboard = tableList[indexPath.row] as! InstallableKeyboard cell.textLabel?.text = keyboard.name cell.tag = indexPath.row - if (Manager.shared.languageID == keyboard.languageID) && (Manager.shared.keyboardID == keyboard.id) { + if Manager.shared.currentKeyboardID == keyboard.fullID { cell.selectionStyle = .none cell.isSelected = true cell.accessoryType = .checkmark diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/KeyboardRepository.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/KeyboardRepository.swift index ab65eed202..13e24a2101 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/KeyboardRepository.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeyboardRepository/KeyboardRepository.swift @@ -26,7 +26,7 @@ public extension KeyboardRepository { guard let keyboard = language.keyboards?.first(where: { $0.id == keyboardID }) else { return nil } - return InstallableKeyboard(keyboard: keyboard, language: language) + return InstallableKeyboard(keyboard: keyboard, language: language, isCustom: false) } public func fetch(completionHandler: CompletionHandler? = nil) { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeymanWebViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanWebViewController.swift index f363c443d2..5ebf8d786c 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeymanWebViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeymanWebViewController.swift @@ -11,6 +11,7 @@ import WebKit // MARK: - UIViewController class KeymanWebViewController: UIViewController { + let storage: Storage weak var delegate: KeymanWebDelegate? var webView: WKWebView! @@ -22,6 +23,15 @@ class KeymanWebViewController: UIViewController { } } + init(storage: Storage) { + self.storage = storage + super.init(nibName: nil, bundle: nil) + } + + required init?(coder aDecoder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + override func loadView() { let config = WKWebViewConfiguration() let prefs = WKPreferences() @@ -62,10 +72,7 @@ extension KeymanWebViewController { let y = CGFloat(Float(components[1])!) let w = CGFloat(Float(components[2])!) let h = CGFloat(Float(components[3])!) - let isPad = UIDevice.current.userInterfaceIdiom == .pad - let adjY: CGFloat = isPad ? -0.5 : -1.0 - let frame = CGRect(x: x - w / 2.0, y: y - adjY, width: w, height: h) - completion(frame) + completion(KeymanWebViewController.keyFrame(x: x, y: y, w: w, h: h)) } } @@ -121,26 +128,20 @@ extension KeymanWebViewController { // family does not have to match the name in the font file. It only has to be unique. return [ "family": "\(keyboardID)__\(isOsk ? "osk" : "display")", - "files": font.source + "files": font.source.map { storage.fontURL(forKeyboardID: keyboardID, filename: $0).absoluteString } ] } - func setKeyboard(id: String, - name: String, - languageID: String, - languageName: String, - fileURL: URL, - font: Font?, - oskFont: Font?) { + func setKeyboard(_ keyboard: InstallableKeyboard) { var stub: [String: Any] = [ - "KI": "Keyboard_\(id)", - "KN": name, - "KLC": languageID, - "KL": languageName, - "KF": fileURL.absoluteString + "KI": "Keyboard_\(keyboard.id)", + "KN": keyboard.name, + "KLC": keyboard.languageID, + "KL": keyboard.languageName, + "KF": storage.keyboardURL(for: keyboard).absoluteString ] - let displayFont = fontObject(from: font, keyboardID: id, isOsk: false) - let oskFont = fontObject(from: oskFont, keyboardID: id, isOsk: true) ?? displayFont + let displayFont = fontObject(from: keyboard.font, keyboardID: keyboard.id, isOsk: false) + let oskFont = fontObject(from: keyboard.oskFont, keyboardID: keyboard.id, isOsk: true) ?? displayFont if let displayFont = displayFont { stub["KFont"] = displayFont } @@ -160,6 +161,7 @@ extension KeymanWebViewController { return } + log.debug("Keyboard stub: \(stubString)") webView.evaluateJavaScript("setKeymanLanguage(\(stubString));", completionHandler: nil) } } @@ -198,7 +200,7 @@ extension KeymanWebViewController: WKScriptMessageHandler { let h = CGFloat(Float(fragment[hKey.upperBound.. CGRect { - let isPad = UIDevice.current.userInterfaceIdiom == .pad - let adjY: CGFloat = isPad ? -0.5 : -1.0 - return CGRect(x: x - w / 2.0, y: y - adjY, width: w, height: h) + private static func keyFrame(x: CGFloat, y: CGFloat, w: CGFloat, h: CGFloat) -> CGRect { + // kmw adds w/2 to x. + return CGRect(x: x - w / 2.0, y: y, width: w, height: h) } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardPickerViewController.swift b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardPickerViewController.swift index 3ff1f82b3b..8e0967c5d3 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardPickerViewController.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/LanguagePicker/KeyboardPickerViewController.swift @@ -164,7 +164,7 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { cell.detailTextLabel?.text = kb.name cell.tag = indexPath.row - if isCurrentKeyboard(languageID: kb.languageID, keyboardID: kb.id) { + if Manager.shared.currentKeyboardID == kb.fullID { cell.selectionStyle = .blue cell.isSelected = true cell.accessoryType = .detailDisclosureButton @@ -273,16 +273,7 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { } private func loadUserKeyboards() { - let userData = Storage.active.userDefaults - - if let userKeyboards = userData.userKeyboards { - self.userKeyboards = userKeyboards - } else { - userKeyboards = [Defaults.keyboard] - userData.userKeyboards = userKeyboards - userData.synchronize() - } - + userKeyboards = Storage.active.userDefaults.userKeyboards ?? [] tableView.reloadData() } @@ -365,7 +356,7 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { private func scroll(toSelectedKeyboard animated: Bool) { let index = userKeyboards.index { kb in - return isCurrentKeyboard(languageID: kb.languageID, keyboardID: kb.id) + return Manager.shared.currentKeyboardID == kb.fullID } if let index = index { @@ -388,11 +379,6 @@ class KeyboardPickerViewController: UITableViewController, UIAlertViewDelegate { } } - private func isCurrentKeyboard(languageID: String?, keyboardID: String?) -> Bool { - return Manager.shared.keyboardID == keyboardID && - Manager.shared.languageID == languageID - } - @objc func hideToolbarDelayed(_ timer: Timer) { navigationController?.setToolbarHidden(true, animated: true) } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift index be001782e5..73fd483250 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Manager.swift @@ -91,8 +91,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat /// Set this to `UIApplication.shared.openURL` in your application. public var openURL: ((URL) -> Bool)? - var keyboardID: String? - var languageID: String? + var currentKeyboardID: FullKeyboardID? weak var keymanWebDelegate: KeymanWebDelegate? var currentRequest: HTTPDownloadRequest? var shouldReloadKeyboard = false @@ -139,24 +138,12 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat URLProtocol.registerClass(KeymanURLProtocol.self) - if !Util.isSystemKeyboard { - if let shared = Storage.shared, - let nonShared = Storage.nonShared { - let keysToCopy = [Key.userKeyboardsList, Key.userCurrentKeyboard, - Key.engineVersion, Key.keyboardPickerDisplayed] - nonShared.copyUserDefaults(to: shared, withKeys: keysToCopy, shouldOverwrite: false) - do { - try nonShared.copyFiles(to: shared) - } catch { - log.error("Failed to copy files to shared container: \(error)") - } - } - let userData = Storage.active.userDefaults - let isKPDisplayed = userData.bool(forKey: Key.keyboardPickerDisplayed) - if isKPDisplayed { - isKeymanHelpOn = false - } - } else { + Migrations.migrate(storage: Storage.active) + if Storage.active.userDefaults.userKeyboards?.isEmpty ?? true { + Storage.active.userDefaults.userKeyboards = [Defaults.keyboard] + } + + if Util.isSystemKeyboard || Storage.active.userDefaults.bool(forKey: Key.keyboardPickerDisplayed) { isKeymanHelpOn = false } @@ -173,7 +160,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat updateUserKeyboards(with: Defaults.keyboard) - keymanWeb = KeymanWebViewController(nibName: nil, bundle: nil) + keymanWeb = KeymanWebViewController(storage: Storage.active) keymanWeb.frame = CGRect(origin: .zero, size: keyboardSize) keymanWeb.delegate = self reloadKeyboard(in: keymanWeb) @@ -206,8 +193,8 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat /// - SeeAlso: /// - addKeyboard() /// - Returns: Whether the keyboard was set successfully - public func setKeyboard(withID keyboardID: String, languageID: String) -> Bool { - if let keyboard = Storage.active.userDefaults.userKeyboard(withID: keyboardID, languageID: languageID) { + public func setKeyboard(withFullID fullID: FullKeyboardID) -> Bool { + if let keyboard = Storage.active.userDefaults.userKeyboard(withFullID: fullID) { return setKeyboard(keyboard) } return false @@ -217,29 +204,26 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat /// /// - Returns: Whether the keyboard was set successfully public func setKeyboard(_ kb: InstallableKeyboard) -> Bool { - if kb.languageID == self.languageID && kb.id == self.keyboardID { - log.info("Keyboard unchanged: \(kb.languageID)_\(kb.id)") + if kb.fullID == currentKeyboardID { + log.info("Keyboard unchanged: \(kb.fullID)") return false } - log.info("Setting language: \(kb.languageID)_\(kb.id)") + log.info("Setting language: \(kb.fullID)") - self.languageID = kb.languageID - self.keyboardID = kb.id + currentKeyboardID = kb.fullID if let fontFilename = kb.font?.source.first(where: { $0.hasFontExtension }) { - _ = FontManager.shared.registerFont(at: Storage.active.fontURL(forFilename: fontFilename)) + _ = FontManager.shared.registerFont(at: Storage.active.fontURL(forKeyboardID: kb.id, filename: fontFilename)) } if let oskFontFilename = kb.oskFont?.source.first(where: { $0.hasFontExtension }) { - _ = FontManager.shared.registerFont(at: Storage.active.fontURL(forFilename: oskFontFilename)) + _ = FontManager.shared.registerFont(at: Storage.active.fontURL(forKeyboardID: kb.id, filename: oskFontFilename)) } - keymanWeb.setKeyboard(id: kb.id, name: kb.name, languageID: kb.languageID, languageName: kb.languageName, - fileURL: Storage.active.keyboardURL(for: kb), font: kb.font, oskFont: kb.oskFont) + keymanWeb.setKeyboard(kb) let userData = Util.isSystemKeyboard ? UserDefaults.standard : Storage.active.userDefaults - - userData.currentKeyboard = kb + userData.currentKeyboardID = kb.fullID userData.synchronize() if isKeymanHelpOn { @@ -269,7 +253,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat var userKeyboards = userDefaults.userKeyboards ?? [] // Update keyboard if it exists - if let index = userKeyboards.index(where: { $0.id == keyboard.id && $0.languageID == keyboard.languageID }) { + if let index = userKeyboards.index(where: { $0.fullID == keyboard.fullID }) { userKeyboards[index] = keyboard } else { userKeyboards.append(keyboard) @@ -282,9 +266,9 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat /// Removes a keyboard from the list in the keyboard picker if it exists. /// - Returns: The keyboard exists and was removed - public func removeKeyboard(withID keyboardID: String, languageID: String) -> Bool { + public func removeKeyboard(withFullID fullID: FullKeyboardID) -> Bool { // Remove keyboard from the list if it exists - let index = Storage.active.userDefaults.userKeyboards?.index { $0.id == keyboardID && $0.languageID == languageID } + let index = Storage.active.userDefaults.userKeyboards?.index { $0.fullID == fullID } if let index = index { return removeKeyboard(at: index) } @@ -311,28 +295,42 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat userData.set([Date()], forKey: Key.synchronizeSWKeyboard) userData.synchronize() + log.info("Removing keyboard with ID \(kb.id) and languageID \(kb.languageID)") + // Set a new keyboard if deleting the current one - if kb.id == keyboardID && kb.languageID == languageID { + if kb.fullID == currentKeyboardID { setKeyboard(userKeyboards[0]) } + if !userKeyboards.contains(where: { $0.id == kb.id }) { + let keyboardDir = Storage.active.keyboardDir(forID: kb.id) + FontManager.shared.unregisterFonts(in: keyboardDir, fromSystemOnly: false) + log.info("Deleting directory \(keyboardDir)") + if (try? FileManager.default.removeItem(at: keyboardDir)) == nil { + log.error("Failed to delete \(keyboardDir)") + } + } else { + log.info("User has another language installed. Skipping delete of keyboard files.") + } + NotificationCenter.default.post(name: Notifications.keyboardRemoved, object: self, value: kb) return true } /// - Returns: Info for the current keyboard, if a keyboard is set - public var currentKeyboardInfo: InstallableKeyboard? { - guard let keyboardID = keyboardID, let languageID = languageID else { + public var currentKeyboard: InstallableKeyboard? { + guard let fullID = currentKeyboardID else { return nil } - return Storage.active.userDefaults.userKeyboard(withID: keyboardID, languageID: languageID) + return Storage.active.userDefaults.userKeyboard(withFullID: fullID) } /// Switch to the next keyboard. /// - Returns: Index of the newly selected keyboard. public func switchToNextKeyboard() -> Int? { guard let userKeyboards = Storage.active.userDefaults.userKeyboards, - let index = userKeyboards.index(where: { isCurrentKeyboard($0) }) else { + let index = userKeyboards.index(where: { self.currentKeyboardID == $0.fullID }) + else { return nil } let newIndex = (index + 1) % userKeyboards.count @@ -340,21 +338,13 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat return newIndex } - func isCurrentKeyboard(withID keyboardID: String?, languageID: String?) -> Bool { - return self.keyboardID == keyboardID && self.languageID == languageID - } - - func isCurrentKeyboard(_ keyboard: InstallableKeyboard) -> Bool { - return keyboard.id == self.keyboardID && keyboard.languageID == self.languageID - } - /// - Returns: The font name for the given keyboard ID and languageID, or returns nil if /// - The keyboard doesn't have a font /// - The keyboard info is not available in the user keyboards list - public func fontNameForKeyboard(withID keyboardID: String, languageID: String) -> String? { - let kb = Storage.active.userDefaults.userKeyboard(withID: keyboardID, languageID: languageID) + public func fontNameForKeyboard(withFullID fullID: FullKeyboardID) -> String? { + let kb = Storage.active.userDefaults.userKeyboard(withFullID: fullID) if let filename = kb?.font?.source.first(where: { $0.hasFontExtension }) { - let fontURL = Storage.active.fontURL(forFilename: filename) + let fontURL = Storage.active.fontURL(forKeyboardID: fullID.keyboardID, filename: filename) return FontManager.shared.fontName(at: fontURL) } return nil @@ -363,10 +353,10 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat /// - Returns: the OSK font name for the given keyboard ID and languageID, or returns nil if /// - The keyboard doesn't have an OSK font /// - The keyboard info is not available in the user keyboards list - func oskFontNameForKeyboard(withID keyboardID: String, languageID: String) -> String? { - let kb = Storage.active.userDefaults.userKeyboard(withID: keyboardID, languageID: languageID) + func oskFontNameForKeyboard(withFullID fullID: FullKeyboardID) -> String? { + let kb = Storage.active.userDefaults.userKeyboard(withFullID: fullID) if let filename = kb?.oskFont?.source.first(where: { $0.hasFontExtension }) { - let fontURL = Storage.active.fontURL(forFilename: filename) + let fontURL = Storage.active.fontURL(forKeyboardID: fullID.keyboardID, filename: filename) return FontManager.shared.fontName(at: fontURL) } return nil @@ -431,8 +421,15 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat return } - let keyboardURL = options.keyboardBaseURL.appendingPathComponent(filename) + do { + try FileManager.default.createDirectory(at: Storage.active.keyboardDir(forID: keyboardID), + withIntermediateDirectories: true) + } catch { + log.error("Could not create dir for download: \(error)") + return + } + let keyboardURL = options.keyboardBaseURL.appendingPathComponent(filename) let fontURLs = Array(Set(keyboardFontURLs(forFont: keyboard.font, options: options) + keyboardFontURLs(forFont: keyboard.oskFont, options: options))) @@ -451,7 +448,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat for (i, url) in fontURLs.enumerated() { request = HTTPDownloadRequest(url: url, userInfo: commonUserData) - request.destinationFile = Storage.active.fontURL(forFilename: url.lastPathComponent).path + request.destinationFile = Storage.active.fontURL(forKeyboardID: keyboardID, filename: url.lastPathComponent).path request.tag = i + 1 downloadQueue!.addRequest(request) } @@ -499,7 +496,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat private func downloadKeyboard(_ keyboardAPI: KeyboardAPICall) { let keyboard = keyboardAPI.keyboard let installableKeyboards = keyboard.languages!.map { language in - InstallableKeyboard(keyboard: keyboard, language: language) + InstallableKeyboard(keyboard: keyboard, language: language, isCustom: true) } let filename = keyboard.filename @@ -522,9 +519,17 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat return } - let isUpdate = latestKeyboardFileVersion(withID: keyboard.id) != nil + do { + try FileManager.default.createDirectory(at: Storage.active.keyboardDir(forID: keyboard.id), + withIntermediateDirectories: true) + } catch { + log.error("Could not create dir for download: \(error)") + return + } - downloadQueue = HTTPDownloader.init(self) + let isUpdate = Storage.active.userDefaults.userKeyboards?.contains { $0.id == keyboard.id } ?? false + + downloadQueue = HTTPDownloader(self) let commonUserData: [String: Any] = [ Key.keyboardInfo: installableKeyboards, Key.update: isUpdate @@ -538,7 +543,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat downloadQueue!.addRequest(request) for (i, url) in fontURLs.enumerated() { request = HTTPDownloadRequest(url: url, userInfo: commonUserData) - request.destinationFile = Storage.active.fontURL(forFilename: url.lastPathComponent).path + request.destinationFile = Storage.active.fontURL(forKeyboardID: keyboard.id, filename: url.lastPathComponent).path request.tag = i + 1 downloadQueue!.addRequest(request) } @@ -550,14 +555,18 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat if keyboardIdForCurrentRequest() == keyboardID { return .downloading } - guard let latestDownloadedVersion = latestKeyboardFileVersion(withID: keyboardID) else { + let userKeyboards = Storage.active.userDefaults.userKeyboards + guard let userKeyboard = userKeyboards?.first(where: { $0.id == keyboardID }) else { return .needsDownload } // Check version - if let latestRepositoryVersion = apiKeyboardRepository.keyboards?[keyboardID]?.version, - compareVersions(latestDownloadedVersion, latestRepositoryVersion) == .orderedAscending { - return .needsUpdate + if let repositoryVersionString = apiKeyboardRepository.keyboards?[keyboardID]?.version { + let downloadedVersion = Version(userKeyboard.version) ?? Version.fallback + let repositoryVersion = Version(repositoryVersionString) ?? Version.fallback + if downloadedVersion < repositoryVersion { + return .needsUpdate + } } return .upToDate } @@ -594,16 +603,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat // MARK: - HTTPDownloadDelegate methods - func downloadQueueFinished(_ queue: HTTPDownloader) { - log.debug { - let fontContents = try? FileManager.default.contentsOfDirectory(atPath: Storage.active.fontDir.path) - return "Font Directory contents: \(fontContents ?? [])" - } - log.debug { - let langContents = try? FileManager.default.contentsOfDirectory(atPath: Storage.active.languageDir.path) - return "Language Directory contents: \(langContents ?? [])" - } - } + func downloadQueueFinished(_ queue: HTTPDownloader) { } func downloadRequestStarted(_ request: HTTPDownloadRequest) { // If we're downloading a new keyboard. @@ -688,79 +688,18 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat } // MARK: - Loading custom keyboards - - /// Preloads a .js file for a language so that the keyboard is available without downloading. - /// - Precondition: - /// - The .js filename must remain the same as when obtained from Keyman. - /// - The .js file must be bundled in your application. - public func preloadKeyboardFile(at url: URL, shouldOverwrite: Bool) throws { - try Storage.copyAndExcludeFromBackup(at: url, - to: Storage.active.languageDir.appendingPathComponent(url.lastPathComponent), - shouldOverwrite: shouldOverwrite) - } - - /// Preloads a .ttf or .otf file to be available without downloading. - /// - Precondition: - /// - The font file must be bundled in your application. - /// - SeeAlso: `registerCustomFonts()` - public func preloadFontFile(at url: URL, shouldOverwrite: Bool) throws { - try Storage.copyAndExcludeFromBackup(at: url, - to: Storage.active.fontDir.appendingPathComponent(url.lastPathComponent), - shouldOverwrite: shouldOverwrite) + /// Preloads the JS and font files required for a keyboard. + public func preloadFiles(forKeyboardID keyboardID: String, at urls: [URL], shouldOverwrite: Bool) throws { + let keyboardDir = Storage.active.keyboardDir(forID: keyboardID) + try FileManager.default.createDirectory(at: keyboardDir, withIntermediateDirectories: true) + for url in urls { + try Storage.copyAndExcludeFromBackup(at: url, + to: keyboardDir.appendingPathComponent(url.lastPathComponent), + shouldOverwrite: shouldOverwrite) + } } // MARK: - File system and UserData management - func latestKeyboardFileVersion(withID keyboardID: String) -> String? { - guard let dirContents = try? FileManager.default.contentsOfDirectory(atPath: Storage.active.languageDir.path) else { - return nil - } - - var latestVersion: String? - for filename in dirContents where filename.hasPrefix("\(keyboardID)-") && filename.hasJavaScriptExtension { - let dashRange = filename.range(of: "-", options: .backwards)! - let extensionRange = filename.range(of: ".js", options: .backwards)! - let version = String(filename[dashRange.upperBound.. ComparisonResult? { - if v1.isEmpty || v2.isEmpty { - return nil - } - let components1 = v1.components(separatedBy: ".") - let components2 = v2.components(separatedBy: ".") - - let len = max(components1.count, components2.count) - for i in 0..= 0 else { - return nil - } - guard let val2 = Int(component2), val2 >= 0 else { - return nil - } - if val1 < val2 { - return .orderedAscending - } - if val1 > val2 { - return .orderedDescending - } - } - return .orderedSame - } /// Updates the user's installed keyboards and current keyboard with information in newKeyboard. /// - Parameter newKeyboard: Info for updated keyboard. @@ -777,27 +716,13 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat if kb.languageID == newKeyboard.languageID { kb = newKeyboard } else { - kb.version = newKeyboard.id + kb.version = newKeyboard.version } userKeyboards[i] = kb } } userData.userKeyboards = userKeyboards userData.synchronize() - - // Set version for current keyboard - let currentUserData = Util.isSystemKeyboard ? UserDefaults.standard : Storage.active.userDefaults - if var kb = currentUserData.currentKeyboard { - if kb.id == newKeyboard.id { - if kb.languageID == newKeyboard.languageID { - kb = newKeyboard - } else { - kb.version = newKeyboard.id - } - currentUserData.currentKeyboard = kb - currentUserData.synchronize() - } - } } func synchronizeSWKeyboard() { @@ -964,9 +889,8 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat } @objc func resetKeyboard() { - let keyboard = currentKeyboardInfo - keyboardID = nil - languageID = nil + let keyboard = currentKeyboard + currentKeyboardID = nil if let keyboard = keyboard { setKeyboard(keyboard) @@ -984,12 +908,12 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat } keymanWeb.languageMenuPosition { keyFrame in - self.showHelpBubble(at: keyFrame.origin) + self.showHelpBubble(for: keyFrame) } } // TODO: The bulk of this should be moved to PopoverView - func showHelpBubble(at point: CGPoint) { + func showHelpBubble(for keyFrame: CGRect) { self.helpBubbleView?.removeFromSuperview() let helpBubbleView = PopoverView(frame: CGRect.zero) self.helpBubbleView = helpBubbleView @@ -1001,48 +925,24 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat let isPad = UIDevice.current.userInterfaceIdiom == .pad let sizeMultiplier = CGFloat(isPad ? 1.5 : 1.0) - let frameWidth = 90.0 * sizeMultiplier - let frameHeight = (40.0 + helpBubbleView.arrowHeight) * sizeMultiplier + let popupWidth = 90.0 * sizeMultiplier + let popupHeight = 40.0 * sizeMultiplier + helpBubbleView.arrowHeight let fontSize = 10.0 * sizeMultiplier let inputViewFrame = keymanWeb.view.frame let screenWidth = inputViewFrame.size.width - // TODO: Refactor this out - let isPortrait: Bool - if Util.isSystemKeyboard { - isPortrait = InputViewController.isPortrait - } else { - isPortrait = UIDevice.current.orientation.isPortrait - } + let x = CGFloat.maximum(0, CGFloat.minimum(screenWidth - popupWidth, keyFrame.midX - popupWidth / 2)) + let adjY = CGFloat(3.0) // Tweak the positioning of the popup + let y = keyFrame.minY - popupHeight + adjY - let adjY: CGFloat - if isPortrait { - adjY = Util.isSystemKeyboard ? 9.0 : 4.0 - } else { - adjY = Util.isSystemKeyboard ? 3.0 : 4.0 - } - let px = point.x - let py = point.y + adjY + (isPad ? 2.0 : 1.0) - var x = px - frameWidth / 2 - let y = py - frameHeight - if x < 0 { - x = 0 - } else if x + frameWidth > screenWidth { - x = screenWidth - frameWidth - } + helpBubbleView.frame = CGRect(x: x, y: y, width: popupWidth, height: popupHeight) + helpBubbleView.arrowPosX = keyFrame.midX - x - helpBubbleView.frame = CGRect(x: x, y: y, width: frameWidth, height: frameHeight) - if x == 0 { - helpBubbleView.arrowPosX = px - } else if x == screenWidth - frameWidth { - helpBubbleView.arrowPosX = (px - x) - } else { - helpBubbleView.arrowPosX = frameWidth / 2 - } - - let helpText = UILabel(frame: CGRect(x: 5, y: 0, - width: frameWidth - 10, height: frameHeight - helpBubbleView.arrowHeight)) + let helpText = UILabel(frame: CGRect(x: 5, + y: 0, + width: popupWidth - 10, + height: popupHeight - helpBubbleView.arrowHeight)) helpText.backgroundColor = UIColor.clear helpText.font = helpText.font.withSize(fontSize) helpText.textAlignment = .center @@ -1134,13 +1034,11 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat keymanWeb.setDeviceType(UIDevice.current.userInterfaceIdiom) var newKb = Defaults.keyboard - if (keyboardID == nil || languageID == nil) && !shouldReloadKeyboard { + if currentKeyboardID == nil && !shouldReloadKeyboard { let userData = Util.isSystemKeyboard ? UserDefaults.standard : Storage.active.userDefaults - if let currentKb = userData.currentKeyboard { - let kbID = currentKb.id - let langID = currentKb.languageID - if Storage.active.userDefaults.userKeyboard(withID: kbID, languageID: langID) != nil { - newKb = currentKb + if let id = userData.currentKeyboardID { + if let kb = Storage.active.userDefaults.userKeyboard(withFullID: id) { + newKb = kb } } else if let userKbs = Storage.active.userDefaults.userKeyboards, !userKbs.isEmpty { newKb = userKbs[0] @@ -1178,8 +1076,8 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat keyPreviewView = KeyPreviewView(frame: keyFrame) keyPreviewView!.setLabelText(preview) - var oskFontName = oskFontNameForKeyboard(withID: keyboardID!, languageID: languageID!) - oskFontName = oskFontName ?? fontNameForKeyboard(withID: keyboardID!, languageID: languageID!) + var oskFontName = oskFontNameForKeyboard(withFullID: currentKeyboardID!) + oskFontName = oskFontName ?? fontNameForKeyboard(withFullID: currentKeyboardID!) keyPreviewView!.setLabelFont(oskFontName) keymanWeb.view.addSubview(keyPreviewView!) } @@ -1308,9 +1206,9 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat let isPad = UIDevice.current.userInterfaceIdiom == .pad let fontSize = isPad ? UIFont.buttonFontSize * 2 : UIFont.buttonFontSize - var oskFontName = oskFontNameForKeyboard(withID: keyboardID!, languageID: languageID!) + var oskFontName = oskFontNameForKeyboard(withFullID: currentKeyboardID!) if oskFontName == nil { - oskFontName = fontNameForKeyboard(withID: keyboardID!, languageID: languageID!) + oskFontName = fontNameForKeyboard(withFullID: currentKeyboardID!) } if subKeyIDs.isEmpty { @@ -1397,7 +1295,7 @@ public class Manager: NSObject, HTTPDownloadDelegate, UIGestureRecognizerDelegat if (!didSynchronize || shouldSynchronize) && Storage.shared != nil { synchronizeSWKeyboard() - if keyboardID != nil && languageID != nil { + if currentKeyboardID != nil { shouldReloadKeyboard = true reloadKeyboard(in: keymanWeb) } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Migrations.swift b/ios/engine/KMEI/KeymanEngine/Classes/Migrations.swift new file mode 100644 index 0000000000..39de5c6123 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/Migrations.swift @@ -0,0 +1,224 @@ +// +// Migrations.swift +// KeymanEngine +// +// Created by Gabriel Wong on 2017-12-08. +// Copyright © 2017 SIL International. All rights reserved. +// + +import Foundation + +private enum MigrationLevel { + static let initial = 0 + static let migratedUserDefaultsToStructs = 10 + static let migratedForKMP = 20 +} + +enum Migrations { + static func migrate(storage: Storage) { + if storage.userDefaults.migrationLevel < MigrationLevel.migratedUserDefaultsToStructs { + migrateUserDefaultsToStructs(storage: storage) + storage.userDefaults.migrationLevel = MigrationLevel.migratedUserDefaultsToStructs + } else { + log.info("UserDefaults migration to structs already performed. Skipping.") + } + if storage.userDefaults.migrationLevel < MigrationLevel.migratedForKMP { + migrateForKMP(storage: storage) + storage.userDefaults.migrationLevel = MigrationLevel.migratedForKMP + } else { + log.info("KMP directory migration already performed. Skipping.") + } + storage.userDefaults.synchronize() + } + + static func migrateUserDefaultsToStructs(storage: Storage) { + guard let userKeyboardObject = storage.userDefaults.object(forKey: Key.userKeyboardsList), + let currentKeyboardObject = storage.userDefaults.object(forKey: Key.userCurrentKeyboard) + else { + log.info("User keyboard list or current keyboard missing. Skipping migration.") + return + } + guard let oldUserKeyboards = userKeyboardObject as? [[String: String]], + let oldCurrentKeyboard = currentKeyboardObject as? [String: String] + else { + log.error("User keyboard list or current keyboard has an unexpected type") + return + } + + let userKeyboards = oldUserKeyboards.flatMap { installableKeyboard(from: $0) } + let currentKeyboardID = fullKeyboardID(from: oldCurrentKeyboard) + + storage.userDefaults.userKeyboards = userKeyboards + if userKeyboards.contains(where: { $0.fullID == currentKeyboardID }) { + storage.userDefaults.currentKeyboardID = currentKeyboardID + } else { + storage.userDefaults.currentKeyboardID = nil + } + } + + private static func installableKeyboard(from kbDict: [String: String]) -> InstallableKeyboard? { + log.debug("Migrating keyboard dictionary: \(kbDict)") + guard let id = kbDict["kbId"], + let name = kbDict["kbName"], + let languageID = kbDict["langId"], + let languageName = kbDict["langName"], + let version = kbDict["version"] + else { + log.error("Missing required fields in keyboard dictionary: \(kbDict)") + return nil + } + let rtl = kbDict["rtl"] == "Y" + let isCustom = kbDict["CustomKeyboard"] == "Y" + let displayFont = font(from: kbDict["font"]) + let oskFont = font(from: kbDict["oskFont"]) + let kb = InstallableKeyboard(id: id, + name: name, + languageID: languageID, + languageName: languageName, + version: version, + isRTL: rtl, + font: displayFont, + oskFont: oskFont, + isCustom: isCustom) + log.debug("Migrated keyboard dictionary to keyboard \(kb)") + return kb + } + + private static func fullKeyboardID(from kbDict: [String: String]) -> FullKeyboardID? { + log.debug("Migrating keyboard dictionary to FullKeyboardID: \(kbDict)") + guard let keyboardID = kbDict["kbId"], + let languageID = kbDict["langId"] + else { + log.error("Missing required fields in keyboard dictionary for FullKeyboardID: \(kbDict)") + return nil + } + let id = FullKeyboardID(keyboardID: keyboardID, languageID: languageID) + log.debug("Migrated keyboard dictionary to \(id)") + return id + } + + private static func font(from jsonString: String?) -> Font? { + guard let jsonString = jsonString else { + return nil + } + guard let data = jsonString.data(using: .utf8) else { + log.error("Failed to encode string: \(jsonString)") + return nil + } + guard let fontDict = (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] else { + log.error("Error parsing String as JSON: \(jsonString)") + return nil + } + guard let family = fontDict["family"] as? String else { + log.error("Missing 'family' String: \(fontDict)") + return nil + } + let files: [String] + if let filesString = fontDict["files"] as? String { + files = [filesString] + } else if let filesArray = fontDict["files"] as? [String] { + files = filesArray + } else { + log.error("Missing 'files': \(fontDict)") + return nil + } + return Font(family: family, source: files) + } + + static func migrateForKMP(storage: Storage) { + let languageDir = storage.baseDir.appendingPathComponent("languages") + let fontDir = storage.baseDir.appendingPathComponent("fonts") + + log.info("Migrating from base directory: \(storage.baseDir)") + + guard var userKeyboards = storage.userDefaults.userKeyboards else { + log.info("No user keyboards to migrate") + return + } + + var urlsForKeyboard: [String: Set] = [:] + for i in userKeyboards.indices { + let keyboard = userKeyboards[i] + guard let version = latestKeyboardFileVersion(withID: keyboard.id, dirPath: languageDir.path) else { + log.warning("Could not find JS file for keyboard \(keyboard.id) in \(languageDir.path)") + continue + } + var urls = urlsForKeyboard[keyboard.id] ?? Set() + urls.insert(languageDir.appendingPathComponent("\(keyboard.id)-\(version.string).js")) + + let fontFiles = (keyboard.font?.source ?? []) + (keyboard.oskFont?.source ?? []) + for file in fontFiles { + guard file.hasFontExtension else { + log.info("Skipping copy of \(file) for keyboard \(keyboard.id) since it is not a font file.") + continue + } + let url = fontDir.appendingPathComponent(file) + guard FileManager.default.fileExists(atPath: url.path) else { + log.warning("Font file \(url) for keyboard \(keyboard.id) does not exist") + continue + } + urls.insert(url) + } + urlsForKeyboard[keyboard.id] = urls + userKeyboards[i].version = version.string + } + + var successfulKeyboards: [String] = [] + + // Copy files + for (keyboardID, urls) in urlsForKeyboard { + let keyboardDir = storage.keyboardDir(forID: keyboardID) + do { + try FileManager.default.createDirectory(at: keyboardDir, + withIntermediateDirectories: true, + attributes: nil) + } catch { + log.error("Failed to create keyboard directory at \(keyboardDir)") + continue + } + + var successful = true + for srcURL in urls { + let dstURL = keyboardDir.appendingPathComponent(srcURL.lastPathComponent) + do { + try FileManager.default.copyItem(at: srcURL, to: dstURL) + } catch { + log.error("Failed to copy from \(srcURL) to \(dstURL) for keyboard \(keyboardID)") + successful = false + } + } + if successful { + successfulKeyboards.append(keyboardID) + log.info("Succesfully copied keyboard files for keyboard \(keyboardID)") + } + } + + // Remove keyboards that were not copied successfully + let filteredUserKeyboards = userKeyboards.filter { successfulKeyboards.contains($0.id) } + storage.userDefaults.userKeyboards = filteredUserKeyboards + + // TODO: Remove old directory + } + + private static func latestKeyboardFileVersion(withID keyboardID: String, dirPath: String) -> Version? { + guard let dirContents = try? FileManager.default.contentsOfDirectory(atPath: dirPath) else { + return nil + } + + var latestVersion: Version? + for filename in dirContents where filename.hasPrefix("\(keyboardID)-") && filename.hasJavaScriptExtension { + let dashRange = filename.range(of: "-", options: .backwards)! + let extensionRange = filename.range(of: ".js", options: .backwards)! + guard let version = Version(String(filename[dashRange.upperBound.. Bool { + return lhs.keyboardID == rhs.keyboardID && lhs.languageID == rhs.languageID + } +} + +// MARK: - CustomStringConvertible +extension FullKeyboardID: CustomStringConvertible { + public var description: String { + return "" + } +} diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableKeyboard.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableKeyboard.swift index d98681108e..3161136e47 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableKeyboard.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/InstallableKeyboard.swift @@ -20,6 +20,10 @@ public struct InstallableKeyboard: Codable { public var oskFont: Font? public var isCustom: Bool + public var fullID: FullKeyboardID { + return FullKeyboardID(keyboardID: id, languageID: languageID) + } + public init(id: String, name: String, languageID: String, @@ -40,7 +44,7 @@ public struct InstallableKeyboard: Codable { self.isCustom = isCustom } - public init(keyboard: Keyboard, language: Language) { + public init(keyboard: Keyboard, language: Language, isCustom: Bool) { self.id = keyboard.id self.name = keyboard.name self.languageID = language.id @@ -49,6 +53,6 @@ public struct InstallableKeyboard: Codable { self.isRTL = keyboard.isRTL self.font = keyboard.font self.oskFont = keyboard.oskFont - self.isCustom = false + self.isCustom = isCustom } } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Model/Version.swift b/ios/engine/KMEI/KeymanEngine/Classes/Model/Version.swift new file mode 100644 index 0000000000..c60f6bbf05 --- /dev/null +++ b/ios/engine/KMEI/KeymanEngine/Classes/Model/Version.swift @@ -0,0 +1,50 @@ +// +// Version.swift +// KeymanEngine +// +// Created by Gabriel Wong on 2017-11-23. +// Copyright © 2017 SIL International. All rights reserved. +// + +import Foundation + +/// Dotted-decimal version. +public struct Version: Comparable { + public static let fallback = Version("1.0")! + + private let components: [Int] + public let string: String + + public init?(_ string: String) { + let stringComponents = string.components(separatedBy: ".") + var components: [Int] = [] + for s in stringComponents { + guard let i = Int(s), i >= 0 else { + return nil + } + components.append(i) + } + + self.string = string + self.components = components + } + + public static func <(lhs: Version, rhs: Version) -> Bool { + let len = max(lhs.components.count, rhs.components.count) + for i in 0.. rightComponent { + return false + } + } + return false + } + + public static func ==(lhs: Version, rhs: Version) -> Bool { + return lhs.components == rhs.components + } +} diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Storage.swift b/ios/engine/KMEI/KeymanEngine/Classes/Storage.swift index e83da84199..209eb30538 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Storage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Storage.swift @@ -47,8 +47,7 @@ extension Storage { class Storage { let baseDir: URL - let languageDir: URL - let fontDir: URL + let keyboardDir: URL let userDefaults: UserDefaults let kmwURL: URL @@ -57,23 +56,19 @@ class Storage { private init?(baseURL: URL, userDefaults: UserDefaults) { guard let baseDir = Storage.createSubdirectory(baseDir: baseURL, name: "keyman"), - let languageDir = Storage.createSubdirectory(baseDir: baseDir, name: "languages"), - let fontDir = Storage.createSubdirectory(baseDir: baseDir, name: "fonts") else { + let keyboardDir = Storage.createSubdirectory(baseDir: baseDir, name: "keyboards") + else { return nil } self.baseDir = baseDir - self.languageDir = languageDir - self.fontDir = fontDir + self.keyboardDir = keyboardDir self.userDefaults = userDefaults kmwURL = baseDir.appendingPathComponent(Resources.kmwFilename) specialOSKFontURL = baseDir.appendingPathComponent(Resources.oskFontFilename) } - private static func createSubdirectory(baseDir: URL?, name: String) -> URL? { - guard let baseDir = baseDir else { - return nil - } + private static func createSubdirectory(baseDir: URL, name: String) -> URL? { let newDir = baseDir.appendingPathComponent(name) do { try FileManager.default.createDirectory(at: newDir, @@ -81,20 +76,45 @@ class Storage { attributes: nil) return newDir } catch { + log.error("Failed to create subdirectory at \(newDir)") return nil } } + func keyboardDir(forID keyboardID: String) -> URL { + return keyboardDir.appendingPathComponent(keyboardID) + } + func keyboardURL(for keyboard: InstallableKeyboard) -> URL { return keyboardURL(forID: keyboard.id, version: keyboard.version) } func keyboardURL(forID keyboardID: String, version: String) -> URL { - return languageDir.appendingPathComponent("\(keyboardID)-\(version).js") + return keyboardDir(forID: keyboardID).appendingPathComponent("\(keyboardID)-\(version).js") } - func fontURL(forFilename filename: String) -> URL { - return fontDir.appendingPathComponent(filename) + func fontURL(forKeyboardID keyboardID: String, filename: String) -> URL { + return keyboardDir(forID: keyboardID).appendingPathComponent(filename) + } + + var keyboardDirs: [URL]? { + let contents: [URL] + do { + contents = try FileManager.default.contentsOfDirectory(at: keyboardDir, + includingPropertiesForKeys: [.isDirectoryKey]) + } catch { + log.error("Failed to list contents at \(keyboardDir) with error \(error)") + return nil + } + return contents.filter { url in + do { + let values = try url.resourceValues(forKeys: [.isDirectoryKey]) + return values.isDirectory ?? false + } catch { + log.error(error) + return false + } + } } } @@ -103,28 +123,32 @@ extension Storage { func copyKMWFiles(from bundle: Bundle) throws { try Storage.copy(from: bundle, resourceName: Resources.kmwFilename, - dstDir: Storage.active.baseDir) + dstDir: baseDir) try Storage.copy(from: bundle, resourceName: "keymanios.js", - dstDir: Storage.active.baseDir) - try Storage.copy(from: bundle, - resourceName: "\(Defaults.keyboard.id)-\(Defaults.keyboard.version).js", - dstDir: Storage.active.languageDir) - try Storage.copy(from: bundle, - resourceName: "DejaVuSans.ttf", - dstDir: Storage.active.fontDir) + dstDir: baseDir) try Storage.copy(from: bundle, resourceName: "kmwosk.css", - dstDir: Storage.active.baseDir) + dstDir: baseDir) try Storage.copy(from: bundle, - resourceName: Resources.oskFontFilename, - dstDir: Storage.active.baseDir) + resourceName: "keymanweb-osk.ttf", + dstDir: baseDir) + let defaultKeyboardDir = self.keyboardDir(forID: Defaults.keyboard.id) + try FileManager.default.createDirectory(at: defaultKeyboardDir, withIntermediateDirectories: true) + try Storage.copy(from: bundle, + resourceName: "\(Defaults.keyboard.id)-\(Defaults.keyboard.version).js", + dstDir: defaultKeyboardDir) + try Storage.copy(from: bundle, + resourceName: "DejaVuSans.ttf", + dstDir: defaultKeyboardDir) } func copyFiles(to dst: Storage) throws { - try Storage.copyDirectoryContents(at: baseDir, to: dst.baseDir) - try Storage.copyDirectoryContents(at: languageDir, to: dst.languageDir) - try Storage.copyDirectoryContents(at: fontDir, to: dst.fontDir) + if FileManager.default.fileExists(atPath: dst.baseDir.path) { + log.info("Deleting \(dst.baseDir) for copy from \(baseDir) to \(dst.baseDir)") + try FileManager.default.removeItem(at: dst.baseDir) + } + try FileManager.default.copyItem(at: baseDir, to: dst.baseDir) } func copyUserDefaults(to dst: Storage, withKeys keys: [String], shouldOverwrite: Bool) { diff --git a/ios/engine/KMEI/KeymanEngine/Classes/TextField.swift b/ios/engine/KMEI/KeymanEngine/Classes/TextField.swift index 38b245fea4..0e656d55f4 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/TextField.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/TextField.swift @@ -144,8 +144,7 @@ public class TextField: UITextField { return } - // TODO: Get font name directly from keyboard object - let fontName = Manager.shared.fontNameForKeyboard(withID: kb.id, languageID: kb.languageID) + let fontName = Manager.shared.fontNameForKeyboard(withFullID: kb.fullID) let fontSize = font?.pointSize ?? UIFont.systemFontSize if let fontName = fontName { font = UIFont(name: fontName, size: fontSize) @@ -240,10 +239,8 @@ extension TextField: UITextFieldDelegate { let textWD = baseWritingDirection(for: beginningOfDocument, in: .forward) let isRTL: Bool - if let keyboardID = Manager.shared.keyboardID, - let languageID = Manager.shared.languageID { - let keyboard = Storage.active.userDefaults.userKeyboard(withID: keyboardID, languageID: languageID) - isRTL = keyboard?.isRTL ?? false + if let keyboard = Manager.shared.currentKeyboard { + isRTL = keyboard.isRTL } else { isRTL = false } @@ -278,9 +275,8 @@ extension TextField: UITextFieldDelegate { Manager.shared.keymanWebDelegate = self let fontName: String? - if let keyboardID = Manager.shared.keyboardID, - let languageID = Manager.shared.languageID { - fontName = Manager.shared.fontNameForKeyboard(withID: keyboardID, languageID: languageID) + if let id = Manager.shared.currentKeyboardID { + fontName = Manager.shared.fontNameForKeyboard(withFullID: id) } else { fontName = nil } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/TextView.swift b/ios/engine/KMEI/KeymanEngine/Classes/TextView.swift index 800ace08d9..df52e8a896 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/TextView.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/TextView.swift @@ -133,8 +133,7 @@ public class TextView: UITextView { return } - // TODO: Get font name directly from keyboard - let fontName = Manager.shared.fontNameForKeyboard(withID: kb.id, languageID: kb.languageID) + let fontName = Manager.shared.fontNameForKeyboard(withFullID: kb.fullID) let fontSize = font?.pointSize ?? UIFont.systemFontSize if let fontName = fontName { font = UIFont(name: fontName, size: fontSize) @@ -239,10 +238,8 @@ extension TextView: UITextViewDelegate { let textWD = baseWritingDirection(for: beginningOfDocument, in: .forward) let isRTL: Bool - if let keyboardID = Manager.shared.keyboardID, - let languageID = Manager.shared.languageID { - let keyboard = Storage.active.userDefaults.userKeyboard(withID: keyboardID, languageID: languageID) - isRTL = keyboard?.isRTL ?? false + if let keyboard = Manager.shared.currentKeyboard { + isRTL = keyboard.isRTL } else { isRTL = false } @@ -279,9 +276,8 @@ extension TextView: UITextViewDelegate { Manager.shared.keymanWebDelegate = self let fontName: String? - if let keyboardID = Manager.shared.keyboardID, - let languageID = Manager.shared.languageID { - fontName = Manager.shared.fontNameForKeyboard(withID: keyboardID, languageID: languageID) + if let id = Manager.shared.currentKeyboardID { + fontName = Manager.shared.fontNameForKeyboard(withFullID: id) } else { fontName = nil } diff --git a/ios/keyman/Keyman/Keyman/InfoViewController/InfoViewController.swift b/ios/keyman/Keyman/Keyman/InfoViewController/InfoViewController.swift index a34fa82789..de3d37e88a 100644 --- a/ios/keyman/Keyman/Keyman/InfoViewController/InfoViewController.swift +++ b/ios/keyman/Keyman/Keyman/InfoViewController/InfoViewController.swift @@ -52,7 +52,7 @@ class InfoViewController: UIViewController, UIWebViewDelegate { } private func loadFromServer() { - let keyboardInfo = Manager.shared.currentKeyboardInfo + let keyboardInfo = Manager.shared.currentKeyboard let currentKeyboardId = keyboardInfo?.id ?? Defaults.keyboard.id let userData = AppDelegate.activeUserDefaults() let keyboards = userData.userKeyboards diff --git a/ios/keyman/Keyman/Keyman/KMWebBrowser/WebBrowserViewController.swift b/ios/keyman/Keyman/Keyman/KMWebBrowser/WebBrowserViewController.swift index 4fdab8df1c..2e14f2799e 100644 --- a/ios/keyman/Keyman/Keyman/KMWebBrowser/WebBrowserViewController.swift +++ b/ios/keyman/Keyman/Keyman/KMWebBrowser/WebBrowserViewController.swift @@ -273,7 +273,7 @@ class WebBrowserViewController: UIViewController, UIWebViewDelegate, UIAlertView } private func keyboardChanged(_ kb: InstallableKeyboard) { - if let fontName = Manager.shared.fontNameForKeyboard(withID: kb.id, languageID: kb.languageID) { + if let fontName = Manager.shared.fontNameForKeyboard(withFullID: kb.fullID) { newFontFamily = fontName } else { newFontFamily = UIFont.systemFont(ofSize: UIFont.systemFontSize).fontName diff --git a/ios/keyman/Keyman/Keyman/MainViewController.swift b/ios/keyman/Keyman/Keyman/MainViewController.swift index 2a0fe05355..185559c2bd 100644 --- a/ios/keyman/Keyman/Keyman/MainViewController.swift +++ b/ios/keyman/Keyman/Keyman/MainViewController.swift @@ -470,7 +470,7 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg didDownload = false } - checkProfile(forKeyboardID: kb.id, languageID: kb.languageID, doListCheck: listCheck) + checkProfile(forFullID: kb.fullID, doListCheck: listCheck) } private func keyboardDownloadStarted() { @@ -869,8 +869,8 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg } } - private func profileName(withKeyboardID kbID: String, languageID langID: String) -> String? { - guard let keyboard = AppDelegate.activeUserDefaults().userKeyboard(withID: kbID, languageID: langID), + private func profileName(withFullID fullID: FullKeyboardID) -> String? { + guard let keyboard = AppDelegate.activeUserDefaults().userKeyboard(withFullID: fullID), let font = keyboard.font else { return nil } @@ -882,15 +882,15 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg return font.source.first { $0.lowercased().hasSuffix(FileExtensions.configurationProfile) } } - private func checkProfile(forKeyboardID kbID: String, languageID langID: String, doListCheck: Bool) { - if kbID == Defaults.keyboard.id && langID == Defaults.keyboard.languageID { + private func checkProfile(forFullID fullID: FullKeyboardID, doListCheck: Bool) { + if fullID == Defaults.keyboard.fullID { return } if profileName != nil { return // already installing a profile } - guard let profile = profileName(withKeyboardID: kbID, languageID: langID) else { + guard let profile = profileName(withFullID: fullID) else { return } @@ -906,7 +906,7 @@ class MainViewController: UIViewController, TextViewDelegate, UIActionSheetDeleg if doInstall { profileName = profile - let keyboard = AppDelegate.activeUserDefaults().userKeyboard(withID: kbID, languageID: langID)! + let keyboard = AppDelegate.activeUserDefaults().userKeyboard(withFullID: fullID)! let languageName = keyboard.languageName let title = "\(languageName) Font" let msg = "Touch Install to make \(languageName) display correctly in all your apps" diff --git a/ios/keyman/Keyman/Keyman/SetUpViewController/SetUpViewController.swift b/ios/keyman/Keyman/Keyman/SetUpViewController/SetUpViewController.swift index a58ad75a11..464d21d91e 100644 --- a/ios/keyman/Keyman/Keyman/SetUpViewController/SetUpViewController.swift +++ b/ios/keyman/Keyman/Keyman/SetUpViewController/SetUpViewController.swift @@ -57,7 +57,7 @@ class SetUpViewController: UIViewController, UIWebViewDelegate { } private func loadFromServer() { - let keyboardInfo = Manager.shared.currentKeyboardInfo + let keyboardInfo = Manager.shared.currentKeyboard let currentKeyboardId = keyboardInfo?.id ?? Defaults.keyboard.id let userData = AppDelegate.activeUserDefaults() let keyboards = userData.userKeyboards diff --git a/ios/samples/KMSample1/KMSample1/ViewController.swift b/ios/samples/KMSample1/KMSample1/ViewController.swift index 7172846fe2..b6b9b64667 100644 --- a/ios/samples/KMSample1/KMSample1/ViewController.swift +++ b/ios/samples/KMSample1/KMSample1/ViewController.swift @@ -15,17 +15,7 @@ class ViewController: UIViewController, TextViewDelegate { super.viewDidLoad() Manager.shared.openURL = UIApplication.shared.openURL Manager.shared.isKeymanHelpOn = false - do { - try Manager.shared.preloadKeyboardFile(at: Bundle.main.url(forResource: "tamil99m-1.1", withExtension: "js")!, - shouldOverwrite: true) - try Manager.shared.preloadFontFile(at: Bundle.main.url(forResource: "aava1", withExtension: "ttf")!, - shouldOverwrite: true) - } catch { - print("Error preloading: \(error)") - } - FontManager.shared.registerCustomFonts() - Manager.shared.addKeyboard(Defaults.keyboard) let kb = InstallableKeyboard(id: "tamil99m", name: "Tamil 99M", languageID: "tam", @@ -35,6 +25,18 @@ class ViewController: UIViewController, TextViewDelegate { font: Font(filename: "aava1.ttf"), oskFont: nil, isCustom: true) + let urls = [ + Bundle.main.url(forResource: "tamil99m-1.1", withExtension: "js")!, + Bundle.main.url(forResource: "aava1", withExtension: "ttf")! + ] + do { + try Manager.shared.preloadFiles(forKeyboardID: kb.id, at: urls, shouldOverwrite: true) + } catch { + print("Error preloading: \(error)") + } + FontManager.shared.registerCustomFonts() + + Manager.shared.addKeyboard(Defaults.keyboard) Manager.shared.addKeyboard(kb) textView.setKeymanDelegate(self) diff --git a/ios/samples/KMSample2/KMSample2/ViewController.swift b/ios/samples/KMSample2/KMSample2/ViewController.swift index c67e75cba9..5cdb44657f 100644 --- a/ios/samples/KMSample2/KMSample2/ViewController.swift +++ b/ios/samples/KMSample2/KMSample2/ViewController.swift @@ -16,19 +16,8 @@ class ViewController: UIViewController, TextViewDelegate { super.viewDidLoad() Manager.shared.openURL = UIApplication.shared.openURL - Manager.shared.isKeymanHelpOn = false - do { - try Manager.shared.preloadKeyboardFile(at: Bundle.main.url(forResource: "tamil99m-1.1", withExtension: "js")!, - shouldOverwrite: true) - try Manager.shared.preloadFontFile(at: Bundle.main.url(forResource: "aava1", withExtension: "ttf")!, - shouldOverwrite: true) - } catch { - print("Error preloading: \(error)") - } - FontManager.shared.registerCustomFonts() - Manager.shared.addKeyboard(Defaults.keyboard) let kb = InstallableKeyboard(id: "tamil99m", name: "Tamil 99M", languageID: "tam", @@ -38,6 +27,18 @@ class ViewController: UIViewController, TextViewDelegate { font: Font(filename: "aava1.ttf"), oskFont: nil, isCustom: true) + let urls = [ + Bundle.main.url(forResource: "tamil99m-1.1", withExtension: "js")!, + Bundle.main.url(forResource: "aava1", withExtension: "ttf")! + ] + do { + try Manager.shared.preloadFiles(forKeyboardID: kb.id, at: urls, shouldOverwrite: true) + } catch { + print("Error preloading: \(error)") + } + FontManager.shared.registerCustomFonts() + + Manager.shared.addKeyboard(Defaults.keyboard) Manager.shared.addKeyboard(kb) textView.setKeymanDelegate(self)