diff --git a/HISTORY.md b/HISTORY.md index 1e121ddb86..8f7978032e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,14 @@ # Keyman Version History +## 17.0.214 alpha 2023-11-20 + +* fix(ios): fv: replace Zip framework to prevent crash on startup (#10018) + +## 17.0.213 alpha 2023-11-17 + +* fix(linux): Fix packaging GHA (#10020) +* fix(android): Always display HTML banner when suggestions aren't available (#9696) + ## 17.0.212 alpha 2023-11-16 * chore(web): splits banner.ts into separate files per banner type (#9987) diff --git a/VERSION.md b/VERSION.md index 8bd573dbaf..87185aaedf 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -17.0.213 \ No newline at end of file +17.0.215 \ No newline at end of file diff --git a/android/KMAPro/kMAPro/src/main/assets/banner.html b/android/KMAPro/kMAPro/src/main/assets/banner.html new file mode 100644 index 0000000000..41107f1142 --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/assets/banner.html @@ -0,0 +1,11 @@ + +
+ + + + + +
+
+
+
diff --git a/android/KMAPro/kMAPro/src/main/assets/banner/keyman_banner.svg b/android/KMAPro/kMAPro/src/main/assets/banner/keyman_banner.svg new file mode 100644 index 0000000000..3888a0937f --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/assets/banner/keyman_banner.svg @@ -0,0 +1,372 @@ + + + + image/svg+xml diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java new file mode 100644 index 0000000000..f716faa9aa --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/BannerController.java @@ -0,0 +1,29 @@ +package com.keyman.android; + +import android.content.Context; + +import com.keyman.engine.KMManager; +import com.keyman.engine.util.FileUtils; + +import java.io.File; + +public class BannerController { + + // Paths relative to assets folder for banner themes + public static final String KM_BANNER_DIR = "banner"; + public static final String KM_BANNER_THEME_KEYMAN = "banner.html"; + + public static void setHTMLBanner(Context context, KMManager.KeyboardType keyboardType) { + if (keyboardType == KMManager.KeyboardType.KEYBOARD_TYPE_UNDEFINED) { + return; + } + + KMManager.copyHTMLBannerAssets(context, KM_BANNER_DIR); + + // Always use Keyman banner theme + String contents = FileUtils.readContents(context, KM_BANNER_THEME_KEYMAN); + KMManager.setHTMLBanner(keyboardType, contents); + KMManager.setBanner(keyboardType, KMManager.BannerType.HTML); + KMManager.showBanner(true); + } +} diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index 697825094b..cf919deddb 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -6,6 +6,7 @@ package com.keyman.android; import com.tavultesoft.kmapro.BuildConfig; import com.tavultesoft.kmapro.KeymanSettingsActivity; +import com.keyman.android.BannerController; import com.keyman.engine.KMManager; import com.keyman.engine.KMManager.KeyboardType; import com.keyman.engine.KMHardwareKeyboardInterpreter; @@ -71,6 +72,9 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven KMManager.SpacebarText spacebarText = KMManager.SpacebarText.fromString(prefs.getString(KeymanSettingsActivity.spacebarTextKey, KMManager.SpacebarText.LANGUAGE_KEYBOARD.toString())); KMManager.setSpacebarText(spacebarText); + // Set the system keyboard HTML banner + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); + boolean mayHaveHapticFeedback = prefs.getBoolean(KeymanSettingsActivity.hapticFeedbackKey, false); KMManager.setHapticFeedback(mayHaveHapticFeedback); @@ -93,6 +97,9 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven @Override public void onInitializeInterface() { super.onInitializeInterface(); + + // KeymanWeb reloaded, so we have to pass the banner again + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); } /** 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 6b92d4c2eb..16899f7c2d 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 @@ -18,6 +18,7 @@ import java.util.List; import java.util.Map; import com.keyman.android.CheckInstallReferrer; +import com.keyman.android.BannerController; import com.keyman.engine.BaseActivity; import com.keyman.engine.KMHelpFileActivity; import com.keyman.engine.KMKeyboardDownloaderActivity; @@ -499,6 +500,8 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene @Override public void onKeyboardShown() { + // Refresh banner theme + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_INAPP); resizeTextView(true); } diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index afa2825e33..7930c76f11 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -8,6 +8,9 @@ if(window.parent && window.parent.jsInterface && !window.jsInterface) { var device = window.jsInterface.getDeviceType(); var oskHeight = Math.ceil(window.jsInterface.getKeyboardHeight() / window.devicePixelRatio); var oskWidth = 0; +var bannerHeight = 0; +var bannerImagePath = ''; +var bannerHTMLContents = ''; var fragmentToggle = 0; var sentryManager = new KeymanSentryManager({ @@ -37,11 +40,13 @@ function init() { oninserttext: insertText, root:'./' }).then(function () { // Note: For non-upgraded API 21, arrow functions will break the keyboard! - const bannerHeight = Math.ceil(window.jsInterface.getDefaultBannerHeight() / window.devicePixelRatio); + bannerHeight = Math.ceil(window.jsInterface.getDefaultBannerHeight() / window.devicePixelRatio); + if (bannerHeight > 0) { - // The OSK is not available until initialization is complete. - keyman.osk.bannerView.activeBannerHeight = bannerHeight; - keyman.refreshOskLayout(); + // The OSK is not available until initialization is complete. + keyman.osk.bannerView.activeBannerHeight = bannerHeight; + keyman.refreshOskLayout(); + } }); keyman.addEventListener('keyboardloaded', setIsChiral); @@ -53,6 +58,29 @@ function init() { notifyHost('pageLoaded'); } +function showBanner(flag) { + console_debug("Setting banner display for dictionaryless keyboards to " + flag); + console_debug("bannerHTMLContents: " + bannerHTMLContents); + var bc = keyman.osk.bannerController; + if (bc) { + if (bannerHTMLContents != '') { + bc.inactiveBanner = flag ? new bc.HTMLBanner(bannerHTMLContents) : null; + } else { + bc.inactiveBanner = flag ? new bc.ImageBanner(bannerImagePath) : null; + } + } +} + +function setBannerImage(path) { + bannerImagePath = path; +} + +// Set the HTML banner to use when predictive-text is not available +// contents - HTML content to use for the banner +function setBannerHTML(contents) { + bannerHTMLContents = contents; +} + function notifyHost(event, params) { console_debug('notifyHost(event='+event+',params='+params+')'); // TODO: Update all other host notifications to use notifyHost instead of directly setting window.location.hash @@ -65,12 +93,19 @@ function notifyHost(event, params) { } // Update the KMW banner height +// h is in dpi (different from iOS) function setBannerHeight(h) { if (h > 0) { - var osk = keyman.osk; - osk.banner.height = Math.ceil(h / window.devicePixelRatio); + // The banner itself may not be loaded yet. This will preemptively help set + // its eventual display height. + bannerHeight = Math.ceil(h / window.devicePixelRatio); + + if (keyman.osk) { + keyman.osk.bannerView.activeBannerHeight = bannerHeight; + } } - // Refresh KMW OSK + + // Refresh KMW's OSK keyman.refreshOskLayout(); } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java index 6471e87510..215630cd03 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboard.java @@ -7,13 +7,11 @@ package com.keyman.engine; import java.io.File; import java.util.ArrayList; import java.util.HashMap; -import java.util.List; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import com.keyman.engine.BaseActivity; import com.keyman.engine.data.Keyboard; import com.keyman.engine.data.KeyboardController; import com.keyman.engine.KMManager.KeyboardType; @@ -25,19 +23,11 @@ import com.keyman.engine.util.FileUtils; import com.keyman.engine.util.KMLog; import com.keyman.engine.util.KMString; -import android.Manifest; import android.annotation.SuppressLint; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; -import android.content.pm.PackageManager; import android.content.res.Configuration; -import android.graphics.Color; -import android.graphics.Rect; -import android.graphics.RectF; -import android.graphics.Typeface; -import android.os.Build; -import android.os.Bundle; import android.os.Handler; import android.util.DisplayMetrics; import android.util.Log; @@ -46,7 +36,6 @@ import android.view.Gravity; import android.view.LayoutInflater; import android.view.MotionEvent; import android.view.View; -import android.view.ViewGroup; import android.view.WindowManager; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; @@ -56,12 +45,9 @@ import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.widget.Button; -import android.widget.FrameLayout; -import android.widget.GridLayout; import android.widget.PopupWindow; import android.widget.PopupWindow.OnDismissListener; import android.widget.RelativeLayout; -import android.widget.TextView; import android.widget.Toast; import io.sentry.Breadcrumb; @@ -84,19 +70,10 @@ final class KMKeyboard extends WebView { private static String currentKeyboard = null; - /** - * Banner state value: "blank" - no banner available. - */ - protected static final String KM_BANNER_STATE_BLANK = "blank"; - /** - * Banner state value: "suggestion" - dictionary suggestions are shown. - */ - protected static final String KM_BANNER_STATE_SUGGESTION = "suggestion"; - /** * Current banner state. */ - protected static String currentBanner = KM_BANNER_STATE_BLANK; + protected static KMManager.BannerType currentBanner = KMManager.BannerType.HTML; private static String txtFont = ""; private static String oskFont = null; @@ -105,6 +82,10 @@ final class KMKeyboard extends WebView { private GestureDetector gestureDetector; private static ArrayList kbEventListeners = null; + // Stores the current html string for use by the Banner + // when predictive text is not active + protected String htmlBannerString = ""; + // Facilitates a 'lazy init' - we'll only check the preference when it matters, // rather than at construction time. private Boolean _shouldShowHelpBubble = null; @@ -400,6 +381,9 @@ final class KMKeyboard extends WebView { int bannerHeight = KMManager.getBannerHeight(context); int oskHeight = KMManager.getKeyboardHeight(context); + if (this.htmlBannerString != null && !this.htmlBannerString.isEmpty()) { + setHTMLBanner(this.htmlBannerString); + } loadJavascript(KMString.format("setBannerHeight(%d)", bannerHeight)); loadJavascript(KMString.format("setOskWidth(%d)", newConfig.screenWidthDp)); loadJavascript(KMString.format("setOskHeight(%d)", oskHeight)); @@ -425,22 +409,15 @@ final class KMKeyboard extends WebView { return currentKeyboard; } - public static void setCurrentBanner(String banner) { - currentBanner = banner; - } - - public static String currentBanner() { return currentBanner; } - protected void toggleSuggestionBanner(HashMap associatedLexicalModel, boolean keyboardChanged) { //reset banner state if new language has no lexical model - if (currentBanner != null && currentBanner.equals(KM_BANNER_STATE_SUGGESTION) + if (currentBanner == KMManager.BannerType.SUGGESTION && associatedLexicalModel == null) { - setCurrentBanner(KMKeyboard.KM_BANNER_STATE_BLANK); + currentBanner = KMManager.BannerType.HTML; } - if(keyboardChanged) { - setLayoutParams(KMManager.getKeyboardLayoutParams()); - } + showBanner(true); + // Since there's always a banner, no need to update setLayoutParams() } /** @@ -653,6 +630,30 @@ final class KMKeyboard extends WebView { return retVal; } + public void showBanner(boolean flag) { + String jsString = KMString.format("showBanner(%b)", flag); + loadJavascript(jsString); + } + + public KMManager.BannerType getBanner() { + return currentBanner; + } + + public void setBanner(KMManager.BannerType bannerType) { + currentBanner = bannerType; + } + + public String getHTMLBanner() { + return this.htmlBannerString; + } + + public void setHTMLBanner(String contents) { + this.htmlBannerString = contents; + String jsString = KMString.format("setBannerHTML(%s)", + JSONObject.quote(this.htmlBannerString)); + loadJavascript(jsString); + } + public void setChirality(boolean flag) { this.isChiral = flag; } diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java index e7188a94f9..4d458bd061 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMKeyboardWebViewClient.java @@ -173,8 +173,7 @@ public final class KMKeyboardWebViewClient extends WebViewClient { if (KMManager.currentLexicalModel != null) { modelPredictionPref = prefs.getBoolean(KMManager.getLanguagePredictionPreferenceKey(KMManager.currentLexicalModel.get(KMManager.KMKey_LanguageID)), true); } - kmKeyboard.setCurrentBanner((isModelActive && modelPredictionPref) ? - KMKeyboard.KM_BANNER_STATE_SUGGESTION : KMKeyboard.KM_BANNER_STATE_BLANK); + KMManager.setBannerOptions(isModelActive && modelPredictionPref); RelativeLayout.LayoutParams params = KMManager.getKeyboardLayoutParams(); kmKeyboard.setLayoutParams(params); } else if (url.indexOf("suggestPopup") >= 0) { diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java index cbcb28053a..df90f1d869 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/KMManager.java @@ -25,32 +25,21 @@ import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.content.res.Configuration; -import android.graphics.Bitmap; -import android.graphics.RectF; import android.graphics.Typeface; import android.inputmethodservice.InputMethodService; import android.net.ConnectivityManager; import android.net.NetworkInfo; -import android.net.Uri; import android.os.Build; -import android.os.Handler; import android.os.IBinder; -import android.os.Looper; import android.text.InputType; import android.util.Log; -import android.view.HapticFeedbackConstants; -import android.view.KeyEvent; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.ExtractedText; -import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; -import android.webkit.JavascriptInterface; import android.webkit.WebView; -import android.webkit.WebViewClient; import android.widget.FrameLayout; import android.widget.RelativeLayout; @@ -71,7 +60,6 @@ import com.keyman.engine.packages.JSONUtils; import com.keyman.engine.packages.LexicalModelPackageProcessor; import com.keyman.engine.packages.PackageProcessor; import com.keyman.engine.util.BCP47; -import com.keyman.engine.util.CharSequenceUtil; import com.keyman.engine.util.DependencyUtil; import com.keyman.engine.util.DependencyUtil.LibraryType; import com.keyman.engine.util.FileUtils; @@ -157,6 +145,34 @@ public final class KMManager { } }; + // Maps to enum BannerType in bannerView.ts + public enum BannerType { + BLANK, + IMAGE, + SUGGESTION, + HTML; + + public static BannerType fromString(String mode) { + if (mode == null) return BLANK; + switch (mode) { + case "BLANK": + return BLANK; + case "image": + return IMAGE; + case "suggestion": + return SUGGESTION; + case "html": + return HTML; + } + return BLANK; + } + + public String toString() { + String modes[] = { "blank", "image", "suggestion", "html"}; + return modes[this.ordinal()]; + } + } + protected static InputMethodService IMService; private static boolean debugMode = false; @@ -287,6 +303,9 @@ public final class KMManager { public static final String KMFilename_LexicalModelsList = "lexical_models_list.dat"; + public static final String KMBLACK_BANNER = "
"; + public static final String KMGRAY_BANNER = "
"; + private static Context appContext; public static String getResourceRoot() { @@ -636,6 +655,16 @@ public final class KMManager { keyboard.addJavascriptInterface(new KMKeyboardJSHandler(appContext, keyboard), "jsInterface"); keyboard.loadKeyboard(); + if (!isTestMode()) { + // For apps that don't specify an HTML banner, specify a default phone/tablet HTML banner + if (getFormFactor() == FormFactor.PHONE) { + keyboard.setHTMLBanner(KMBLACK_BANNER); + } else { + keyboard.setHTMLBanner(KMGRAY_BANNER); + } + keyboard.setBanner(KMManager.BannerType.HTML); + keyboard.showBanner(true); + } setEngineWebViewVersionStatus(appContext, keyboard); } @@ -793,6 +822,31 @@ public final class KMManager { return hasPermission(context, Manifest.permission.INTERNET); } + /** + * Copy HTML banner assets to the app + * @param context - The context + * @param path - Folder relative to assets/ containing the banner file. + * @return boolean - true if assets copied + */ + public static boolean copyHTMLBannerAssets(Context context, String path) { + AssetManager assetManager = context.getAssets(); + try { + File bannerDir = new File(getResourceRoot() + File.separator + path); + if (!bannerDir.exists()) { + bannerDir.mkdir(); + } + + String[] bannerFiles = assetManager.list(path); + for (String bannerFile : bannerFiles) { + copyAsset(context, bannerFile, path, true); + } + return true; + } catch (Exception e) { + KMLog.LogException(TAG, "copyHTMLBannerAssets() failed. Error: ", e); + } + return false; + } + private static void copyAssets(Context context) { AssetManager assetManager = context.getAssets(); try { @@ -1370,7 +1424,13 @@ public final class KMManager { KeyboardPickerActivity.deleteLexicalModel(context, position, silenceNotification); } - public static boolean setBannerOptions(boolean mayPredict) { + /** + * setBannerOptions - Update KMW whether to generate predictions. + * For now, also display banner + * @param mayPredict - boolean whether KMW should generate predictions + * @return boolean - Success + */ + public static boolean setBannerOptions(boolean mayPredict) { String url = KMString.format("setBannerOptions(%s)", mayPredict); if (InAppKeyboard != null) { InAppKeyboard.loadJavascript(url); @@ -1379,6 +1439,73 @@ public final class KMManager { if (SystemKeyboard != null) { SystemKeyboard.loadJavascript(url); } + + return true; + } + + /** + * Update KeymanWeb banner type + * @param {KeyboardType} keyboard + * @param {BannerType} bannerType + * @return status + */ + public static boolean setBanner(KeyboardType keyboard, BannerType bannerType) { + if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) { + InAppKeyboard.setBanner(bannerType); + } else if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) { + SystemKeyboard.setBanner(bannerType); + } else { + return false; + } + return true; + } + + /** + * Set the HTML content to use with the HTML banner + * @param {KeyboardType} keyboard + * @param {String} HTMl string + * @return {boolean} + */ + public static boolean setHTMLBanner(KeyboardType keyboard, String htmlContent) { + if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) { + InAppKeyboard.setHTMLBanner(htmlContent); + } else if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) { + SystemKeyboard.setHTMLBanner(htmlContent); + } else { + Log.d(TAG, "setHTMLBanner() but keyboard is null"); + return false; + } + return true; + } + + /** + * Get the HTML content associated with the HTML banner + * @param {KeyboardType} keyboard + * @return {String} + */ + public static String getHTMLBanner(KeyboardType keyboard) { + if (keyboard == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) { + return InAppKeyboard.getHTMLBanner(); + } else if (keyboard == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) { + return SystemKeyboard.getHTMLBanner(); + } + return ""; + } + + /** + * showBanner - Update KMW whether to display banner. + * For now, always keep displaying banner + * @param flag - boolean whether KMW should display banner + * @return boolean - Success + */ + public static boolean showBanner(boolean flag) { + if (InAppKeyboard != null) { + InAppKeyboard.showBanner(flag); + } + + if (SystemKeyboard != null) { + SystemKeyboard.showBanner(flag); + } return true; } @@ -1845,9 +1972,9 @@ public final class KMManager { public static int getBannerHeight(Context context) { int bannerHeight = 0; - if (InAppKeyboard != null && InAppKeyboard.currentBanner().equals(KMKeyboard.KM_BANNER_STATE_SUGGESTION)) { + if (InAppKeyboard != null && InAppKeyboard.getBanner() != BannerType.BLANK) { bannerHeight = (int) context.getResources().getDimension(R.dimen.banner_height); - } else if (SystemKeyboard != null && SystemKeyboard.currentBanner().equals(KMKeyboard.KM_BANNER_STATE_SUGGESTION)) { + } else if (SystemKeyboard != null && SystemKeyboard.getBanner() != BannerType.BLANK) { bannerHeight = (int) context.getResources().getDimension(R.dimen.banner_height); } return bannerHeight; diff --git a/android/KMEA/app/src/main/java/com/keyman/engine/util/FileUtils.java b/android/KMEA/app/src/main/java/com/keyman/engine/util/FileUtils.java index d5610dedec..6b04b974f8 100644 --- a/android/KMEA/app/src/main/java/com/keyman/engine/util/FileUtils.java +++ b/android/KMEA/app/src/main/java/com/keyman/engine/util/FileUtils.java @@ -4,16 +4,22 @@ package com.keyman.engine.util; import android.content.Context; +import android.content.res.AssetManager; +import android.util.Log; + +import com.keyman.engine.KMManager; import org.json.JSONObject; import org.json.JSONArray; import java.io.BufferedInputStream; +import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.nio.charset.StandardCharsets; @@ -248,6 +254,35 @@ public final class FileUtils { return result; } + /** + * Read the contents of asset file as a string + * Reference: https://stackoverflow.com/questions/16110002/read-assets-file-as-string + * @param context + * @param path - path of file relative to assets folder + * @return String + */ + public static String readContents(Context context, String path) { + StringBuilder sb = new StringBuilder(); + String str = ""; + AssetManager assetManager = context.getAssets(); + try { + InputStream inputStream = assetManager.open(path); + if (inputStream == null) { + KMLog.LogInfo(TAG, "Unable to read contents of asset: " + path); + return str; + } + BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8)); + while ((str = reader.readLine()) != null) { + sb.append(str); + } + reader.close(); + } catch (Exception e) { + KMLog.LogException(TAG, "Error reading asset file", e); + return str; + } + return sb.toString(); + } + /** * Utility to parse a URL and extract the filename * @param urlStr String diff --git a/core/tests/unit/ldml/ldml.cpp b/core/tests/unit/ldml/ldml.cpp index 546af55ef4..e22367a7f9 100644 --- a/core/tests/unit/ldml/ldml.cpp +++ b/core/tests/unit/ldml/ldml.cpp @@ -128,16 +128,9 @@ apply_action( assert(context.back().character == ch); context.pop_back(); } else { - assert(act.backspace.expected_type == KM_CORE_BT_UNKNOWN); // else it must be unknown - // pop to first character or empty - while (!context.empty() && context.back().type != KM_CORE_CT_CHAR) { - // pop off all non-character entries - context.pop_back(); - } - if (!context.empty()) { - // pop off the next character - context.pop_back(); - } + // assume it's otherwise KM-coRE_BT_UNKNOWN + assert(act.backspace.expected_type == KM_CORE_BT_UNKNOWN); + assert(context.empty()); // if KM_CORE_BT_UNKNOWN, context should be empty. } } break; diff --git a/ios/Cartfile b/ios/Cartfile index b265ef77af..57ec6de1e2 100644 --- a/ios/Cartfile +++ b/ios/Cartfile @@ -1,4 +1,4 @@ -github "marmelroy/Zip" +github "weichsel/ZIPFoundation" ~> 0.9 github "keymanapp/dependency-XCGLogger" "master" github "devicekit/DeviceKit" ~> 5.0 github "ashleymills/Reachability.swift" diff --git a/ios/Cartfile.resolved b/ios/Cartfile.resolved index ff30a8871d..5245b0dfa7 100644 --- a/ios/Cartfile.resolved +++ b/ios/Cartfile.resolved @@ -1,5 +1,5 @@ github "ashleymills/Reachability.swift" "v5.1.0" -github "devicekit/DeviceKit" "5.0.0" -github "getsentry/sentry-cocoa" "8.7.0" +github "devicekit/DeviceKit" "5.1.0" +github "getsentry/sentry-cocoa" "8.15.2" github "keymanapp/dependency-XCGLogger" "57a7b975dbb6fe4fe90cef3d1bc52b8adbd89113" -github "marmelroy/Zip" "2.1.2" +github "weichsel/ZIPFoundation" "0.9.17" diff --git a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj index f0205767d5..374f08bdcc 100644 --- a/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj +++ b/ios/engine/KMEI/KeymanEngine.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ 1645D5952036C6FF0076C51B /* KeymanPackage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1645D5942036C6FF0076C51B /* KeymanPackage.swift */; }; 1645D5972036C9F80076C51B /* KMPKeyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1645D5962036C9F80076C51B /* KMPKeyboard.swift */; }; 165EB3A12098993900040A69 /* KeyboardError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165EB3A02098993900040A69 /* KeyboardError.swift */; }; + 296EF2C72AFA26C700E3E384 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 296EF2C62AFA26C700E3E384 /* ZIPFoundation.xcframework */; }; 377D10DE26846B8900467431 /* SpacebarTextViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 377D10DD26846B8900467431 /* SpacebarTextViewController.swift */; }; 6CD5DFAA150F6DC8007A5DDE /* icon.png in Resources */ = {isa = PBXBuildFile; fileRef = 6CD5DFA8150F6DC8007A5DDE /* icon.png */; }; 6CD5DFAB150F6DC8007A5DDE /* icon@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 6CD5DFA9150F6DC8007A5DDE /* icon@2x.png */; }; @@ -121,7 +122,6 @@ CE5C8BE324B5B3BA00FAFB7F /* Queries+LexicalModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5C8BE224B5B3BA00FAFB7F /* Queries+LexicalModel.swift */; }; CE5C8BE524B5BD1C00FAFB7F /* QueryModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE5C8BE424B5BD1C00FAFB7F /* QueryModelTests.swift */; }; CE5EDDC526522EAA001733AC /* XCGLogger.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC026522EA9001733AC /* XCGLogger.xcframework */; }; - CE5EDDC626522EAA001733AC /* Zip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC126522EA9001733AC /* Zip.xcframework */; }; CE5EDDC726522EAA001733AC /* Sentry.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC226522EAA001733AC /* Sentry.xcframework */; }; CE5EDDC826522EAA001733AC /* Reachability.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC326522EAA001733AC /* Reachability.xcframework */; }; CE5EDDC926522EAA001733AC /* ObjcExceptionBridging.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CE5EDDC426522EAA001733AC /* ObjcExceptionBridging.xcframework */; }; @@ -308,6 +308,7 @@ 2949146F2738DA6700400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/ResourceInfoView.strings"; sourceTree = ""; }; 294914702738DD7400400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/Localizable.strings"; sourceTree = ""; }; 294914712738DD9700400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = "ff-NG"; path = "ff-NG.lproj/Localizable.stringsdict"; sourceTree = ""; }; + 296EF2C62AFA26C700E3E384 /* ZIPFoundation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ZIPFoundation.xcframework; path = ../../Carthage/Build/ZIPFoundation.xcframework; sourceTree = ""; }; 297810FE297FAEDF007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/ResourceInfoView.strings; sourceTree = ""; }; 297810FF297FAEF8007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/Localizable.strings; sourceTree = ""; }; 29781100297FAF06007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = kn; path = kn.lproj/Localizable.stringsdict; sourceTree = ""; }; @@ -436,7 +437,6 @@ CE5C8BE424B5BD1C00FAFB7F /* QueryModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryModelTests.swift; sourceTree = ""; }; CE5EDDBB26522EA3001733AC /* DeviceKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = DeviceKit.xcframework; path = ../../Carthage/Build/DeviceKit.xcframework; sourceTree = ""; }; CE5EDDC026522EA9001733AC /* XCGLogger.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = XCGLogger.xcframework; path = ../../Carthage/Build/XCGLogger.xcframework; sourceTree = ""; }; - CE5EDDC126522EA9001733AC /* Zip.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Zip.xcframework; path = ../../Carthage/Build/Zip.xcframework; sourceTree = ""; }; CE5EDDC226522EAA001733AC /* Sentry.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Sentry.xcframework; path = ../../Carthage/Build/Sentry.xcframework; sourceTree = ""; }; CE5EDDC326522EAA001733AC /* Reachability.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Reachability.xcframework; path = ../../Carthage/Build/Reachability.xcframework; sourceTree = ""; }; CE5EDDC426522EAA001733AC /* ObjcExceptionBridging.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ObjcExceptionBridging.xcframework; path = ../../Carthage/Build/ObjcExceptionBridging.xcframework; sourceTree = ""; }; @@ -560,8 +560,8 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 296EF2C72AFA26C700E3E384 /* ZIPFoundation.xcframework in Frameworks */, CE5EDDC526522EAA001733AC /* XCGLogger.xcframework in Frameworks */, - CE5EDDC626522EAA001733AC /* Zip.xcframework in Frameworks */, CE5EDDD52652372A001733AC /* DeviceKit.xcframework in Frameworks */, CE5EDDC726522EAA001733AC /* Sentry.xcframework in Frameworks */, CE5EDDC826522EAA001733AC /* Reachability.xcframework in Frameworks */, @@ -949,11 +949,11 @@ F243888114BBD43000A3E055 /* Frameworks */ = { isa = PBXGroup; children = ( + 296EF2C62AFA26C700E3E384 /* ZIPFoundation.xcframework */, CE5EDDC426522EAA001733AC /* ObjcExceptionBridging.xcframework */, CE5EDDC326522EAA001733AC /* Reachability.xcframework */, CE5EDDC226522EAA001733AC /* Sentry.xcframework */, CE5EDDC026522EA9001733AC /* XCGLogger.xcframework */, - CE5EDDC126522EA9001733AC /* Zip.xcframework */, CE5EDDBB26522EA3001733AC /* DeviceKit.xcframework */, 9A079DE52231A69D00581263 /* Foundation.framework */, ); diff --git a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift index 28d89b7ee1..a13cef142c 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/KeymanPackage.swift @@ -7,7 +7,7 @@ // import Foundation -import Zip +import ZIPFoundation // KMPErrors may be passed to UIAlertControllers, so they need localization. public enum KMPError : String, Error { @@ -443,13 +443,28 @@ public class KeymanPackage { @available(*, deprecated, message: "Use of the completion block is unnecessary; this method now returns synchronously.") static public func extract(fileUrl: URL, destination: URL, complete: @escaping (KeymanPackage?) -> Void) throws { - try unzipFile(fileUrl: fileUrl, destination: destination) { - do { - let package = try KeymanPackage.parse(destination) - complete(package) - } catch { - SentryManager.captureAndLog(error, sentryLevel: .info) - complete(nil) + let fileManager = FileManager() + do { + try fileManager.unzipItem(at: fileUrl, to: destination) + let package = try KeymanPackage.parse(destination) + complete(package) + } catch { + SentryManager.captureAndLog(error, sentryLevel: .info) + complete(nil) + } + } + + static public func clearDirectory(destination: URL) throws { + // First check to see if directory exists. If not, then do nothing. + var isDirectory: ObjCBool = false + if(FileManager.default.fileExists(atPath: destination.path, isDirectory: &isDirectory)){ + if (isDirectory.boolValue) { + // it exists and is actually a directory, so remove every file it contains + let fileArray = try FileManager.default.contentsOfDirectory(atPath: destination.path) + try fileArray.forEach { file in + let fileUrl = destination.appendingPathComponent(file) + try FileManager.default.removeItem(atPath: fileUrl.path) + } } } } @@ -460,7 +475,8 @@ public class KeymanPackage { } static public func unzipFile(fileUrl: URL, destination: URL, complete: @escaping () -> Void = {}) throws { - try Zip.unzipFile(fileUrl, destination: destination, overwrite: true, password: nil) + let fileManager = FileManager() + try fileManager.unzipItem(at: fileUrl, to: destination) complete() } diff --git a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift index 11740ce42b..d3831413cb 100644 --- a/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift +++ b/ios/engine/KMEI/KeymanEngine/Classes/Resource Management/ResourceFileManager.swift @@ -183,6 +183,9 @@ public class ResourceFileManager { var extractionFolder = cacheDirectory extractionFolder.appendPathComponent("temp/\(archiveUrl.lastPathComponent)") + // first clear extraction folder to avoid creating duplicates + try KeymanPackage.clearDirectory(destination: extractionFolder) + do { if let package = try KeymanPackage.extract(fileUrl: archiveUrl, destination: extractionFolder) { return package diff --git a/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift b/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift index ee91cf96b9..baf61ee487 100644 --- a/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift +++ b/ios/engine/KMEI/KeymanEngineTests/KeymanPackageTests.swift @@ -29,14 +29,13 @@ class KeymanPackageTests: XCTestCase { } } - func testKeyboardPackageExtraction() throws { + func testKeyboardPackage_extractWithoutKmpExtension_succeeds() throws { let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] - let khmerPackageZip = cacheDirectory.appendingPathComponent("khmer_angkor.kmp.zip") + let khmerPackageZip = cacheDirectory.appendingPathComponent("khmer_angkor.kmp") try ResourceFileManager.shared.copyWithOverwrite(from: TestUtils.Keyboards.khmerAngkorKMP, to: khmerPackageZip) let destinationFolderURL = cacheDirectory.appendingPathComponent("khmer_angkor") - // Requires that the source file is already .zip, not .kmp. It's a ZipUtils limitation. do { if let kmp = try KeymanPackage.extract(fileUrl: khmerPackageZip, destination: destinationFolderURL) { // Run assertions on the package's kmp.info. @@ -58,14 +57,104 @@ class KeymanPackageTests: XCTestCase { } } + func testKeyboardPackage_clearNonexistentDirectory_doesNothing() throws { + let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let destinationDirectory = cacheDirectory.appendingPathComponent("doesnotexist") + + do { + // clear directory + try KeymanPackage.clearDirectory(destination: destinationDirectory) + } catch { + XCTFail("error clearing the nonexistent directory \(error)") + } + } + + func testKeyboardPackage_clearEmptyDirectory_throwsNoError() throws { + let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let destinationDirectory = cacheDirectory.appendingPathComponent("destination") + + do { + // create directory + try FileManager.default.createDirectory( + atPath: destinationDirectory.path, + withIntermediateDirectories: false, + attributes: nil + ) + + // clear directory + try KeymanPackage.clearDirectory(destination: destinationDirectory) + } catch { + XCTFail("error clearing the empty directory \(error)") + } + + let fileArray = try FileManager.default.contentsOfDirectory(atPath: destinationDirectory.path) + XCTAssert(fileArray.count == 0, "directory still contains \(fileArray.count) items") + } + + func testKeyboardPackage_clearNonEmptyDirectory_directoryIsEmpty() throws { + let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let destinationDirectory = cacheDirectory.appendingPathComponent("destination") + + do { + // create directory + try FileManager.default.createDirectory( + atPath: destinationDirectory.path, + withIntermediateDirectories: false, + attributes: nil + ) + + // add some files + FileManager.default.createFile(atPath: destinationDirectory.appendingPathComponent("fileone").path, contents: nil) + FileManager.default.createFile(atPath: destinationDirectory.appendingPathComponent("filetwo").path, contents: nil) + FileManager.default.createFile(atPath: destinationDirectory.appendingPathComponent("filethree").path, contents: nil) + + // clear directory + try KeymanPackage.clearDirectory(destination: destinationDirectory) + } catch { + XCTFail("error clearing the empty directory \(error)") + } + + let fileArray = try FileManager.default.contentsOfDirectory(atPath: destinationDirectory.path) + XCTAssert(fileArray.count == 0, "directory still contains \(fileArray.count) items") + } + + func testKeyboardPackage_extractTwice_noDuplicateFileError() throws { + let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let khmerPackageZip = cacheDirectory.appendingPathComponent("khmer_angkor.kmp") + try ResourceFileManager.shared.copyWithOverwrite(from: TestUtils.Keyboards.khmerAngkorKMP, to: khmerPackageZip) + + let destinationFolderURL = cacheDirectory.appendingPathComponent("khmer_angkor") + + do { + if let kmp = try KeymanPackage.extract(fileUrl: khmerPackageZip, destination: destinationFolderURL) { + log.info("*** first unzip of \(kmp.id)") + do { + // clear directory before second extract + try KeymanPackage.clearDirectory(destination: destinationFolderURL) + + if let secondKmp = try KeymanPackage.extract(fileUrl: khmerPackageZip, destination: destinationFolderURL) { + log.info("*** second unzip of \(secondKmp.id)") + } else { + XCTAssert(false, "*** second unzip failed") + } + } catch { + XCTFail("unzip 2 failure with error \(error)") + } + } else { + XCTAssert(false, "*** first unzip failed") + } + } catch { + XCTFail("unzip 1 failure with error \(error)") + } + } + func testLexicalModelPackageExtraction() throws { let cacheDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] - let mtntZip = cacheDirectory.appendingPathComponent("nrc.en.mtnt.kmp.zip") + let mtntZip = cacheDirectory.appendingPathComponent("nrc.en.mtnt.kmp") try ResourceFileManager.shared.copyWithOverwrite(from: TestUtils.LexicalModels.mtntKMP, to: mtntZip) let destinationFolderURL = cacheDirectory.appendingPathComponent("nrc.en.mtnt.model") - // Requires that the source file is already .zip, not .kmp. It's a ZipUtils limitation. do { if let kmp = try KeymanPackage.extract(fileUrl: mtntZip, destination: destinationFolderURL) { // Run assertions on the package's kmp.info. diff --git a/ios/engine/KMEI/KeymanEngineTests/TestUtils/EngineStateBundler.swift b/ios/engine/KMEI/KeymanEngineTests/TestUtils/EngineStateBundler.swift index 1f3bd4e27c..b8950dc9c2 100644 --- a/ios/engine/KMEI/KeymanEngineTests/TestUtils/EngineStateBundler.swift +++ b/ios/engine/KMEI/KeymanEngineTests/TestUtils/EngineStateBundler.swift @@ -8,7 +8,8 @@ import Foundation import XCTest -import Zip +import ZIPFoundation + @testable import KeymanEngine extension TestUtils { @@ -64,12 +65,17 @@ extension TestUtils { try FileManager.default.moveItem(at: pListPath.appendingPathComponent(testEngineFilename), to: pListPath.appendingPathComponent(appGroupFilename)) } - let attachmentFile = try Zip.quickZipFiles([bundleConstructionURL], fileName: "bundleArchive") - log.info("Archive source: \(bundleConstructionURL)") - let attachment = XCTAttachment(contentsOfFile: attachmentFile) - attachment.lifetime = .keepAlways - - return attachment - } + let archiveURL = bundleConstructionURL.appendingPathComponent("bundleArchive.zip") + do { + _ = try Archive(url: archiveURL, accessMode: .create) + log.info("archiveURL: \(archiveURL)") + let attachment = XCTAttachment(contentsOfFile: archiveURL) + attachment.lifetime = .keepAlways + return attachment + } catch let error { + print (error.localizedDescription) + return XCTAttachment(string: error.localizedDescription) + } + } } } diff --git a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj index 5f2fad4ced..ddaa7a831c 100644 --- a/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj +++ b/ios/keyman/Keyman/Keyman.xcodeproj/project.pbxproj @@ -14,6 +14,8 @@ 162E2C9F2092D36800F40769 /* UIDevice+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 162E2C9E2092D36800F40769 /* UIDevice+Extensions.swift */; }; 165EB39A2097165B00040A69 /* UIColor+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165EB3992097165B00040A69 /* UIColor+Extensions.swift */; }; 165EB39C2097181D00040A69 /* KMView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 165EB39B2097181D00040A69 /* KMView.swift */; }; + 296EF2C42AFA267500E3E384 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 296EF2C32AFA267500E3E384 /* ZIPFoundation.xcframework */; }; + 296EF2C52AFA267500E3E384 /* ZIPFoundation.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 296EF2C32AFA267500E3E384 /* ZIPFoundation.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 981AFA8F19EF44DE006706BF /* 724-info@2x.png in Resources */ = {isa = PBXBuildFile; fileRef = 9815725518E4F0930014DF0C /* 724-info@2x.png */; }; 981AFA9619EF44DE006706BF /* MainViewController_iPhone.xib in Resources */ = {isa = PBXBuildFile; fileRef = 98ABADCE176935E500B62590 /* MainViewController_iPhone.xib */; }; 981AFA9819EF44DE006706BF /* 724-info.png in Resources */ = {isa = PBXBuildFile; fileRef = 9815725418E4F0930014DF0C /* 724-info.png */; }; @@ -155,8 +157,6 @@ CEBD34422654FEB400EB2EA8 /* Sentry.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD34382654FEB400EB2EA8 /* Sentry.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CEBD34432654FEB400EB2EA8 /* XCGLogger.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD34392654FEB400EB2EA8 /* XCGLogger.xcframework */; }; CEBD34442654FEB400EB2EA8 /* XCGLogger.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD34392654FEB400EB2EA8 /* XCGLogger.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - CEBD34452654FEB400EB2EA8 /* Zip.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD343A2654FEB400EB2EA8 /* Zip.xcframework */; }; - CEBD34462654FEB400EB2EA8 /* Zip.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = CEBD343A2654FEB400EB2EA8 /* Zip.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CEF4E55623E95B7B0065B9C7 /* ImageBanner.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEF4E55523E95B7B0065B9C7 /* ImageBanner.xib */; }; CEF4E55723E95B7B0065B9C7 /* ImageBanner.xib in Resources */ = {isa = PBXBuildFile; fileRef = CEF4E55523E95B7B0065B9C7 /* ImageBanner.xib */; }; CEF4E55923E967140065B9C7 /* ImageBannerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF4E55823E967140065B9C7 /* ImageBannerViewController.swift */; }; @@ -183,11 +183,11 @@ files = ( CEBD34402654FEB400EB2EA8 /* Reachability.xcframework in Embed Frameworks */, CEBD343E2654FEB400EB2EA8 /* ObjcExceptionBridging.xcframework in Embed Frameworks */, + 296EF2C52AFA267500E3E384 /* ZIPFoundation.xcframework in Embed Frameworks */, CEBD343C2654FEB400EB2EA8 /* DeviceKit.xcframework in Embed Frameworks */, CEBD34422654FEB400EB2EA8 /* Sentry.xcframework in Embed Frameworks */, CEBD34442654FEB400EB2EA8 /* XCGLogger.xcframework in Embed Frameworks */, CE80AD34257F2B4B008D2150 /* KeymanEngine.framework in Embed Frameworks */, - CEBD34462654FEB400EB2EA8 /* Zip.xcframework in Embed Frameworks */, ); name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; @@ -205,6 +205,7 @@ 16A9229D20325253003CC98E /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = Keyman/Info.plist; sourceTree = SOURCE_ROOT; }; 293EA3E027059C6900545EED /* ha */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ha; path = ha.lproj/Localizable.strings; sourceTree = ""; }; 294914742738DF7700400732 /* ff-NG */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "ff-NG"; path = "ff-NG.lproj/Localizable.strings"; sourceTree = ""; }; + 296EF2C32AFA267500E3E384 /* ZIPFoundation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ZIPFoundation.xcframework; path = ../../Carthage/Build/ZIPFoundation.xcframework; sourceTree = ""; }; 297810FD297FA818007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/Localizable.strings; sourceTree = ""; }; 298566B829802828004ACA95 /* cs */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = cs; path = cs.lproj/Localizable.strings; sourceTree = ""; }; 298566C42980C493004ACA95 /* sv */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = sv; path = sv.lproj/Localizable.strings; sourceTree = ""; }; @@ -344,7 +345,6 @@ CEBD34372654FEB400EB2EA8 /* Reachability.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Reachability.xcframework; path = ../../Carthage/Build/Reachability.xcframework; sourceTree = ""; }; CEBD34382654FEB400EB2EA8 /* Sentry.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Sentry.xcframework; path = ../../Carthage/Build/Sentry.xcframework; sourceTree = ""; }; CEBD34392654FEB400EB2EA8 /* XCGLogger.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = XCGLogger.xcframework; path = ../../Carthage/Build/XCGLogger.xcframework; sourceTree = ""; }; - CEBD343A2654FEB400EB2EA8 /* Zip.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Zip.xcframework; path = ../../Carthage/Build/Zip.xcframework; sourceTree = ""; }; CEBD3458265511B700EB2EA8 /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/Localizable.strings; sourceTree = ""; }; CEEC468226F2F7BA009A5B7D /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/Localizable.strings"; sourceTree = ""; }; CEEF81B92673019600EE6A07 /* es-419 */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "es-419"; path = "es-419.lproj/Localizable.strings"; sourceTree = ""; }; @@ -359,11 +359,11 @@ files = ( CEBD343F2654FEB400EB2EA8 /* Reachability.xcframework in Frameworks */, CEBD343D2654FEB400EB2EA8 /* ObjcExceptionBridging.xcframework in Frameworks */, + 296EF2C42AFA267500E3E384 /* ZIPFoundation.xcframework in Frameworks */, CEBD343B2654FEB400EB2EA8 /* DeviceKit.xcframework in Frameworks */, CEBD34412654FEB400EB2EA8 /* Sentry.xcframework in Frameworks */, CEBD34432654FEB400EB2EA8 /* XCGLogger.xcframework in Frameworks */, CE80AD33257F2B4A008D2150 /* KeymanEngine.framework in Frameworks */, - CEBD34452654FEB400EB2EA8 /* Zip.xcframework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -544,12 +544,12 @@ 98ABADB2176935E400B62590 /* Frameworks */ = { isa = PBXGroup; children = ( + 296EF2C32AFA267500E3E384 /* ZIPFoundation.xcframework */, CEBD34352654FEB400EB2EA8 /* DeviceKit.xcframework */, CEBD34362654FEB400EB2EA8 /* ObjcExceptionBridging.xcframework */, CEBD34372654FEB400EB2EA8 /* Reachability.xcframework */, CEBD34382654FEB400EB2EA8 /* Sentry.xcframework */, CEBD34392654FEB400EB2EA8 /* XCGLogger.xcframework */, - CEBD343A2654FEB400EB2EA8 /* Zip.xcframework */, CE80AD32257F2B4A008D2150 /* KeymanEngine.framework */, ); name = Frameworks; diff --git a/ios/keymanios.xcworkspace/xcshareddata/xcschemes/Keyman.xcscheme b/ios/keymanios.xcworkspace/xcshareddata/xcschemes/Keyman.xcscheme index abc85ae142..8f86bea533 100644 --- a/ios/keymanios.xcworkspace/xcshareddata/xcschemes/Keyman.xcscheme +++ b/ios/keymanios.xcworkspace/xcshareddata/xcschemes/Keyman.xcscheme @@ -40,7 +40,8 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" - shouldUseLaunchSchemeArgsEnv = "NO"> + shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES"> +
+ + + + + +
+ +
diff --git a/oem/firstvoices/android/app/src/main/assets/banner/red-logo.svg b/oem/firstvoices/android/app/src/main/assets/banner/red-logo.svg new file mode 100644 index 0000000000..cf89f67db5 --- /dev/null +++ b/oem/firstvoices/android/app/src/main/assets/banner/red-logo.svg @@ -0,0 +1,38 @@ + + + + + + + + + + diff --git a/oem/firstvoices/android/app/src/main/java/com/firstvoices/android/BannerController.java b/oem/firstvoices/android/app/src/main/java/com/firstvoices/android/BannerController.java new file mode 100644 index 0000000000..a0df15b2f1 --- /dev/null +++ b/oem/firstvoices/android/app/src/main/java/com/firstvoices/android/BannerController.java @@ -0,0 +1,29 @@ +package com.firstvoices.android; + +import android.content.Context; + +import com.keyman.engine.KMManager; +import com.keyman.engine.util.FileUtils; + +import java.io.File; + +public class BannerController { + + // Paths relative to assets folder for banner themes + public static final String FV_BANNER_DIR = "banner"; + public static final String FV_BANNER_THEME = "banner.html"; + + public static void setHTMLBanner(Context context, KMManager.KeyboardType keyboardType) { + if (keyboardType == KMManager.KeyboardType.KEYBOARD_TYPE_UNDEFINED) { + return; + } + + KMManager.copyHTMLBannerAssets(context, FV_BANNER_DIR); + + // Always use FirstVoices banner theme + String contents = FileUtils.readContents(context, FV_BANNER_THEME); + KMManager.setHTMLBanner(keyboardType, contents); + KMManager.setBanner(keyboardType, KMManager.BannerType.HTML); + KMManager.showBanner(true); + } +} diff --git a/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/SystemKeyboard.java b/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/SystemKeyboard.java index 95d0c51b37..0fb770c9b5 100644 --- a/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/SystemKeyboard.java +++ b/oem/firstvoices/android/app/src/main/java/com/firstvoices/keyboards/SystemKeyboard.java @@ -20,6 +20,7 @@ import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputConnection; import android.widget.FrameLayout; +import com.firstvoices.android.BannerController; import com.keyman.engine.KMManager; import com.keyman.engine.KMManager.KeyboardType; import com.keyman.engine.KMHardwareKeyboardInterpreter; @@ -33,7 +34,6 @@ import io.sentry.android.core.SentryAndroid; import io.sentry.Sentry; public class SystemKeyboard extends InputMethodService implements OnKeyboardEventListener { - private View inputView = null; private static ExtractedText exText = null; private KMHardwareKeyboardInterpreter interpreter = null; @@ -64,6 +64,9 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven interpreter = new KMHardwareKeyboardInterpreter(getApplicationContext(), KeyboardType.KEYBOARD_TYPE_SYSTEM); KMManager.setInputMethodService(this); // for HW interface + + // Set the system keyboard HTML banner + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); } @Override @@ -79,7 +82,10 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven * is called after creation and any configuration change. */ @Override public void onInitializeInterface() { - super.onInitializeInterface(); + super.onInitializeInterface(); + + // KeymanWeb reloaded, so we have to pass the banner again + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); } /** Called by the framework when your view for creating input needs to diff --git a/oem/firstvoices/ios/Cartfile b/oem/firstvoices/ios/Cartfile index 10efbc3d5a..57ec6de1e2 100644 --- a/oem/firstvoices/ios/Cartfile +++ b/oem/firstvoices/ios/Cartfile @@ -1,3 +1,4 @@ +github "weichsel/ZIPFoundation" ~> 0.9 github "keymanapp/dependency-XCGLogger" "master" github "devicekit/DeviceKit" ~> 5.0 github "ashleymills/Reachability.swift" diff --git a/oem/firstvoices/ios/Cartfile.resolved b/oem/firstvoices/ios/Cartfile.resolved index 5ac3a55318..5245b0dfa7 100644 --- a/oem/firstvoices/ios/Cartfile.resolved +++ b/oem/firstvoices/ios/Cartfile.resolved @@ -1,4 +1,5 @@ github "ashleymills/Reachability.swift" "v5.1.0" -github "devicekit/DeviceKit" "5.0.0" -github "getsentry/sentry-cocoa" "8.7.0" +github "devicekit/DeviceKit" "5.1.0" +github "getsentry/sentry-cocoa" "8.15.2" github "keymanapp/dependency-XCGLogger" "57a7b975dbb6fe4fe90cef3d1bc52b8adbd89113" +github "weichsel/ZIPFoundation" "0.9.17" diff --git a/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj b/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj index 83e34c42a6..dcb81b684f 100644 --- a/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj +++ b/oem/firstvoices/ios/FirstVoices.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 290FB4632AFB358F00249D58 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */; }; + 290FB4642AFB358F00249D58 /* ZIPFoundation.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 290FB4652AFB359A00249D58 /* ZIPFoundation.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */; }; 294D40E9279FE62600DB37F6 /* KeyboardRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294D40E8279FE62600DB37F6 /* KeyboardRepository.swift */; }; 294D40EC27A110BC00DB37F6 /* KeyboardSettingsRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 294D40EB27A110BC00DB37F6 /* KeyboardSettingsRepository.swift */; }; 29694DC227A27EC300EA6C18 /* LexicalModelRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 29694DC127A27EC300EA6C18 /* LexicalModelRepository.swift */; }; @@ -74,6 +77,7 @@ files = ( CEBD34202654D76900EB2EA8 /* Reachability.xcframework in Embed Frameworks */, CEBD341E2654D76800EB2EA8 /* ObjcExceptionBridging.xcframework in Embed Frameworks */, + 290FB4642AFB358F00249D58 /* ZIPFoundation.xcframework in Embed Frameworks */, CEBD341B2654D76600EB2EA8 /* DeviceKit.xcframework in Embed Frameworks */, CEBD34232654D76B00EB2EA8 /* Sentry.xcframework in Embed Frameworks */, CEBD34262654D76C00EB2EA8 /* XCGLogger.xcframework in Embed Frameworks */, @@ -96,6 +100,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = ZIPFoundation.xcframework; path = Carthage/Build/ZIPFoundation.xcframework; sourceTree = ""; }; 294D40E8279FE62600DB37F6 /* KeyboardRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardRepository.swift; sourceTree = ""; }; 294D40EB27A110BC00DB37F6 /* KeyboardSettingsRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyboardSettingsRepository.swift; sourceTree = ""; }; 29694DC127A27EC300EA6C18 /* LexicalModelRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LexicalModelRepository.swift; sourceTree = ""; }; @@ -153,6 +158,7 @@ files = ( CEBD341D2654D76800EB2EA8 /* ObjcExceptionBridging.xcframework in Frameworks */, CEBD341F2654D76900EB2EA8 /* Reachability.xcframework in Frameworks */, + 290FB4632AFB358F00249D58 /* ZIPFoundation.xcframework in Frameworks */, CEBD34222654D76B00EB2EA8 /* Sentry.xcframework in Frameworks */, CEBD341A2654D76600EB2EA8 /* DeviceKit.xcframework in Frameworks */, CEBD34022654D41200EB2EA8 /* KeymanEngine.xcframework in Frameworks */, @@ -166,6 +172,7 @@ files = ( CEDB327F265C9C58000A2009 /* DeviceKit.xcframework in Frameworks */, CEDB3280265C9C58000A2009 /* ObjcExceptionBridging.xcframework in Frameworks */, + 290FB4652AFB359A00249D58 /* ZIPFoundation.xcframework in Frameworks */, CEDB3281265C9C58000A2009 /* Reachability.xcframework in Frameworks */, CEDB3282265C9C58000A2009 /* Sentry.xcframework in Frameworks */, CEDB3283265C9C58000A2009 /* XCGLogger.xcframework in Frameworks */, @@ -284,6 +291,7 @@ 98C9A9FB1BFC19ED009E9A4F /* Frameworks */ = { isa = PBXGroup; children = ( + 290FB4622AFB358E00249D58 /* ZIPFoundation.xcframework */, CEBD34082654D5AA00EB2EA8 /* ObjcExceptionBridging.xcframework */, CEBD340C2654D5AB00EB2EA8 /* Reachability.xcframework */, CEBD34092654D5AA00EB2EA8 /* Sentry.xcframework */,