diff --git a/HISTORY.md b/HISTORY.md index f230e1f8b05..f77fed4b78c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -95,6 +95,19 @@ * chore(common): move to 18.0 alpha (#10713) * chore: move to 18.0 alpha +## 17.0.314 beta 2024-04-25 + +* fix(android/engine): URIEncode strings passed to Javascript (#11206) +* fix(android/app): Update storage permissions for Android 12.0+ (#11299) +* test(developer): keyboard info compiler messages unit tests 2 (#11253) + +## 17.0.313 beta 2024-04-23 + +* chore(common): Set fetch-latest-cldr.sh executable (#11289) +* chore(common): Fix missing entries in HISTORY.md (#11290) +* fix(developer): report missing help to sentry instead of local xml (#11271) +* fix(developer): "use strict" for downlevel browsers in Server (#11276) + ## 17.0.312 beta 2024-04-22 * fix(developer): emit JSON strings as characters, not surrogate pairs (#11243) diff --git a/android/KMAPro/kMAPro/src/main/AndroidManifest.xml b/android/KMAPro/kMAPro/src/main/AndroidManifest.xml index d4028e8a70e..7196abdb229 100644 --- a/android/KMAPro/kMAPro/src/main/AndroidManifest.xml +++ b/android/KMAPro/kMAPro/src/main/AndroidManifest.xml @@ -7,6 +7,10 @@ + + + + diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/CheckPermissions.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/CheckPermissions.java index bf2506e1067..888d430b0b7 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/CheckPermissions.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/CheckPermissions.java @@ -26,13 +26,19 @@ public static boolean isPermissionOK(Activity activity) { return permissionsOK; } - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.R) { - permissionsOK = Environment.isExternalStorageManager(); - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.TIRAMISU) { - // TODO: Workout scoped storage permission #10659 - } - } else { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + // < API 30 permissionsOK = checkPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); + } else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + // API 30-32 + permissionsOK = Environment.isExternalStorageManager() || + checkPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE); + } else { + // API 33+ + //https://developer.android.com/about/versions/13/behavior-changes-13#granular-media-permissions + // Manifest.permission.READ_MEDIA_AUDIO doesn't seem to be needed + permissionsOK = permissionsOK && checkPermission(activity, Manifest.permission.READ_MEDIA_IMAGES); + permissionsOK = permissionsOK && checkPermission(activity, Manifest.permission.READ_MEDIA_VIDEO); } return permissionsOK; diff --git a/android/KMEA/app/src/main/assets/android-host.js b/android/KMEA/app/src/main/assets/android-host.js index 7491b56ea7a..0c9af9a7846 100644 --- a/android/KMEA/app/src/main/assets/android-host.js +++ b/android/KMEA/app/src/main/assets/android-host.js @@ -176,7 +176,8 @@ function setKeymanLanguage(k) { } function setSpacebarText(mode) { - keyman.config.spacebarText = mode; + var text = (mode == undefined) || !mode.text ? '' : mode.text; + keyman.config.spacebarText = text; } // #6665: we need to know when the user has pressed a hardware key so we don't @@ -234,10 +235,8 @@ function setNumericLayer() { } } -function updateKMText(text) { - if(text == undefined) { - text = ''; - } +function updateKMText(k) { + var text = (k == undefined) || !k.text ? '' : k.text; console_debug('updateKMText(text=' + text + ') with: \n' + build_context_string(keyman.context)); 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 78c31f32757..5e54e73f4a7 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 @@ -29,6 +29,7 @@ import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.res.Configuration; +import android.net.Uri; import android.os.Handler; import android.util.DisplayMetrics; import android.util.Log; @@ -149,13 +150,19 @@ protected void setShouldIgnoreSelectionChange(boolean ignore) { protected boolean updateText(String text) { boolean result = false; + JSONObject reg = new JSONObject(); String kmText = ""; if (text != null) { - kmText = text.toString().replace("\\", "\\u005C").replace("'", "\\u0027").replace("\n", "\\n"); + // Use JSON to handle passing string to Javascript + try { + reg.put("text", text.toString()); + } catch (JSONException e) { + KMLog.LogException(TAG, "", e); + } } if (KMManager.isKeyboardLoaded(this.keyboardType) && !shouldIgnoreTextChange) { - this.loadJavascript(KMString.format("updateKMText('%s')", kmText)); + this.loadJavascript(KMString.format("updateKMText(%s)", reg.toString())); result = true; } @@ -229,6 +236,7 @@ public void initKMKeyboard(final Context context) { getSettings().setCacheMode(WebSettings.LOAD_NO_CACHE); getSettings().setSupportZoom(false); + getSettings().setTextZoom(100); getSettings().setUseWideViewPort(true); getSettings().setLoadWithOverviewMode(true); @@ -350,7 +358,8 @@ public void run() { allCalls.append(";"); } - loadUrl("javascript:" + allCalls.toString()); + // Ensure strings safe for Javascript. TODO: font strings + loadUrl("javascript:" + Uri.encode(allCalls.toString())); if(javascriptAfterLoad.size() > 0 && keyboardSet) { callJavascriptAfterLoad(); @@ -1098,8 +1107,17 @@ public void reloadAfterError() { } public void setSpacebarText(KMManager.SpacebarText mode) { - String jsString = KMString.format("setSpacebarText('%s')", mode.toString()); - loadJavascript(jsString); + JSONObject reg = new JSONObject(); + if (mode != null) { + // Use JSON to handle passing string to Javascript + try { + reg.put("text", mode.toString()); + } catch (JSONException e) { + KMLog.LogException(TAG, "", e); + } + } + + this.loadJavascript(KMString.format("setSpacebarText(%s)", reg.toString())); } /* Implement handleTouchEvent to catch long press gesture without using Android system default time diff --git a/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kmp b/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kmp index 30a4f87663b..f0380aabe53 100644 Binary files a/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kmp and b/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kmp differ diff --git a/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kps b/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kps index 28e1436f705..157aefd71cd 100644 --- a/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kps +++ b/android/Tests/KeyboardHarness/app/src/main/assets/keyboardharness.kps @@ -20,13 +20,13 @@ - ..\..\..\..\..\..\..\web\testing\chirality\chirality.js + ..\..\..\..\..\..\..\web\src\test\manual\web\chirality\chirality.js File chirality.js 0 .js - ..\..\..\..\..\..\..\web\testing\platform\platformtest.js + ..\..\..\..\..\..\..\web\src\test\manual\web\platform\platformtest.js File platformtest.js 0 .js @@ -62,7 +62,7 @@ - longpress + longpress '"\|5% + longpress 1.0 code2001.ttf diff --git a/android/Tests/KeyboardHarness/app/src/main/assets/longpress.js b/android/Tests/KeyboardHarness/app/src/main/assets/longpress.js index 1f146f8f923..641c3f73350 100644 --- a/android/Tests/KeyboardHarness/app/src/main/assets/longpress.js +++ b/android/Tests/KeyboardHarness/app/src/main/assets/longpress.js @@ -2,7 +2,7 @@ KeymanWeb.KR(new Keyboard_longpress()); function Keyboard_longpress() { this.KI = "Keyboard_longpress"; - this.KN = "longpress"; + this.KN = "longpress '\"\\|5% +"; this.KV = null; this.KH = ''; this.KM = 0; diff --git a/android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java b/android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java index 0768601ad3e..7d9f57ff061 100644 --- a/android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java +++ b/android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java @@ -54,7 +54,7 @@ protected void onCreate(Bundle savedInstanceState) { Keyboard longpressKBbInfo = new Keyboard( "keyboardharness", "longpress", - "Longpress Keyboard", + "longpress '\"\\|5% +", "en", "English", "1.0", diff --git a/android/help/android_images/keyman-storage-permission-34b.png b/android/help/android_images/keyman-storage-permission-34b.png new file mode 100644 index 00000000000..c124a9f31ad Binary files /dev/null and b/android/help/android_images/keyman-storage-permission-34b.png differ diff --git a/android/help/android_images/keyman-storage-permission-ap.png b/android/help/android_images/keyman-storage-permission-ap.png index ff2762f6431..4ed8fa3b68a 100644 Binary files a/android/help/android_images/keyman-storage-permission-ap.png and b/android/help/android_images/keyman-storage-permission-ap.png differ diff --git a/android/help/troubleshooting/grant-storage-permission.md b/android/help/troubleshooting/grant-storage-permission.md index a6f04d754af..1b8554d7f0c 100644 --- a/android/help/troubleshooting/grant-storage-permission.md +++ b/android/help/troubleshooting/grant-storage-permission.md @@ -2,15 +2,30 @@ title: How To - Granting Storage Permission --- -## Granting storage permission +## Granting Storage Permission If Keyman for Android is permanently denied storage access, attempts to install custom packages will fail with the notification "Storage permission request was denied". Perform these steps to grant Keyman for Android access to storage +### Android 11.0 to 12L Devices + Step 1) Go to Android Settings. Step 2) Depending on your device, click "Apps", "Apps & notifications", or "App permissions" and grant Keyman -storage permission. The screenshot below is from Android 9.0 Pie. +permission to access storage / "file and media". The screenshot below is from Android 12.0 S. + +![](../android_images/keyman-storage-permission-ap.png) + +### Android 13.0+ Devices + +Step 1) +Go to Android Settings. + +Step 2) +Depending on your device, click "Apps" and grant Keyman permission to the following: + * Photos and videos --> Always allow all + +The screenshot below is from Android 14.0. -![](../android_images/keyman-storage-permission-ap.png) \ No newline at end of file +![](../android_images/keyman-storage-permission-34b.png) diff --git a/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler-messages.ts b/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler-messages.ts index 610aa82133a..8a9c2652ba0 100644 --- a/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler-messages.ts +++ b/developer/src/kmc-keyboard-info/test/test-keyboard-info-compiler-messages.ts @@ -13,6 +13,18 @@ beforeEach(function() { callbacks.clear(); }); +const KHMER_ANGKOR_JS = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.js'); +const KHMER_ANGKOR_KPS = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); +const KHMER_ANGKOR_KMP = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.kmp'); + +const KHMER_ANGKOR_SOURCES = { + kmpFilename: KHMER_ANGKOR_KMP, + sourcePath: 'release/k/khmer_angkor', + kpsFilename: KHMER_ANGKOR_KPS, + jsFilename: KHMER_ANGKOR_JS, + forPublishing: true, +}; + describe('KeyboardInfoCompilerMessages', function () { it('should have a valid KeyboardInfoCompilerMessages object', function() { return verifyCompilerMessagesObject(KeyboardInfoCompilerMessages, CompilerErrorNamespace.KeyboardInfoCompiler); @@ -22,22 +34,17 @@ describe('KeyboardInfoCompilerMessages', function () { it('should generate ERROR_FileDoesNotExist error if .js file does not exist', async function() { const jsFilename = makePathToFixture('khmer_angkor', 'build', 'xxx.js'); - const kpsFilename = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); - const kmpFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.kmp'); const sources = { - kmpFilename, - sourcePath: 'release/k/khmer_angkor', - kpsFilename, - jsFilename: jsFilename, - forPublishing: true, + ...KHMER_ANGKOR_SOURCES, + jsFilename, }; const compiler = new KeyboardInfoCompiler(); assert.isTrue(await compiler.init(callbacks, {sources})); let result: KeyboardInfoCompilerResult = null; try { - result = await compiler.run(kmpFilename, null); + result = await compiler.run(KHMER_ANGKOR_KMP, null); } catch(e) { callbacks.printMessages(); throw e; @@ -61,7 +68,7 @@ describe('KeyboardInfoCompilerMessages', function () { kmpFilename, sourcePath: 'release/k/no-kmp', kpsFilename, - jsFilename: jsFilename, + jsFilename, forPublishing: true, }; @@ -87,27 +94,15 @@ describe('KeyboardInfoCompilerMessages', function () { // ERROR_FileDoesNotExist (font file not in package) it('should generate ERROR_FileDoesNotExist error if font file is missing from package', async function() { - const jsFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.js'); - const kpsFilename = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); - const kmpFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.kmp'); - - const sources = { - kmpFilename, - sourcePath: 'release/k/khmer_angkor', - kpsFilename, - jsFilename: jsFilename, - forPublishing: true, - }; - + const sources = KHMER_ANGKOR_SOURCES; const compiler = new KeyboardInfoCompiler(); assert.isTrue(await compiler.init(callbacks, {sources})); const kmpJsonData: KmpJsonFile.KmpJsonFile = {system: {fileVersion: "7.0", keymanDeveloperVersion: "17.0.204"}, options: {}, files: []} const source = ["Mondulkiri-R.ttf"] - const result = await compiler['fontSourceToKeyboardInfoFont'](kpsFilename, kmpJsonData, source) + const result = await compiler['fontSourceToKeyboardInfoFont'](KHMER_ANGKOR_KPS, kmpJsonData, source) assert.isNull(result); - assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.ERROR_FileDoesNotExist), `ERROR_FileDoesNotExist not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_FileDoesNotExist).includes(source[0]), @@ -117,27 +112,15 @@ describe('KeyboardInfoCompilerMessages', function () { // ERROR_FileDoesNotExist (font file not on disk) it('should generate ERROR_FileDoesNotExist error if font file is missing from disk', async function() { - const jsFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.js'); - const kpsFilename = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); - const kmpFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.kmp'); - - const sources = { - kmpFilename, - sourcePath: 'release/k/khmer_angkor', - kpsFilename, - jsFilename: jsFilename, - forPublishing: true, - }; - + const sources = KHMER_ANGKOR_SOURCES; const compiler = new KeyboardInfoCompiler(); assert.isTrue(await compiler.init(callbacks, {sources})); const kmpJsonData: KmpJsonFile.KmpJsonFile = {system: {fileVersion: "7.0", keymanDeveloperVersion: "17.0.204"}, options: {}, files: [{name: "../shared/fonts/khmer/mondulkiri/xxx.ttf", description: "Font not on disk"}]} const source = ["xxx.ttf"] - const result = await compiler['fontSourceToKeyboardInfoFont'](kpsFilename, kmpJsonData, source) + const result = await compiler['fontSourceToKeyboardInfoFont'](KHMER_ANGKOR_KPS, kmpJsonData, source) assert.isNull(result); - assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.ERROR_FileDoesNotExist), `ERROR_FileDoesNotExist not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_FileDoesNotExist).includes(kmpJsonData.files[0].name), @@ -147,24 +130,12 @@ describe('KeyboardInfoCompilerMessages', function () { // ERROR_LicenseFileIsMissing it('should generate ERROR_LicenseFileIsMissing error if license file is missing from disk', async function() { - const jsFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.js'); - const kpsFilename = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); - const kmpFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.kmp'); - - const sources = { - kmpFilename, - sourcePath: 'release/k/khmer_angkor', - kpsFilename, - jsFilename: jsFilename, - forPublishing: true, - }; - + const sources = KHMER_ANGKOR_SOURCES; const compiler = new KeyboardInfoCompiler(); assert.isTrue(await compiler.init(callbacks, {sources})); const licenseFilename = makePathToFixture('khmer_angkor', 'xxx.md'); const result = compiler['isLicenseMIT'](licenseFilename) assert.isFalse(result); - assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.ERROR_LicenseFileIsMissing), `ERROR_LicenseFileIsMissing not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_LicenseFileIsMissing).includes(licenseFilename), @@ -174,18 +145,7 @@ describe('KeyboardInfoCompilerMessages', function () { // ERROR_LicenseFileIsDamaged (error on decode) it('should generate ERROR_LicenseFileIsDamaged error if license file throws error on decode', async function() { - const jsFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.js'); - const kpsFilename = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); - const kmpFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.kmp'); - - const sources = { - kmpFilename, - sourcePath: 'release/k/khmer_angkor', - kpsFilename, - jsFilename: jsFilename, - forPublishing: true, - }; - + const sources = KHMER_ANGKOR_SOURCES; const compiler = new KeyboardInfoCompiler(); assert.isTrue(await compiler.init(callbacks, {sources})); const licenseFilename = makePathToFixture('khmer_angkor', 'LICENSE.md'); @@ -194,7 +154,6 @@ describe('KeyboardInfoCompilerMessages', function () { const result = compiler['isLicenseMIT'](licenseFilename) TextDecoder.prototype.decode = originalDecode assert.isFalse(result); - assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.ERROR_LicenseFileIsDamaged), `ERROR_LicenseFileIsDamaged not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_LicenseFileIsDamaged).includes(licenseFilename), @@ -204,27 +163,15 @@ describe('KeyboardInfoCompilerMessages', function () { // ERROR_LicenseFileIsDamaged (null on decode) it('should generate ERROR_LicenseFileIsDamaged error if license file returns null on decode', async function() { - const jsFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.js'); - const kpsFilename = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); - const kmpFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.kmp'); - - const sources = { - kmpFilename, - sourcePath: 'release/k/khmer_angkor', - kpsFilename, - jsFilename: jsFilename, - forPublishing: true, - }; - + const sources = KHMER_ANGKOR_SOURCES; const compiler = new KeyboardInfoCompiler(); assert.isTrue(await compiler.init(callbacks, {sources})); const licenseFilename = makePathToFixture('khmer_angkor', 'LICENSE.md'); const originalDecode = TextDecoder.prototype.decode - TextDecoder.prototype.decode = () => { return null } + TextDecoder.prototype.decode = () => null; const result = compiler['isLicenseMIT'](licenseFilename) TextDecoder.prototype.decode = originalDecode assert.isFalse(result); - assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.ERROR_LicenseFileIsDamaged), `ERROR_LicenseFileIsDamaged not generated, instead got: `+JSON.stringify(callbacks.messages,null,2)); assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_LicenseFileIsDamaged).includes(licenseFilename), @@ -234,16 +181,9 @@ describe('KeyboardInfoCompilerMessages', function () { // ERROR_LicenseIsNotValid it('should generate ERROR_LicenseIsNotValid error if license file is invalid', async function() { - const jsFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.js'); - const kpsFilename = makePathToFixture('khmer_angkor', 'source', 'khmer_angkor.kps'); - const kmpFilename = makePathToFixture('khmer_angkor', 'build', 'khmer_angkor.kmp'); - const sources = { - kmpFilename, + ...KHMER_ANGKOR_SOURCES, sourcePath: 'release/k/invalid-license', - kpsFilename, - jsFilename: jsFilename, - forPublishing: true, }; const compiler = new KeyboardInfoCompiler(); @@ -329,7 +269,7 @@ describe('KeyboardInfoCompilerMessages', function () { kmpFilename, sourcePath: 'release/k/font-meta-data-is-invalid', kpsFilename, - jsFilename: jsFilename, + jsFilename, forPublishing: true, }; diff --git a/web/src/engine/osk/src/views/oskView.ts b/web/src/engine/osk/src/views/oskView.ts index 6a53d8f1ebe..88ab2dd3923 100644 --- a/web/src/engine/osk/src/views/oskView.ts +++ b/web/src/engine/osk/src/views/oskView.ts @@ -266,6 +266,7 @@ export default abstract class OSKView this.setBaseTouchEventListeners(); } + this._Box.style.display = 'none'; this.initialized = true; }