Merge pull request #2961 from keymanapp/feat/android/kmp-browser-activity

feat(android): Add KMPBrowserActivity for cloud keyboard searches
This commit is contained in:
Darcy Wong 2020-04-15 05:37:28 +07:00 • committed by GitHub
commit ada66eba95
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 292 additions and 26 deletions

View file

@ -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<CloudApiTypes.CloudApiParam> 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);
@ -331,14 +349,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);

View file

@ -12,6 +12,11 @@
<!-- Have application handle initializing Sentry -->
<meta-data android:name="io.sentry.auto-init" android:value="false" />
<activity
android:name=".KMPBrowserActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"
android:label="@string/app_name">
</activity>
<activity
android:name="com.tavultesoft.kmea.KeyboardPickerActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|screenSize|smallestScreenSize"

View file

@ -0,0 +1,156 @@
/**
* Copyright (C) 2020 SIL International. All rights reserved.
*/
package com.tavultesoft.kmea;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.PorterDuff.Mode;
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;
import android.webkit.WebSettings;
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;
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;
@SuppressLint({"SetJavaScriptEnabled", "InflateParams"})
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = this;
setContentView(R.layout.activity_kmp_browser);
webView = (WebView) findViewById(R.id.kmpBrowserWebView);
webView.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setUseWideViewPort(true);
webView.getSettings().setLoadWithOverviewMode(true);
webView.getSettings().setBuiltInZoomControls(true);
webView.getSettings().setSupportZoom(true);
webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);
webView.setWebChromeClient(new WebChromeClient() {
public void onProgressChanged(WebView view, int progress) {
}
});
webView.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
didFinishLoading = true;
isLoading = false;
}
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
String lowerURL = url.toLowerCase();
if (lowerURL.equals("about:blank")) {
return true; // never load a blank page, e.g. when the component initializes
}
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);
// Finish activity
finish();
}
if (lowerURL.startsWith("keyman:")) {
// Warn for unsupported keyman schemes
Log.d(TAG, "Scheme for " + url + " not handled");
return true;
}
// Display URL
return false;
}
@Override
public void onPageStarted(WebView view, String url, Bitmap favicon) {
isLoading = true;
didFinishLoading = false;
}
@Override
public void onPageFinished(WebView view, String url) {
didFinishLoading = true;
isLoading = false;
}
});
// If language ID is provided, include it in the keyboard search
String languageID = getIntent().getStringExtra("languageCode");
String languageStr = (languageID != null) ? String.format(KMP_LANGUAGE_FORMATSTR, languageID) : "";
String appVersion = KMManager.getVersion();
String kmpSearchUrl = String.format(KMP_SEARCH_URL_FORMATSTR, languageStr, appVersion);
webView.loadUrl(kmpSearchUrl);
}
@Override
protected void onResume() {
super.onResume();
if (webView != null) {
webView.reload();
}
}
@Override
protected void onPause() {
super.onPause();
}
@Override
protected void onDestroy() {
super.onDestroy();
}
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (webView != null) {
if (resultCode == RESULT_OK && data != null) {
String url = data.getStringExtra("url");
if (url != null)
webView.loadUrl(url);
}
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
}
@Override
public void onBackPressed() {
if (webView != null && webView.canGoBack()) {
webView.goBack();
} else {
super.onBackPressed();
finish();
}
}
}

View file

@ -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<String, String> 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;
}
}

View file

@ -238,13 +238,18 @@ 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 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!
// 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);
i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
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);

View file

@ -109,12 +109,15 @@ 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 cloud catalog
// 2. cached file
// 3. local kmp.json files in packages/
if (KMManager.hasConnection(context) || CloudDataJsonUtil.getKeyboardCacheFile(context).exists() ||
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);

View file

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

View file

@ -239,14 +239,47 @@ 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 valid keyman:<method>
* 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
*/
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);
// 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;
}
/**
* Utility to compare two version strings
* @param v1 String

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".KMPBrowserActivity"
android:orientation="vertical">
<WebView
android:id="@+id/kmpBrowserWebView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>

View file

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

View file

@ -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<String, String> geez = new HashMap<String, String>();

View file

@ -53,6 +53,31 @@ 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:download?keyboard"));
Assert.assertTrue(FileUtils.isKeymanLink("Keyman:Download?keyboard"));
// 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?"));
}
@Test
public void test_compareVersions() {
Assert.assertEquals(FileUtils.VERSION_INVALID, FileUtils.compareVersions(null, "1.0"));