mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-25 17:17:43 +00:00
Merge pull request #2961 from keymanapp/feat/android/kmp-browser-activity
feat(android): Add KMPBrowserActivity for cloud keyboard searches
This commit is contained in:
commit
ada66eba95
12 changed files with 292 additions and 26 deletions
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue