Merge branch 'master' into chore/developer/scatteredCode/cleanup_VKeys
Some checks are pending
Keyman Build Summary / Summarize build status checks (push) Waiting to run

This commit is contained in:
SabineSIL 2026-07-02 09:37:43 +02:00 committed by GitHub
commit 9ca40faac7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2290 changed files with 75763 additions and 37001 deletions

1
.gitattributes vendored
View file

@ -7,3 +7,4 @@
# EOF
# ```
Dockerfile text eol=lf
*.sh text eol=lf

View file

@ -25,7 +25,7 @@ runs:
using: 'composite'
steps:
- name: Download Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: keyman-srcpkg
path: artifacts/keyman-srcpkg
@ -49,7 +49,7 @@ runs:
echo '```' >> $GITHUB_STEP_SUMMARY
- name: Store binary packages
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: keyman-binarypkgs-${{ inputs.dist }}_${{ inputs.arch }}
path: |

195
.github/labeler.yml vendored
View file

@ -5,86 +5,173 @@
# common ones. The others are commented out. There is still some variance between
# folder names and labels; consider this documentation of that ;-)
docs: docs/**
docs:
- changed-files:
- any-glob-to-any-file: 'docs/**'
#
# Add labels based on changed files using actions/labeler
#
android/:
- android/**
- resources/teamcity/includes/**
- resources/teamcity/android/**
android/app/: android/KMAPro/**
android/engine/: android/KMEA/**
android/samples/: android/Samples/**
- changed-files:
- any-glob-to-any-file:
- android/**
- resources/teamcity/includes/**
- resources/teamcity/android/**
android/app/:
- changed-files:
- any-glob-to-any-file:
- android/KMAPro/**
android/engine/:
- changed-files:
- any-glob-to-any-file:
- android/KMEA/**
android/samples/:
- changed-files:
- any-glob-to-any-file:
- android/Samples/**
common/:
- common/**
- resources/teamcity/includes/**
- resources/teamcity/common/**
common/web/: common/web/**
- changed-files:
- any-glob-to-any-file:
- common/**
- resources/teamcity/includes/**
- resources/teamcity/common/**
common/web/:
- changed-files:
- any-glob-to-any-file:
- common/web/**
core/:
- core/**
- resources/teamcity/includes/**
- resources/teamcity/core/**
- changed-files:
- any-glob-to-any-file:
- core/**
- resources/teamcity/includes/**
- resources/teamcity/core/**
developer/:
- developer/**
- resources/teamcity/includes/**
- resources/teamcity/developer/**
- changed-files:
- any-glob-to-any-file:
- developer/**
- resources/teamcity/includes/**
- resources/teamcity/developer/**
developer/compilers/:
- developer/src/kmc/**
- developer/src/kmcmplib/**
- developer/src/kmc-*/**
- changed-files:
- any-glob-to-any-file:
- developer/src/kmc/**
- developer/src/kmcmplib/**
- developer/src/kmc-*/**
developer/ide/:
- developer/src/server/**
- developer/src/tike/**
- changed-files:
- any-glob-to-any-file:
- developer/src/server/**
- developer/src/tike/**
ios/:
- ios/**
- resources/teamcity/includes/**
- resources/teamcity/ios/**
ios/app/: ios/keyman/**
ios/engine/: ios/engine/**
ios/samples/: ios/samples/**
- changed-files:
- any-glob-to-any-file:
- ios/**
- resources/teamcity/includes/**
- resources/teamcity/ios/**
ios/app/:
- changed-files:
- any-glob-to-any-file:
- ios/keyman/**
ios/engine/:
- changed-files:
- any-glob-to-any-file:
- ios/engine/**
ios/samples/:
- changed-files:
- any-glob-to-any-file:
- ios/samples/**
linux/:
- linux/**
- resources/teamcity/includes/**
- resources/teamcity/linux/**
linux/config/: linux/keyman-config/**
linux/engine/: linux/ibus-keyman/**
- changed-files:
- any-glob-to-any-file:
- linux/**
- resources/teamcity/includes/**
- resources/teamcity/linux/**
linux/config/:
- changed-files:
- any-glob-to-any-file:
- linux/keyman-config/**
linux/engine/:
- changed-files:
- any-glob-to-any-file:
- linux/ibus-keyman/**
mac/:
- mac/**
- resources/teamcity/includes/**
- resources/teamcity/mac/**
- changed-files:
- any-glob-to-any-file:
- mac/**
- resources/teamcity/includes/**
- resources/teamcity/mac/**
# mac/config/:
# mac/engine/: mac/**
oem/: oem/**
oem/fv/: oem/firstvoices/**
oem/fv/android/: oem/firstvoices/android/**
oem/fv/ios/: oem/firstvoices/ios/**
oem/fv/windows/: oem/firstvoices/windows/**
oem/:
- changed-files:
- any-glob-to-any-file:
- oem/**
oem/fv/:
- changed-files:
- any-glob-to-any-file:
- oem/firstvoices/**
oem/fv/android/:
- changed-files:
- any-glob-to-any-file:
- oem/firstvoices/android/**
oem/fv/ios/:
- changed-files:
- any-glob-to-any-file:
- oem/firstvoices/ios/**
oem/fv/windows/:
- changed-files:
- any-glob-to-any-file:
- oem/firstvoices/windows/**
resources/: resources/**
resources/:
- changed-files:
- any-glob-to-any-file:
- resources/**
web/:
- web/**
- resources/teamcity/includes/**
- resources/teamcity/web/**
- changed-files:
- any-glob-to-any-file:
- web/**
- resources/teamcity/includes/**
- resources/teamcity/web/**
# web/bookmarklet/
web/engine/: web/source/**
web/ui/: web/source/kmwui*
web/samples/: web/samples/**
web/predictive-text/: web/src/engine/predictive-text/**
web/engine/:
- changed-files:
- any-glob-to-any-file:
- web/source/**
web/ui/:
- changed-files:
- any-glob-to-any-file:
- web/source/kmwui*
web/samples/:
- changed-files:
- any-glob-to-any-file:
- web/samples/**
web/predictive-text/:
- changed-files:
- any-glob-to-any-file:
- web/src/engine/predictive-text/**
windows/:
- windows/**
- resources/teamcity/includes/**
- resources/teamcity/windows/**
windows/config/: windows/src/desktop/**
windows/engine/: windows/src/engine/**
- changed-files:
- any-glob-to-any-file:
- windows/**
- resources/teamcity/includes/**
- resources/teamcity/windows/**
windows/config/:
- changed-files:
- any-glob-to-any-file:
- windows/src/desktop/**
windows/engine/:
- changed-files:
- any-glob-to-any-file:
- windows/src/engine/**

144
.github/workflows/README.md vendored Normal file
View file

@ -0,0 +1,144 @@
# api-verification.yml: checks Core API for incompatible changes
This action automatically verifies that the API of Keyman Core did not
change in incompatible ways without incrementing the API version number.
It applies to all platforms that use Core, even though we only run it on
Linux.
- The action gets triggered by the completion of the Ubuntu packaging
workflow (`deb-packaging.yml`).
- The API version is defined in `core/CORE_API_VERSION.md`.
- The baseline file for comparing the API is
`linux/debian/libkeymancore${API_VERSION}.symbols`. The action will
re-create this file and and makes it available as an artifact.
- The API checks are implemented in `linux/scripts/verify_api.inc.sh`.
More information about the API verification can be found in
`docs/core-api-verification.md`.
# auto-merge-keyman-server-pr.yml
TODO
# build-test-publish-docker.yml
TODO
# cleanup-ghcr.yml
TODO
# close-linked-issues-for-merged-prs.yml
TODO
# core-arm64-windows-test.yml: Test Keyman Core on Windows/ARM64
This action allows us to test Keyman Core for Windows on ARM64 architecture with
a GitHub agent, as we do not have any TeamCity Windows build agents with ARM64
arch.
It will normally be triggered as part of a test build from the
resources/teamcity/triggers/trigger-test-builds.sh script.
You can manually trigger a core-arm64-windows-test action on GitHub on your fork
of keymanapp/keyman. This is normally only needed to test changes to the action
itself:
- push the changes to your fork of keymanapp/keyman: `git push myfork HEAD:master`
- make sure you have GHA enabled in the settings of your fork
- seet `GITHUB_TOKEN=<your_gh_token>`
- trigger a build with:
```bash
curl --write-out '\n' --request POST \
--header "Accept: application/vnd.github+json" \
--header "Authorization: token $GITHUB_TOKEN" \
--data "{ \"event_type\": \"core-arm64-windows-test: master\", \
\"client_payload\": { \
\"buildSha\": \"$(git rev-parse refs/heads/master)\", \
\"branch\": \"master\", \
\"user\": \"${USER}\", \
\"isTestBuild\": \"true\", \
\"force\": \"true\"}" \
https://api.github.com/repos/<yourgithubname>/keyman/dispatches
```
To trigger a build for PR #1234 in a branch `my-pr-branch` this would look similar
to this:
```bash
curl --write-out '\n' --request POST \
--header "Accept: application/vnd.github+json" \
--header "Authorization: token $GITHUB_TOKEN" \
--data "{ \"event_type\": \"core-arm64-windows-test: PR #1234\", \
\"client_payload\": { \
\"buildSha\": \"$(git rev-parse refs/heads/my-pr-branch)\", \
\"branch\": \"my-pr-branch\", \
\"user\": \"${USER}\", \
\"isTestBuild\": \"true\", \
\"force\": \"true\"}" \
https://api.github.com/repos/<yourgithubname>/keyman/dispatches
```
....
# crowdin.yml: Upload translation sources to [Crowdin](translate.keyman.com)
This action automatically updates the English source strings on Crowdin
https://crowdin.com/project/keyman/sources/files
based on the root [crowdin.yml](../../crowdin.yml) configuration file. The daily cron job currently runs at 6:00AM.
This should match the behavior of locally using the [Crowdin CLI to upload sources](https://crowdin.github.io/crowdin-cli/commands/crowdin-upload-sources).
# deb-packaging.yml: Ubuntu Packaging GitHub Action
You can manually trigger a deb-packaging action on GitHub on your fork
of keymanapp/keyman. This is normally only needed to test changes to the action
itself:
- push the changes to your fork of keymanapp/keyman: `git push myfork HEAD:master`
- make sure you have GHA enabled in the settings of your fork
- seet `GITHUB_TOKEN=<your_gh_token>`
- trigger a build with:
```bash
curl --write-out '\n' --request POST \
--header "Accept: application/vnd.github+json" \
--header "Authorization: token $GITHUB_TOKEN" \
--data "{ \"event_type\": \"deb-pr-packaging: master\", \
\"client_payload\": { \
\"buildSha\": \"$(git rev-parse refs/heads/master)\", \
\"branch\": \"master\", \
\"baseBranch\": \"master\", \
\"baseRef\": \"$(git rev-parse refs/heads/master^)\", \
\"user\": \"${USER}\", \
\"isTestBuild\": \"true\", \
\"force\": \"true\"}" \
https://api.github.com/repos/<yourgithubname>/keyman/dispatches
```
To trigger a build for PR #1234 in a branch `pr-1234` this would look similar
to this:
```bash
curl --write-out '\n' --request POST \
--header "Accept: application/vnd.github+json" \
--header "Authorization: token $GITHUB_TOKEN" \
--data "{ \"event_type\": \"deb-pr-packaging: PR #1234\", \
\"client_payload\": { \
\"buildSha\": \"$(git rev-parse refs/heads/pr-1234)\", \
\"branch\": \"pr-1234\", \
\"baseBranch\": \"master\", \
\"baseRef\": \"$(git rev-parse refs/heads/master)\", \
\"user\": \"${USER}\", \
\"isTestBuild\": \"true\", \
\"force\": \"true\"}" \
https://api.github.com/repos/<yourgithubname>/keyman/dispatches
```
# labeler.yml
TODO
# npm-publish.yml
TODO
# pr-build-status.yml
TODO

View file

@ -23,10 +23,11 @@ jobs:
GIT_BRANCH: ${{ steps.environment_step.outputs.GIT_BRANCH }}
GIT_BASE_BRANCH: ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}
GIT_USER: ${{ steps.environment_step.outputs.GIT_USER }}
SKIP_API_CHECK: ${{ steps.environment_step.outputs.SKIP_API_CHECK }}
steps:
- name: Restore artifacts
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
artifacts
@ -40,7 +41,7 @@ jobs:
- name: Set pending status on PR builds
id: set_status
if: steps.environment_step.outputs.IS_TEST_BUILD == 'true'
if: steps.environment_step.outputs.IS_TEST_BUILD == 'true' && steps.environment_step.outputs.SKIP_API_CHECK != 'true'
shell: bash
run: |
gh api \
@ -53,7 +54,8 @@ jobs:
-f context="$STATUS_CONTEXT"
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
if: steps.environment_step.outputs.SKIP_API_CHECK != 'true'
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ steps.environment_step.outputs.GIT_SHA }}'
fetch-depth: 0
@ -61,13 +63,15 @@ jobs:
path: ${{ github.workspace }}/keyman/
- name: Install devscripts
if: steps.environment_step.outputs.SKIP_API_CHECK != 'true'
uses: ./keyman/.github/actions/apt-install
with:
packages: devscripts equivs
- name: "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }})"
if: steps.environment_step.outputs.SKIP_API_CHECK != 'true'
run: |
echo "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }}):" >> $GITHUB_STEP_SUMMARY
echo "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }}):" | tee -a ${GITHUB_STEP_SUMMARY}
BIN_PACKAGE=$(ls "${GITHUB_WORKSPACE}/artifacts/" | grep "${PKG_NAME}[0-9]*_${{ steps.environment_step.outputs.KEYMAN_VERSION }}-1${{ steps.environment_step.outputs.PRERELEASE_TAG }}+$(lsb_release -c -s)1_amd64.deb")
cd ${{ github.workspace }}/keyman/linux
@ -76,20 +80,20 @@ jobs:
--bin-pkg "${GITHUB_WORKSPACE}/artifacts/${BIN_PACKAGE}" \
--git-sha "${{ steps.environment_step.outputs.GIT_SHA }}" \
--git-base "${{ steps.environment_step.outputs.GIT_BASE }}" \
verify 2>> $GITHUB_STEP_SUMMARY
verify 2> >(tee -a ${GITHUB_STEP_SUMMARY} >&2)
- name: Archive .symbols file
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: steps.environment_step.outputs.SKIP_API_CHECK != 'true' && always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: libkeymancore.symbols
path: ${{ github.workspace }}/keyman/linux/debian/tmp/DEBIAN/symbols
if: always()
set_status:
name: Set result status on PR builds
needs: [api_verification]
runs-on: ubuntu-latest
if: ${{ always() && needs.api_verification.outputs.IS_TEST_BUILD == 'true' }}
if: ${{ always() && needs.api_verification.outputs.IS_TEST_BUILD == 'true' && needs.api_verification.outputs.SKIP_API_CHECK != 'true' }}
steps:
- name: Set success
if: needs.api_verification.result == 'success'

View file

@ -1,10 +1,9 @@
#
# Automatically merges pull requests opened by keyman-server
# The initial use of this action is to increment the patch
# version of Keyman with resources/build/increment-version.sh.
# That script creates a PR and then this workflow will be
# triggered to approve the PR (from github-actions account), and
# then automerge it.
# Automatically merges pull requests opened by keyman-server The initial use of
# this action is to increment the patch version of Keyman with
# resources/teamcity/triggers/trigger-release-builds.sh. That script creates a PR and
# then this workflow will be triggered to approve the PR (from github-actions
# account), and then automerge it.
#
name: Auto Merge PRs from keyman-server
on:
@ -14,15 +13,17 @@ on:
jobs:
build:
runs-on: ubuntu-latest
if: ${{ github.repository == 'keymanapp/keyman' && github.actor == 'keyman-server' && startsWith(github.event.pull_request.title, 'auto:') }}
if: ${{ github.repository == 'keymanapp/keyman' && github.actor == 'keyman-server' && startsWith(github.head_ref, 'auto/') }}
steps:
- name: auto approve PR from keyman-server
uses: hmarr/auto-approve-action@f0939ea97e9205ef24d872e76833fa908a770363
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: auto merge PR from keyman-server
uses: "pascalgn/automerge-action@7854d3bd607dccdaf0b2c134b699a812c8960213"
shell: bash
env:
GITHUB_TOKEN: "${{ secrets.AUTOINC_GITHUB_TOKEN }}"
MERGE_LABELS: "automerge"
MERGE_FORKS: false
GH_TOKEN: "${{ github.token }}"
run: |
gh pr review -R keymanapp/keyman --approve ${{ github.event.pull_request.number }}
- name: mark PR for auto-merge from keyman-server
shell: bash
env:
GH_TOKEN: "${{ secrets.AUTOINC_GITHUB_TOKEN }}"
run: |
gh pr merge -R keymanapp/keyman --merge --auto ${{ github.event.pull_request.number }}

View file

@ -0,0 +1,79 @@
# This workflow runs the build, test and publish steps for the
# Keyman build Docker images.
# It is triggered by a schedule and repository_dispatch event.
name: "Docker images: Build, publish and test "
run-name: "Docker images: Build, publish and test (branch ${{ github.ref_name }} on ${{ github.event_name }}/${{ github.event.action }} by ${{ github.actor }})"
defaults:
run:
shell: bash
on:
schedule:
#run every Fri, 3 pm UTC
- cron: '0 15 * * 5'
repository_dispatch:
types: ['build-test-publish-docker:*']
env:
REGISTRY: ghcr.io
USERNAME: "${{ github.actor }}"
PASSWORD: "${{ secrets.KEYMAN_SERVER_REPO_ACCESS_PAT }}"
jobs:
message:
name: Print message
runs-on: ubuntu-latest
if: ${{ github.event.act }}
steps:
- name: Print message
id: print-message
run: |
echo "Docker images: Build, publish and test (branch ${{ github.ref_name }} on ${{ github.event_name }}/${{ github.event.action }} by ${{ github.actor }})"
build:
name: Build Docker images
runs-on: ubuntu-latest
steps:
- name: Checkout repository
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Build Docker images
id: build
run: |
if [ -z "${{ env.USERNAME }}" ]; then
resources/docker-images/build.sh build: --registry ${{ env.REGISTRY }}
else
echo ${{ env.PASSWORD }} | resources/docker-images/build.sh build: --registry ${{ env.REGISTRY }} --username ${{ env.USERNAME }}
fi
- name: Publish Docker images
id: publish
run: |
if [ -z "${{ env.USERNAME }}" ]; then
resources/docker-images/build.sh publish: --registry ${{ env.REGISTRY }}
else
echo ${{ env.PASSWORD }} | resources/docker-images/build.sh publish: --registry ${{ env.REGISTRY }} --username ${{ env.USERNAME }}
fi
test:
name: Test Docker images
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout repository
id: checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Test Docker images
id: test
run: |
if [ -z "${{ env.USERNAME }}" ]; then
resources/docker-images/build.sh test: --registry ${{ env.REGISTRY }}
else
echo ${{ env.PASSWORD }} | resources/docker-images/build.sh test: --registry ${{ env.REGISTRY }} --username ${{ env.USERNAME }}
fi

45
.github/workflows/cleanup-ghcr.yml vendored Normal file
View file

@ -0,0 +1,45 @@
# This workflow removes all untagged Docker images from the
# GitHub Container Registry.
# It is triggered by a schedule event or a manual workflow dispatch event.
name: "Docker images: Cleanup GitHub Container Registry"
run-name: "Docker images: Cleanup GitHub Container Registry (branch ${{ github.ref_name }} on ${{ github.event_name }} by ${{ github.actor }})"
defaults:
run:
shell: bash
on:
schedule:
#run every Fri, 6 pm UTC
- cron: '0 18 * * 5' #
workflow_dispatch:
jobs:
message:
name: Print message
runs-on: ubuntu-latest
if: ${{ github.event.act }}
steps:
- name: Print message
id: print-message
run: |
echo "Docker images: Cleanup GitHub Container Registry (branch ${{ github.ref_name }} on ${{ github.event_name }} by ${{ github.actor }})"
cleanup-images:
name: Cleanup GitHub Container Registry images
runs-on: ubuntu-latest
steps:
- name: Removing all untagged images from GitHub Container Registry
id: cleanup-ghcr
uses: dataaxiom/ghcr-cleanup-action@cd0cdb900b5dbf3a6f2cc869f0dbb0b8211f50c4 # v1.0.16
with:
dry-run: false
expand-packages: true
packages: keyman-*-ci
delete-untagged: true
token: ${{ secrets.KEYMAN_SERVER_REPO_ACCESS_PAT }}

View file

@ -0,0 +1,167 @@
#
# This workflow tests Keyman Core on ARM64/Windows; we cannot use our TeamCity
# agents because we have no ARM64 Windows agents.
#
# It is triggered by a repository_dispatch event with the type
# `core-arm64-windows-test:*`.
#
name: "Keyman Core - ARM64/Windows test"
run-name: "Keyman Core - ARM64/Windows test - ${{ github.event.client_payload.branch }} by @${{ github.event.client_payload.user }}"
on:
repository_dispatch:
types: ['core-arm64-windows-test:*']
# Inputs:
#
# buildSha: The SHA of the commit to build, e.g. of the branch or
# refs/pull/1234/head for PR
# isTestBuild: true for PR and test builds, false for release builds
# force: true to run this action on a fork of keymanapp/keyman
#
# Descriptive inputs:
#
# branch: Name of the branch to build, generally, `master`, `beta`,
# `stable-x.y`, or for a PR `#1234`. Used only in the action
# title
# user: The user that triggered the build or created the PR. Used
# only in the action title
env:
GH_TOKEN: ${{ github.token }}
STATUS_CONTEXT: 'Keyman Core - ARM64 test'
_builder_timings: false
jobs:
core-arm64-windows-test:
name: Keyman Core ARM64/Windows test
if: github.repository == 'keymanapp/keyman' || github.event.client_payload.force
runs-on: windows-11-arm
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
- name: Set pending status on PR builds
id: set_status
if: github.event.client_payload.isTestBuild == 'true'
shell: bash
run: |
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.buildSha }} \
-f state='pending' \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
-f description='Keyman Core - ARM64 test started' \
-f context="$STATUS_CONTEXT"
- name: Report initial npm/node versions
run: |
echo "Initial npm/node versions"
npm -v
node -v
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: '3.13'
- uses: actions/setup-node@v6
with:
# This needs to be kept in sync with minimum-versions.sh
node-version: '20.19'
# As of Dec 2025, ngrok not yet supported on ARM Windows,
# google-closure-compiler (and others?) does not support arm64 yet, so
# use the x64 version of node. This is needed to avoid `npm ci` failure,
# even though we don't use ngrok directly; google-closure-compiler is
# used by emscripten
architecture: x64
- name: Report on current environment
shell: bash
run: |
echo "-- Environment --"
echo "pythonLocation=$pythonLocation"
echo "-- Current npm/node versions --"
npm -v
node -v
- name: Setup environment
shell: bash
run: |
# As of Dec 2025, Emscripten does not support Win11 ARM natively
echo "EMSDK_ARCH=x86_64" >> $GITHUB_ENV
echo "EMSDK_KEEP_DOWNLOADS=1" >> $GITHUB_ENV
echo "KEYMAN_USE_EMSDK=1" >> $GITHUB_ENV
echo "PATH=$PATH:$pythonLocation/Scripts" >> $GITHUB_ENV
echo "KEYMAN_CI_SKIP_NVM=1" >> $GITHUB_ENV
- name: Install emscripten
shell: bash
run: |
source resources/build/minimum-versions.inc.sh
mkdir ../emsdk
pushd ../emsdk
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install "${KEYMAN_MIN_VERSION_EMSCRIPTEN}"
./emsdk activate "${KEYMAN_MIN_VERSION_EMSCRIPTEN}"
cd upstream/emscripten
npm install
echo "EMSCRIPTEN_BASE=$(pwd)" >> $GITHUB_ENV
popd
- name: Install meson
shell: bash
run: |
pip3 install --user meson==1.11.0
- name: Run test
shell: bash
run: |
# Run the test
./core/build.sh configure,build,test:arm64
set_status:
name: Set result status on PR builds
needs: [core-arm64-windows-test]
runs-on: windows-11-arm
if: ${{ always() && github.event.client_payload.isTestBuild == 'true' }}
steps:
- name: Set success
if: needs.core-arm64-windows-test.result == 'success'
shell: bash
run: |
echo "RESULT=success" >> $GITHUB_ENV
echo "MSG=Keyman Core ARM64 test succeeded" >> $GITHUB_ENV
- name: Set cancelled
if: needs.core-arm64-windows-test.result == 'cancelled'
shell: bash
run: |
echo "RESULT=error" >> $GITHUB_ENV
echo "MSG=Keyman Core ARM64 test cancelled" >> $GITHUB_ENV
- name: Set failure
if: needs.core-arm64-windows-test.result == 'failure'
shell: bash
run: |
echo "RESULT=failure" >> $GITHUB_ENV
echo "MSG=Keyman Core ARM64 test failed" >> $GITHUB_ENV
- name: Set final status
shell: bash
run: |
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.buildSha }} \
-f state="$RESULT" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
-f description="$MSG" \
-f context="$STATUS_CONTEXT"

View file

@ -13,10 +13,10 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: crowdin action
uses: crowdin/github-action@2.7.0
uses: crowdin/github-action@v2.7.0
with:
upload_sources: true

View file

@ -1,41 +0,0 @@
# Ubuntu Packaging GitHub Action
You can manually trigger a deb-packaging action on GitHub, e.g. to test changes:
- push the changes to your fork of keymanapp/keyman: git push myfork HEAD:master
- make sure you have GHA enabled in the settings of your fork and created an
access token on GitHub
- trigger a build with:
```bash
curl --write-out '\n' --request POST \
--header "Accept: application/vnd.github+json" \
--header "Authorization: token $GITHUB_TOKEN" \
--data "{ \"event_type\": \"deb-pr-packaging: master\", \
\"client_payload\": { \
\"buildSha\": \"$(git rev-parse refs/heads/master)\", \
\"branch\": \"master\", \
\"baseBranch\": \"master\",
\"baseRef\": \"$(git rev-parse refs/heads/master^)\",
\"user\": \"${USER}\",
\"isTestBuild\": \"true\"}" \
https://api.github.com/repos/<yourgithubname>/keyman/dispatches
```
To trigger a build for PR #1234 in a branch `pr-1234` this would look similar
to this:
```bash
curl --write-out '\n' --request POST \
--header "Accept: application/vnd.github+json" \
--header "Authorization: token $GITHUB_TOKEN" \
--data "{ \"event_type\": \"deb-pr-packaging: PR #1234\", \
\"client_payload\": { \
\"buildSha\": \"$(git rev-parse refs/heads/pr-1234)\", \
\"branch\": \"pr-1234\", \
\"baseBranch\": \"master\",
\"baseRef\": \"$(git rev-parse refs/heads/master)\",
\"user\": \"${USER}\",
\"isTestBuild\": \"true\"}" \
https://api.github.com/repos/<yourgithubname>/keyman/dispatches
```

View file

@ -23,25 +23,28 @@ on:
# baseRef: The ref of the previous commit. For a PR the same as `baseBranch`.
# user: The user that triggered the build or created the PR
# isTestBuild: false for Releases, otherwise true
# skipApiCheck: true to skip the API check, otherwise false
env:
COLOR_GREEN: "\e[32m"
GH_TOKEN: ${{ github.token }}
STATUS_CONTEXT: 'Ubuntu Packaging'
STATUS_CONTEXT_SOURCE_VERIFICATION: 'Linux source verification'
DEBFULLNAME: 'Keyman GHA packager'
DEBEMAIL: 'support@keyman.com'
_builder_timings: false
jobs:
sourcepackage:
name: Build source package
if: github.repository == 'keymanapp/keyman'
if: github.repository == 'keymanapp/keyman' || github.event.client_payload.force
runs-on: ubuntu-24.04
outputs:
KEYMAN_VERSION: ${{ steps.version_step.outputs.KEYMAN_VERSION }}
PRERELEASE_TAG: ${{ steps.prerelease_tag.outputs.PRERELEASE_TAG }}
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
@ -86,7 +89,14 @@ jobs:
shell: bash
run: |
THIS_SCRIPT="$GITHUB_WORKSPACE/.github/workflows/deb-packaging.yml"
. "${THIS_SCRIPT%/*}/../../resources/build/build-utils.sh"
if [[ -f "${THIS_SCRIPT%/*}/../../resources/build/builder-basic.inc.sh" ]]; then
. "${THIS_SCRIPT%/*}/../../resources/build/builder-basic.inc.sh"
else
# TODO: remove following block post v19 release (file was renamed in v19 alpha)
if [[ -f "${THIS_SCRIPT%/*}/../../resources/build/build-utils.sh" ]]; then
. "${THIS_SCRIPT%/*}/../../resources/build/build-utils.sh"
fi
fi
echo "KEYMAN_VERSION=${KEYMAN_VERSION:-${VERSION}}" >> $GITHUB_OUTPUT
- name: Set prerelease tag as output parameter
@ -112,7 +122,7 @@ jobs:
echo "- $(find . -name keyman_\*.dsc)" >> $GITHUB_STEP_SUMMARY
- name: Store source package
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: keyman-srcpkg
path: |
@ -127,11 +137,11 @@ jobs:
strategy:
fail-fast: true
matrix:
dist: [jammy, noble, plucky]
dist: [jammy, noble, questing]
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
sparse-checkout: '.github/actions/'
@ -153,11 +163,11 @@ jobs:
strategy:
fail-fast: true
matrix:
dist: [questing]
dist: [devel]
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
sparse-checkout: '.github/actions/'
@ -177,28 +187,53 @@ jobs:
runs-on: ubuntu-latest
steps:
# - name: Download Artifacts
# uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
# with:
# path: artifacts
# merge-multiple: true
# - name: Install dependencies
# run: |
# sudo DEBIAN_FRONTEND=noninteractive apt-get -q -y install autopkgtest qemu-system qemu-utils autodep8 genisoimage python3-distro-info
# - name: Build test image
# run: |
# cd "${GITHUB_WORKSPACE}/artifacts"
# autopkgtest-buildvm-ubuntu-cloud -v --release=jammy
# - name: Run tests
# run: |
# cd "${GITHUB_WORKSPACE}/artifacts"
# autopkgtest -B *.deb keyman_*.dsc -- qemu autopkgtest-jammy-amd64.img
- name: Ignore
- name: Free disk space
run: |
echo "Ignored for now - until working solution is in place (#13777)"
# Free disk space by removing some large directories that we don't use (#15171)
# see https://dev.to/mathio/squeezing-disk-space-from-github-actions-runners-an-engineers-guide-3pjg
# Remove Android SDKs (frees 12 GB on Ubuntu 24.04)
sudo rm -rf /usr/local/lib/android
# Remove Haskell (GHC) (frees 6.4 GB)
sudo rm -rf /usr/local/.ghcup
# Remove CodeQL and other toolcaches (frees 5.8 GB)
sudo rm -rf /opt/hostedtoolcache
# Remove .NET SDKs (frees 4.0 GB)
sudo rm -rf /usr/share/dotnet
- name: Download Source Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: keyman-srcpkg
path: artifacts
- name: Download Binary Artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: artifacts
pattern: keyman-binarypkgs-*
merge-multiple: true
- name: Install dependencies
run: |
sudo apt update
sudo DEBIAN_FRONTEND=noninteractive apt-get -q -y install autopkgtest qemu-system qemu-utils autodep8 genisoimage python3-distro-info
- name: Build test image
run: |
cd "${GITHUB_WORKSPACE}/artifacts"
sudo chown ${USER} /dev/kvm
autopkgtest-buildvm-ubuntu-cloud -v --release=$(lsb_release -c -s)
- name: Run tests
run: |
# Tests are defined in linux/debian/tests/test-build
cd "${GITHUB_WORKSPACE}/artifacts"
DIST="$(lsb_release -c -s)"
autopkgtest -B *${DIST}*.deb keyman_*.dsc -- qemu autopkgtest-${DIST}-amd64.img
deb_signing:
name: Sign source and binary packages
@ -247,7 +282,7 @@ jobs:
echo "::endgroup::"
- name: Download Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: keyman-signedpkgs
@ -309,7 +344,7 @@ jobs:
steps:
- name: Download Artifacts
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
path: artifacts
pattern: keyman-*
@ -325,9 +360,10 @@ jobs:
echo "GIT_BRANCH=${{ github.event.client_payload.branch }}" >> artifacts/env
echo "GIT_BASE_BRANCH=${{ github.event.client_payload.baseBranch }}" >> artifacts/env
echo "GIT_USER=${{ github.event.client_payload.user }}" >> artifacts/env
echo "SKIP_API_CHECK=${{ github.event.client_payload.skipApiCheck }}" >> artifacts/env
- name: Cache artifacts
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3
uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
with:
path: |
artifacts
@ -368,3 +404,112 @@ jobs:
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
-f description="$MSG" \
-f context="$STATUS_CONTEXT"
#---------------------------------------------------------------------------------------------
source_verification:
name: Verify source tarball and Linux source package
if: github.repository == 'keymanapp/keyman' || github.event.client_payload.force
needs: sourcepackage
runs-on: ubuntu-latest
outputs:
KEYMAN_VERSION: ${{ needs.sourcepackage.outputs.KEYMAN_VERSION }}
HAS_VERIFY_SOURCE: ${{ steps.check_requirements.outputs.HAS_VERIFY_SOURCE }}
steps:
- name: Set pending status on PR builds
id: set_status
if: github.event.client_payload.isTestBuild == 'true'
shell: bash
run: |
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
/repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.buildSha }} \
-f state='pending' \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
-f description='Linux source verification started' \
-f context="$STATUS_CONTEXT_SOURCE_VERIFICATION"
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
fetch-depth: 0
clean: false
path: ${{ github.workspace }}/keyman/
# TODO: Remove after release of Keyman 19
- name: Check requirements
id: check_requirements
shell: bash
run: |
if [[ -f "${GITHUB_WORKSPACE}/keyman/linux/scripts/verify_source.sh" ]]; then
echo "HAS_VERIFY_SOURCE=true" >> $GITHUB_OUTPUT
fi
- name: Install devscripts
if: steps.check_requirements.outputs.HAS_VERIFY_SOURCE == 'true'
uses: ./keyman/.github/actions/apt-install
with:
packages: devscripts equivs
- name: Install dependencies
if: steps.check_requirements.outputs.HAS_VERIFY_SOURCE == 'true'
run: |
cd ${{ github.workspace }}/keyman/linux
./scripts/deb-packaging.sh --gha dependencies
- name: Verify source tarball
if: steps.check_requirements.outputs.HAS_VERIFY_SOURCE == 'true'
shell: bash
run: |
cd ${{ github.workspace }}/keyman/linux
./scripts/verify_source.sh --source-only --target-dir "${{ github.workspace }}/keyman-${{ needs.sourcepackage.outputs.KEYMAN_VERSION }}"
- name: Verify Ubuntu source package
if: steps.check_requirements.outputs.HAS_VERIFY_SOURCE == 'true'
shell: bash
run: |
cd ${{ github.workspace }}/keyman/linux
./scripts/verify_source.sh --no-create-tarball --launchpad-only --target-dir "${{ github.workspace }}/keyman-${{ needs.sourcepackage.outputs.KEYMAN_VERSION }}"
set_status_source_verification:
name: Set result status of source verification on PR builds
needs: [source_verification]
runs-on: ubuntu-latest
if: ${{ always() && github.event.client_payload.isTestBuild == 'true' && (github.repository == 'keymanapp/keyman' || github.event.client_payload.force) }}
steps:
- name: Requirements not met
if: needs.source_verification.result == 'success' && needs.source_verification.outputs.HAS_VERIFY_SOURCE != 'true'
run: |
echo "RESULT=success" >> $GITHUB_ENV
echo "MSG=Linux source verification requirements not met" >> $GITHUB_ENV
- name: Set success
if: needs.source_verification.result == 'success' && needs.source_verification.outputs.HAS_VERIFY_SOURCE == 'true'
run: |
echo "RESULT=success" >> $GITHUB_ENV
echo "MSG=Linux source verification succeeded" >> $GITHUB_ENV
- name: Set cancelled
if: needs.source_verification.result == 'cancelled'
run: |
echo "RESULT=error" >> $GITHUB_ENV
echo "MSG=Linux source verification cancelled" >> $GITHUB_ENV
- name: Set failure
if: needs.source_verification.result == 'failure'
run: |
echo "RESULT=failure" >> $GITHUB_ENV
echo "MSG=Linux source verification failed" >> $GITHUB_ENV
- name: Set final status
run: |
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
/repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.buildSha }} \
-f state="$RESULT" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
-f description="$MSG" \
-f context="$STATUS_CONTEXT_SOURCE_VERIFICATION"

View file

@ -8,12 +8,12 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Update labels based on changed files
uses: actions/labeler@v4
uses: actions/labeler@f27b608878404679385c85cfa523b85ccb86e213 # v6.1.0
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Update labels based on PR title
id: labeler
uses: fuxingloh/multi-labeler@f5bd7323b53b0833c1e4ed8d7b797ae995ef75b4 # v2.0.1
uses: fuxingloh/multi-labeler@bcd50af464202999e57f556b4aefcf05a34abf85 # v5.0.0
with:
github-token: ${{secrets.GITHUB_TOKEN}}
config-path: .github/multi-labeler.yml

153
.github/workflows/npm-publish.yml vendored Normal file
View file

@ -0,0 +1,153 @@
#
# Keyman is copyright (C) SIL Global. MIT License.
#
# Created by mcdurdin on 2025-10-27
#
# This workflow publishes all our npm packages using Trusted Publishers OIDC
# permissions. It is triggered by a repository_dispatch event with the types
# `npm-publish:*`, from the trigger-builds infrastructure. For test builds, this
# does a `npm pack` only.
#
# Inputs:
# buildSha: The SHA of the commit to build, e.g. of the branch or
# refs/pull/1234/head for PR
# user: The user that triggered the build or created the PR
# isTestBuild: false for Releases, otherwise true
name: "npm pack/publish"
run-name: "npm pack/publish - branch: ${{ github.event.client_payload.branch }} sha: ${{ github.event.client_payload.buildSha }}, user: @${{ github.event.client_payload.user }}, isTestBuild: ${{ github.event.client_payload.isTestBuild }}"
on:
repository_dispatch:
types: ['npm-publish:*']
permissions:
id-token: write # Required for OIDC
contents: read
statuses: write
env:
GH_TOKEN: ${{ github.token }}
STATUS_CONTEXT: 'npm pack/publish'
IS_TEST_BUILD: ${{ github.event.client_payload.isTestBuild }}
# This property causes node.inc.sh to skip invocation of nvm, as otherwise, we
# end up downgrading to node v20 as of Keyman 19. See #15040 and corresponding
# change in node.inc.sh; we should be able to remove this in Keyman 20.
KEYMAN_CI_SKIP_NVM: true
jobs:
npm_publish:
name: Publish or pack @keymanapp packages
if: github.repository == 'keymanapp/keyman' || github.event.client_payload.force
runs-on: ubuntu-24.04
steps:
- name: Set env var for pack vs publish
run: |
if [[ "$IS_TEST_BUILD" == true ]]; then
echo "NPM_ACTION=pack" >> $GITHUB_ENV
else
echo "NPM_ACTION=publish" >> $GITHUB_ENV
fi
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: '${{ github.event.client_payload.buildSha }}'
- name: Set pending status
id: set_status
if: github.event.client_payload.isTestBuild == 'true'
shell: bash
run: |
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
/repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.buildSha }} \
-f state='pending' \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
-f description="npm $NPM_ACTION started" \
-f context="$STATUS_CONTEXT"
- name: Report initial npm/node versions
run: |
echo "Initial npm/node versions"
npm -v
node -v
- uses: actions/setup-node@v6
with:
node-version: '24.10'
- name: Report current npm/node versions
run: |
echo "Current npm/node versions"
npm -v
node -v
- name: Configure build environment
run: |
./resources/build/ci/npm-publish.sh configure
- name: npm pack/publish
run: |
export KEYMAN_TIER=$(cat TIER.md)
export GHA_TEST_BUILD="${{ github.event.client_payload.isTestBuild }}"
export GHA_BRANCH="${{ github.event.client_payload.branch }}"
echo "KEYMAN_TIER=$KEYMAN_TIER" >> $GITHUB_ENV
echo "GHA_TEST_BUILD=${GHA_TEST_BUILD}" >> $GITHUB_ENV
echo "GHA_BRANCH=${GHA_BRANCH}" >> $GITHUB_ENV
echo "KEYMAN_TIER: $KEYMAN_TIER"
echo "GHA_TEST_BUILD: $GHA_TEST_BUILD"
echo "GHA_BRANCH: $GHA_BRANCH"
if [[ -f ./resources/build/ci/npm-publish.sh ]]; then
./resources/build/ci/npm-publish.sh $NPM_ACTION
else
echo WARNING: npm-publish.sh is not yet available
fi
set_status:
name: Set result status on PR builds
needs: [npm_publish]
runs-on: ubuntu-latest
if: ${{ always() }}
steps:
- name: Set env var for pack vs publish
run: |
if [[ "$IS_TEST_BUILD" == true ]]; then
echo "NPM_ACTION=pack" >> $GITHUB_ENV
else
echo "NPM_ACTION=publish" >> $GITHUB_ENV
fi
- name: Set success
if: needs.npm_publish.result == 'success'
run: |
echo "RESULT=success" >> $GITHUB_ENV
echo "MSG=npm $NPM_ACTION succeeded" >> $GITHUB_ENV
- name: Set cancelled
if: needs.npm_publish.result == 'cancelled'
run: |
echo "RESULT=error" >> $GITHUB_ENV
echo "MSG=npm $NPM_ACTION cancelled" >> $GITHUB_ENV
- name: Set failure
if: needs.npm_publish.result == 'failure'
run: |
echo "RESULT=failure" >> $GITHUB_ENV
echo "MSG=npm $NPM_ACTION failed" >> $GITHUB_ENV
- name: Set final status
if: github.event.client_payload.isTestBuild == 'true'
run: |
gh api \
--method POST \
-H "Accept: application/vnd.github+json" \
/repos/$GITHUB_REPOSITORY/statuses/${{ github.event.client_payload.buildSha }} \
-f state="$RESULT" \
-f target_url="$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \
-f description="$MSG" \
-f context="$STATUS_CONTEXT"

View file

@ -1,40 +1,40 @@
# GENERATED FILE - DO NOT EDIT!
#
# Keyman is copyright (C) SIL Global. MIT License.
#
# Do not modify the script in .github/workflows/pr-build-status.yml directly;
# instead work on the sources in resources/build/pr-build-status and use the
# build.sh script to rebuild the .github/workflows/pr-build-status.yml file from
# them.
#
# build.sh will append the relevant portions of
# resources/build/pr-build-status/pr-build-status.mjs to the content in
# resources/build/pr-build-status/pr-build-status.prefix.yml to form the .github
# workflow file.
#
name: Keyman Build Summary
on:
status:
push:
branches-ignore:
- master
- beta
- stable-*
workflow_dispatch:
inputs:
commit:
description: 'Commit sha'
required: true
type: string
jobs:
run_pr_build_status:
name: Summarize build status checks
runs-on: ubuntu-latest
steps:
- name: Check PR build status
id: run_pr_build_status_script
uses: actions/github-script@v7
with:
#
# Keyman is copyright (C) SIL Global. MIT License.
#
# Do not modify the script in .github/workflows/pr-build-status.yml directly;
# instead work on the sources in resources/build/pr-build-status and use the
# build.sh script to rebuild the .github/workflows/pr-build-status.yml file from
# them.
#
# build.sh will append the relevant portions of
# resources/build/pr-build-status/pr-build-status.mjs to the content in
# resources/build/pr-build-status/pr-build-status.prefix.yml to form the .github
# workflow file.
#
name: Keyman Build Summary
on:
status:
push:
branches-ignore:
- master
- beta
- stable-*
workflow_dispatch:
inputs:
commit:
description: 'Commit sha'
required: true
type: string
jobs:
run_pr_build_status:
name: Summarize build status checks
runs-on: ubuntu-latest
steps:
- name: Check PR build status
id: run_pr_build_status_script
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
// This code is copied out of resources/build/pr-build-status/pr-build-status.mjs
// where it is tested. It is copied inline here in order to avoid requiring the
@ -55,43 +55,70 @@ jobs:
// check/web/file-size
//
function addLog(message) {
console.log(message);
return message + String.fromCharCode(10);
}
function addStatus(o, type, context, state) {
if(!o[context]) {
o[context] = {type, state};
return addLog(`Found ${context}: ${state}`);
} else {
return addLog(`Skipping repeated ${context}: ${state}`);
}
}
function reduceStatuses(statuses) {
let summary = '';
const filtered_statuses = statuses.reduce((o, status) => {
if(status.creator?.login == 'keyman-server' && status.context.startsWith('Test')) {
if(!o[status.context]) o[status.context] = {type: 'build', state: status.state};
summary += addStatus(o, 'build', status.context, status.state);
} else if(status.creator?.login == 'keymanapp-test-bot[bot]' && status.context == 'user_testing') {
if(!o[status.context]) o[status.context] = {type: 'user-test', state: status.state};;
summary += addStatus(o, 'user-test', status.context, status.state);
} else if(status.context == 'API Verification') {
if(!o[status.context]) o[status.context] = {type: 'check', state: status.state};
summary += addStatus(o, 'check', status.context, status.state);
} else if(status.context == 'Ubuntu Packaging') {
if(!o[status.context]) o[status.context] = {type: 'build', state: status.state};
summary += addStatus(o, 'build', status.context, status.state);
} else if(status.context == 'Linux source verification') {
// Ignore Linux source verification -- we won't block automerge
// for this at this point
summary += addLog(`Skipping ${status.context}`);
} else if(status.context == 'npm pack/publish') {
summary += addStatus(o, 'build', status.context, status.state);
} else if(status.context == 'Keyman Core - ARM64 test') {
summary += addStatus(o, 'build', status.context, status.state);
} else if(status.context == 'check/web/file-size') {
// Ignore check/web/file-size -- we won't block automerge for this at this point
summary += addLog(`Skipping ${status.context}`);
} else {
// We fail with an 'unknown status' response if we get a new status check
// so we can be sure we are not skipping known status checks
summary += addLog(`Found unknown ${status.context}: ${status.state}`);
o[status.context] = {type: 'unknown', state: status.state};
}
return o;
}, {});
return filtered_statuses;
return { filtered_statuses, summary };
}
//
// Given the collection of status checks we care about, return
// an aggregate status -- error, failed, pending, or success,
// an aggregate status -- error, failure, pending, or success,
// and a summary description
//
function calculateFinalStatus(filtered_statuses) {
function calculateFinalStatus(filtered_statuses, summary) {
const counts = {};
let hasBuilds = false;
for(const context of Object.keys(filtered_statuses)) {
const { state, type } = filtered_statuses[context];
if(type == 'unknown') {
// We special-case for unknown status checks, and never permit them
// Note: function reduceStatuses() is where new status checks can be added
console.error(`An unknown context "${context}" was found, cannot calculate build status.`);
return [
'error', `An unknown context ${context} was found, cannot calculate build status.`
'error', `An unknown context "${context}" was found, cannot calculate build status.`, summary
];
}
if(type == 'build') {
@ -102,12 +129,13 @@ jobs:
// If we do not have any statuses yet, we wait
if(Object.keys(filtered_statuses).length == 0 || !hasBuilds) {
return ['pending', 'Builds have not yet been triggered ⌛'];
console.log(`Builds not yet triggered`);
return ['pending', 'Builds have not yet been triggered ⌛', summary];
}
const state =
counts.error ? 'error' :
counts.failed ? 'failed' :
counts.failure ? 'failure' :
counts.pending ? 'pending' :
'success';
@ -118,14 +146,16 @@ jobs:
description += `${count} check${count == 1 ? '' : 's'} ${state}`;
}
appendDescription(counts.error, 'in an error state ❌');
appendDescription(counts.failed, 'failed ❌');
appendDescription(counts.failure, 'failed ❌');
appendDescription(counts.pending, 'pending ⌛');
appendDescription(counts.success, 'completed successfully ✅');
return [ state, description ];
console.log(`Finished filtering and reviewing status checks: ${state}: ${description}`);
return [ state, description, summary ];
}
async function getCommitStatuses(github, owner, repo, sha) {
console.log(`Getting commit statuses for ${sha}`);
const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{sha}/statuses', {
owner,
repo,
@ -138,6 +168,7 @@ jobs:
}
async function getCommitCheckRuns(github, owner, repo, sha) {
console.log(`Getting commit check runs for ${sha}`);
const statuses = await github.paginate('GET /repos/{owner}/{repo}/commits/{sha}/check-runs', {
owner,
repo,
@ -151,13 +182,11 @@ jobs:
function calculateCheckResult(statuses) {
if(!Array.isArray(statuses)) {
return ['error', 'Failed to retrieve status checks from GitHub ❌'];
return ['error', 'Failed to retrieve status checks from GitHub ❌', 'statuses is not an array'];
}
const filtered_statuses = reduceStatuses(statuses);
const result = calculateFinalStatus(filtered_statuses);
return result;
const { filtered_statuses, summary } = reduceStatuses(statuses);
return calculateFinalStatus(filtered_statuses, summary);
}
async function test(github, owner, repo, sha) {
@ -177,7 +206,7 @@ jobs:
return check.data.id;
}
async function updateCheck(github, owner, repo, checkRunId, status, description) {
async function updateCheck(github, owner, repo, checkRunId, status, description, summary) {
const checkStatus = status == 'pending' ? 'in_progress' : 'completed';
const conclusion = checkStatus == 'in_progress' ? undefined : (status == 'success' ? 'success' : 'failure');
@ -189,7 +218,7 @@ jobs:
conclusion,
output: {
title: description,
summary: ''
summary
}
});
}
@ -204,5 +233,5 @@ jobs:
const checkRunId = await createCheck(github, owner, repo, sha);
const res = await test(github, owner, repo, sha);
await updateCheck(github, owner, repo, checkRunId, res[0], res[1]);
await updateCheck(github, owner, repo, checkRunId, res[0], res[1], res[2]);

10
.gitignore vendored
View file

@ -103,8 +103,8 @@
/windows/src/engine/keyman32/*.cod
/windows/src/engine/keyman32/*.pdb
# /engine/keymanx64/
/windows/src/engine/keymanx64/x64
# /engine/keymanhp/
/windows/src/engine/keymanhp/x64
# /engine/kmcomapi/
/windows/src/engine/kmcomapi/kmcomapi.tlb
@ -152,8 +152,8 @@
# Atom/Nuclide
compile_flags.txt
# Auto-generated resource script
resources/environment.sh
# Auto-generated resource script for xcode
resources/build/mac/xcode-environment.inc.sh
# Copy of the master VERSION.md used for Linux packaging
/core/VERSION.md
@ -164,7 +164,7 @@ tsconfig.tsbuildinfo
# Windows system files
Thumbs.db
# Temporary file for logging scripts in xcode runs, see build-utils.sh for
# Temporary file for logging scripts in xcode runs, see builder-basic.inc.sh for
# details
/xcodebuild-scripts.log

View file

@ -1,5 +1,6 @@
external-sources=true
# If you get SC1091 for sourced files, add the following setting to
# your .vscode/settings.json file:
# "shellcheck.useWorkspaceRootAsCwd": false
source-path=resources/build/
source-path=resources/
source-path=SCRIPTDIR
source-path=linux/ibus-keyman/tests/scripts/

1113
HISTORY.md

File diff suppressed because it is too large Load diff

View file

@ -1 +1 @@
19.0.83
19.0.251

View file

@ -3,19 +3,12 @@ buildscript {
repositories {
google()
mavenCentral()
flatDir {
dirs "kMAPro/libs/com/stepstone/stepper/material-stepper/4.3.1/"
}
}
dependencies {
classpath 'com.android.tools.build:gradle:8.9.2'
// sentry-android-gradle-plugin 2.1.5+ requires AGP 7.0
classpath 'io.sentry:sentry-android-gradle-plugin:4.6.0'
classpath 'io.sentry:sentry-android-gradle-plugin:5.9.0'
classpath 'name.remal:gradle-plugins:1.9.2'
// From jcenter() which was deprecated August 2024
// https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/
classpath 'com.stepstone.stepper:material-stepper:4.3.1@aar'
}
}

View file

@ -3,11 +3,10 @@
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../resources/build/builder.inc.sh"
. "${THIS_SCRIPT%/*}/../../resources/build/builder-full.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh"
. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh"
. "$KEYMAN_ROOT/resources/build/utils.inc.sh"
. "$KEYMAN_ROOT/resources/build/build-help.inc.sh"
. "$KEYMAN_ROOT/resources/build/build-download-resources.sh"
@ -28,8 +27,8 @@ builder_describe "Builds Keyman for Android app." \
"configure" \
"build" \
"test Runs lint and unit tests." \
"publish Publishes symbols to Sentry and the APK to the Play Store." \
"--upload-sentry Upload to sentry"
"publish-symbols Publishes symbols to Sentry." \
"publish-play-store Publishes the APK to the Play Store."
# parse before describe_outputs to check debug flags
builder_parse "$@"
@ -41,9 +40,13 @@ if builder_is_debug_build; then
TEST_FLAGS="-x assembleDebug lintDebug testDebug"
fi
KEYBOARD_PACKAGE_ID="sil_euro_latin"
KEYBOARDS_TARGET="android/KMAPro/kMAPro/src/main/assets/${KEYBOARD_PACKAGE_ID}.kmp"
MODEL_PACKAGE_ID="nrc.en.mtnt"
MODELS_TARGET="android/KMAPro/kMAPro/src/main/assets/${MODEL_PACKAGE_ID}.model.kmp"
builder_describe_outputs \
configure /android/KMAPro/kMAPro/libs/keyman-engine.aar \
build /android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION}.apk
configure "/${MODELS_TARGET}" \
build /android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk
#### Build
@ -54,18 +57,6 @@ if builder_is_ci_build; then
DAEMON_FLAG=--no-daemon
fi
#### Build action definitions ####
function makeLocalSentryRelease() {
echo "Making a Sentry release for tag $KEYMAN_VERSION_GIT_TAG"
sentry-cli upload-dif -p keyman-android --include-sources
sentry-cli releases -p keyman-android files $KEYMAN_VERSION_GIT_TAG upload-sourcemaps ./
echo "Finalizing release tag $KEYMAN_VERSION_GIT_TAG"
sentry-cli releases finalize "$KEYMAN_VERSION_GIT_TAG"
}
#### Build action definitions ####
# Check about cleaning artifact paths
@ -76,13 +67,8 @@ fi
if builder_start_action configure; then
KEYBOARD_PACKAGE_ID="sil_euro_latin"
KEYBOARDS_TARGET="$KEYMAN_ROOT/android/KMAPro/kMAPro/src/main/assets/${KEYBOARD_PACKAGE_ID}.kmp"
MODEL_PACKAGE_ID="nrc.en.mtnt"
MODELS_TARGET="$KEYMAN_ROOT/android/KMAPro/kMAPro/src/main/assets/${MODEL_PACKAGE_ID}.model.kmp"
downloadKeyboardPackage "$KEYBOARD_PACKAGE_ID" "$KEYBOARDS_TARGET"
downloadModelPackage "$MODEL_PACKAGE_ID" "$MODELS_TARGET"
downloadKeyboardPackage "$KEYBOARD_PACKAGE_ID" "${KEYMAN_ROOT}/$KEYBOARDS_TARGET"
downloadModelPackage "$MODEL_PACKAGE_ID" "${KEYMAN_ROOT}/$MODELS_TARGET"
builder_finish_action success configure
fi
@ -90,7 +76,7 @@ fi
if builder_start_action build; then
# Copy Keyman Engine for Android
cp "$KEYMAN_ROOT/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "$KEYMAN_ROOT/android/KMAPro/kMAPro/libs/keyman-engine.aar"
cp "${KEYMAN_ROOT}/android/KMEA/app/build/outputs/aar/keyman-engine-${CONFIG}.aar" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/libs/keyman-engine.aar"
# Convert markdown to html for offline help
build_help_html android KMAPro/kMAPro/src/main/assets/info
@ -98,9 +84,7 @@ if builder_start_action build; then
echo "BUILD_FLAGS $BUILD_FLAGS"
./gradlew $DAEMON_FLAG clean $BUILD_FLAGS
if builder_has_option --upload-sentry; then
makeLocalSentryRelease
fi
mv "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION}.apk" "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/$CONFIG/keyman-${KEYMAN_VERSION_FOR_FILENAME}.apk"
builder_finish_action success build
fi
@ -113,12 +97,23 @@ if builder_start_action test; then
builder_finish_action success test
fi
if builder_start_action publish; then
# Copy Release Notes
generateReleaseNotes
publish_symbols() {
if builder_is_ci_build && builder_is_ci_build_level_release; then
# TODO: what does publishSentry even do?
./gradlew $DAEMON_FLAG publishSentry
builder_echo "Making a Sentry release for tag $KEYMAN_VERSION_GIT_TAG"
sentry-cli upload-dif -p keyman-android --include-sources
sentry-cli releases -p keyman-android files $KEYMAN_VERSION_GIT_TAG upload-sourcemaps ./
fi
}
# Publish symbols and Keyman for Android to Play Store
./gradlew $DAEMON_FLAG publishSentry publishReleaseApk
publish_play_store() {
if builder_is_ci_build && builder_is_ci_build_level_release; then
generateReleaseNotes
# Publish Keyman for Android to Play Store
./gradlew $DAEMON_FLAG publishReleaseApk
fi
}
builder_finish_action success publish
fi
builder_run_action publish-symbols publish_symbols
builder_run_action publish-play-store publish_play_store

View file

@ -11,8 +11,14 @@ apply from: "$rootPath/version.gradle"
String tier = new File('../../TIER.md').getText('UTF-8').trim();
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
android {
compileSdk 34
compileSdk 35
namespace="com.tavultesoft.kmapro"
// Don't compress kmp files so they can be copied via AssetManager
@ -24,15 +30,10 @@ android {
buildConfig = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
defaultConfig {
applicationId "com.tavultesoft.kmapro"
minSdkVersion 21
targetSdkVersion 34
targetSdkVersion 35
//println "===DUMPING PROPERTIES==="
//dumpProperties(project) // Use this to dump all external properties for debugging TeamCity integration
@ -113,6 +114,12 @@ android {
disable 'MissingQuantity', 'MissingTranslation'
lintConfig file("lint.xml")
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
}
}
}
// how to configure the sentry android gradle plugin
@ -157,21 +164,20 @@ if (env_keys_json_file != null && file(env_keys_json_file).exists()) {
}
repositories {
flatDir {
dirs 'libs'
}
google()
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.stepstone.stepper:material-stepper:4.3.1'
api(name: 'keyman-engine', ext: 'aar')
implementation 'io.sentry:sentry-android:7.8.0'
implementation 'androidx.webkit:webkit:1.14.0'
implementation files('libs/keyman-engine.aar')
implementation 'io.sentry:sentry-android:8.19.1'
implementation 'androidx.preference:preference:1.2.1'
implementation "com.android.installreferrer:installreferrer:2.2"
implementation 'com.android.installreferrer:installreferrer:2.2'
// Add dependency for generating QR Codes
// (Even though it's embedded in KMEA, because we're manually copying keyman-engine.aar,

View file

@ -5,10 +5,10 @@
package com.keyman.android;
import com.keyman.engine.util.DownloadFileUtils;
import com.tavultesoft.kmapro.AdjustLongpressDelayActivity;
import com.tavultesoft.kmapro.BuildConfig;
import com.tavultesoft.kmapro.DefaultLanguageResource;
import com.tavultesoft.kmapro.KeymanSettingsActivity;
import com.tavultesoft.kmapro.PreferencesManager;
import com.keyman.engine.KMManager;
import com.keyman.engine.KMManager.KeyboardType;
import com.keyman.engine.KMHardwareKeyboardInterpreter;
@ -30,12 +30,12 @@ import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.widget.FrameLayout;
import android.content.Intent;
import io.sentry.android.core.SentryAndroid;
import io.sentry.Sentry;
@ -43,7 +43,6 @@ import io.sentry.Sentry;
public class SystemKeyboard extends InputMethodService implements OnKeyboardEventListener {
private static View inputView = null;
private static ExtractedText exText = null;
private KMHardwareKeyboardInterpreter interpreter = null;
private int inputType = InputType.TYPE_NULL;
private int lastOrientation = Configuration.ORIENTATION_UNDEFINED;
@ -125,8 +124,9 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
}
ViewGroup parent = (ViewGroup) inputView.getParent();
if (parent != null)
if (parent != null) {
parent.removeView(inputView);
}
return inputView;
}
@ -140,6 +140,22 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
KMManager.updateSelectionRange(KMManager.KeyboardType.KEYBOARD_TYPE_SYSTEM);
}
private void sendCurrentFontName() {
Keyboard keyboardInfo = KMManager.getCurrentKeyboardInfo(this);
if (keyboardInfo != null) {
String fontName = keyboardInfo.getFont();
Intent intent;
if (BuildConfig.DEBUG) {
intent = new Intent("com.tavultesoft.kmapro.debug.keyboard_changed");
} else {
intent = new Intent("com.tavultesoft.kmapro.keyboard_changed");
}
intent.putExtra("fontName", fontName);
sendBroadcast(intent);
}
}
/**
* This is the main point where we do our initialization of the input method
* to begin operating on an application. At this point we have been
@ -187,10 +203,9 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
return value, so we test for that as well (#11479)
*/
if (icText != null && icText.text != null) {
boolean didUpdateText = KMManager.updateText(KeyboardType.KEYBOARD_TYPE_SYSTEM, icText.text.toString());
boolean didUpdateSelection = KMManager.updateSelectionRange(KeyboardType.KEYBOARD_TYPE_SYSTEM);
if (!didUpdateText || !didUpdateSelection)
exText = icText;
// Update the text selection but ignore the returned statuses
KMManager.updateText(KeyboardType.KEYBOARD_TYPE_SYSTEM, icText.text.toString());
KMManager.updateSelectionRange(KeyboardType.KEYBOARD_TYPE_SYSTEM);
}
}
@ -198,6 +213,10 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
if (KMManager.isNumericField(inputType)) {
KMManager.setNumericLayer(KeyboardType.KEYBOARD_TYPE_SYSTEM);
}
if (KMManager.isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)) {
sendCurrentFontName();
}
}
@Override
@ -238,7 +257,7 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
KMManager.onConfigurationChanged(newConfig);
}
// We should extend the touchable region so that Keyman sub keys menu can receive touch events outside the keyboard frame
// Update the touchable region of the Keyman keyboard
Point size = KMManager.getWindowSize(getApplicationContext());
int inputViewHeight = 0;
@ -246,9 +265,10 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
inputViewHeight = inputView.getHeight();
}
int navigationHeight = KMManager.getNavigationBarHeight(this, KeyboardType.KEYBOARD_TYPE_SYSTEM);
int bannerHeight = KMManager.getBannerHeight(this);
int kbHeight = KMManager.getKeyboardHeight(this);
outInsets.contentTopInsets = inputViewHeight - bannerHeight - kbHeight;
outInsets.contentTopInsets = inputViewHeight - bannerHeight - kbHeight - navigationHeight;
outInsets.visibleTopInsets = outInsets.contentTopInsets;
outInsets.touchableInsets = InputMethodService.Insets.TOUCHABLE_INSETS_REGION;
outInsets.touchableRegion.set(0, outInsets.contentTopInsets, size.x, size.y);
@ -256,15 +276,15 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
@Override
public void onKeyboardLoaded(KeyboardType keyboardType) {
if (keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM) {
if (exText != null)
exText = null;
}
// Do nothing
}
@Override
public void onKeyboardChanged(String newKeyboard) {
// Refresh banner theme
BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM);
KMManager.showSystemKeyboard();
sendCurrentFontName();
}
@Override
@ -278,6 +298,21 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
// Do nothing
}
@Override
public boolean onEvaluateInputViewShown() {
// On Android API 36+, the OSK defaults to not appearing when a physical keyboard is connected.
// If the default implementation returns true, recommend honoring it
// Reference: https://android.googlesource.com/platform/frameworks/base/+/7b739a8%5E%21/
if (super.onEvaluateInputViewShown()) {
return true;
};
Context context = getApplicationContext();
SharedPreferences prefs = context.getSharedPreferences(PreferencesManager.kma_prefs_name, Context.MODE_PRIVATE);
boolean showOSK = prefs.getBoolean(KeymanSettingsActivity.oskWithPhysicalKeyboardKey, false);
return showOSK;
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
// Determine if Physical keystroke should be passed off

View file

@ -22,29 +22,28 @@ import com.keyman.engine.BaseActivity;
import com.keyman.engine.KMManager;
/**
* Settings menu for adjusting the keyboard height. The value for the current device orientation
* is saved in shared preferences.
* Settings menu for adjusting the keyboard height.
* The value for the current device orientation is saved via KMManager.
*/
public class AdjustKeyboardHeightActivity extends BaseActivity {
private static final String TAG = "AdjustKbdHeight";
public static final String adjustKeyboardHeightKey = "AdjustKeyboardHeight";
private static Button resetButton = null;
private static ImageView sampleKeyboard = null;
// Keeps track of the adjusted keyboard height for saving
private static int currentHeight = 0;
private static ViewGroup.LayoutParams layoutParams;
private Button resetButton;
private ImageView sampleKeyboard;
private TextView percentageDisplay;
private int currentHeight;
private ViewGroup.LayoutParams layoutParams;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
final Context context = this;
setContentView(R.layout.activity_adjust_keyboard_height);
Toolbar toolbar = (Toolbar) findViewById(R.id.titlebar);
setupEdgeToEdge(R.id.adjust_keyboard_layout);
setupStatusBarColors(R.color.keyman_blue, android.R.color.white);
Toolbar toolbar = findViewById(R.id.titlebar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
@ -57,75 +56,162 @@ public class AdjustKeyboardHeightActivity extends BaseActivity {
actionBar.setBackgroundDrawable(new ColorDrawable(ContextCompat.getColor(this, R.color.keyman_blue)));
}
TextView adjustKeyboardHeightActivityTitle = (TextView) findViewById(R.id.bar_title);
String titleStr = getString(R.string.adjust_keyboard_height);
TextView adjustKeyboardHeightActivityTitle = findViewById(R.id.bar_title);
adjustKeyboardHeightActivityTitle.setTextColor(ContextCompat.getColor(this, R.color.ms_white));
adjustKeyboardHeightActivityTitle.setText(titleStr);
adjustKeyboardHeightActivityTitle.setText(getString(R.string.adjust_keyboard_height));
sampleKeyboard = findViewById(R.id.sample_keyboard);
percentageDisplay = findViewById(R.id.keyboard_height_percentage);
View resizeHandle = findViewById(R.id.resize_handle);
sampleKeyboard = (ImageView) findViewById(R.id.sample_keyboard);
layoutParams = sampleKeyboard.getLayoutParams();
currentHeight = KMManager.getKeyboardHeight(context);
refreshSampleKeyboard(context);
currentHeight = KMManager.getKeyboardHeight(this);
refreshSampleKeyboard();
resetButton = (Button) findViewById(R.id.reset_to_defaults);
resetButton.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
// Clear the keyboard height preferences to reset
KMManager.applyKeyboardHeight(context, KMManager.KeyboardHeight_Reset);
// Restore default height
currentHeight = KMManager.getKeyboardHeight(context);
refreshSampleKeyboard(context);
}
resetButton = findViewById(R.id.reset_to_defaults);
resetButton.setOnClickListener(v -> {
KMManager.applyKeyboardHeight(this, KMManager.KeyboardHeight_Reset);
currentHeight = KMManager.getKeyboardHeight(this);
refreshSampleKeyboard();
});
sampleKeyboard.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent event) {
int y = (int)event.getY();
switch(event.getAction()) {
case MotionEvent.ACTION_MOVE:
// Update currentHeight as the user drags (moves)
// Increasing the keyboard height is a negative y
currentHeight -= y;
// Apply lower and upper bounds on currentHeight
int defaultHeight = (int) context.getResources().getDimension(R.dimen.keyboard_height);
currentHeight = Math.max(defaultHeight/2, currentHeight);
currentHeight = Math.min(defaultHeight*2, currentHeight);
refreshSampleKeyboard(context);
break;
case MotionEvent.ACTION_UP:
// Save the currentHeight when the user releases
KMManager.applyKeyboardHeight(context, currentHeight);
break;
}
return true;
View.OnTouchListener touchListener = (view, event) -> {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
view.setPressed(true);
break;
case MotionEvent.ACTION_MOVE:
int[] location = new int[2];
sampleKeyboard.getLocationOnScreen(location);
int viewBottom = location[1] + sampleKeyboard.getHeight();
int touchY = (int) event.getRawY();
currentHeight = calculateKeyboardHeightFromTouch(touchY, viewBottom);
refreshSampleKeyboard();
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
view.setPressed(false);
KMManager.applyKeyboardHeight(this, currentHeight);
break;
}
});
return true;
};
resizeHandle.setOnTouchListener(touchListener);
sampleKeyboard.setOnTouchListener(touchListener);
}
/**
* Refresh the layout for the sample keyboard
* @param context
* Refresh the layout for the sample keyboard and update percentage display.
*/
private void refreshSampleKeyboard(Context context) {
layoutParams.height = currentHeight;
sampleKeyboard.setLayoutParams(layoutParams);
private void refreshSampleKeyboard() {
if (layoutParams != null && sampleKeyboard != null) {
layoutParams.height = currentHeight;
sampleKeyboard.requestLayout();
if (percentageDisplay != null) {
percentageDisplay.setText(createLiveKeyboardHeightString());
}
}
}
/**
* Calculate percentage from height and default height.
* @param height Current height
* @param defaultHeight Default height
* @return Percentage rounded to nearest integer
*/
private static int calculatePercentage(int height, int defaultHeight) {
if (defaultHeight == 0 || height == 0) {
return 100;
}
return (int) Math.round((double) height / defaultHeight * 100);
}
/**
* Create a string showing the keyboard height as percentages for both orientations.
* @param context Context
* @return String in format "100% Portrait | 100% Landscape"
*/
public static String createKeyboardHeightString(Context context) {
int portraitHeight = KMManager.getKeyboardHeight(context, Configuration.ORIENTATION_PORTRAIT);
int landscapeHeight = KMManager.getKeyboardHeight(context, Configuration.ORIENTATION_LANDSCAPE);
int portraitDefault = KMManager.getDefaultKeyboardHeight(Configuration.ORIENTATION_PORTRAIT);
int landscapeDefault = KMManager.getDefaultKeyboardHeight(Configuration.ORIENTATION_LANDSCAPE);
int portraitPercent = calculatePercentage(portraitHeight, portraitDefault);
int landscapePercent = calculatePercentage(landscapeHeight, landscapeDefault);
return context.getString(R.string.keyboard_height_format, portraitPercent, landscapePercent);
}
/**
* Calculate keyboard height from touch Y coordinate and clamp
* within min-max bounds.
* @param touchY Touch Y coordinate in screen coordinates
* @param viewBottom Bottom Y coordinate of keyboard view
* @return Keyboard height in pixels, clamped to min/max bounds
*/
private int calculateKeyboardHeightFromTouch(int touchY, int viewBottom) {
int height = viewBottom - touchY;
int minKeyboardHeight = KMManager.getKeyboardHeightMin(this);
int maxKeyboardHeight = KMManager.getKeyboardHeightMax(this);
return Math.max(minKeyboardHeight, Math.min(maxKeyboardHeight, height));
}
/**
* Create a string showing keyboard height percentages for both orientations,
* using the current (unsaved) height for the active orientation.
* @return String in format "100% Portrait | 100% Landscape"
*/
private String createLiveKeyboardHeightString() {
int currentOrientation = KMManager.getOrientation(this);
int portraitDefault = KMManager.getDefaultKeyboardHeight(Configuration.ORIENTATION_PORTRAIT);
int landscapeDefault = KMManager.getDefaultKeyboardHeight(Configuration.ORIENTATION_LANDSCAPE);
int portraitHeight;
int landscapeHeight;
if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
portraitHeight = currentHeight;
landscapeHeight = KMManager.getKeyboardHeight(this, Configuration.ORIENTATION_LANDSCAPE);
} else {
landscapeHeight = currentHeight;
portraitHeight = KMManager.getKeyboardHeight(this, Configuration.ORIENTATION_PORTRAIT);
}
int portraitPercent = calculatePercentage(portraitHeight, portraitDefault);
int landscapePercent = calculatePercentage(landscapeHeight, landscapeDefault);
return getString(R.string.keyboard_height_format, portraitPercent, landscapePercent);
}
@Override
protected void onPause() {
super.onPause();
// Ensure height is saved even if user switches tasks or navigates away abnormally
// This prevents the touch zone from being out of sync with the keyboard visual
if (currentHeight > 0) {
KMManager.applyKeyboardHeight(this, currentHeight);
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// IMPORTANT: Apply current height BEFORE switching orientations
// This ensures the old orientation's height is saved before loading the new one
if (currentHeight > 0) {
KMManager.applyKeyboardHeight(this, currentHeight);
}
layoutParams = sampleKeyboard.getLayoutParams();
// When the user rotates the device, restore currentHeight
// When the user rotates the device, restore currentHeight for the new orientation
currentHeight = KMManager.getKeyboardHeight(this);
refreshSampleKeyboard(this);
refreshSampleKeyboard();
}
@Override

View file

@ -57,6 +57,10 @@ public class AdjustLongpressDelayActivity extends BaseActivity {
final Context context = this;
setContentView(R.layout.activity_adjust_longpress_delay);
setupEdgeToEdge(R.id.adjust_longpress_layout);
setupStatusBarColors(R.color.keyman_blue, android.R.color.white);
Toolbar toolbar = (Toolbar) findViewById(R.id.titlebar);
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();

View file

@ -34,6 +34,7 @@ public class InfoActivity extends BaseActivity {
final Context context = this;
setContentView(R.layout.activity_info);
setupEdgeToEdge(R.id.info_layout);
// Navigation buttons
final ImageButton backButton = (ImageButton) findViewById(R.id.back_button);

View file

@ -11,6 +11,7 @@ import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.LocaleList;
import android.util.AndroidRuntimeException;
import android.util.Log;
import android.view.View;
@ -38,7 +39,7 @@ public class KMPBrowserActivity extends BaseActivity {
private static final String TAG = "KMPBrowserActivity";
// URL for keyboard search web page presented to user when they add a keyboard in the app.
private static final String KMP_SEARCH_KEYBOARDS_FORMATSTR = "https://%s/go/android/%s/download-keyboards%s";
private static final String KMP_SEARCH_KEYBOARDS_FORMATSTR = "https://%s/go/android/%s/download-keyboards%s?lang=%s";
private static final String KMP_SEARCH_KEYBOARDS_LANGUAGES = "/languages/%s";
// Patterns for determining if a link should be opened in external browser
@ -79,6 +80,9 @@ public class KMPBrowserActivity extends BaseActivity {
return;
}
setupEdgeToEdge(R.id.kmp_browser_layout);
setupStatusBarColors(R.color.complementary_5, android.R.color.white);
webView = (WebView) findViewById(R.id.kmpBrowserWebView);
webView.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);
webView.getSettings().setJavaScriptEnabled(true);
@ -160,13 +164,7 @@ public class KMPBrowserActivity extends BaseActivity {
}
});
// Tier determines the keyboard search host
String host = KMPLink.getHost();
// If language ID is provided, include it in the keyboard search
String languageID = getIntent().getStringExtra("languageCode");
String languageStr = (languageID != null) ? KMString.format(KMP_SEARCH_KEYBOARDS_LANGUAGES, languageID) : "";
String appMajorVersion = KMManager.getMajorVersion();
String kmpSearchUrl = KMString.format(KMP_SEARCH_KEYBOARDS_FORMATSTR, host, appMajorVersion, languageStr);
String kmpSearchUrl = determineSearchUrl();
webView.loadUrl(kmpSearchUrl);
}
@ -175,7 +173,8 @@ public class KMPBrowserActivity extends BaseActivity {
super.onResume();
if (webView != null) {
webView.reload();
String kmpSearchUrl = determineSearchUrl();
webView.loadUrl(kmpSearchUrl);
}
}
@ -218,6 +217,32 @@ public class KMPBrowserActivity extends BaseActivity {
}
}
/**
* Parse the keyman.com keyboard search URL while accounting for:
* host (production or staging)
* languageID - language ID to search for
* display language - request search results to be displayed in this language
* @return String
*/
private String determineSearchUrl() {
// Tier determines the keyboard search host
String host = KMPLink.getHost();
// If display language is provided, include it in the keyboard search. Otherwise fallback to default locale
String fallbackLang = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) ?
LocaleList.getDefault().get(0).toLanguageTag() : "en";
Bundle bundle = getIntent().getExtras();
String displayLang = (bundle != null) && bundle.containsKey("lang") ? bundle.getString("lang") : fallbackLang;
// If language ID is provided for keyboard search, include it in the keyboard search
String languageID = getIntent().getStringExtra("languageCode");
String languageStr = (languageID != null) ? KMString.format(KMP_SEARCH_KEYBOARDS_LANGUAGES, languageID) : "";
String appMajorVersion = KMManager.getMajorVersion();
String kmpSearchUrl = KMString.format(KMP_SEARCH_KEYBOARDS_FORMATSTR, host, appMajorVersion, languageStr, displayLang);
return kmpSearchUrl;
}
/**
* Check if a URL is a valid internal Keyman keyboard link
* @param url String of the URL to parse

View file

@ -5,7 +5,6 @@ import android.os.Bundle;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.KMManager;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
public class KeymanSettingsActivity extends BaseActivity {
@ -14,6 +13,7 @@ public class KeymanSettingsActivity extends BaseActivity {
protected static final String sendCrashReport = "SendCrashReport";
public static final String spacebarTextKey = "SpacebarText";
public static final String hapticFeedbackKey = "HapticFeedback";
public static final String oskWithPhysicalKeyboardKey = "ShowOSK";
protected KeymanSettingsFragment innerFragment;
@ -22,6 +22,10 @@ public class KeymanSettingsActivity extends BaseActivity {
super.onCreate(savedInstanceState);
setContentView(R.layout.keyman_settings_layout);
setupEdgeToEdge(R.id.keyman_settings_layout);
setupStatusBarColors(R.color.keyman_blue, android.R.color.white);
Toolbar toolbar = (Toolbar) findViewById(R.id.keyman_settings_toolbar);
toolbar.setTitleTextColor(getResources().getColor(android.R.color.white));
toolbar.setTitle(R.string.keyman_settings);

View file

@ -6,6 +6,7 @@ package com.tavultesoft.kmapro;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.provider.Settings;
import android.view.inputmethod.InputMethodManager;
@ -20,6 +21,7 @@ import androidx.preference.SwitchPreference;
import com.keyman.engine.DisplayLanguages;
import com.keyman.engine.KMManager;
import com.keyman.engine.data.Dataset;
import com.tavultesoft.kmapro.PreferencesManager;
import java.util.HashMap;
@ -104,6 +106,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat {
adjustKeyboardHeight.setKey(AdjustKeyboardHeightActivity.adjustKeyboardHeightKey);
adjustKeyboardHeight.setTitle(getString(R.string.adjust_keyboard_height));
adjustKeyboardHeight.setWidgetLayoutResource(R.layout.preference_height_icon_layout);
adjustKeyboardHeight.setSummary(AdjustKeyboardHeightActivity.createKeyboardHeightString(context));
Intent adjustKeyboardHeightIntent = new Intent(context, AdjustKeyboardHeightActivity.class);
adjustKeyboardHeight.setIntent(adjustKeyboardHeightIntent);
@ -157,6 +160,31 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat {
}
});
SwitchPreference oskWithPhysicalKeyboardPreference = new SwitchPreference(context);
oskWithPhysicalKeyboardPreference.setKey(KeymanSettingsActivity.oskWithPhysicalKeyboardKey);
oskWithPhysicalKeyboardPreference.setTitle(getString(R.string.show_osk));
oskWithPhysicalKeyboardPreference.setSummary(getString(R.string.show_osk_hint));
SharedPreferences prefs = context.getSharedPreferences(
PreferencesManager.kma_prefs_name, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
boolean showOSK = prefs.getBoolean(
KeymanSettingsActivity.oskWithPhysicalKeyboardKey, false);
oskWithPhysicalKeyboardPreference.setDefaultValue(showOSK);
oskWithPhysicalKeyboardPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
@Override
public boolean onPreferenceChange(Preference preference, Object newValue) {
if (newValue == null) {
return false;
}
editor.putBoolean(KeymanSettingsActivity.oskWithPhysicalKeyboardKey, (boolean)newValue);
editor.commit();
return true;
}
});
SwitchPreference hapticFeedbackPreference = new SwitchPreference(context);
hapticFeedbackPreference.setKey(KeymanSettingsActivity.hapticFeedbackKey);
hapticFeedbackPreference.setTitle(getString(R.string.haptic_feedback));
@ -198,6 +226,8 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat {
screen.addPreference(adjustKeyboardHeight);
screen.addPreference(adjustLongpressDelay);
screen.addPreference(oskWithPhysicalKeyboardPreference);
screen.addPreference(spacebarTextPreference);
screen.addPreference(hapticFeedbackPreference);
@ -239,5 +269,7 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat {
// Update the language / keyboard count when we return to this menu from deeper levels.
languagesPreference.setTitle(getInstalledLanguagesText());
adjustKeyboardHeight.setSummary(AdjustKeyboardHeightActivity.createKeyboardHeightString(context));
}
}

View file

@ -7,11 +7,12 @@ import java.util.ArrayList;
import java.util.HashMap;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import androidx.preference.PreferenceManager;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
@ -24,11 +25,13 @@ import android.os.Bundle;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.DisplayLanguages;
import com.keyman.engine.KMManager;
import com.keyman.engine.data.KeyboardController;
import com.keyman.engine.util.MapCompat;
public class KeymanSettingsInstallActivity extends AppCompatActivity {
public class KeymanSettingsInstallActivity extends BaseActivity {
private static final String TAG = "SettingsInstallActivity";
private static ArrayList<HashMap<String, String>> installOptionList = null;
private static Typeface titleFont = null;
@ -45,6 +48,8 @@ public class KeymanSettingsInstallActivity extends AppCompatActivity {
context = this;
setContentView(R.layout.activity_list_layout);
setupEdgeToEdge(R.id.list_toolbar);
final Toolbar toolbar = findViewById(R.id.list_toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@ -125,12 +130,19 @@ public class KeymanSettingsInstallActivity extends AppCompatActivity {
HashMap<String, String> hashMap = (HashMap<String, String>) parent.getItemAtPosition(position);
String itemTitle = MapCompat.getOrDefault(hashMap, titleKey, "");
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String languageTag = prefs.getString(DisplayLanguages.displayLanguageKey, "");
// Install from keyman.com
if (itemTitle.equals(getString(R.string.install_from_keyman_dot_com))) {
if (KMManager.hasConnection(context)) {
Intent i = new Intent(context, KMPBrowserActivity.class);
i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
i.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
if (languageTag != null && !languageTag.isEmpty()) {
Bundle bundle = new Bundle();
bundle.putString("lang", languageTag);
i.putExtras(bundle);
}
context.startActivity(i);
} else {
AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(context);

View file

@ -25,10 +25,12 @@ import android.widget.TextView;
import androidx.annotation.IdRes;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.SwitchCompat;
import androidx.appcompat.widget.Toolbar;
import androidx.preference.PreferenceManager;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.DisplayLanguages;
import com.keyman.engine.KeyboardPickerActivity;
import com.keyman.engine.KMManager;
import com.keyman.engine.ModelPickerActivity;
@ -43,7 +45,7 @@ import java.util.HashMap;
* Keyman Settings --> Languages Settings --> Language Settings
* Displays a list of installed keyboards and some lexical model switches.
*/
public final class LanguageSettingsActivity extends AppCompatActivity {
public final class LanguageSettingsActivity extends BaseActivity {
private Context context;
private static Toolbar toolbar = null;
private static ListView listView = null;
@ -66,6 +68,8 @@ public final class LanguageSettingsActivity extends AppCompatActivity {
context = this;
setContentView(R.layout.language_settings_list_layout);
setupEdgeToEdge(R.id.list_toolbar);
toolbar = (Toolbar) findViewById(R.id.list_toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
@ -199,11 +203,19 @@ public final class LanguageSettingsActivity extends AppCompatActivity {
if (KMManager.hasConnection(context)){
// Scenario 1: Connection to keyman.com catalog
// Pass the BCP47 language code to the KMPBrowserActivity
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
String languageTag = prefs.getString(DisplayLanguages.displayLanguageKey, "");
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);
if (languageTag != null && !languageTag.isEmpty()) {
Bundle bundle = new Bundle();
bundle.putString("lang", languageTag);
i.putExtras(bundle);
}
context.startActivity(i);
/*
} else if (KeyboardPickerActivity.hasKeyboardFromPackage()) {

View file

@ -19,9 +19,9 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.data.Dataset;
import com.keyman.engine.data.adapters.AdapterFilter;
import com.keyman.engine.data.adapters.NestedAdapter;
@ -36,7 +36,7 @@ import java.util.List;
* Keyman Settings --> Languages Settings
* Displays a list of installed languages and a count of their associated installed keyboards.
*/
public final class LanguagesSettingsActivity extends AppCompatActivity {
public final class LanguagesSettingsActivity extends BaseActivity {
private Context context;
private static Toolbar toolbar = null;
@ -53,6 +53,8 @@ public final class LanguagesSettingsActivity extends AppCompatActivity {
context = this;
setContentView(R.layout.languages_settings_list_layout);
setupEdgeToEdge(R.id.languages_list_layout);
toolbar = (Toolbar) findViewById(R.id.list_toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);

View file

@ -67,6 +67,7 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.widget.Toolbar;
import androidx.appcompat.app.AlertDialog;
import androidx.constraintlayout.widget.ConstraintLayout;
import android.content.ClipData;
import android.content.Context;
import android.content.DialogInterface;
@ -85,7 +86,6 @@ import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import android.provider.Settings;
import android.text.Html;
@ -98,8 +98,6 @@ import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.webkit.WebView;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.SeekBar;
@ -118,6 +116,7 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
private static final String TAG = "MainActivity";
private ConstraintLayout constraintLayout;
private KMTextView textView;
private final int minTextSize = 16;
private final int maxTextSize = 72;
@ -170,6 +169,10 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
setContentView(R.layout.activity_main);
constraintLayout = (ConstraintLayout)findViewById(R.id.constraintLayout);
setupEdgeToEdge(R.id.constraintLayout);
setupStatusBarColors(android.R.color.white, R.color.neutral_2);
toolbar = (Toolbar) findViewById(R.id.titlebar);
setSupportActionBar(toolbar);
getSupportActionBar().setTitle(null);
@ -616,16 +619,21 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
getTheme().resolveAttribute(android.R.attr.actionBarSize, outValue, true);
int actionBarHeight = getResources().getDimensionPixelSize(outValue.resourceId);
// *** TO DO: Try to check if status bar is visible, set statusBarHeight to 0 if it is not visible ***
// Status bar height only used for portrait orientation
int statusBarHeight = 0;
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0)
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
if (lastOrientation == Configuration.ORIENTATION_PORTRAIT) {
// *** TO DO: Try to check if status bar is visible, set statusBarHeight to 0 if it is not visible ***
int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android");
if (resourceId > 0) {
statusBarHeight = getResources().getDimensionPixelSize(resourceId);
}
}
int navigationBarHeight = KMManager.getNavigationBarHeight(context, KeyboardType.KEYBOARD_TYPE_INAPP);
Point size = KMManager.getWindowSize(context);
int screenHeight = size.y;
Log.d(TAG, "Main resizeTextView bannerHeight: " + bannerHeight);
textView.setHeight(screenHeight - statusBarHeight - actionBarHeight - bannerHeight - keyboardHeight);
textView.setHeight(
screenHeight - statusBarHeight - actionBarHeight - bannerHeight - keyboardHeight - navigationBarHeight);
}
private void showInfo() {
@ -864,6 +872,9 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene
private void displayUpdateChrome() {
// TextView's default string is to update Chrome
TextView textView = (TextView)findViewById(R.id.kmWebViewChromeTextView);
textView.setText(String.format(getString(R.string.text_require_chrome_version),
WebViewUtils.KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME));
Button button = (Button)findViewById(R.id.webViewChromeButton);
button.setText(getString(R.string.button_update_chrome));

View file

@ -4,7 +4,6 @@
package com.tavultesoft.kmapro;
import android.annotation.SuppressLint;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AlertDialog;
import androidx.fragment.app.Fragment;
@ -18,6 +17,7 @@ import android.widget.Toast;
import com.stepstone.stepper.StepperLayout;
import com.stepstone.stepper.VerificationError;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.KMManager;
import com.keyman.engine.KeyboardEventHandler;
import com.keyman.engine.KmpInstallMode;
@ -34,7 +34,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
public class PackageActivity extends AppCompatActivity implements
public class PackageActivity extends BaseActivity implements
StepperLayout.StepperListener, WebViewFragment.OnInstallClickedListener,
SelectLanguageFragment.OnLanguagesSelectedListener {
private final static String TAG = "PackageActivity";
@ -52,6 +52,7 @@ public class PackageActivity extends AppCompatActivity implements
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_package_installer);
setupEdgeToEdge(R.id.package_installer_layout);
KmpInstallMode installMode = KmpInstallMode.Full;
String languageID = null;
@ -84,7 +85,8 @@ public class PackageActivity extends AppCompatActivity implements
if (pkgTarget.equals(PackageProcessor.PP_TARGET_LEXICAL_MODELS)) {
kmpProcessor = new LexicalModelPackageProcessor(resourceRoot);
} else if (!pkgTarget.equals(PackageProcessor.PP_TARGET_KEYBOARDS)) {
showErrorToast(getString(R.string.no_targets_to_install));
// Not using showErrorToast to avoid cluttering Sentry with noise
showErrorDialog(context, pkgId, getString(R.string.no_targets_to_install));
return;
}
tempPackagePath = kmpProcessor.unzipKMP(kmpFile);

View file

@ -0,0 +1,11 @@
/**
* Keyman is copyright (C) SIL Global. MIT License.
*
*/
package com.tavultesoft.kmapro;
public class PreferencesManager {
// TODO: Separate this from the Keyman Engine preference key name in strings.xml: #13633
public static final String kma_prefs_name = "KMAPreferences";
}

View file

@ -8,11 +8,11 @@ import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.appcompat.app.AppCompatActivity;
import androidx.fragment.app.Fragment;
import com.stepstone.stepper.StepperLayout;
import com.stepstone.stepper.VerificationError;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.KMManager;
import com.keyman.engine.data.Keyboard;
import com.keyman.engine.packages.PackageProcessor;
@ -29,7 +29,7 @@ import java.util.List;
* Also available during keyboard package installation that involves a language choice.
* Displays a list of available language names for the user to add for a given installed packageID/keyboardID.
*/
public class SelectLanguageActivity extends AppCompatActivity implements
public class SelectLanguageActivity extends BaseActivity implements
StepperLayout.StepperListener, SelectLanguageFragment.OnLanguagesSelectedListener {
private final static String TAG = "SelectLanguageActivity";
private File packagePath;
@ -43,6 +43,7 @@ public class SelectLanguageActivity extends AppCompatActivity implements
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_select_language);
setupEdgeToEdge(R.id.select_language_layout);
Bundle bundle = getIntent().getExtras();
keyboard = (Keyboard)bundle.getSerializable("keyboard");

View file

@ -16,9 +16,9 @@ import android.widget.ListView;
import android.widget.SimpleAdapter;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.widget.Toolbar;
import com.keyman.engine.BaseActivity;
import com.keyman.engine.KMManager;
import com.keyman.engine.data.Keyboard;
import com.keyman.engine.data.KeyboardController;
@ -33,7 +33,7 @@ import java.util.List;
* Keyman Settings --> KeymanInstallActivity --> SelectPackageActivity
* Displays a list of installed package ID / keyboard IDs so the user can select a language from the kmp.json
*/
public final class SelectPackageActivity extends AppCompatActivity {
public final class SelectPackageActivity extends BaseActivity {
private static final String TAG = "SelectPackageActivity";
private static ArrayList<HashMap<String, String>> list = null;
private static Typeface titleFont = null;
@ -49,6 +49,8 @@ public final class SelectPackageActivity extends AppCompatActivity {
context = this;
setContentView(R.layout.activity_list_layout);
setupEdgeToEdge(R.id.activity_list_layout);
final Toolbar toolbar = findViewById(R.id.list_toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/ms_white"
android:pathData="M12,20 L4,8 L20,8 Z" />
</vector>

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/ms_white"
android:pathData="M12,4 L4,16 L20,16 Z" />
</vector>

View file

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Resize handle bar background with grip lines -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Base background with Keyman blue -->
<item>
<shape android:shape="rectangle">
<solid android:color="@color/keyman_blue" />
<corners
android:topLeftRadius="8dp"
android:topRightRadius="8dp" />
</shape>
</item>
<!-- Grip indicator - three horizontal lines -->
<item
android:gravity="center"
android:top="12dp"
android:bottom="12dp">
<layer-list>
<item android:top="0dp" android:bottom="10dp">
<shape android:shape="rectangle">
<solid android:color="@color/ms_white" />
<corners android:radius="1dp" />
<size android:width="40dp" android:height="3dp" />
</shape>
</item>
<item android:top="6dp" android:bottom="6dp">
<shape android:shape="rectangle">
<solid android:color="@color/ms_white" />
<corners android:radius="1dp" />
<size android:width="40dp" android:height="3dp" />
</shape>
</item>
<item android:top="10dp" android:bottom="0dp">
<shape android:shape="rectangle">
<solid android:color="@color/ms_white" />
<corners android:radius="1dp" />
<size android:width="40dp" android:height="3dp" />
</shape>
</item>
</layer-list>
</item>
</layer-list>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Resize handle bar with grip lines and touch feedback -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Pressed state - solid darker background -->
<item android:state_pressed="true">
<layer-list>
<item>
<shape android:shape="rectangle">
<solid android:color="#0E5196" />
<corners
android:topLeftRadius="8dp"
android:topRightRadius="8dp" />
</shape>
</item>
<item
android:width="40dp"
android:height="4dp"
android:gravity="center">
<shape android:shape="rectangle">
<solid android:color="@color/ms_white" />
<corners android:radius="2dp" />
</shape>
</item>
</layer-list>
</item>
<!-- Default state - gradient background with grip lines -->
<item android:drawable="@drawable/resize_handle_background" />
</selector>

View file

@ -1,47 +1,115 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:id="@+id/adjust_keyboard_layout"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/titlebar" />
<TextView
android:id="@+id/drag_the_keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/titlebar"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:text="@string/drag_the_keyboard"
android:textSize="@dimen/adjust_keyboard_height_label_textsize"
/>
<TextView
android:id="@+id/rotate_the_device"
android:layout_below="@id/drag_the_keyboard"
android:id="@+id/adjust_instructions"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:text="@string/rotate_the_device"
android:textSize="@dimen/adjust_keyboard_height_label_textsize"
/>
<Button
android:id="@+id/reset_to_defaults"
style="@style/Widget.AppCompat.Button.Colored"
android:background="@color/keyman_blue"
android:textColor="@color/ms_white"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/rotate_the_device"
android:layout_centerInParent="true"
android:singleLine="true"
android:layout_marginEnd="@dimen/activity_horizontal_margin"
android:layout_marginTop="8dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/reset_to_defaults"
android:textSize="@dimen/adjust_keyboard_height_label_textsize" />
android:text="@string/adjust_keyboard_instructions"
android:textSize="@dimen/adjust_keyboard_height_label_textsize"
app:layout_constraintTop_toBottomOf="@id/titlebar" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/button_layout"
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/adjust_instructions">
<Button
android:id="@+id/reset_to_defaults"
style="@style/Widget.AppCompat.Button.Colored"
android:background="@color/keyman_blue"
android:textColor="@color/ms_white"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:singleLine="true"
android:layout_marginTop="8dp"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:text="@string/reset_to_defaults"
android:textSize="@dimen/adjust_keyboard_height_label_textsize"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<!-- Percentage Display -->
<TextView
android:id="@+id/keyboard_height_percentage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:text="100% Portrait | 100% Landscape"
app:layout_constraintTop_toBottomOf="@id/button_layout"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<!-- Grabbable Resize Handle Bar -->
<LinearLayout
android:id="@+id/resize_handle"
android:layout_width="match_parent"
android:layout_height="40dp"
android:background="@drawable/resize_handle_bar"
android:orientation="horizontal"
android:gravity="center_vertical"
android:clickable="true"
android:focusable="true"
android:contentDescription="@string/adjust_keyboard_instructions"
app:layout_constraintBottom_toTopOf="@id/sample_keyboard"
app:layout_constraintStart_toStartOf="parent">
<!-- Up arrow on left -->
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="16dp"
android:src="@drawable/ic_arrow_up"
android:contentDescription="Resize up" />
<!-- Spacer to center -->
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<!-- Center grip lines (visual from background drawable) -->
<View
android:layout_width="60dp"
android:layout_height="match_parent" />
<!-- Spacer to right -->
<View
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1" />
<!-- Down arrow on right -->
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginEnd="16dp"
android:src="@drawable/ic_arrow_down"
android:contentDescription="Resize down" />
</LinearLayout>
<!-- Resizable keyboard "image -->
<ImageView
@ -51,11 +119,7 @@
android:contentDescription="@string/adjust_keyboard_height"
android:scaleType="fitXY"
android:src="@drawable/blank_osk"
android:background="@drawable/dash_border"
android:layout_alignParentBottom="true"
android:paddingStart="@dimen/blank_osk_padding"
android:paddingEnd="@dimen/blank_osk_padding"
android:paddingTop="@dimen/blank_osk_padding"
android:paddingBottom="@dimen/blank_osk_padding"/>
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,8 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/adjust_longpress_layout"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/titlebar" />
@ -13,7 +16,8 @@
android:gravity="center"
android:orientation="horizontal"
android:paddingBottom="25dp"
android:paddingTop="60dp" >
android:paddingTop="60dp"
app:layout_constraintTop_toBottomOf="@id/titlebar" >
<ImageButton
android:id="@+id/delayTimeDownButton"
@ -56,6 +60,6 @@
android:layout_marginStart="@dimen/activity_horizontal_margin"
android:text="@string/longpress_delay_time"
android:textSize="@dimen/longpress_delay_label_textsize"
/>
app:layout_constraintTop_toBottomOf="@id/linearLayout" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,75 +1,92 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/info_layout"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="@color/ms_white"
tools:context=".InfoActivity" >
<androidx.appcompat.widget.Toolbar
android:id="@+id/titlebar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:layout_alignParentTop="true"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
<androidx.appcompat.widget.Toolbar
android:id="@+id/titlebar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:orientation="horizontal"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/close_button" >
<!-- Navigation buttons in a LinearLayout to mirror on RTL -->
<ImageButton
android:id="@+id/back_button"
style="?android:attr/actionButtonStyle"
app:tint="@color/image_button_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:contentDescription="@string/label_back"
android:src="@drawable/ic_action_back" />
<!-- Navigation buttons in a LinearLayout to mirror on RTL -->
<ImageButton
android:id="@+id/back_button"
style="?android:attr/actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="start"
android:contentDescription="@string/label_back"
android:src="@drawable/ic_action_back"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/image_button_color" />
<ImageButton
android:id="@+id/forward_button"
style="?android:attr/actionButtonStyle"
app:tint="@color/image_button_color"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/label_forward"
android:src="@drawable/ic_action_forward" />
</LinearLayout>
<ImageButton
android:id="@+id/forward_button"
style="?android:attr/actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:contentDescription="@string/label_forward"
android:src="@drawable/ic_action_forward"
app:layout_constraintStart_toEndOf="@id/back_button"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/image_button_color" />
<ImageButton
android:id="@+id/close_button"
style="?android:attr/actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:contentDescription="@string/label_close"
android:src="@drawable/ic_action_close" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
<ImageButton
android:id="@+id/close_button"
style="?android:attr/actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
android:contentDescription="@string/label_close"
android:src="@drawable/ic_action_close"
app:layout_constraintEnd_toEndOf="parent" />
<WebView
android:id="@+id/infoWebView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/titlebar"
android:layout_marginBottom="@dimen/info_label_height" />
</androidx.appcompat.widget.Toolbar>
<!-- layout_height 0dp to fill the remaining area -->
<WebView
android:id="@+id/infoWebView"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_below="@id/titlebar"
android:layout_marginStart="4dp"
app:layout_constraintTop_toBottomOf="@id/titlebar"
app:layout_constraintBottom_toTopOf="@+id/infoVersion"
app:layout_constraintStart_toStartOf="parent" />
<!-- Version string on the bottom of the page -->
<TextView
android:id="@+id/infoVersion"
android:layout_width="match_parent"
android:layout_height="@dimen/info_label_height"
android:layout_gravity="center_horizontal"
android:gravity="center"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/title_version"
android:textSize="@dimen/info_label_textsize"
android:layout_alignParentBottom="true" />
android:id="@id/infoVersion"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:gravity="center"
android:ellipsize="end"
android:singleLine="true"
android:text="@string/title_version"
android:textSize="@dimen/info_label_textsize"
android:layout_alignParentBottom="true"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,6 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/kmp_browser_layout"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".KMPBrowserActivity"
@ -9,6 +12,8 @@
<WebView
android:id="@+id/kmpBrowserWebView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,35 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:layout_above="@+id/KMKeyboard"
android:orientation="vertical"
android:background="@color/yellow_background"
android:background="@android:color/white"
tools:context=".MainActivity">
<include layout="@layout/titlebar" />
<include layout="@layout/titlebar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<include layout="@layout/check_chrome_webview_layout"
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/titlebar"/>
<include
layout="@layout/check_chrome_webview_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/titlebar"
android:visibility="gone"
app:layout_constraintTop_toBottomOf="@id/titlebar" />
<com.keyman.engine.KMTextView
android:id="@+id/kmTextView"
android:background="@drawable/textview_bg"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/checkWebViewChromeLayout"
android:background="@drawable/textview_bg"
android:ems="10"
android:inputType="textMultiLine|textNoSuggestions"
android:hint="@string/textview_hint"
android:gravity="top"
android:scrollbars="vertical">
android:hint="@string/textview_hint"
android:inputType="textMultiLine|textNoSuggestions"
android:scrollbars="vertical"
app:layout_constraintTop_toBottomOf="@id/checkWebViewChromeLayout">
<requestFocus />
</com.keyman.engine.KMTextView>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/package_installer_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@ -23,6 +24,17 @@
app:ms_nextButtonText="@string/label_next"
app:ms_completeButtonColor="@color/keyman_blue"
app:ms_completeButtonText="@string/label_ok"
app:ms_stepperType="dots" />
app:ms_stepperType="dots"
app:layout_constraintTop_toBottomOf="@+id/titlebar"
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation"
app:layout_constraintStart_toStartOf="parent" />
</RelativeLayout>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,7 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:fitsSystemWindows="true"
android:id="@+id/select_language_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
@ -15,7 +17,7 @@
android:id="@+id/stepperLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_below="@+id/titlebar"
app:ms_showBackButtonOnFirstStep="true"
app:ms_backButtonColor="@color/keyman_blue"
app:ms_backButtonText="@string/label_back"
@ -23,6 +25,18 @@
app:ms_nextButtonText="@string/label_next"
app:ms_completeButtonColor="@color/keyman_blue"
app:ms_completeButtonText="@string/label_ok"
app:ms_stepperType="dots" />
app:ms_stepperType="dots"
app:layout_constraintTop_toBottomOf="@id/titlebar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/bottom_navigation" />
</RelativeLayout>
<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_bias="1.0"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -15,7 +15,7 @@
android:background="@android:color/white"
android:gravity="start"
android:layout_gravity="center_vertical"
android:text="@string/text_require_chrome_57"
android:text="@string/text_require_chrome_version"
android:layout_marginStart="@dimen/button_playstore_margin"
android:textSize="@dimen/update_chrome_label_textsize" />

View file

@ -1,23 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/keyman_settings_layout"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
<androidx.appcompat.widget.Toolbar
android:id="@+id/keyman_settings_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:elevation="0dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/keyman_settings_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/keyman_blue">
</androidx.appcompat.widget.Toolbar>
</com.google.android.material.appbar.AppBarLayout>
android:layout_height="?attr/actionBarSize"
android:background="@color/keyman_blue"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<fragment
android:layout_marginTop="?attr/actionBarSize"
@ -26,4 +21,4 @@
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -116,9 +116,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">ሁሉም ቋንቋዎች ቀደም ሲል ተጭኗል</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">የፊደል ገበታ ሰሌዳውን በመጎተት ቁመቱን አስተካክል</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">ዲቫይሱን በማሽከርከር ፖርትሬቱንና ላንድስኬፑን አስተካክል</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">የፊደል ገበታ ሰሌዳውን በመጎተት ቁመቱን አስተካክል ዲቫይሱን በማሽከርከር ፖርትሬቱንና ላንድስኬፑን አስተካክል</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">ወደ ቀደመው ሴቲንግ መልስ</string>
<!-- Context: Keyman Web Browser -->

View file

@ -122,9 +122,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">كل اللغات مثبتة مسبقا</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">اسحب لوحة المفاتيح لتغيير مدى ارتفاعها</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">دور الجهاز لضبط الوضع العمودي والوضع الأفقي</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">اسحب لوحة المفاتيح لتغيير مدى ارتفاعها. دور الجهاز لضبط الوضع العمودي والوضع الأفقي.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">إعادة ضبط الافتراضيات</string>
<!-- Context: Keyman Web Browser -->

View file

@ -114,9 +114,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Bütün dillər artıq quraşdırılıb</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Hündürlüyünün ölcüsü tənzimləmək üçün klaviaturanı dartın</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Portret və landşaft tənzimləmək üçün cihazı çevirin</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Hündürlüyünün ölcüsü tənzimləmək üçün klaviaturanı dartın. Portret və landşaft tənzimləmək üçün cihazı çevirin.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">İlkin vəziyyətinə qaytar</string>
<!-- Context: Keyman Web Browser -->

View file

@ -120,9 +120,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Ὅλες οἱ γλῶσσες ἔχουν ἤδη ἐγκατασταθεῖ</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Σύρετε τὸ πληκτρολόγιο γιὰ νὰ ἀλλάξετε τὸ ὕψος του</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Περιστρέψτε τὴν συσκευὴ γιὰ προσαρμογὴ τοῦ ὕψους σὲ λειτουργία πορτραίτου καὶ τοπίου</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Σύρετε τὸ πληκτρολόγιο γιὰ νὰ ἀλλάξετε τὸ ὕψος του. Περιστρέψτε τὴν συσκευὴ γιὰ προσαρμογὴ τοῦ ὕψους σὲ λειτουργία πορτραίτου καὶ τοπίου.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Ἐπαναφέρετε τὶς προεπιλεγμένες ρυθμίσεις</string>
<!-- Context: Adjust Longpress Delay Time menu -->

View file

@ -120,9 +120,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Ya están instalados todos los idiomas</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Arrastra el teclado para ajustar la altura</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Rota el dispositivo para ajustar el modo vertical y horizontal</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Arrastra el teclado para ajustar la altura. Rota el dispositivo para ajustar el modo vertical y horizontal.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Restaurar valores predeterminados</string>
<!-- Context: Adjust Longpress Delay Time menu -->

View file

@ -117,9 +117,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Luggat chatta dakhalo</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Ajubdal keyboard ley badalan tula</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Barrumal shu\'ul ley badalan shaba binshaf</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Ajubdal keyboard ley badalan tula. Barrumal shu\'ul ley badalan shaba binshaf.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Eingalab ley shaba gabul</string>
<!-- Context: Keyman Web Browser -->

View file

@ -114,9 +114,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Hulfur mnyayeri natǝ mdǝku kurtǝ ka hara instal ɗa</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Ndǝkǝr keyboard kamnyar jaktǝ wala kǝmta saɓǝlkur ni</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Shinar waya wala hansetu ni kamnyar katsa nzi thlat-thlata wala peri-periari</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Ndǝkǝr keyboard kamnyar jaktǝ wala kǝmta saɓǝlkur ni. Shinar waya wala hansetu ni kamnyar katsa nzi thlat-thlata wala peri-periari.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Kalaktǝni katsa ni tsu apa tǝ mdǝ nam ni</string>
<!-- Context: Keyman Web Browser -->

View file

@ -117,9 +117,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Shanga nyayere ndǝ kurtǝ vu tǝdar</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Ndǝrti kibod akuya ka gǝ kǝnta dzugamkurir ko saɓǝlkur su</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Mwari aka sǝr ʻya su akuyaka gǝ dzuguti njaiyir lanskep</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Ndǝrti kibod akuya ka gǝ kǝnta dzugamkurir ko saɓǝlkur su. Mwari aka sǝr ʻya su akuyaka gǝ dzuguti njaiyir lanskep.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Ɓǝla ʻyati tǝni apa Waci</string>
<!-- Context: Keyman Web Browser -->

View file

@ -122,9 +122,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Všechny jazyky jsou již nainstalovány</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Přetažením klávesnice změníte výšku</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Otočit zařízení pro nastavení na výšku a na šířku</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Přetažením klávesnice změníte výšku. Otočit zařízení pro nastavení na výšku a na šířku.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Obnovit výchozí nastavení</string>
<!-- Context: Adjust Longpress Delay Time menu -->

View file

@ -19,8 +19,9 @@
<string name="action_install_updates" comment="Menu notification that keyboard or dictionary updates available">Updates installieren</string>
<!-- Context: Title -->
<string name="title_version" comment="Title of Keyman for Android version">Version: %1$s</string>
<!-- Context: Text label when old Chrome version installed -->
<!-- Context: Text label when old Chrome version installed. Label replaced in Keyman 19.0 -->
<string name="text_require_chrome_57" comment="Banner text when old version of Chrome is installed"> Keyman benötigt Chrome Version 57 oder neuer.</string>
<string name="text_require_chrome_version" comment="Banner text when old version of Chrome is installed"> Keyman benötigt Chrome-Version %1$s oder neuer.</string>
<!-- Context: Button label -->
<string name="button_update_chrome" comment="Prompt to upgrade Chrome in the Play Store">Chrome aktualisieren</string>
<!-- Context: Textfield prompt (&#8230; is the ellipsis character "...") -->
@ -68,6 +69,8 @@
<!-- Context: Keyman Settings menu -->
<string name="adjust_keyboard_height" comment="Menu action to adjust keyboard height">Tastaturhöhe anpassen</string>
<!-- Context: Keyman Settings menu -->
<string name="adjust_longpress_delay" comment="Menu action to adjust longpress delay duration">Langes Drücken anpassen</string>
<!-- Context: Keyman Settings menu -->
<string name="spacebar_caption" comment="Menu action to set the spacebar caption">Leertasten-Beschriftung</string>
<!-- Context: Keyman Settings menu / Spacebar caption -->
<string name="spacebar_caption_keyboard" comment="Spacebar caption option - keyboard">Tastatur</string>
@ -85,13 +88,17 @@
<string name="spacebar_caption_hint_language_keyboard" comment="Spacebar caption hint - language + keyboard">Zeige Sprache und Tastaturnamen auf Leertaste</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_blank" comment="Spacebar caption hint - blank">Beschriftung auf Leertaste nicht anzeigen</string>
<!-- Context: Keyman Settings menu / Show OSK with Physical Keyboard -->
<string name="show_osk" comment="show OSK when physical keyboard is connected">Bildschirmtastatur anzeigen</string>
<!-- Context: Keyman Settings menu / Show OSK with Physical Keyboard hint -->
<string name="show_osk_hint" comment="When physical keyboard is connected">Wenn die physische Tastatur verbunden ist</string>
<!-- Context: Keyman Settings menu / Haptic feedback -->
<string name="haptic_feedback" comment="haptic feedback on key press">Beim Tippen vibrieren</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner" comment="text suggestions banner">Banner immer anzeigen</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner_on" comment="Description when toggle is on">Nicht implementiert</string>
<!-- Context: Keyman Settings menu -->
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
<string name="show_banner_off" comment="Description when toggle is off">Wenn ausgeschaltet, wird nur angezeigt, wenn vorhersagender Text aktiviert ist</string>
<!-- Context: Keyman Settings menu -->
<string name="show_send_crash_report" comment="permission for sending crash reports">Erlaube das Senden von Fehlerberichten</string>
@ -118,11 +125,19 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Alle Sprachen bereits installiert</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Ziehen Sie die Tastatur, um die Höhe zu ändern</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Drehen Sie das Gerät um Hoch- und Querformat anzupassen</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Ziehen Sie die Tastatur, um die Höhe zu verändern. Drehen Sie das Gerät, um Hoch- oder Querformat anzupassen.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Einstellungen auf Standard zurücksetzen</string>
<!-- Context: Keyboard height display format. %1$d = portrait percentage, %2$d = landscape percentage -->
<string name="keyboard_height_format" comment="Format for displaying keyboard height percentages">%1$d%% Hochformat | %2$d%% Querformat</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">Verzögerung: %1$.1f Sekunden</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_up" comment="Make longpress delay longer">Verzögerung verlängern</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_delay_time_down" comment="Make longpress delay shorter">Verzögerung verkürzen</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="ic_longpress_delay_slider" comment="Adjust longpress delay time">Regler für Verzögerung beim langen Drücken</string>
<!-- Context: Keyman Web Browser -->
<string name="hint_text" comment="Prompt to type in the browser search bar">Suchen oder URL eingeben</string>
<!-- Context: Keyman Web Browser -->

View file

@ -120,9 +120,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Todos los idiomas ya están instalados</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Mueve el teclado para reajustar la altura</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Rotar el dispositivo para ajustar a modo vertical u horizontal</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Mueve el teclado para reajustar la altura. Rotar el dispositivo para ajustar a modo vertical u horizontal.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Restablecer valores predeterminados</string>
<!-- Context: Adjust Longpress Delay Time menu -->

View file

@ -114,9 +114,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Ɗemngal man fuu arti wa`inni</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Dasa kibod man ngam de`itina juutugo</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Waylutu na`ura man ngam a` de`itina dow bee les</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Dasa kibod man ngam de`itina juutugo. Waylutu na`ura man ngam a` de`itina dow bee les.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Lornu ɗum ko naftirta tum</string>
<!-- Context: Keyman Web Browser -->

View file

@ -115,9 +115,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Ɗemɗe fof aafaama kisa</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Daas tappirde ndee ngam waylude tooweendi</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Yiiltu kaɓirgol ngol ngam fotndude naddaro e leddaro</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Daas tappirde ndee ngam waylude tooweendi. Yiiltu kaɓirgol ngol ngam fotndude naddaro e leddaro.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Artir e Goowaaɗe</string>
<!-- Context: Keyman Web Browser -->

View file

@ -118,9 +118,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Toutes les langues de cet ensemble sont déjà installées</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Faites glisser le clavier pour redimensionner la hauteur</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Faire pivoter l\'appareil pour ajuster le portrait et le paysage</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Faites glisser le clavier pour redimensionner la hauteur. Faire pivoter l\'appareil pour ajuster le portrait et le paysage.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Réinitialiser aux valeurs par défaut</string>
<!-- Context: Keyman Web Browser -->

View file

@ -114,9 +114,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">An riga da an shigar da dukkanin yarukan</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Danne madanni domin sake girman</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Juya na\'urar domin daidaita hoto da gyara shi</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Danne madanni domin sake girman. Juya na\'urar domin daidaita hoto da gyara shi.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Gyara saiti zuwa yadda ya dace</string>
<!-- Context: Keyman Web Browser -->

View file

@ -115,9 +115,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Sma gwaɗaha luwunda falo</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">An tiɗat keyboard ka virata mbakat fiwuku</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Awava tina ngana ka gulat foto nda plavant bugo</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">An tiɗat keyboard ka virata mbakat fiwuku. Awava tina ngana ka gulat foto nda plavant bugo.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Virata magat ina kol ɗughwana</string>
<!-- Context: Keyman Web Browser -->

View file

@ -113,9 +113,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Semua bahasa telah terpasang</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Drag the keyboard to resize the height</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Rotate the device to adjust portrait and landscape</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Drag the keyboard to resize the height. Rotate the device to adjust portrait and landscape.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Reset to Defaults</string>
<!-- Context: Keyman Web Browser -->

View file

@ -19,8 +19,9 @@
<string name="action_install_updates" comment="Menu notification that keyboard or dictionary updates available">Installa Aggiornamenti</string>
<!-- Context: Title -->
<string name="title_version" comment="Title of Keyman for Android version">Versione: %1$s</string>
<!-- Context: Text label when old Chrome version installed -->
<!-- Context: Text label when old Chrome version installed. Label replaced in Keyman 19.0 -->
<string name="text_require_chrome_57" comment="Banner text when old version of Chrome is installed"> Keyman richiede la versione 57 di Chrome o superiore.</string>
<string name="text_require_chrome_version" comment="Banner text when old version of Chrome is installed"> Keyman richiede Chrome versione %1$s o superiore.</string>
<!-- Context: Button label -->
<string name="button_update_chrome" comment="Prompt to upgrade Chrome in the Play Store">Aggiorna Chrome</string>
<!-- Context: Textfield prompt (&#8230; is the ellipsis character "...") -->
@ -87,6 +88,10 @@
<string name="spacebar_caption_hint_language_keyboard" comment="Spacebar caption hint - language + keyboard">Lingua e tastiera nella barra spaziatrice</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_blank" comment="Spacebar caption hint - blank">Non mostrare la didascalia sulla barra spaziatrice</string>
<!-- Context: Keyman Settings menu / Show OSK with Physical Keyboard -->
<string name="show_osk" comment="show OSK when physical keyboard is connected">Mostra tastiera a schermo</string>
<!-- Context: Keyman Settings menu / Show OSK with Physical Keyboard hint -->
<string name="show_osk_hint" comment="When physical keyboard is connected">Quando la tastiera fisica è connessa</string>
<!-- Context: Keyman Settings menu / Haptic feedback -->
<string name="haptic_feedback" comment="haptic feedback on key press">Vibra durante la digitazione</string>
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
@ -120,11 +125,11 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Tutte le lingue già installate</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Trascina la tastiera per ridimensionare l\'altezza</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Ruota il dispositivo per regolare verticale e orizzontale</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Trascina la tastiera per ridimensionarne l\'altezza. Ruota il dispositivo per regolare verticale e orizzontale.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Ripristina impostazioni predefinite</string>
<!-- Context: Keyboard height display format. %1$d = portrait percentage, %2$d = landscape percentage -->
<string name="keyboard_height_format" comment="Format for displaying keyboard height percentages">Altezza %1$d%% | Larghezza %2$d%%</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">Tempo ritardo: %1$.1f secondi</string>
<!-- Context: Adjust Longpress Delay Time menu -->

View file

@ -119,9 +119,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">ភាសា​ទាំង​អស់​ដែល​បាន​ដំឡើង​ហើយ</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">ទាញក្ដារចុចដើម្បីកែកម្ពស់</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">បង្វិលឧបករណ៍ដើម្បីកែបញ្ឈ និងផ្ដេក</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">ទាញក្ដារចុចដើម្បីកែកម្ពស់ បង្វិលឧបករណ៍ដើម្បីកែបញ្ឈ និងផ្ដេក</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">ប្ដូរ​កំណត់​ទៅលំនាំដើម</string>
<!-- Context: Adjust Longpress Delay Time menu -->

View file

@ -118,9 +118,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">ಎಲ್ಲಾ ಭಾಷೆಗಳನ್ನು ಈಗಾಗಲೇ ಸ್ಥಾಪಿಸಲಾಗಿದೆ</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">ಎತ್ತರವನ್ನು ಬದಲಾಯಿಸಲು ಕೀಲಿಮಣೆಯನ್ನು ಎಳೆಯಿರಿ</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">ಪೋರ್ಟ್ರೇಟ್ ಮತ್ತು ಲ್ಯಾಂಡ್ಸ್ಕೇಪ್ ಹೊಂದಿಸಲು ಸಾಧನವನ್ನು ತಿರುಗಿಸಿ</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">ಎತ್ತರವನ್ನು ಬದಲಾಯಿಸಲು ಕೀಲಿಮಣೆಯನ್ನು ಎಳೆಯಿರಿ. ಪೋರ್ಟ್ರೇಟ್ ಮತ್ತು ಲ್ಯಾಂಡ್ಸ್ಕೇಪ್ ಹೊಂದಿಸಲು ಸಾಧನವನ್ನು ತಿರುಗಿಸಿ.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">ಡೀಫಾಲ್ಟ್ ಮರುಹೊಂದಿಸಿ</string>
<!-- Context: Keyman Web Browser -->

View file

@ -114,9 +114,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Tǝlam samma ikke</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Keyboard dǝ girne datǝnzǝ dǝ fulutǝ ro</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Device dǝ suwarne daatanzǝ daata dǝa kuru baatanzǝ dǝa</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Keyboard dǝ girne datǝnzǝ dǝ fulutǝ ro. Device dǝ suwarne daatanzǝ daata dǝa kuru baatanzǝ dǝa.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Bǝlin sarro waltǝne setinne</string>
<!-- Context: Keyman Web Browser -->

View file

@ -114,9 +114,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Baɗumma naraha na an fa amikwa ŋanna</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Piya keyboard gé fare tsa ne adaliya</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Mbeɗanahe nja nawura ŋanna gé portrait antara landscape</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Piya keyboard gé fare tsa ne adaliya. Mbeɗanahe nja nawura ŋanna gé portrait antara landscape.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Reset to Defaults</string>
<!-- Context: Keyman Web Browser -->

View file

@ -120,9 +120,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">စုတ်ဘာသာသီုဖအိုတ်တုဲ</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">သ္ဂောံချိၚ်သမၠုၚ် ကဳဗုဒ်ဂှ် ဂြဲညိ</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">ဂွံချိၚ် လံက်သတိက် လံက်ဇမၠိၚ်ဂှ် ဗဂေတ်ကဵုစက်ပိုဲညိ။</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">သ္ဂောံချိၚ်သမၠုၚ် ကဳဗုဒ်ဂှ် ဂြဲညိ ဂွံချိၚ် လံက်သတိက် လံက်ဇမၠိၚ်ဂှ် ဗဂေတ်ကဵုစက်ပိုဲညိ။</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">ကလေၚ်သ္ပနူပကတိ</string>
<!-- Context: Adjust Longpress Delay Time menu -->

View file

@ -114,9 +114,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Ca ŋya yer kuku du mji akuya installed</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Cina keyboard gadibar ngilaba dzigamkur ari</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Shina su ari ga ngilaba dzigamkur baɗa ŋga ari</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Cina keyboard gadibar ngilaba dzigamkur ari. Shina su ari ga ngilaba dzigamkur baɗa ŋga ari.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Iri ga munagu alu lagu ginda nur dang</string>
<!-- Context: Keyman Web Browser -->

View file

@ -114,9 +114,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Alle talen zijn al geïnstalleerd</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Sleep het toetsenbord om de hoogte aan te passen</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Draai het apparaat om portret en liggend aan te passen</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Sleep het toetsenbord om de hoogte aan te passen. Draai het apparaat om portret en liggend aan te passen.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Standaardinstellingen herstellen</string>
<!-- Context: Keyman Web Browser -->

View file

@ -116,9 +116,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Wszystkie języki są już zainstalowane</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Przeciągnij klawiaturę, aby zmienić wysokość</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Obróć urządzenie, aby dostosować pionowo i poziomo</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Przeciągnij klawiaturę, aby zmienić wysokość. Obróć urządzenie, aby dostosować pionowo i poziomo.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Przywróć domyślne</string>
<!-- Context: Keyman Web Browser -->

View file

@ -19,8 +19,9 @@
<string name="action_install_updates" comment="Menu notification that keyboard or dictionary updates available">Instalar Atualizações</string>
<!-- Context: Title -->
<string name="title_version" comment="Title of Keyman for Android version">Versão: %1$s</string>
<!-- Context: Text label when old Chrome version installed -->
<!-- Context: Text label when old Chrome version installed. Label replaced in Keyman 19.0 -->
<string name="text_require_chrome_57" comment="Banner text when old version of Chrome is installed"> O Keyman requer a versão 57 ou mais recente.</string>
<string name="text_require_chrome_version" comment="Banner text when old version of Chrome is installed"> O Keyman requer a versão Chrome %1$s ou mais recente.</string>
<!-- Context: Button label -->
<string name="button_update_chrome" comment="Prompt to upgrade Chrome in the Play Store">Atualizar Chrome</string>
<!-- Context: Textfield prompt (&#8230; is the ellipsis character "...") -->
@ -87,6 +88,10 @@
<string name="spacebar_caption_hint_language_keyboard" comment="Spacebar caption hint - language + keyboard">Idioma e teclado na barra de espaço</string>
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_blank" comment="Spacebar caption hint - blank">Não mostrar legenda na barra de espaço</string>
<!-- Context: Keyman Settings menu / Show OSK with Physical Keyboard -->
<string name="show_osk" comment="show OSK when physical keyboard is connected">Mostrar Teclado no Ecrã</string>
<!-- Context: Keyman Settings menu / Show OSK with Physical Keyboard hint -->
<string name="show_osk_hint" comment="When physical keyboard is connected">Quando o teclado físico é conectado</string>
<!-- Context: Keyman Settings menu / Haptic feedback -->
<string name="haptic_feedback" comment="haptic feedback on key press">Vibrar ao tocar</string>
<!-- Context: Keyman Settings menu. Removed in Keyman 18 -->
@ -120,11 +125,11 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Todos os idiomas já instalados</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Arraste o teclado para redimensionar a altura</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Gire o dispositivo para ajustar o retrato e a paisagem</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Arraste o teclado para redimensionar a altura. Gire o dispositivo para ajustar os modos retrato e paisagem.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Restaurar padrões</string>
<!-- Context: Keyboard height display format. %1$d = portrait percentage, %2$d = landscape percentage -->
<string name="keyboard_height_format" comment="Format for displaying keyboard height percentages">%1$d%% Modo Retrato %2$d%% Modo Paisagem</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">Tempo de atraso: %1$.1f segundos</string>
<!-- Context: Adjust Longpress Delay Time menu -->

View file

@ -117,9 +117,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Все языки уже установлены</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Перетащите клавиатуру для изменения высоты</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Повернуть устройство для настройки портрета и ландшафта</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Перетащите клавиатуру для изменения высоты. Повернуть устройство для настройки портрета и ландшафта.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Сброс по умолчанию</string>
<!-- Context: Keyman Web Browser -->

View file

@ -115,9 +115,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Alla språk är redan installerade</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Dra tangentbordet för att ändra storlek på höjden</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Rotera enheten för att justera stående och liggande</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Dra tangentbordet för att ändra storlek på höjden. Rotera enheten för att justera stående och liggande.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Återställ till standard</string>
<!-- Context: Keyman Web Browser -->

View file

@ -117,9 +117,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Всі мови вже встановлені</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Перетягніть клавіатуру, щоб змінити розмір висоти</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Поверніть пристрій, щоб змінити портретну та альбомну орієнтацію</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Перетягніть клавіатуру, щоб змінити розмір висоти. Поверніть пристрій, щоб змінити портретну та альбомну орієнтацію.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Відновити параметри за замовчуванням</string>
<!-- Context: Keyman Web Browser -->

View file

@ -119,9 +119,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">Tất cả ngôn ngữ đã được cài đặt rồi</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Kéo bàn phím để điều chỉnh độ cao</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Xoay thiết bị để điều chỉnh độ ngang độ dọc</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Kéo bàn phím để điều chỉnh độ cao. Xoay thiết bị để điều chỉnh độ ngang độ dọc.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Khôi phục mặc định</string>
<!-- Context: Adjust Longpress Delay Time menu -->

View file

@ -115,9 +115,7 @@
<!-- Context: Select Package and Select Languages menu -->
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">所有语言已被安装</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">拖动键盘以调整高度</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">旋转设备以调整横竖屏模式</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">拖动键盘以调整高度. 旋转设备以调整横竖屏模式.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">重置为默认值</string>
<!-- Context: Keyman Web Browser -->

View file

@ -30,10 +30,13 @@
<!-- Context: Title -->
<string name="title_version" comment="Title of Keyman for Android version">Version: %1$s</string>
<!-- Context: Text label when old Chrome version installed -->
<!-- Context: Text label when old Chrome version installed. Label replaced in Keyman 19.0 -->
<string name="text_require_chrome_57" comment="Banner text when old version of Chrome is installed">
Keyman requires Chrome version 57 or newer.</string>
<string name="text_require_chrome_version" comment="Banner text when old version of Chrome is installed">
Keyman requires Chrome version %1$s or newer.</string>
<!-- Context: Button label -->
<string name="button_update_chrome" comment="Prompt to upgrade Chrome in the Play Store">Update Chrome</string>
@ -143,6 +146,12 @@
<!-- Context: Keyman Settings menu / Spacebar caption hint -->
<string name="spacebar_caption_hint_blank" comment="Spacebar caption hint - blank">Do not show caption on spacebar</string>
<!-- Context: Keyman Settings menu / Show OSK with Physical Keyboard -->
<string name="show_osk" comment="show OSK when physical keyboard is connected">Show On-Screen Keyboard</string>
<!-- Context: Keyman Settings menu / Show OSK with Physical Keyboard hint -->
<string name="show_osk_hint" comment="When physical keyboard is connected">When physical keyboard is connected</string>
<!-- Context: Keyman Settings menu / Haptic feedback -->
<string name="haptic_feedback" comment="haptic feedback on key press">Vibrate when typing</string>
@ -192,14 +201,14 @@
<string name="all_languages_installed" comment="Text when all languages from a keyboard package have been installed">All languages already installed</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="drag_the_keyboard" comment="First line of adjusting keyboard height">Drag the keyboard to resize the height</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="rotate_the_device" comment="Second line of adjusting keyboard height">Rotate the device to adjust portrait and landscape</string>
<string name="adjust_keyboard_instructions" comment="Instructions for adjusting keyboard height">Drag the keyboard to resize the height. Rotate the device to adjust portrait and landscape.</string>
<!-- Context: Adjust Keyboard Height menu -->
<string name="reset_to_defaults" comment="Button to reset to default heights">Reset to Defaults</string>
<!-- Context: Keyboard height display format. %1$d = portrait percentage, %2$d = landscape percentage -->
<string name="keyboard_height_format" comment="Format for displaying keyboard height percentages">%1$d%% Portrait | %2$d%% Landscape</string>
<!-- Context: Adjust Longpress Delay Time menu -->
<string name="longpress_delay_time" comment="Current longpress delay time (seconds)">Delay Time: %1$.1f seconds</string>

View file

@ -12,8 +12,14 @@ base {
archivesName = "keyman-engine"
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
android {
compileSdk 34
compileSdk 35
namespace "com.keyman.engine"
buildFeatures {
@ -21,14 +27,9 @@ android {
buildConfig = true
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_21
targetCompatibility = JavaVersion.VERSION_21
}
defaultConfig {
minSdkVersion 21
targetSdkVersion 34
targetSdkVersion 35
// KEYMAN_VERSION_CODE and KEYMAN_VERSION_NAME from version.gradle but Gradle removes them for libraries
buildConfigField "String", "KEYMAN_ENGINE_VERSION_NAME", "\""+KEYMAN_VERSION_NAME+"\""
@ -65,11 +66,13 @@ android {
dependencies {
// 1.6.0-rc01 needed to resolve https://issuetracker.google.com/issues/238425626
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.1'
// material:1.7.0 will need Gradle plugin 7.1.0+
implementation 'com.google.android.material:material:1.12.0'
implementation 'commons-io:commons-io:2.16.1'
implementation 'io.sentry:sentry-android:7.8.0'
implementation 'io.sentry:sentry-android:8.19.1'
implementation 'androidx.preference:preference:1.2.1'
implementation 'androidx.webkit:webkit:1.14.0'
// Robolectric
testImplementation 'androidx.test.ext:junit:1.2.1'

View file

@ -247,7 +247,7 @@ function deregisterModel(modelID) {
}
function enableSuggestions(model, suggestionType) {
// Set the options first so that KMW's ModelManager can properly handle model enablement states
// Set the options first so that KMW's ModelCache can properly handle model enablement states
// the moment we actually register the new model.
// Use console_debug
console_debug('enableSuggestions(model, maySuggest='+suggestionType+')');

View file

@ -14,4 +14,10 @@ if(!Array.prototype.includes){
Array.prototype.includes = function(search){
return !!~this.indexOf(search);
}
}
// This file is only referenced by the main Web engine; its "global" object is `window`.
// `globalThis` is defined in Chrome 71+.
if(typeof globalThis == 'undefined') {
var globalThis = window;
}

View file

@ -9,10 +9,19 @@ import android.content.ContextWrapper;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.LocaleList;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.activity.EdgeToEdge;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import androidx.preference.PreferenceManager;
import com.keyman.engine.util.ContextUtils;
@ -22,6 +31,7 @@ import java.util.Locale;
public class BaseActivity extends AppCompatActivity {
private static final String TAG = "BaseActivity";
static ContextWrapper localeUpdatedContext;
/**
@ -68,6 +78,58 @@ public class BaseActivity extends AppCompatActivity {
return "";
}
/**
* Setup the activity for edge-to-edge
* https://developer.android.com/develop/ui/views/layout/edge-to-edge-manually
* https://stackoverflow.com/questions/57293449/go-edge-to-edge-on-android-correctly-with-windowinsets
* @param layoutID - Top level ID of the view
*/
public void setupEdgeToEdge(int layoutID) {
EdgeToEdge.enable(this);
View anchor = findViewById(layoutID);
ViewCompat.setOnApplyWindowInsetsListener(anchor,
(view, windowInsets) -> {
// Allocate insets for system bars and display cutout (notch)
Insets insets = windowInsets.getInsets(
WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
view.setPadding(
insets.left,
insets.top,
insets.right,
insets.bottom);
KMManager.applyInsetsToKeyboard(
KMManager.KeyboardType.KEYBOARD_TYPE_INAPP, insets.left, insets.right, insets.bottom);
return windowInsets;
});
}
/**
* If Android API < 35, apply colors to the status bar and navigation bar
* @param statusBarcolor - int value of the color to use on the status bar.
* @param navigationBarColor - int value of the color to use on the navigation bar.
*/
public void setupStatusBarColors(int statusBarColor, int navigationBarColor) {
// Set status bar colors
// https://stackoverflow.com/a/79706054
Window window = getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
WindowInsetsControllerCompat windowInsetsController =
WindowCompat.getInsetsController(window, window.getDecorView());
windowInsetsController.setAppearanceLightStatusBars(true);
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) {
window.setStatusBarColor(getColor(statusBarColor));
window.setNavigationBarColor(getColor(navigationBarColor));
}
}
}
@Override
protected void attachBaseContext(Context newBase) {
// Override the app locale using the BCP 47 tag from shared preferences

View file

@ -43,19 +43,20 @@ public class DisplayLanguages {
new DisplayLanguageType("ar-SA", "(Arabic) العربية"),
new DisplayLanguageType("az-AZ", "Azərbaycanca (Azəricə)"),
new DisplayLanguageType("bwr-NG", "Bura-Pabir"),
new DisplayLanguageType("zh-CN", "中文(简体) (Simplified Chinese)"),
new DisplayLanguageType("cs-CZ", "čeština (Czech)"),
new DisplayLanguageType("ff-NG", "Ɗemngal Fulfulde"),
new DisplayLanguageType("de-DE", "Deutsch (German)"),
new DisplayLanguageType("en", "English"),
new DisplayLanguageType("es-ES", "Español (Spanish)"),
new DisplayLanguageType("es-419", "Español (Spanish - Latin America)"),
new DisplayLanguageType("fr-FR", "French"),
new DisplayLanguageType("fr-FR", "français (French)"),
new DisplayLanguageType("ha-HG", "Hausa"),
new DisplayLanguageType("in-ID", "Indonesian"),
new DisplayLanguageType("it-IT", "Italiana"),
new DisplayLanguageType("de-DE", "German"),
new DisplayLanguageType("kn-IN", "ಕನ್ನಡ (Kannada)"),
new DisplayLanguageType("kr-NG", "Kanuri"),
new DisplayLanguageType("km-KH", "Khmer"),
new DisplayLanguageType("km-KH", "ខ្មែរ (Khmer)"),
new DisplayLanguageType("ckl-NG", "Kibaku"),
new DisplayLanguageType("mfi-NG", "Mandara (Wandala)"),
new DisplayLanguageType("mrt-NG", "Marghi"),
@ -67,12 +68,11 @@ public class DisplayLanguages {
new DisplayLanguageType("pt-PT", "Português de Portugal"),
new DisplayLanguageType("ff-ZA", "Pulaar-Fulfulde"), // or Fulah
new DisplayLanguageType("ru-RU", "Русский (Russian)"),
new DisplayLanguageType("shu-latn", "Shuwa (Latin)"),
new DisplayLanguageType("sv-SE", "svenska (Swedish)"),
new DisplayLanguageType("shu-latn", "Shuwa (Latin script)"),
new DisplayLanguageType("sv-SE", "Svenska (Swedish)"),
new DisplayLanguageType("vi-VN", "Tiếng Việt (Vietnamese)"),
new DisplayLanguageType("uk-UA", "Українська (Ukrainian)"),
new DisplayLanguageType("hia-NG", "Waha"),
new DisplayLanguageType("zh-CN", "中文(简体) (Simplified Chinese)")
new DisplayLanguageType("hia-NG", "Waha")
};
return languages;
}

View file

@ -64,6 +64,8 @@ public class KMHelpFileActivity extends BaseActivity {
}
setContentView(R.layout.activity_help_file_layout);
setupEdgeToEdge(R.id.help_file_layout);
setupStatusBarColors(android.R.color.white, android.R.color.white);
Bundle bundle = getIntent().getExtras();
pkgID = "";

View file

@ -1,5 +1,5 @@
/**
* Copyright (C) 2017-2018 SIL International. All rights reserved.
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
package com.keyman.engine;
@ -23,6 +23,7 @@ import com.keyman.engine.util.DependencyUtil.LibraryType;
import com.keyman.engine.util.FileUtils;
import com.keyman.engine.util.KMLog;
import com.keyman.engine.util.KMString;
import com.keyman.engine.util.WebViewUtils;
import android.annotation.SuppressLint;
import android.content.Context;
@ -85,7 +86,8 @@ final class KMKeyboard extends WebView {
private static String txtFont = "";
private static String oskFont = null;
private static String keyboardRoot = "";
private static String dataRoot = "";
private static String packageRoot = "";
private final String fontUndefined = "undefined";
private GestureDetector gestureDetector;
private static ArrayList<OnKeyboardEventListener> kbEventListeners = null;
@ -366,8 +368,7 @@ final class KMKeyboard extends WebView {
} else {
KMManager.SystemKeyboardWebViewClient.setKeyboardLoaded(false);
}
String htmlPath = "file://" + getContext().getDir("data", Context.MODE_PRIVATE) + "/" + KMManager.KMFilename_KeyboardHtml;
String htmlPath = WebViewUtils.buildAssetUrl(KMManager.KMFilename_KeyboardHtml);
loadUrl(htmlPath);
setBackgroundColor(0);
}
@ -453,10 +454,22 @@ final class KMKeyboard extends WebView {
public void onResume() {
DisplayMetrics dms = context.getResources().getDisplayMetrics();
int kbWidth = (int) (dms.widthPixels / dms.density);
// Get the correct keyboard height for current orientation
int kbHeight = KMManager.getKeyboardHeight(context);
// Check if there's a pending height update for this specific keyboard type and orientation
int currentOrientation = KMManager.getOrientation(context);
if (KMManager.getAndClearPendingHeightUpdate(this.keyboardType, currentOrientation)) {
// There was a pending height update - force apply it now
RelativeLayout.LayoutParams params = KMManager.getKeyboardLayoutParams();
this.setLayoutParams(params);
}
// Ensure window is loaded for javascript functions
loadJavascript(KMString.format(
"window.onload = function(){ setOskWidth(\"%d\");"+
"setOskHeight(\"0\"); };", kbWidth));
"setOskHeight(\"%d\"); };", kbWidth, kbHeight));
if (this.getShouldShowHelpBubble()) {
this.showHelpBubbleAfterDelay(2000);
}
@ -549,6 +562,7 @@ final class KMKeyboard extends WebView {
return oskFont;
}
// REVIEW: this method seems to be unused
/**
* Return the full path to the special OSK font,
* which is with all the keyboard assets at the root app_data folder
@ -634,7 +648,7 @@ final class KMKeyboard extends WebView {
}
String kbKey = KMString.format("%s_%s", languageID, keyboardID);
setKeyboardRoot(packageID);
setPackageRoot(packageID);
// Escape single-quoted names for javascript call
keyboardName = keyboardName.replaceAll("\'", "\\\\'"); // Double-escaped-backslash b/c regex.
@ -690,20 +704,20 @@ final class KMKeyboard extends WebView {
KMManager.getLatestKeyboardFileVersion(getContext(), packageID, keyboardID) : null;
}
setKeyboardRoot(packageID);
setPackageRoot(packageID);
if(kOskFont == null || kOskFont.isEmpty())
kOskFont = kFont;
JSONObject jDisplayFont = makeFontPaths(kFont);
JSONObject jOskFont = makeFontPaths(kOskFont);
JSONObject jDisplayFont = makeFontObject(kFont);
JSONObject jOskFont = makeFontObject(kOskFont);
txtFont = getFontFilename(jDisplayFont);
oskFont = getFontFilename(jOskFont);
String kbKey = KMString.format("%s_%s", languageID, keyboardID);
String keyboardPath = makeKeyboardPath(packageID, keyboardID, keyboardVersion);
String keyboardUrl = makeKeyboardUrl(packageID, keyboardID, keyboardVersion);
JSONObject reg = new JSONObject();
try {
@ -711,7 +725,7 @@ final class KMKeyboard extends WebView {
reg.put("KI", "Keyboard_" + keyboardID);
reg.put("KLC", languageID);
reg.put("KL", languageName);
reg.put("KF", keyboardPath);
reg.put("KF", keyboardUrl);
reg.put("KP", packageID);
if (jDisplayFont != null) reg.put("KFont", jDisplayFont);
@ -797,28 +811,31 @@ final class KMKeyboard extends WebView {
}
// Set the base path of the keyboard depending on the package ID
private void setKeyboardRoot(String packageID) {
private void setPackageRoot(String packageID) {
this.dataRoot = WebViewUtils.buildAssetUrl("");
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
this.keyboardRoot = (context.getDir("data", Context.MODE_PRIVATE).toString() +
File.separator + KMManager.KMDefault_UndefinedPackageID + File.separator);
this.packageRoot = this.dataRoot + KMManager.KMDefault_UndefinedPackageID + "/";
} else {
this.keyboardRoot = (context.getDir("data", Context.MODE_PRIVATE).toString() +
File.separator + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator);
this.packageRoot = this.dataRoot + KMManager.KMDefault_AssetPackages + "/" + packageID + "/";
}
}
public String getKeyboardRoot() {
return this.keyboardRoot;
private String getDataRoot() {
return this.dataRoot;
}
private String makeKeyboardPath(String packageID, String keyboardID, String keyboardVersion) {
String keyboardPath;
private String getPackageRoot() {
return this.packageRoot;
}
private String makeKeyboardUrl(String packageID, String keyboardID, String keyboardVersion) {
String keyboardUrl = getPackageRoot();
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
keyboardPath = getKeyboardRoot() + keyboardID + "-" + keyboardVersion + ".js";
keyboardUrl += keyboardID + "-" + keyboardVersion + ".js";
} else {
keyboardPath = getKeyboardRoot() + keyboardID + ".js";
keyboardUrl += keyboardID + ".js";
}
return keyboardPath;
return keyboardUrl;
}
private void sendKMWError(int lineNumber, String sourceId, String message) {
@ -1030,13 +1047,26 @@ final class KMKeyboard extends WebView {
}
/**
* Take a font JSON object and adjust to pass to JS
* 1. Replace "source" keys for "files" keys
* 2. Create full font paths for .ttf or .svg
* @param font String font JSON object as a string
* @return JSONObject of modified font information with full paths. If font is invalid, return `null`
* Create a JSON object consisting of the font family and the URLs of the
* font files on the local device.
*
* The `font` parameter can either be the filename of the font (with an
* extension recognized as font), or a Font object or JSON string.
* In the former case a new JSON object is created with the font family
* derived from the filename, and the font filename prefixed with path
* to the fonts.
* In the latter case the legacy `sources` key is renamed to `files`.
* If `files` is a single string it will be prefixed with the path to the
* fonts. If `files` is an array, the array is iterated until finding
* the first file with a font extension which is then prefixed with the
* path to the fonts.
*
* @param font A string containing either the font filename or a font JSON
* object as a string
* @return JSONObject of modified font information with full paths. If font
* is invalid, return `null`.
*/
private JSONObject makeFontPaths(String font) {
private JSONObject makeFontObject(String font) {
if(font == null || font.equals("")) {
return null;
@ -1047,14 +1077,13 @@ final class KMKeyboard extends WebView {
JSONObject jfont = new JSONObject();
jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length()-4));
JSONArray jfiles = new JSONArray();
jfiles.put(keyboardRoot + font);
String fontRoot = KMManager.isDefaultFont(font) ? getDataRoot() : getPackageRoot();
jfiles.put(fontRoot + font);
jfont.put(KMManager.KMKey_FontFiles, jfiles);
return jfont;
}
JSONObject fontObj = new JSONObject(font);
JSONArray sourceArray;
String fontFile;
// Replace "sources" key with "files"
if (fontObj.has(KMManager.KMKey_FontSource)) {
@ -1064,16 +1093,18 @@ final class KMKeyboard extends WebView {
Object obj = fontObj.get(KMManager.KMKey_FontFiles);
if (obj instanceof String) {
fontFile = fontObj.getString(KMManager.KMKey_FontFiles);
fontObj.put(KMManager.KMKey_FontFiles, keyboardRoot + obj);
String fontFile = fontObj.getString(KMManager.KMKey_FontFiles);
String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRoot() : getPackageRoot();
fontObj.put(KMManager.KMKey_FontFiles, fontRoot + obj);
return fontObj;
} else if (obj instanceof JSONArray) {
sourceArray = fontObj.optJSONArray(KMManager.KMKey_FontFiles);
JSONArray sourceArray = fontObj.optJSONArray(KMManager.KMKey_FontFiles);
if (sourceArray != null) {
for (int i = 0; i < sourceArray.length(); i++) {
fontFile = sourceArray.getString(i);
String fontFile = sourceArray.getString(i);
if (FileUtils.hasFontExtension(fontFile)) {
fontObj.put(KMManager.KMKey_FontFiles, keyboardRoot + fontFile);
String fontRoot = KMManager.isDefaultFont(fontFile) ? getDataRoot() : getPackageRoot();
fontObj.put(KMManager.KMKey_FontFiles, fontRoot + fontFile);
fontObj.remove(KMManager.KMKey_FontSource);
return fontObj;
}
@ -1082,7 +1113,6 @@ final class KMKeyboard extends WebView {
}
} catch (JSONException e) {
KMLog.LogException(TAG, "Failed to make font for '"+font+"'", e);
return null;
}
return null;

View file

@ -1,5 +1,5 @@
/**
* Copyright (C) 2023 SIL International. All rights reserved.
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
package com.keyman.engine;
@ -9,9 +9,13 @@ import android.graphics.Bitmap;
import android.graphics.RectF;
import android.net.Uri;
import android.util.Log;
import android.webkit.WebResourceRequest;
import android.webkit.WebResourceResponse;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.RelativeLayout;
import androidx.webkit.WebViewAssetLoader;
import androidx.webkit.WebViewAssetLoader.InternalStoragePathHandler;
import com.keyman.engine.KeyboardEventHandler.EventType;
import com.keyman.engine.KMManager;
@ -19,6 +23,7 @@ import com.keyman.engine.KMManager.KeyboardType;
import com.keyman.engine.KMManager.SuggestionType;
import com.keyman.engine.util.KMLog;
import com.keyman.engine.data.Keyboard;
import com.keyman.engine.util.WebViewUtils;
import org.json.JSONObject;
@ -31,11 +36,16 @@ public final class KMKeyboardWebViewClient extends WebViewClient {
public Context context;
private KeyboardType keyboardType;
private boolean keyboardLoaded;
private WebViewAssetLoader assetLoader;
KMKeyboardWebViewClient(Context context, KeyboardType keyboardType) {
this.context = context;
this.keyboardType = keyboardType;
this.keyboardLoaded = false;
this.assetLoader = new WebViewAssetLoader.Builder()
.addPathHandler(WebViewUtils.ASSET_DATA_PATH,
new InternalStoragePathHandler(context, context.getDir("data", Context.MODE_PRIVATE)))
.build();
if (keyboardType != KeyboardType.KEYBOARD_TYPE_INAPP && keyboardType != KeyboardType.KEYBOARD_TYPE_SYSTEM) {
KMLog.LogError(TAG, String.format("Cannot initialize: Invalid keyboard type: %s", keyboardType.toString()));
@ -58,6 +68,11 @@ public final class KMKeyboardWebViewClient extends WebViewClient {
public void onPageStarted(WebView view, String url, Bitmap favicon) {
}
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
return this.assetLoader.shouldInterceptRequest(request.getUrl());
}
@Override
public void onPageFinished(WebView view, String url) {
Log.d("KMEA", String.format("onPageFinished: [%s] %s", keyboardType.toString(), url));
@ -74,46 +89,44 @@ public final class KMKeyboardWebViewClient extends WebViewClient {
kmKeyboard.keyboardSet = false;
KMManager.currentLexicalModel = null;
if (url.startsWith("file")) { // TODO: is this test necessary?
this.keyboardLoaded = true;
this.keyboardLoaded = true;
SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
int index = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0);
if (index < 0) {
index = 0;
SharedPreferences prefs = context.getSharedPreferences(context.getString(R.string.kma_prefs_name), Context.MODE_PRIVATE);
int index = prefs.getInt(KMManager.KMKey_UserKeyboardIndex, 0);
Keyboard keyboardInfo = null;
if (index >= 0) {
keyboardInfo = KMManager.getKeyboardInfo(context, index);
}
String langId = null;
if (keyboardInfo != null) {
langId = keyboardInfo.getLanguageID();
kmKeyboard.setKeyboard(keyboardInfo);
} else {
// Revert to default (index 0) or fallback keyboard
keyboardInfo = KMManager.getKeyboardInfo(context, 0);
if (keyboardInfo == null) {
// Don't log to Sentry because some keyboard apps like FV don't install keyboards until the user chooses
keyboardInfo = KMManager.getDefaultKeyboard(context);
}
Keyboard keyboardInfo = KMManager.getKeyboardInfo(context, index);
String langId = null;
if (keyboardInfo != null) {
langId = keyboardInfo.getLanguageID();
kmKeyboard.setKeyboard(keyboardInfo);
} else {
// Revert to default (index 0) or fallback keyboard
keyboardInfo = KMManager.getKeyboardInfo(context, 0);
if (keyboardInfo == null) {
// Don't log to Sentry because some keyboard apps like FV don't install keyboards until the user chooses
keyboardInfo = KMManager.getDefaultKeyboard(context);
}
if (keyboardInfo != null) {
langId = keyboardInfo.getLanguageID();
kmKeyboard.setKeyboard(keyboardInfo);
}
}
}
KMManager.registerAssociatedLexicalModel(langId);
KMManager.registerAssociatedLexicalModel(langId);
kmKeyboard.showHelpBubbleAfterDelay(2000, true); // check if it should be shown at that time!
kmKeyboard.showHelpBubbleAfterDelay(2000, true); // check if it should be shown at that time!
kmKeyboard.callJavascriptAfterLoad();
kmKeyboard.setSpacebarText(KMManager.getSpacebarText());
kmKeyboard.callJavascriptAfterLoad();
kmKeyboard.setSpacebarText(KMManager.getSpacebarText());
KeyboardEventHandler.notifyListeners(KMTextView.kbEventListeners, keyboardType, EventType.KEYBOARD_LOADED, null);
KeyboardEventHandler.notifyListeners(KMTextView.kbEventListeners, keyboardType, EventType.KEYBOARD_LOADED, null);
// Special handling for in-app TextView context keymanapp/keyman#3809
if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP &&
KMTextView.activeView != null && KMTextView.activeView.getClass() == KMTextView.class) {
KMTextView.updateTextContext();
}
// Special handling for in-app TextView context keymanapp/keyman#3809
if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP &&
KMTextView.activeView != null && KMTextView.activeView.getClass() == KMTextView.class) {
KMTextView.updateTextContext();
}
}

View file

@ -1,5 +1,5 @@
/**
* Copyright (C) 2017 SIL International. All rights reserved.
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
package com.keyman.engine;
@ -42,6 +42,7 @@ import android.view.Display;
import android.view.Surface;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowInsets;
import android.view.WindowManager;
import android.view.WindowMetrics;
import android.view.inputmethod.EditorInfo;
@ -52,6 +53,10 @@ import android.widget.FrameLayout;
import android.widget.RelativeLayout;
import androidx.core.content.ContextCompat;
import androidx.core.graphics.Insets;
import androidx.core.view.DisplayCutoutCompat;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsCompat;
import io.sentry.Breadcrumb;
import io.sentry.Sentry;
@ -86,6 +91,8 @@ public final class KMManager {
private static final String TAG = "KMManager";
private static ResourcesUpdateTool updateTool;
private static int inappBottomInset; // Bottom inset for in-app keyboard
private static int sysBottomInset; // Bottom inset for system keyboard
// Keyboard types
public enum KeyboardType {
@ -301,6 +308,12 @@ public final class KMManager {
private static final String KMKey_KeyboardHeightPortrait = "keyboardHeightPortrait";
private static final String KMKey_KeyboardHeightLandscape = "keyboardHeightLandscape";
// Preference keys for tracking pending keyboard height updates per keyboard type and orientation
private static final String KMKey_PendingHeightUpdate_InappPortrait = "pendingHeightUpdate_inapp_portrait";
private static final String KMKey_PendingHeightUpdate_InappLandscape = "pendingHeightUpdate_inapp_landscape";
private static final String KMKey_PendingHeightUpdate_SystemPortrait = "pendingHeightUpdate_system_portrait";
private static final String KMKey_PendingHeightUpdate_SystemLandscape = "pendingHeightUpdate_system_landscape";
public static final String KMKey_LongpressDelay = "longpressDelay";
public static final String KMKey_CustomHelpLink = "CustomHelpLink";
@ -393,6 +406,10 @@ public final class KMManager {
return getResourceRoot() + KMDefault_LexicalModelPackages + File.separator;
}
public static String getLexicalModelsUrl() {
return WebViewUtils.buildAssetUrl(KMDefault_LexicalModelPackages + "/");
}
public static String getCloudDir() {
return getResourceRoot() + KMDefault_UndefinedPackageID + File.separator;
}
@ -483,6 +500,19 @@ public final class KMManager {
return false;
}
/**
* Check if the given font is the default font or the default OSK font.
* @param fontName String - The font name and extension
* @return boolean true if `fontName` is DejaVuSans.ttf or keymanweb-osk.ttf
* or null or empty string, false otherwise
*/
public static boolean isDefaultFont(String fontName) {
if (fontName == null || fontName.isEmpty()) {
return true;
}
return fontName.equals(KMDefault_KeyboardFont) || fontName.equals(KMFilename_Osk_Ttf_Font);
}
public static void initialize(final Context context, KeyboardType keyboardType) {
appContext = context.getApplicationContext();
@ -495,6 +525,11 @@ public final class KMManager {
didCopyAssets = true;
}
calculateDefaultKeyboardHeights(context);
SharedPreferences prefs = context.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
KeyboardHeight_Context_Portrait_Current = prefs.getInt(KMManager.KMKey_KeyboardHeightPortrait, KMManager.KeyboardHeight_Context_Portrait_Default);
KeyboardHeight_Context_Landscape_Current = prefs.getInt(KMManager.KMKey_KeyboardHeightLandscape, KMManager.KeyboardHeight_Context_Landscape_Default);
if (keyboardType == KeyboardType.KEYBOARD_TYPE_UNDEFINED) {
String msg = "Cannot initialize: Invalid keyboard type";
KMLog.LogError(TAG, msg);
@ -508,12 +543,6 @@ public final class KMManager {
migrateCloudKeyboards(appContext);
CloudDownloadMgr.getInstance().initialize(appContext);
calculateDefaultKeyboardHeights(context);
SharedPreferences prefs = context.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
KeyboardHeight_Context_Portrait_Current = prefs.getInt(KMManager.KMKey_KeyboardHeightPortrait, KMManager.KeyboardHeight_Context_Portrait_Default);
KeyboardHeight_Context_Landscape_Current = prefs.getInt(KMManager.KMKey_KeyboardHeightLandscape, KMManager.KeyboardHeight_Context_Landscape_Default);
}
public static void executeResourceUpdate(Context aContext)
@ -692,6 +721,11 @@ public final class KMManager {
/**
* Adjust the keyboard dimensions. If the suggestion banner is active, use the
* combined banner height and keyboard height
*
* Android API 35+ enforces edge-to-edge so we need to determine the navigation bar height (if visible)
* so the keyboard isn't covered up by the navigation bar.
* In portrait orientation, this affects the keyboard bottom margin.
* On phones in landscape orientation, this affects the left/right side of the keyboard
* @return RelativeLayout.LayoutParams
*/
public static RelativeLayout.LayoutParams getKeyboardLayoutParams() {
@ -700,7 +734,54 @@ public final class KMManager {
RelativeLayout.LayoutParams params = new RelativeLayout.LayoutParams(
RelativeLayout.LayoutParams.MATCH_PARENT, bannerHeight + kbHeight);
params.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM, RelativeLayout.TRUE);
return params;
return params;
}
/**
* Applies padding to the keyboard so it's not covered by system insets
* @param {KeyboardType} keyboard
* @param left int padding to account for notch/navigation bar on left side of screen
* @param right int padding to account for notch/navigation bar on right side of screen
* @param bottom int padding to account for navigation bar on bottom of screen
*/
public static void applyInsetsToKeyboard(KeyboardType keyboardType, int left, int right, int bottom) {
if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP && InAppKeyboard != null) {
applyInsetsPaddingToKeyboardView(InAppKeyboard, left, right, bottom);
}
if (keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM && SystemKeyboard != null) {
applyInsetsPaddingToKeyboardView(SystemKeyboard, left, right, bottom);
}
}
private static void applyInsetsPaddingToKeyboardView(KMKeyboard keyboard, int left, int right, int bottom) {
View parent = (View) keyboard.getParent();
if (keyboard.keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP) {
inappBottomInset = bottom;
} else if (keyboard.keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM) {
sysBottomInset = bottom;
}
if (parent != null) {
// Keep existing top padding, add bottom padding for the inset
parent.setPadding(
left,
parent.getPaddingTop(),
right,
bottom
);
// No need to change WebView margins; keep height as-is
parent.requestLayout();
} else {
// Fallback: apply on the WebView itself
keyboard.setPadding(
left,
keyboard.getPaddingTop(),
right,
bottom
);
keyboard.requestLayout();
}
}
private static void initKeyboard(Context appContext, KeyboardType keyboardType) {
@ -831,28 +912,46 @@ public final class KMManager {
@SuppressLint("InflateParams")
public static View createInputView(InputMethodService inputMethodService) {
//final Context context = appContext;
IMService = inputMethodService;
Context appContext = IMService.getApplicationContext();
final FrameLayout mainLayout = new FrameLayout(appContext);
mainLayout.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
if (SystemKeyboard == null) {
installSystemKeyboardInsetsListener(mainLayout);
return mainLayout;
}
RelativeLayout keyboardLayout = new RelativeLayout(appContext);
keyboardLayout.setLayoutParams(new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT));
ViewGroup parent = (ViewGroup) SystemKeyboard.getParent();
if (parent != null)
if (parent != null) {
parent.removeView(SystemKeyboard);
}
keyboardLayout.addView(SystemKeyboard);
mainLayout.addView(keyboardLayout);
//mainLayout.addView(overlayLayout);
installSystemKeyboardInsetsListener(mainLayout);
return mainLayout;
}
private static void installSystemKeyboardInsetsListener(View anchor) {
ViewCompat.setOnApplyWindowInsetsListener(anchor, (v, insets) -> {
// System bars (status bar and navigation bar)
Insets systemBarInsets = insets.getInsets(
WindowInsetsCompat.Type.systemBars() | WindowInsetsCompat.Type.displayCutout());
sysBottomInset = systemBarInsets.bottom;
applyInsetsToKeyboard(KeyboardType.KEYBOARD_TYPE_SYSTEM, systemBarInsets.left, systemBarInsets.right, sysBottomInset);
return insets;
});
// Trigger an initial pass
ViewCompat.requestApplyInsets(anchor);
}
public static void onStartInput(EditorInfo attribute, boolean restarting) {
if (!restarting && SystemKeyboard != null) {
String packageName = attribute.packageName;
@ -1568,8 +1667,9 @@ public final class KMManager {
String modelID = lexicalModelInfo.get(KMKey_LexicalModelID);
String languageID = lexicalModelInfo.get(KMKey_LanguageID);
boolean modelFileExists = true;
File modelFile = new File(getLexicalModelsDir(), pkgID + File.separator + modelID + ".model.js");
String path = "file://" + modelFile.getAbsolutePath();
String modelFilename = pkgID + File.separator + modelID + ".model.js";
File modelFile = new File(getLexicalModelsDir(), modelFilename);
String url = getLexicalModelsUrl() + modelFilename;
// Disable sugestions if lexical-model file doesn't exist
if (!modelFile.exists()) {
@ -1584,7 +1684,7 @@ public final class KMManager {
modelObj.put("id", modelID);
languageJSONArray.put(languageID);
modelObj.put("languages", languageJSONArray);
modelObj.put("path", path);
modelObj.put("path", url);
modelObj.put("CustomHelpLink", lexicalModelInfo.get(KMKey_CustomHelpLink));
} catch (JSONException e) {
KMLog.LogException(TAG, "Invalid lexical model to register", e);
@ -2143,7 +2243,12 @@ public final class KMManager {
File kmpJSONFile = new File(path);
if (!kmpJSONFile.exists()) {
if (!KMManager.isTestMode()) {
KMLog.LogError(TAG, path + " not found. Returning version 1.0");
if (packageID.equals(KMDefault_PackageID)) {
// Suppress error log for the default fallback keyboard if it's not installed
Log.d(TAG, path + " not found. Returning version 1.0");
} else {
KMLog.LogError(TAG, path + " not found. Returning version 1.0");
}
}
return "1.0";
}
@ -2339,12 +2444,11 @@ public final class KMManager {
}
height = defaultHeightForContext;
} else {
// Applying gating to 50%-200% of default height (following Keyman)
if (height < (defaultHeightForContext / 2)) {
height = (int) (defaultHeightForContext / 2);
} else if (height > (defaultHeightForContext * 2)) {
height = (int) (defaultHeightForContext * 2);
}
// Ensure keyboard height stay within min/max bounds
int minKeyboardHeight = KMManager.getKeyboardHeightMin(context, orientation);
int maxKeyboardHeight = KMManager.getKeyboardHeightMax(context, orientation);
height = Math.max(minKeyboardHeight, height);
height = Math.min(maxKeyboardHeight, height);
// Store the new height based on the current orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
@ -2361,16 +2465,22 @@ public final class KMManager {
KeyboardHeight_Context_Portrait_Current = height;
}
editor.commit();
// Confirm new LayoutParams for in-app or system keyboards
// If keyboard is not currently loaded, set a pending flag so height gets applied when keyboard loads
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_INAPP)) {
InAppKeyboard.loadJavascript(KMString.format("setOskHeight('%s')", height));
RelativeLayout.LayoutParams params = getKeyboardLayoutParams();
InAppKeyboard.setLayoutParams(params);
} else {
setPendingHeightUpdate(KeyboardType.KEYBOARD_TYPE_INAPP, orientation);
}
if (isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)) {
SystemKeyboard.loadJavascript(KMString.format("setOskHeight('%s')", height));
RelativeLayout.LayoutParams params = getKeyboardLayoutParams();
SystemKeyboard.setLayoutParams(params);
} else {
setPendingHeightUpdate(KeyboardType.KEYBOARD_TYPE_SYSTEM, orientation);
}
}
@ -2421,6 +2531,179 @@ public final class KMManager {
KeyboardHeight_Context_Landscape_Default = (int) newResources.getDimension(R.dimen.keyboard_height);
}
/**
* Get the navigation bar height (if visible) in pixels
* @param context - The context
* @param {KeyboardType} - KeyboardType.KEYBOARD_TYPE_INAPP or KeyboardType.KEYBOARD_TYPE_SYSTEM
* @return int - navigation bar height in pixels
*/
public static int getNavigationBarHeight(Context context, KeyboardType keyboardType) {
// bottomInset already updated from the inset listener
if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP) {
return inappBottomInset;
} else if (keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM) {
return sysBottomInset;
}
// Navigation bar height uninitialized, so log and revert to legacy computation.
// Note. Inconsistent for gesture vs 3-button navigation mode #14893
KMLog.LogError(TAG, "bottom inset not initialized for keyboard type: " + keyboardType);
int resourceId = context.getResources().getIdentifier(
"navigation_bar_height", "dimen", "android");
return (resourceId > 0) ? context.getResources().getDimensionPixelSize(resourceId) : 0;
}
/**
* Returns the maximum allowed height of the keyboard frame for the current device orientation.
* The maximum height is calculated as twice the default keyboard height for the orientation.
* Use this method to get the upper bound for keyboard height adjustments.
*
* @param context The context.
* @return The maximum allowed keyboard height in density-independent pixels (dp),
* or {@link #KeyboardHeight_Reset} (0) if orientation is invalid.
* @see #getKeyboardHeightMax(Context, int)
* @see #getKeyboardHeightMin(Context)
* @see #applyKeyboardHeight(Context, int)
*/
public static int getKeyboardHeightMax(Context context) {
int orientation = getOrientation(context);
return getKeyboardHeightMax(context, orientation);
}
/**
* Returns the maximum allowed height of the keyboard frame for the specified orientation.
* The maximum height is calculated as twice the default keyboard height for the orientation.
* Use this method to get the upper bound for keyboard height adjustments.
*
* @param context The context.
* @param orientation The screen orientation: {@link Configuration#ORIENTATION_PORTRAIT}
* or {@link Configuration#ORIENTATION_LANDSCAPE}.
* @return The maximum allowed keyboard height in density-independent pixels (dp),
* or {@link #KeyboardHeight_Reset} (0) if orientation is invalid.
* @see #getKeyboardHeightMin(Context, int)
* @see #applyKeyboardHeight(Context, int, int)
*/
public static int getKeyboardHeightMax(Context context, int orientation) {
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
return (int) KMManager.KeyboardHeight_Context_Landscape_Default * 2;
} else /* orientation == Configuration.ORIENTATION_PORTRAIT */ {
return (int) KMManager.KeyboardHeight_Context_Portrait_Default * 2;
}
}
/**
* Returns the minimum allowed height of the keyboard frame for the current device orientation.
* The minimum height is calculated as half the default keyboard height for the orientation.
* Use this method to get the lower bound for keyboard height adjustments.
*
* @param context The context.
* @return The minimum allowed keyboard height in density-independent pixels (dp),
* or {@link #KeyboardHeight_Reset} (0) if orientation is invalid.
* @see #getKeyboardHeightMin(Context, int)
* @see #getKeyboardHeightMax(Context)
* @see #applyKeyboardHeight(Context, int)
*/
public static int getKeyboardHeightMin(Context context) {
int orientation = getOrientation(context);
return getKeyboardHeightMin(context, orientation);
}
/**
* Returns the minimum allowed height of the keyboard frame for the specified orientation.
* The minimum height is calculated as half the default keyboard height for the orientation.
* Use this method to get the lower bound for keyboard height adjustments.
*
* @param context The context.
* @param orientation The screen orientation: {@link Configuration#ORIENTATION_PORTRAIT}
* or {@link Configuration#ORIENTATION_LANDSCAPE}.
* @return The minimum allowed keyboard height in density-independent pixels (dp),
* or {@link #KeyboardHeight_Reset} (0) if orientation is invalid.
* @see #getKeyboardHeightMax(Context, int)
* @see #applyKeyboardHeight(Context, int, int)
*/
public static int getKeyboardHeightMin(Context context, int orientation) {
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
return (int) KMManager.KeyboardHeight_Context_Landscape_Default / 2;
} else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
return (int) KMManager.KeyboardHeight_Context_Portrait_Default / 2;
} else {
return KMManager.KeyboardHeight_Reset;
}
}
/**
* Returns the preference key for tracking pending keyboard height updates.
* This allows separate tracking for each keyboard type (in-app/system) and orientation.
*
* @param keyboardType The keyboard type: {@link KeyboardType#KEYBOARD_TYPE_INAPP}
* or {@link KeyboardType#KEYBOARD_TYPE_SYSTEM}.
* @param orientation The screen orientation: {@link Configuration#ORIENTATION_PORTRAIT}
* or {@link Configuration#ORIENTATION_LANDSCAPE}.
* @return The preference key string, or null if parameters are invalid.
*/
private static String getPendingHeightKey(KeyboardType keyboardType, int orientation) {
if (keyboardType == KeyboardType.KEYBOARD_TYPE_INAPP) {
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
return KMKey_PendingHeightUpdate_InappPortrait;
} else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
return KMKey_PendingHeightUpdate_InappLandscape;
}
} else if (keyboardType == KeyboardType.KEYBOARD_TYPE_SYSTEM) {
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
return KMKey_PendingHeightUpdate_SystemPortrait;
} else if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
return KMKey_PendingHeightUpdate_SystemLandscape;
}
}
return null;
}
/**
* Sets a flag indicating that a keyboard height update is pending for the specified
* keyboard type and orientation. This is used when the keyboard is not currently loaded,
* so the height update will be applied when the keyboard loads.
*
* @param keyboardType The keyboard type: {@link KeyboardType#KEYBOARD_TYPE_INAPP}
* or {@link KeyboardType#KEYBOARD_TYPE_SYSTEM}.
* @param orientation The screen orientation: {@link Configuration#ORIENTATION_PORTRAIT}
* or {@link Configuration#ORIENTATION_LANDSCAPE}.
*/
private static void setPendingHeightUpdate(KeyboardType keyboardType, int orientation) {
String pendingKey = getPendingHeightKey(keyboardType, orientation);
if (pendingKey == null) {
return;
}
SharedPreferences prefs = appContext.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean(pendingKey, true);
editor.commit();
}
/**
* Checks if there is a pending keyboard height update for the specified keyboard type
* and orientation, and clears the flag if present.
*
* @param keyboardType The keyboard type: {@link KeyboardType#KEYBOARD_TYPE_INAPP}
* or {@link KeyboardType#KEYBOARD_TYPE_SYSTEM}.
* @param orientation The screen orientation: {@link Configuration#ORIENTATION_PORTRAIT}
* or {@link Configuration#ORIENTATION_LANDSCAPE}.
* @return true if there was a pending height update (which has been cleared), false otherwise.
*/
protected static boolean getAndClearPendingHeightUpdate(KeyboardType keyboardType, int orientation) {
String pendingKey = getPendingHeightKey(keyboardType, orientation);
if (pendingKey == null) {
return false;
}
SharedPreferences prefs = appContext.getSharedPreferences(KMManager.KMEngine_PrefsKey, Context.MODE_PRIVATE);
boolean hasPending = prefs.getBoolean(pendingKey, false);
if (hasPending) {
SharedPreferences.Editor editor = prefs.edit();
editor.remove(pendingKey);
editor.commit();
}
return hasPending;
}
/**
* Get the size of the area the window would occupy.
* API 30+

View file

@ -1,5 +1,5 @@
/**
* Copyright (C) 2020 SIL International. All rights reserved.
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
package com.keyman.engine.data;
@ -15,6 +15,7 @@ import com.keyman.engine.util.BCP47;
import com.keyman.engine.util.FileUtils;
import com.keyman.engine.util.KMLog;
import com.keyman.engine.util.KMString;
import com.keyman.engine.util.WebViewUtils;
import org.json.JSONArray;
import org.json.JSONException;
@ -173,24 +174,24 @@ public class Keyboard extends LanguageResource implements Serializable {
return o;
}
private String getKeyboardRoot(Context context) {
String keyboardRoot = context.getDir("data", Context.MODE_PRIVATE).toString() +
File.separator;
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
return keyboardRoot + KMManager.KMDefault_UndefinedPackageID + File.separator;
} else {
return keyboardRoot + KMManager.KMDefault_AssetPackages + File.separator + packageID + File.separator;
}
private String getDataRoot() {
return WebViewUtils.buildAssetUrl("");
}
public String getKeyboardPath(Context context) {
private String getPackageRoot() {
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
return getDataRoot() + KMManager.KMDefault_UndefinedPackageID + "/";
}
return getDataRoot() + KMManager.KMDefault_AssetPackages + "/" + packageID + "/";
}
private String getKeyboardUrl() {
String keyboardID = this.getKeyboardID();
String keyboardVersion = this.getVersion();
if (packageID.equals(KMManager.KMDefault_UndefinedPackageID)) {
return getKeyboardRoot(context) + keyboardID + "-" + keyboardVersion + ".js";
return getPackageRoot() + keyboardID + "-" + keyboardVersion + ".js";
} else {
return getKeyboardRoot(context) + keyboardID + ".js";
return getPackageRoot() + keyboardID + ".js";
}
}
@ -202,17 +203,17 @@ public class Keyboard extends LanguageResource implements Serializable {
stubObj.put("KI", "Keyboard_" + this.getKeyboardID());
stubObj.put("KLC", this.getLanguageID());
stubObj.put("KL", this.getLanguageName());
stubObj.put("KF", this.getKeyboardPath(context));
stubObj.put("KF", this.getKeyboardUrl());
stubObj.put("KP", this.getPackageID());
String displayFont = this.getFont();
if(displayFont != null) {
stubObj.put("KFont", this.buildDisplayFontObject(displayFont, context));
stubObj.put("KFont", this.buildDisplayFontObject(displayFont));
}
String oskFont = this.getOSKFont();
if(oskFont != null) {
stubObj.put("KOskFont", this.buildDisplayFontObject(oskFont, context));
stubObj.put("KOskFont", this.buildDisplayFontObject(oskFont));
}
String displayName = this.getDisplayName();
@ -228,34 +229,33 @@ public class Keyboard extends LanguageResource implements Serializable {
}
/**
* Take a font JSON object and adjust to pass to JS
* 1. Replace "source" keys for "files" keys
* 2. Create full font paths for .ttf or .svg
* @param font String font JSON object as a string
* @return JSONObject of modified font information with full paths. If font is invalid, return `null`
* Create a JSON object consisting of the font family and the font
* file URL.
*
* @param font A string containing the font filename
* @return JSONObject of modified font information with full URL. If font
* is invalid, return `null`.
*/
private JSONObject buildDisplayFontObject(String font, Context context) {
private JSONObject buildDisplayFontObject(String font) {
if(font == null || font.equals("")) {
return null;
}
String keyboardRoot = this.getKeyboardRoot(context);
String fontRoot = KMManager.isDefaultFont(font) ? this.getDataRoot() : this.getPackageRoot();
try {
if (FileUtils.hasFontExtension(font)) {
JSONObject jfont = new JSONObject();
jfont.put(KMManager.KMKey_FontFamily, font.substring(0, font.length() - 4));
JSONArray jfiles = new JSONArray();
jfiles.put(keyboardRoot + font);
jfiles.put(fontRoot + font);
jfont.put(KMManager.KMKey_FontFiles, jfiles);
return jfont;
} else {
return null;
}
} catch (JSONException e) {
KMLog.LogException(TAG, "Failed to make font for '"+font+"'", e);
return null;
}
return null;
}
/**

View file

@ -32,9 +32,38 @@ public final class WebViewUtils {
FULL; // WebView installed and enabled
}
// Min version of Chrome for Keyman for Android for EngineWebViewVersionStatus.DEGRADED
public static final String KEYMAN_MIN_TARGET_VERSION_DEGRADED_ANDROID_CHROME = "37.0";
// Min version of Chrome for Keyman for Android (EngineWebViewVersionStatus.FULL)
// TODO: Keep this version in sync with resources/build/minimum-versions.inc.sh
public static final String KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME = "95.0";
private static final String CHROME_INSTALL_PATTERN_FORMATSTR = "^.*Chrome/([\\d.]+).*$";
private static final Pattern installPattern = Pattern.compile(CHROME_INSTALL_PATTERN_FORMATSTR);
/**
* Reserved magic domain for loading files from the local device. At runtime
* the WebViewAssetLoader will replace the protocol and domain with the
* internal storage path.
* See https://developer.android.com/reference/androidx/webkit/WebViewAssetLoader
*/
private static final String MAGIC_DEFAULT_DOMAIN = "https://appassets.androidplatform.net";
/**
* Path under the asset domain where all assets live
*/
public static final String ASSET_DATA_PATH = "/data/";
/**
* Build a full URL to the provided asset
*/
public static String buildAssetUrl(String assetPath) {
String appendAsset = assetPath == null ? "" :
(assetPath.startsWith("/") ? assetPath.substring(1) : assetPath);
return WebViewUtils.MAGIC_DEFAULT_DOMAIN + WebViewUtils.ASSET_DATA_PATH + appendAsset;
}
/**
* Get the Keyman Engine mode based on the Chrome version.
* @param context - The context
@ -52,9 +81,11 @@ public final class WebViewUtils {
chromeVersion = getChromeVersion(context, webView);
}
if (FileUtils.compareVersions("37.0", chromeVersion) == FileUtils.VERSION_GREATER) {
if (FileUtils.compareVersions(KEYMAN_MIN_TARGET_VERSION_DEGRADED_ANDROID_CHROME, chromeVersion)
== FileUtils.VERSION_GREATER) {
return EngineWebViewVersionStatus.DISABLED;
} else if (FileUtils.compareVersions("57.0", chromeVersion) == FileUtils.VERSION_GREATER) {
} else if (FileUtils.compareVersions(KEYMAN_MIN_TARGET_VERSION_ANDROID_CHROME, chromeVersion)
== FileUtils.VERSION_GREATER) {
return EngineWebViewVersionStatus.DEGRADED;
}

View file

@ -1,5 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/help_file_layout"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
@ -9,36 +12,28 @@
<WebView
android:id="@+id/kmHelpFileWebView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="0dp"
android:layout_below="@+id/titlebar"
android:layout_above="@+id/buttonBar" />
<LinearLayout
android:id="@+id/buttonBar"
style="?android:attr/buttonBarStyle"
android:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<!-- Spacer to pad buttons to the right -->
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
app:layout_constraintTop_toBottomOf="@id/titlebar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toTopOf="@+id/finishButton" />
<Button
android:id="@+id/finishButton"
android:id="@id/finishButton"
style="@style/Widget.AppCompat.Button.Borderless.Colored"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/label_ok"
android:layout_marginStart="10dp"
android:layout_marginEnd="15dp"
android:layout_gravity="end"
android:elevation="1dp"
android:singleLine="true"
android:layout_marginStart="10dp" android:layout_marginEnd="15dp">
<requestFocus/>
android:text="@string/label_ok"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0">
<requestFocus />
</Button>
</LinearLayout>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,13 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/activity_list_layout"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/list_toolbar" />
<include layout="@layout/list_toolbar"
android:id="@+id/list_appbar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<include layout="@layout/list_layout" />
<include layout="@layout/list_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@+id/list_appbar"
app:layout_constraintStart_toStartOf="parent" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,12 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/list_toolbar" />
<include layout="@layout/list_toolbar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<!-- ListView of installed keyboards. TODO: calculate height -->
<include
@ -14,45 +17,35 @@
layout="@layout/list_layout"
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_above="@+id/keyboard_linear_layout"
android:layout_below="@id/list_appbar" />
android:layout_above="@+id/modeling_settings_layout"
android:layout_below="@id/list_appbar"
app:layout_constraintTop_toBottomOf="@id/list_appbar"/>
<LinearLayout
android:id="@+id/keyboard_linear_layout"
android:layout_width="match_parent"
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_button"
style="?android:attr/actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_above="@id/modeling_settings_layout"
android:orientation="horizontal">
<Space
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="10" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_button"
style="?android:attr/actionButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginTop="@dimen/fab_margin"
android:layout_marginBottom="@dimen/fab_margin"
android:layout_weight="1"
android:contentDescription="@string/image_button"
android:src="@drawable/ic_add_white"
app:fabSize="normal" />
</LinearLayout>
android:layout_gravity="end|center_vertical"
android:contentDescription="@string/image_button"
android:src="@drawable/ic_add_white"
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="@dimen/fab_margin"
app:fabSize="normal"
app:layout_constraintBottom_toTopOf="@+id/modeling_settings_layout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"/>
<LinearLayout
android:id="@+id/modeling_settings_layout"
android:id="@id/modeling_settings_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_marginTop="0dp"
android:layout_marginBottom="0dp"
android:orientation="vertical">
android:orientation="vertical"
app:layout_constraintVertical_bias="1.0"
app:layout_constraintBottom_toBottomOf="parent">
<View
android:id="@+id/separator"
@ -117,4 +110,4 @@
android:layout_height="wrap_content"
android:layout_below="@id/model_picker"/> -->
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,31 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/languages_list_layout"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<include layout="@layout/list_toolbar" />
<include layout="@layout/list_toolbar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<include
android:id="@+id/keyboard_list_layout"
layout="@layout/list_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/list_appbar" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:orientation="horizontal">
<Space
android:layout_width="wrap_content"
<include
android:id="@+id/keyboard_list_layout"
layout="@layout/list_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_weight="10" />
android:layout_below="@+id/list_appbar"
app:layout_constraintTop_toBottomOf="@id/list_appbar"
app:layout_constraintStart_toStartOf="parent"/>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/add_button"
@ -33,12 +27,13 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:layout_marginTop="@dimen/fab_margin"
android:layout_marginBottom="@dimen/fab_margin"
android:layout_weight="1"
android:contentDescription="@string/image_button"
android:src="@drawable/ic_add_white"
app:fabSize="normal" />
android:layout_marginEnd="@dimen/fab_margin"
android:layout_marginBottom="@dimen/fab_margin"
app:fabSize="normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"/>
</LinearLayout>
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1,15 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<!-- paddingBottom so last entry doesn't get covered by navigation bar -->
<ListView
style="@style/AppTheme.Light.ListView"
android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:choiceMode="singleChoice"
android:clipToPadding="false" />
android:clipToPadding="false"
android:paddingBottom="?attr/actionBarSize"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"/>
</RelativeLayout>

View file

@ -1,16 +1,20 @@
<com.google.android.material.appbar.AppBarLayout
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.appbar.AppBarLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/list_appbar"
app:theme="@style/AppTheme.ToolbarBackArrow"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:elevation="0dp">
<androidx.appcompat.widget.Toolbar
android:id="@+id/list_toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize">
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/bar_title"
@ -32,6 +36,8 @@
style="@style/AppTheme.Divider"
android:layout_width="match_parent"
android:layout_height="2dp"
android:layout_alignParentBottom="true"/>
app:layout_constraintTop_toBottomOf="@id/list_toolbar"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent" />
</com.google.android.material.appbar.AppBarLayout>

Some files were not shown because too many files have changed in this diff Show more