From f7f73e0ef202bd9c5d575785f8b4febd909c7718 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Tue, 7 Apr 2020 15:27:36 +0700 Subject: [PATCH 1/8] feat(android): Use https://keyman.com/keyboards/ to add keyboards --- .../com/tavultesoft/kmapro/MainActivity.java | 4 +- android/KMEA/app/src/main/AndroidManifest.xml | 5 + .../tavultesoft/kmea/KMPBrowserActivity.java | 185 ++++++++++++++++++ .../kmea/LanguageSettingsActivity.java | 12 +- .../kmea/LanguagesSettingsActivity.java | 12 +- .../main/res/layout/activity_kmp_browser.xml | 14 ++ .../KMEA/app/src/main/res/values/strings.xml | 3 + 7 files changed, 220 insertions(+), 15 deletions(-) create mode 100644 android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java create mode 100644 android/KMEA/app/src/main/res/layout/activity_kmp_browser.xml 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 2ff31baffd..9d017b52b2 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 @@ -21,6 +21,7 @@ import java.util.Map; import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; import com.tavultesoft.kmea.KMManager.KeyboardType; +import com.tavultesoft.kmea.KMPBrowserActivity; import com.tavultesoft.kmea.KMTextView; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardDownloadEventListener; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardEventListener; @@ -659,7 +660,8 @@ public class MainActivity extends AppCompatActivity implements OnKeyboardEventLi } private void showWebBrowser() { - Intent i = new Intent(this, WebBrowserActivity.class); + //Intent i = new Intent(this, WebBrowserActivity.class); + Intent i = new Intent(this, KMPBrowserActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); startActivity(i); overridePendingTransition(android.R.anim.fade_in, R.anim.hold); diff --git a/android/KMEA/app/src/main/AndroidManifest.xml b/android/KMEA/app/src/main/AndroidManifest.xml index f519253f24..91b75bf5ac 100644 --- a/android/KMEA/app/src/main/AndroidManifest.xml +++ b/android/KMEA/app/src/main/AndroidManifest.xml @@ -12,6 +12,11 @@ + + = Build.VERSION_CODES.LOLLIPOP) { + originalIcon = getResources().getDrawable(resId, getTheme()); + } else { + originalIcon = getResources().getDrawable(resId); + } + Drawable icon = enabled ? originalIcon : convertDrawableToGrayScale(originalIcon); + imgButton.setImageDrawable(icon); + } + + private static Drawable convertDrawableToGrayScale(Drawable drawable) { + if (drawable == null) + return null; + + Drawable drw = drawable.mutate(); + drw.setColorFilter(Color.LTGRAY, Mode.SRC_IN); + return drw; + } +} \ No newline at end of file diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageSettingsActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageSettingsActivity.java index 6848b09a32..deed4970bb 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageSettingsActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageSettingsActivity.java @@ -239,13 +239,11 @@ public final class LanguageSettingsActivity extends AppCompatActivity { addButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // Check that available keyboard information can be obtained via: - // 1. connection to cloud catalog - // 2. cached file - // 3. local kmp.json files in packages/ - if (KMManager.hasConnection(context) || CloudRepository.shared.hasCache(context) || - KeyboardPickerActivity.hasKeyboardFromPackage()){ - // Rework to use languuage-specific (KeyboardList) picker! - Intent i = new Intent(context, KeyboardListActivity.class); + // 1. connection to keyman.com + // 2. local kmp.json files in packages/ + if (KMManager.hasConnection(context) || KeyboardPickerActivity.hasKeyboardFromPackage()){ + Intent i = new Intent(context, KMPBrowserActivity.class); + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); i.putExtra("languageCode", lgCode); i.putExtra("languageName", lgName); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java index fdc8edf75a..b143d37891 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java @@ -110,13 +110,11 @@ public final class LanguagesSettingsActivity extends AppCompatActivity { addButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { // Check that available keyboard information can be obtained via: - // 1. connection to cloud catalog - // 2. cached file - // 3. local kmp.json files in packages/ - if (KMManager.hasConnection(context) || CloudDataJsonUtil.getKeyboardCacheFile(context).exists() || - KeyboardPickerActivity.hasKeyboardFromPackage()){ - dismissOnSelect = false; - Intent i = new Intent(context, LanguageListActivity.class); + // 1. connection to keyman.com catalog + // 2. local kmp.json files in packages/ + if (KMManager.hasConnection(context) || KeyboardPickerActivity.hasKeyboardFromPackage()){ + Intent i = new Intent(context, KMPBrowserActivity.class); + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); context.startActivity(i); } else { diff --git a/android/KMEA/app/src/main/res/layout/activity_kmp_browser.xml b/android/KMEA/app/src/main/res/layout/activity_kmp_browser.xml new file mode 100644 index 0000000000..3dd6e2f424 --- /dev/null +++ b/android/KMEA/app/src/main/res/layout/activity_kmp_browser.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/android/KMEA/app/src/main/res/values/strings.xml b/android/KMEA/app/src/main/res/values/strings.xml index 4519145c16..9b42538155 100644 --- a/android/KMEA/app/src/main/res/values/strings.xml +++ b/android/KMEA/app/src/main/res/values/strings.xml @@ -53,6 +53,9 @@ Scan this code to load this\nkeyboard on another device + + Keyboard Search + Would you like to download the latest version of this dictionary? The resource catalog is unavailable From e8d7615a7853fe8ed262e94e3b451fd865825154 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Tue, 7 Apr 2020 17:16:28 +0700 Subject: [PATCH 2/8] feat(android): Pass language ID to keyboard search --- .../com/tavultesoft/kmapro/MainActivity.java | 4 +- .../tavultesoft/kmea/KMPBrowserActivity.java | 56 +++---------------- .../kmea/LanguageSettingsActivity.java | 5 +- .../kmea/LanguagesSettingsActivity.java | 4 +- 4 files changed, 13 insertions(+), 56 deletions(-) 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 9d017b52b2..2ff31baffd 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 @@ -21,7 +21,6 @@ import java.util.Map; import com.tavultesoft.kmea.KMKeyboardDownloaderActivity; import com.tavultesoft.kmea.KMManager; import com.tavultesoft.kmea.KMManager.KeyboardType; -import com.tavultesoft.kmea.KMPBrowserActivity; import com.tavultesoft.kmea.KMTextView; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardDownloadEventListener; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardEventListener; @@ -660,8 +659,7 @@ public class MainActivity extends AppCompatActivity implements OnKeyboardEventLi } private void showWebBrowser() { - //Intent i = new Intent(this, WebBrowserActivity.class); - Intent i = new Intent(this, KMPBrowserActivity.class); + Intent i = new Intent(this, WebBrowserActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); startActivity(i); overridePendingTransition(android.R.anim.fade_in, R.anim.hold); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java index 334ab2eeff..9653277c33 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java @@ -25,9 +25,9 @@ import androidx.appcompat.app.AppCompatActivity; public class KMPBrowserActivity extends AppCompatActivity { private WebView webView; - private static final String fontBaseUri = "https://s.keyman.com/font/deploy/"; - private String loadedFont; - private static final String KMP_SEARCH_BASE_URL = "https://keyman.com/keyboards?embed=linux&version="; // TODO: Update to Android + private static final String KMP_SEARCH_BASE_URL = "https://keyman.com/keyboards"; + private static final String KMP_LANGUAGE = "/languages/"; + private static final String KMP_QUERY = "?embed=linux&version="; // TODO: Update to Android private boolean isLoading = false; private boolean didFinishLoading = false; @@ -50,7 +50,6 @@ public class KMPBrowserActivity extends AppCompatActivity { webView.setWebChromeClient(new WebChromeClient() { public void onProgressChanged(WebView view, int progress) { - //progressBar.setProgress(100 - progress); } }); webView.setWebViewClient(new WebViewClient() { @@ -84,13 +83,14 @@ public class KMPBrowserActivity extends AppCompatActivity { public void onPageFinished(WebView view, String url) { didFinishLoading = true; isLoading = false; - - loadFont(); } }); + // If language ID is provided, include it in the keyboard search + String languageID = getIntent().getStringExtra("languageCode"); + String languageStr = (languageID != null) ? String.format("%s%s", KMP_LANGUAGE, languageID) : ""; String appVersion = KMManager.getVersion(); - String kmpSearchUrl = String.format("%s%s", KMP_SEARCH_BASE_URL, appVersion); + String kmpSearchUrl = String.format("%s%s%s%s", KMP_SEARCH_BASE_URL, languageStr, KMP_QUERY, appVersion); webView.loadUrl(kmpSearchUrl); } @@ -140,46 +140,4 @@ public class KMPBrowserActivity extends AppCompatActivity { } } - private void loadFont() { - String font = KMManager.getKeyboardTextFontFilename(); - if (!font.isEmpty()) { - loadedFont = font; - String fontUrl = String.format("%s%s", fontBaseUri, font); - String jsStr = String.format( - "var style = document.createElement('style');" + - "style.type = 'text/css';" + - "style.innerHTML = '@font-face{font-family:\"KMCustomFont\";src:url(\"%s\");} " + - "*{font-family:\"KMCustomFont\" !important;}';" + - "document.getElementsByTagName('head')[0].appendChild(style);", fontUrl); - webView.loadUrl(String.format("javascript:%s", jsStr)); - } else { - loadedFont = "sans-serif"; - String jsStr = "var style = document.createElement('style');" + - "style.type = 'text/css';" + - "style.innerHTML = '*{font-family:\"sans-serif\" !important;}';" + - "document.getElementsByTagName('head')[0].appendChild(style);"; - webView.loadUrl(String.format("javascript:%s", jsStr)); - } - } - - private void setImageButtonEnabled(ImageButton imgButton, int resId, boolean enabled) { - imgButton.setEnabled(enabled); - Drawable originalIcon; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - originalIcon = getResources().getDrawable(resId, getTheme()); - } else { - originalIcon = getResources().getDrawable(resId); - } - Drawable icon = enabled ? originalIcon : convertDrawableToGrayScale(originalIcon); - imgButton.setImageDrawable(icon); - } - - private static Drawable convertDrawableToGrayScale(Drawable drawable) { - if (drawable == null) - return null; - - Drawable drw = drawable.mutate(); - drw.setColorFilter(Color.LTGRAY, Mode.SRC_IN); - return drw; - } } \ No newline at end of file diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageSettingsActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageSettingsActivity.java index deed4970bb..ea8bc23ac3 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageSettingsActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageSettingsActivity.java @@ -242,9 +242,10 @@ public final class LanguageSettingsActivity extends AppCompatActivity { // 1. connection to keyman.com // 2. local kmp.json files in packages/ if (KMManager.hasConnection(context) || KeyboardPickerActivity.hasKeyboardFromPackage()){ + // Pass the BCP47 language code to the KMPBrowserActivity Intent i = new Intent(context, KMPBrowserActivity.class); - i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); - i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); i.putExtra("languageCode", lgCode); i.putExtra("languageName", lgName); context.startActivity(i); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java index b143d37891..a0c561736d 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java @@ -114,8 +114,8 @@ public final class LanguagesSettingsActivity extends AppCompatActivity { // 2. local kmp.json files in packages/ if (KMManager.hasConnection(context) || KeyboardPickerActivity.hasKeyboardFromPackage()){ Intent i = new Intent(context, KMPBrowserActivity.class); - i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); - i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); context.startActivity(i); } else { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context); From 142d17a5c233dc35237bd0de8262026f39207471 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Tue, 7 Apr 2020 17:20:30 +0700 Subject: [PATCH 3/8] chore(android): Clean up unused string and spacing --- .../src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java | 2 +- android/KMEA/app/src/main/res/values/strings.xml | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java index 9653277c33..efd2aff0ff 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java @@ -140,4 +140,4 @@ public class KMPBrowserActivity extends AppCompatActivity { } } -} \ No newline at end of file +} diff --git a/android/KMEA/app/src/main/res/values/strings.xml b/android/KMEA/app/src/main/res/values/strings.xml index 9b42538155..4519145c16 100644 --- a/android/KMEA/app/src/main/res/values/strings.xml +++ b/android/KMEA/app/src/main/res/values/strings.xml @@ -53,9 +53,6 @@ Scan this code to load this\nkeyboard on another device - - Keyboard Search - Would you like to download the latest version of this dictionary? The resource catalog is unavailable From b2db224bbc9deb9fa3384343e40ab8b1f02e76b5 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Wed, 8 Apr 2020 08:35:53 +0700 Subject: [PATCH 4/8] chore(android): Address review comments --- .../tavultesoft/kmea/KMPBrowserActivity.java | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java index efd2aff0ff..ec1238d124 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java @@ -14,6 +14,7 @@ import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Build; import android.os.Bundle; +import android.util.Log; import android.view.View; import android.widget.ImageButton; import android.webkit.WebChromeClient; @@ -23,11 +24,10 @@ import android.webkit.WebViewClient; import androidx.appcompat.app.AppCompatActivity; public class KMPBrowserActivity extends AppCompatActivity { - + private static final String TAG = "KMPBrowserActivity"; private WebView webView; - private static final String KMP_SEARCH_BASE_URL = "https://keyman.com/keyboards"; - private static final String KMP_LANGUAGE = "/languages/"; - private static final String KMP_QUERY = "?embed=linux&version="; // TODO: Update to Android + private static final String KMP_SEARCH_URL_FORMATSTR = "https://keyman.com/keyboards%s?embed=linux&version=%s"; // TODO: Update to Android + private static final String KMP_LANGUAGE_FORMATSTR = "/languages/%s"; private boolean isLoading = false; private boolean didFinishLoading = false; @@ -61,13 +61,19 @@ public class KMPBrowserActivity extends AppCompatActivity { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { - if (!url.toLowerCase().equals("about:blank")) { - if (url.startsWith("keyman:download")) { - // KMAPro main activity will handle this intent - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); - startActivityForResult(intent, 1); + String lowerURL = url.toLowerCase(); + if (!lowerURL.equals("about:blank")) { + if (lowerURL.startsWith("keyman:")) { + if (lowerURL.startsWith("keyman:download")) { + // KMAPro main activity will handle this intent + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(lowerURL)); + startActivityForResult(intent, 1); + } else { + Log.d(TAG, "Scheme for " + lowerURL + " not handled"); + return true; + } } else { - view.loadUrl(url); + return false; } } return true; @@ -88,9 +94,9 @@ public class KMPBrowserActivity extends AppCompatActivity { // If language ID is provided, include it in the keyboard search String languageID = getIntent().getStringExtra("languageCode"); - String languageStr = (languageID != null) ? String.format("%s%s", KMP_LANGUAGE, languageID) : ""; + String languageStr = (languageID != null) ? String.format(KMP_LANGUAGE_FORMATSTR, languageID) : ""; String appVersion = KMManager.getVersion(); - String kmpSearchUrl = String.format("%s%s%s%s", KMP_SEARCH_BASE_URL, languageStr, KMP_QUERY, appVersion); + String kmpSearchUrl = String.format(KMP_SEARCH_URL_FORMATSTR, languageStr, appVersion); webView.loadUrl(kmpSearchUrl); } From 703e6afc72dfa20bbe9bf7bdfb4f098f39ee0e17 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Wed, 8 Apr 2020 14:24:28 +0700 Subject: [PATCH 5/8] feat(android): Add utility to check for valid keyman schemes --- .../tavultesoft/kmea/KMPBrowserActivity.java | 17 ++++++---- .../com/tavultesoft/kmea/util/FileUtils.java | 33 ++++++++++++++++++- .../tavultesoft/kmea/util/FileUtilsTest.java | 24 ++++++++++++++ 3 files changed, 66 insertions(+), 8 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java index ec1238d124..d4df31f86e 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java @@ -23,6 +23,8 @@ import android.webkit.WebView; import android.webkit.WebViewClient; import androidx.appcompat.app.AppCompatActivity; +import com.tavultesoft.kmea.util.FileUtils; + public class KMPBrowserActivity extends AppCompatActivity { private static final String TAG = "KMPBrowserActivity"; private WebView webView; @@ -63,16 +65,17 @@ public class KMPBrowserActivity extends AppCompatActivity { public boolean shouldOverrideUrlLoading(WebView view, String url) { String lowerURL = url.toLowerCase(); if (!lowerURL.equals("about:blank")) { - if (lowerURL.startsWith("keyman:")) { - if (lowerURL.startsWith("keyman:download")) { - // KMAPro main activity will handle this intent - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(lowerURL)); - startActivityForResult(intent, 1); - } else { + if (FileUtils.isKeymanLink(lowerURL)) { + // KMAPro main activity will handle this intent + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(lowerURL)); + startActivityForResult(intent, 1); + } else { + if (lowerURL.startsWith("keyman:")) { + // Warn for unsupported keyman schemes Log.d(TAG, "Scheme for " + lowerURL + " not handled"); return true; } - } else { + // Display URL return false; } } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/FileUtils.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/FileUtils.java index acfe437351..04dd353a85 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/FileUtils.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/FileUtils.java @@ -239,14 +239,45 @@ public final class FileUtils { if (u == null) { return ret; } + String lowerU = u.toLowerCase(); Pattern pattern = Pattern.compile("^http(s)?://(.+\\.)?keyman.com/.*"); - Matcher matcher = pattern.matcher(u); + Matcher matcher = pattern.matcher(lowerU); if (matcher.matches()) { ret = false; } return ret; } + /** + * Utility to parse a URL and determine if it's a keyman: + * Currently, primarily matching for "keyman://" or "keyman:download?" + * along with a path + * @param u String of the URL + * @return boolean true if URL is a supported Keyman link + */ + public static boolean isKeymanLink(String u) { + boolean ret = false; + if (u == null) { + return ret; + } + String lowerU = u.toLowerCase(); + Pattern pattern = Pattern.compile("^keyman:(\\w+)?(//|\\?)(.+)"); + Matcher matcher = pattern.matcher(lowerU); + if (matcher.matches()) { + // Check URL starts with "keyman" + if (matcher.group(1) != null) { + // For now, only handle "download" + if (matcher.group(1).equalsIgnoreCase("download")) { + ret = true; + } + } else if (matcher.group(3) != null) { + // contains keyboard portion + ret = true; + } + } + return ret; + } + /** * Utility to compare two version strings * @param v1 String diff --git a/android/KMEA/app/src/test/java/com/tavultesoft/kmea/util/FileUtilsTest.java b/android/KMEA/app/src/test/java/com/tavultesoft/kmea/util/FileUtilsTest.java index f652b3d2cb..84d14b53e1 100644 --- a/android/KMEA/app/src/test/java/com/tavultesoft/kmea/util/FileUtilsTest.java +++ b/android/KMEA/app/src/test/java/com/tavultesoft/kmea/util/FileUtilsTest.java @@ -53,6 +53,30 @@ public class FileUtilsTest { Assert.assertTrue(FileUtils.isCustomKeyboard("https://amerikeyman.com/")); } + @Test + public void test_isKeymanLink() { + Assert.assertFalse(FileUtils.isKeymanLink(null)); + Assert.assertFalse(FileUtils.isKeymanLink("")); + + // Valid Keyman links + Assert.assertTrue(FileUtils.isKeymanLink("keyman://keyboard")); + Assert.assertTrue(FileUtils.isKeymanLink("Keyman://keyboard")); + Assert.assertTrue(FileUtils.isKeymanLink("keyman:download?keyboard")); + Assert.assertTrue(FileUtils.isKeymanLink("Keyman:Download?keyboard")); + Assert.assertTrue(FileUtils.isKeymanLink("keyman:download//keyboard")); + Assert.assertTrue(FileUtils.isKeymanLink("keyman://download/keyboard")); + + // links missing query + Assert.assertFalse(FileUtils.isKeymanLink("keyman://")); + Assert.assertFalse(FileUtils.isKeymanLink("keyman:download?")); + + // Other methods not supported + Assert.assertFalse(FileUtils.isKeymanLink("keyman:method//keyboard")); + Assert.assertFalse(FileUtils.isKeymanLink("keyman:method?keyboard")); + + Assert.assertFalse(FileUtils.isKeymanLink("example:keyman//")); + } + @Test public void test_compareVersions() { Assert.assertEquals(FileUtils.VERSION_INVALID, FileUtils.compareVersions(null, "1.0")); From 32f272ebc708e40669f3ac292bd7024a2babf8bf Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Thu, 9 Apr 2020 11:03:50 +0700 Subject: [PATCH 6/8] fix(android): Address more review comments * Remove handling deprecated keyman:// protocol * Pass original URL * lower-case BCP 47 tag when checking for match * Clean up logic nesting in KMPBrowserActivity --- .../com/tavultesoft/kmapro/MainActivity.java | 20 +++++++----- .../tavultesoft/kmea/KMPBrowserActivity.java | 31 ++++++++++--------- .../tavultesoft/kmea/packages/JSONUtils.java | 2 +- .../com/tavultesoft/kmea/util/FileUtils.java | 30 +++++++++--------- .../kmea/packages/JSONUtilsTest.java | 1 + .../kmea/packages/PackageProcessorTest.java | 2 +- .../tavultesoft/kmea/util/FileUtilsTest.java | 15 ++++----- 7 files changed, 55 insertions(+), 46 deletions(-) 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 2ff31baffd..00f03f5af9 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 @@ -331,14 +331,18 @@ public class MainActivity extends AppCompatActivity implements OnKeyboardEventLi downloadKMP(scheme); break; case "keyman" : - // Convert opaque URI to hierarchical URI so the query parameters can be parsed - Builder builder = new Uri.Builder(); - builder.scheme("https") - .authority("keyman.com") - .appendPath("keyboards") - .encodedQuery(data.getEncodedQuery()); - data = Uri.parse(builder.build().toString()); - downloadKMP(scheme); + if (FileUtils.isKeymanLink(data.toString())) { + // Convert opaque URI to hierarchical URI so the query parameters can be parsed + Builder builder = new Uri.Builder(); + builder.scheme("https") + .authority("keyman.com") + .appendPath("keyboards") + .encodedQuery(data.getEncodedQuery()); + data = Uri.parse(builder.build().toString()); + downloadKMP(scheme); + } else { + Log.e(TAG, "Unrecognized scheme: " + scheme); + } break; default : Log.e(TAG, "Unrecognized scheme: " + scheme); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java index d4df31f86e..4c5c2f92ed 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java @@ -64,22 +64,23 @@ public class KMPBrowserActivity extends AppCompatActivity { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { String lowerURL = url.toLowerCase(); - if (!lowerURL.equals("about:blank")) { - if (FileUtils.isKeymanLink(lowerURL)) { - // KMAPro main activity will handle this intent - Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(lowerURL)); - startActivityForResult(intent, 1); - } else { - if (lowerURL.startsWith("keyman:")) { - // Warn for unsupported keyman schemes - Log.d(TAG, "Scheme for " + lowerURL + " not handled"); - return true; - } - // Display URL - return false; - } + if (lowerURL.equals("about:blank")) { + return true; // never load a blank page, e.g. when the component initializes } - return true; + if (FileUtils.isKeymanLink(lowerURL)) { + // KMAPro main activity will handle this intent + // Pass original url because path and query are case-sensitive + Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); + startActivityForResult(intent, 1); + } + if (lowerURL.startsWith("keyman:")) { + // Warn for unsupported keyman schemes + Log.d(TAG, "Scheme for " + url + " not handled"); + return true; + } + + // Display URL + return false; } @Override diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/packages/JSONUtils.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/packages/JSONUtils.java index 414f7b36db..a233269d4c 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/packages/JSONUtils.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/packages/JSONUtils.java @@ -139,7 +139,7 @@ public class JSONUtils { try { for (int i=0; i < a.length(); i++) { JSONObject o = a.getJSONObject(i); - if (o.getString("id").equals(id)) { + if (o.getString("id").toLowerCase().equals(id.toLowerCase())) { return i; } } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/FileUtils.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/FileUtils.java index 04dd353a85..69c31be4ea 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/FileUtils.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/util/FileUtils.java @@ -249,9 +249,9 @@ public final class FileUtils { } /** - * Utility to parse a URL and determine if it's a keyman: - * Currently, primarily matching for "keyman://" or "keyman:download?" - * along with a path + * Utility to parse a URL and determine if it's a valid keyman: + * Currently, only "keyman" scheme with "download" path and query is supported. + * Legacy keyman:// protocol is deprecated and not supported. * @param u String of the URL * @return boolean true if URL is a supported Keyman link */ @@ -261,18 +261,20 @@ public final class FileUtils { return ret; } String lowerU = u.toLowerCase(); - Pattern pattern = Pattern.compile("^keyman:(\\w+)?(//|\\?)(.+)"); + Pattern pattern = Pattern.compile("^keyman:(\\w+)\\?(.+)"); Matcher matcher = pattern.matcher(lowerU); - if (matcher.matches()) { - // Check URL starts with "keyman" - if (matcher.group(1) != null) { - // For now, only handle "download" - if (matcher.group(1).equalsIgnoreCase("download")) { - ret = true; - } - } else if (matcher.group(3) != null) { - // contains keyboard portion - ret = true; + // Check URL starts with "keyman" + if (matcher.matches() && (matcher.group(1) != null)) { + // For now, only handle "download" + switch (matcher.group(1).toLowerCase()) { + case "download": + if (matcher.group(2) != null) { + // Contains query + ret = true; + } + break; + default: + ret = false; } } return ret; diff --git a/android/KMEA/app/src/test/java/com/tavultesoft/kmea/packages/JSONUtilsTest.java b/android/KMEA/app/src/test/java/com/tavultesoft/kmea/packages/JSONUtilsTest.java index 0918911670..4a425d9ba1 100644 --- a/android/KMEA/app/src/test/java/com/tavultesoft/kmea/packages/JSONUtilsTest.java +++ b/android/KMEA/app/src/test/java/com/tavultesoft/kmea/packages/JSONUtilsTest.java @@ -69,6 +69,7 @@ public class JSONUtilsTest { // Verify first and last language for sil_cameroon_qwerty Assert.assertEquals(0, JSONUtils.findID(languagesArray, "aal-Latn")); + Assert.assertEquals(0, JSONUtils.findID(languagesArray, "aal-latn")); Assert.assertEquals(EXPECTED_NUM_LANGUAGES-1, JSONUtils.findID(languagesArray, "zuy-Latn")); } catch (JSONException e) { diff --git a/android/KMEA/app/src/test/java/com/tavultesoft/kmea/packages/PackageProcessorTest.java b/android/KMEA/app/src/test/java/com/tavultesoft/kmea/packages/PackageProcessorTest.java index cec2c9819e..cb5f97590e 100644 --- a/android/KMEA/app/src/test/java/com/tavultesoft/kmea/packages/PackageProcessorTest.java +++ b/android/KMEA/app/src/test/java/com/tavultesoft/kmea/packages/PackageProcessorTest.java @@ -157,7 +157,7 @@ public class PackageProcessorTest { Assert.assertEquals(amharic, keyboards[0]); Assert.assertEquals(TEST_GFF_KBD_COUNT, keyboards.length); - languageID = "gez"; + languageID = "GEZ"; keyboards = PP.processEntry(json.getJSONArray("keyboards").getJSONObject(0), "gff_amh_7_test_json", pkgVersion, languageID); HashMap geez = new HashMap(); diff --git a/android/KMEA/app/src/test/java/com/tavultesoft/kmea/util/FileUtilsTest.java b/android/KMEA/app/src/test/java/com/tavultesoft/kmea/util/FileUtilsTest.java index 84d14b53e1..b1f4663a00 100644 --- a/android/KMEA/app/src/test/java/com/tavultesoft/kmea/util/FileUtilsTest.java +++ b/android/KMEA/app/src/test/java/com/tavultesoft/kmea/util/FileUtilsTest.java @@ -59,22 +59,23 @@ public class FileUtilsTest { Assert.assertFalse(FileUtils.isKeymanLink("")); // Valid Keyman links - Assert.assertTrue(FileUtils.isKeymanLink("keyman://keyboard")); - Assert.assertTrue(FileUtils.isKeymanLink("Keyman://keyboard")); Assert.assertTrue(FileUtils.isKeymanLink("keyman:download?keyboard")); Assert.assertTrue(FileUtils.isKeymanLink("Keyman:Download?keyboard")); - Assert.assertTrue(FileUtils.isKeymanLink("keyman:download//keyboard")); - Assert.assertTrue(FileUtils.isKeymanLink("keyman://download/keyboard")); - // links missing query - Assert.assertFalse(FileUtils.isKeymanLink("keyman://")); + // keyman:// invalid + Assert.assertFalse(FileUtils.isKeymanLink("keyman://keyboard")); + Assert.assertFalse(FileUtils.isKeymanLink("Keyman://keyboard")); + Assert.assertFalse(FileUtils.isKeymanLink("keyman:download//keyboard")); + Assert.assertFalse(FileUtils.isKeymanLink("keyman://download/keyboard")); + + // link missing query Assert.assertFalse(FileUtils.isKeymanLink("keyman:download?")); // Other methods not supported Assert.assertFalse(FileUtils.isKeymanLink("keyman:method//keyboard")); Assert.assertFalse(FileUtils.isKeymanLink("keyman:method?keyboard")); - Assert.assertFalse(FileUtils.isKeymanLink("example:keyman//")); + Assert.assertFalse(FileUtils.isKeymanLink("example:keyman?")); } @Test From 406428a1d5a783fcafefd9d20b64c52553548575 Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Tue, 14 Apr 2020 13:50:58 +0700 Subject: [PATCH 7/8] feat(android): Also download dictionary after installing keyboard package --- .../com/tavultesoft/kmapro/MainActivity.java | 18 ++++++++++++++++++ .../tavultesoft/kmea/KMPBrowserActivity.java | 3 +++ .../kmea/KeyboardPickerActivity.java | 6 ++++-- .../kmea/LanguagesSettingsActivity.java | 13 +++++++++---- 4 files changed, 34 insertions(+), 6 deletions(-) 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 00f03f5af9..b7b34c7f64 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 @@ -24,6 +24,7 @@ import com.tavultesoft.kmea.KMManager.KeyboardType; import com.tavultesoft.kmea.KMTextView; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardDownloadEventListener; import com.tavultesoft.kmea.KeyboardEventHandler.OnKeyboardEventListener; +import com.tavultesoft.kmea.cloud.CloudApiTypes; import com.tavultesoft.kmea.packages.PackageProcessor; import com.tavultesoft.kmea.util.FileUtils; import com.tavultesoft.kmea.util.FileProviderUtils; @@ -90,6 +91,8 @@ import android.widget.Toast; import io.sentry.android.core.SentryAndroid; import io.sentry.core.Sentry; +import static com.tavultesoft.kmea.KMKeyboardDownloaderActivity.kKeymanApiModelURL; + public class MainActivity extends AppCompatActivity implements OnKeyboardEventListener, OnKeyboardDownloadEventListener, ActivityCompat.OnRequestPermissionsResultCallback { public static Context context; @@ -145,6 +148,21 @@ public class MainActivity extends AppCompatActivity implements OnKeyboardEventLi Intent packageIntent = new Intent(getApplicationContext(), PackageActivity.class); packageIntent.putExtras(bundle); startActivity(packageIntent); + + // Determine if associated lexical model should be downloaded + if (FileUtils.hasKeymanPackageExtension(kmpFilename) && !FileUtils.hasLexicalModelPackageExtension(kmpFilename) + && (languageID != null) && !languageID.isEmpty()) { + String keyboardID = kmpFilename.substring(0, kmpFilename.lastIndexOf(FileUtils.KEYMANPACKAGE)); + ArrayList cloudQueries = new ArrayList<>(); + String _remoteLexicalModelUrl = String.format("%s?q=bcp47:%s", kKeymanApiModelURL, languageID); + cloudQueries.add(new CloudApiTypes.CloudApiParam( + CloudApiTypes.ApiTarget.KeyboardLexicalModels, _remoteLexicalModelUrl) + .setType(CloudApiTypes.JSONType.Array)); + // Keyboard package already downloaded, so this will just download associated lexical model. + // Can't call downloadLexicalModel() because it needs to already know the model ID + KMKeyboardDownloaderActivity.downloadKeyboard(context, languageID, keyboardID, cloudQueries); + } + break; } super.onReceiveResult(resultCode, resultData); diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java index 4c5c2f92ed..53095176db 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/KMPBrowserActivity.java @@ -72,6 +72,9 @@ public class KMPBrowserActivity extends AppCompatActivity { // Pass original url because path and query are case-sensitive Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url)); startActivityForResult(intent, 1); + + // Finish activity + finish(); } if (lowerURL.startsWith("keyman:")) { // Warn for unsupported keyman schemes 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 f67de35c24..e7ee0086b3 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 @@ -273,10 +273,12 @@ public final class KeyboardPickerActivity extends AppCompatActivity { return pos; } + // Previously, keyboard from package = custom keyboard + // Now, keyboard from package has a package ID != "cloud" protected static boolean hasKeyboardFromPackage() { for(HashMap kbInfo: keyboardsList) { - String customKeyboard = MapCompat.getOrDefault(kbInfo, KMManager.KMKey_CustomKeyboard, "N"); - if (customKeyboard.equalsIgnoreCase("Y")) { + String pkgID = MapCompat.getOrDefault(kbInfo, KMManager.KMKey_PackageID, KMManager.KMDefault_UndefinedPackageID); + if (!pkgID.equalsIgnoreCase(KMManager.KMDefault_UndefinedPackageID)) { return true; } } diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java index a0c561736d..4a34558115 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguagesSettingsActivity.java @@ -109,14 +109,19 @@ public final class LanguagesSettingsActivity extends AppCompatActivity { addButton = (ImageButton) findViewById(R.id.add_button); addButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { - // Check that available keyboard information can be obtained via: - // 1. connection to keyman.com catalog - // 2. local kmp.json files in packages/ - if (KMManager.hasConnection(context) || KeyboardPickerActivity.hasKeyboardFromPackage()){ + // Check scenarios to add available keyboards: + if (KMManager.hasConnection(context)) { + // Scenario 1: Connection to keyman.com catalog Intent i = new Intent(context, KMPBrowserActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP); context.startActivity(i); + } else if (KeyboardPickerActivity.hasKeyboardFromPackage()) { + // Scenario 2: Local kmp.json files in packages/ + dismissOnSelect = false; + Intent i = new Intent(context, LanguageListActivity.class); + i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + context.startActivity(i); } else { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context); dialogBuilder.setTitle(getString(R.string.title_add_keyboard)); From 1b2cecfa3fbbaafa855969172bbf38bf644b2a5b Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Tue, 14 Apr 2020 14:49:07 +0700 Subject: [PATCH 8/8] feat(android): Update language settings activity for offline --- .../tavultesoft/kmea/LanguageSettingsActivity.java | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageSettingsActivity.java b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageSettingsActivity.java index ea8bc23ac3..e8bcc56df7 100644 --- a/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageSettingsActivity.java +++ b/android/KMEA/app/src/main/java/com/tavultesoft/kmea/LanguageSettingsActivity.java @@ -238,10 +238,9 @@ public final class LanguageSettingsActivity extends AppCompatActivity { addButton = (ImageButton) findViewById(R.id.add_button); addButton.setOnClickListener(new View.OnClickListener() { public void onClick(View v) { - // Check that available keyboard information can be obtained via: - // 1. connection to keyman.com - // 2. local kmp.json files in packages/ - if (KMManager.hasConnection(context) || KeyboardPickerActivity.hasKeyboardFromPackage()){ + // Check scenarios to add available keyboards: + if (KMManager.hasConnection(context)){ + // Scenario 1: Connection to keyman.com catalog // Pass the BCP47 language code to the KMPBrowserActivity Intent i = new Intent(context, KMPBrowserActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); @@ -249,6 +248,13 @@ public final class LanguageSettingsActivity extends AppCompatActivity { i.putExtra("languageCode", lgCode); i.putExtra("languageName", lgName); context.startActivity(i); + } else if (KeyboardPickerActivity.hasKeyboardFromPackage()) { + // Scenario 2: Local kmp.json files in packages/ + Intent i = new Intent(context, KeyboardListActivity.class); + i.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + i.putExtra("languageCode", lgCode); + i.putExtra("languageName", lgName); + context.startActivity(i); } else { AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context); dialogBuilder.setTitle(getString(R.string.title_add_keyboard));