diff --git a/.gitattributes b/.gitattributes index cd9921d963..22ad2cc2a3 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,3 +7,4 @@ # EOF # ``` Dockerfile text eol=lf +*.sh text eol=lf diff --git a/.github/workflows/api-verification.yml b/.github/workflows/api-verification.yml index e8cc9aa95a..af6bc682dc 100644 --- a/.github/workflows/api-verification.yml +++ b/.github/workflows/api-verification.yml @@ -23,6 +23,7 @@ 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 @@ -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,6 +54,7 @@ jobs: -f context="$STATUS_CONTEXT" - name: Checkout + if: steps.environment_step.outputs.SKIP_API_CHECK != 'true' uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 #v4.2.2 with: ref: '${{ steps.environment_step.outputs.GIT_SHA }}' @@ -61,11 +63,13 @@ 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 @@ -79,17 +83,17 @@ jobs: verify 2>> $GITHUB_STEP_SUMMARY - name: Archive .symbols file + if: steps.environment_step.outputs.SKIP_API_CHECK != 'true' && always() uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 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' diff --git a/.github/workflows/auto-merge-keyman-server-pr.yml b/.github/workflows/auto-merge-keyman-server-pr.yml index 0c920c2650..771fd6ae5e 100644 --- a/.github/workflows/auto-merge-keyman-server-pr.yml +++ b/.github/workflows/auto-merge-keyman-server-pr.yml @@ -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: @@ -17,12 +16,15 @@ jobs: if: ${{ github.repository == 'keymanapp/keyman' && github.actor == 'keyman-server' && startsWith(github.event.pull_request.title, '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 + if: contains(github.event.pull_request.labels.*.name, 'automerge') + env: + GH_TOKEN: "${{ secrets.AUTOINC_GITHUB_TOKEN }}" + run: | + gh pr merge -R keymanapp/keyman --merge --auto ${{ github.event.pull_request.number }} diff --git a/.github/workflows/build-test-publish-docker.yml b/.github/workflows/build-test-publish-docker.yml new file mode 100644 index 0000000000..3c85d4ff91 --- /dev/null +++ b/.github/workflows/build-test-publish-docker.yml @@ -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@v4 + - 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@v4 + + - 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 diff --git a/.github/workflows/cleanup-ghcr.yml b/.github/workflows/cleanup-ghcr.yml new file mode 100644 index 0000000000..15e0a14ec8 --- /dev/null +++ b/.github/workflows/cleanup-ghcr.yml @@ -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 }} diff --git a/.github/workflows/deb-packaging.README.md b/.github/workflows/deb-packaging.README.md index 9dcfce20db..662526a875 100644 --- a/.github/workflows/deb-packaging.README.md +++ b/.github/workflows/deb-packaging.README.md @@ -18,7 +18,8 @@ You can manually trigger a deb-packaging action on GitHub, e.g. to test changes: \"baseBranch\": \"master\", \"baseRef\": \"$(git rev-parse refs/heads/master^)\", \"user\": \"${USER}\", - \"isTestBuild\": \"true\"}" \ + \"isTestBuild\": \"true\", + \"force\": \"true\"}" \ https://api.github.com/repos//keyman/dispatches ``` @@ -36,6 +37,7 @@ You can manually trigger a deb-packaging action on GitHub, e.g. to test changes: \"baseBranch\": \"master\", \"baseRef\": \"$(git rev-parse refs/heads/master)\", \"user\": \"${USER}\", - \"isTestBuild\": \"true\"}" \ + \"isTestBuild\": \"true\", + \"force\": \"true\"}" \ https://api.github.com/repos//keyman/dispatches ``` diff --git a/.github/workflows/deb-packaging.yml b/.github/workflows/deb-packaging.yml index a06d7ed3cd..28440c4211 100644 --- a/.github/workflows/deb-packaging.yml +++ b/.github/workflows/deb-packaging.yml @@ -23,6 +23,7 @@ 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" @@ -34,7 +35,7 @@ env: 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 }} @@ -86,7 +87,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 @@ -177,28 +185,36 @@ jobs: runs-on: ubuntu-latest steps: - # - name: Download Artifacts - # uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 - # with: - # path: artifacts - # merge-multiple: true + - name: Download Source Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + name: keyman-srcpkg + path: artifacts - # - name: Install dependencies - # run: | - # sudo DEBIAN_FRONTEND=noninteractive apt-get -q -y install autopkgtest qemu-system qemu-utils autodep8 genisoimage python3-distro-info + - name: Download Binary Artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 + with: + path: artifacts + pattern: keyman-binarypkgs-* + merge-multiple: true - # - 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: Install dependencies run: | - echo "Ignored for now - until working solution is in place (#13777)" + 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 @@ -325,6 +341,7 @@ 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 diff --git a/.github/workflows/pr-build-status.yml b/.github/workflows/pr-build-status.yml index 236970f141..20d8007d9b 100644 --- a/.github/workflows/pr-build-status.yml +++ b/.github/workflows/pr-build-status.yml @@ -55,43 +55,61 @@ 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 == '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 + 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 +120,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 +137,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 +159,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 +173,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 +197,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 +209,7 @@ jobs: conclusion, output: { title: description, - summary: '' + summary } }); } @@ -204,5 +224,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]); diff --git a/.gitignore b/.gitignore index 57e542eb41..7b87219588 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/.shellcheckrc b/.shellcheckrc index 1c46a510eb..0124e83789 100644 --- a/.shellcheckrc +++ b/.shellcheckrc @@ -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/ diff --git a/HISTORY.md b/HISTORY.md index ea259c5a99..f8e5b771ec 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,258 @@ # Keyman Version History +## 19.0.128 alpha 2025-09-26 + +* fix(windows): sort setup UI language selector according to language names (#13573) +* feat(windows): improve startup time by caching locale names (#14795) + +## 19.0.127 alpha 2025-09-25 + +* fix(common): registry clean-up enabled, no terminal in github action (#14824) +* maint(windows): check code signatures only in CI builds (#14815) +* chore(deps-dev): bump tar-fs from 3.0.9 to 3.1.1 (#14827) +* fix(windows): disable MSI shortcut advertisement (#14814) +* maint(resources): lookup 7z on path on Windows if SEVENZ_HOME is not set (#14816) +* fix(android): Apply bottom insets to system keyboard (#14808) +* fix(android): Fix bottom insets for Sample and FirstVoices apps (#14828) +* docs(android): Document getNavigationBarHeight API (#14830) + +## 19.0.126 alpha 2025-09-24 + +* maint(linux): remove several shellcheck directives (#14819) + +## 19.0.125 alpha 2025-09-23 + +* fix(common): some more tests running successfully in a container (#14784) + +## 19.0.124 alpha 2025-09-22 + +* maint(linux): improve `upload-to-debian` script (#14798) +* refactor(linux): remove a meson deprecation warning (#14801) +* refactor(linux): remove shellcheck warnings (#14802) +* refactor(resources): use array for Docker arguments (#14803) + +## 19.0.123 alpha 2025-09-19 + +* chore(linux): Update debian changelog (#14783) + +## 19.0.122 alpha 2025-09-18 + +* fix(android): Refresh the banner theme when system keyboard is changed (#14768) +* maint(common): update HISTORY.md for 18.0.241 (#14780) + +## 19.0.121 alpha 2025-09-17 + +* docs(developer): document `fileVersion` (#14766) +* maint(linux): allow to force package build on non-Keyman repo (#14749) +* feat(linux): add breadcrumbs and system information to Sentry error reports (#14765) +* maint(resources): fix broken test build trigger for `master` branch (#14770) +* chore(linux): remove automatic installation of `onboard-keyman` (#14772) + +## 19.0.120 alpha 2025-09-16 + +* refactor(linux): remove warning (#14761) +* fix(linux): don't crash on invalid languages in keyboard (#14760) + +## 19.0.119 alpha 2025-09-15 + +* fix(developer): transform slashes to backslashes in paths in Package Editor (#14721) +* fix(developer): restrict invalid characters in identifiers in kmcmplib (#14746) +* fix(common): add name for cleanup ghcr (#14755) + +## 19.0.118 alpha 2025-09-12 + +* chore(common): Update to Unicode 17.0 (#14728) +* fix(resources): match cr to lf change in pr-build-status.mjs (#14734) +* fix(common): merging build and publish step for docker (#14747) +* chore(developer): use icons.res to verify that configure is done (#14733) + +## 19.0.117 alpha 2025-09-11 + +* fix(resources): ignore KEYMAN_VERSION_FOR_FILENAME in test (#14730) +* fix(resources): pr build status - find 'failure' state in status checks and log (#14732) + +## 19.0.116 alpha 2025-09-10 + +* fix(windows): kmshell continues when sm is in waitingrestart and returns to idle (#14644) +* maint(linux): allow to skip API check (#14699) +* maint(resources): adding GitHub actions workflow for Docker builds (#14674) +* maint(developer): use versioned filenames for PR artifacts (#14701) +* maint(windows): use versioned filenames for PR artifacts (#14702) +* maint(android): use versioned filenames for PR artifacts (#14703) +* maint(ios): use versioned filenames for PR artifacts (#14704) +* maint(mac): use versioned filenames for PR artifacts (#14705) +* maint(resources): rename KEYMAN_VERSION_FOR_PR_FILENAME (#14720) + +## 19.0.115 alpha 2025-09-09 + +* fix(linux): don't crash if `kmp.json` is missing `keyboards` section (#14708) + +## 19.0.114 alpha 2025-09-08 + +* docs(web): Improve empty What's New page for Web (#14660) +* maint(linux): remove obsolete Makefile (#14694) +* maint(resources): honor configure comment character in commit message (#14695) +* fix(web): fix error trying to calculate style scaling effects (#14684) +* refactor(resources): remove shellcheck warnings (#14698) + +## 19.0.113 alpha 2025-09-04 + +* fix(android): Update KMSample1 layout when changing orientation (#14678) + +## 19.0.112 alpha 2025-09-03 + +* fix(developer): add clear messages about validation failures to New LM Project Parameters form (#14659) +* docs(developer): tweak get-started for kmc and add entry to HISTORY.md (#14667) +* fix(android): Cleanup list layout in Engine and FirstVoices for edge to edge (#14655) +* docs(common): update license date and owner (#14668) +* maint(linux): skip tarball creation for `Build-bot: build` buildLevel (#14671) +* docs(common): update docs Keyman Desktop → Keyman for Windows (#14669) +* feat(web): adds transform cataloging methods to predictive-text worker (#14581) +* feat(web): batch correction-search paths by related subsets (#14592) +* refactor(web): simplify edit-distance calculation-object inputs (#14618) + +## 19.0.111 alpha 2025-09-02 + +* change(developer): update SIL logo (#14656) +* fix(developer): remove incorrect offset for compiler error line (#14651) +* fix(developer): add clear messages about validation failures to New Project Parameters form (#14657) +* fix(developer): add clear messages about validation failures to New LDML Project Parameters form (#14658) +* maint: add registry usage for Docker images (#14648) + +## 19.0.110 alpha 2025-09-01 + +* maint(windows): for `build` buildlevel, do not publish artifacts (#14654) +* maint(linux): generate tar-ignore in `dist.sh` (#14639) +* fix: fixing some issues for bulding and testing with Docker containers (#14645) +* maint(linux): create temporary worktree for packaging (#14598) + +## 19.0.109 alpha 2025-08-31 + +* fix(developer): handle missing model id in package with compiler error (#14649) +* fix(developer): handle failure to create destination path when generating new project (#14650) + +## 19.0.108 alpha 2025-08-29 + +* fix(android): Fix insets for in-app vs system keyboard (#14619) +* maint(resources): ensure REPO_ROOT is fully resolved (#14643) + +## 19.0.107 alpha 2025-08-28 + +* maint(resources): fix parse bug in build-bot and add extra logging (#14611) +* fix(android): Update Gradle files to remove flatDir warnings (#14583) +* maint(resources): move build trigger scripts into resources/teamcity (#14607) +* chore(linux): Update debian changelog (#14590) +* docs: update package-editor page section details tab (#14585) +* fix(android): polyfill globalThis for API 28 devices (#14541) +* feat: support pagination in build bot PR commit reading (#14638) +* refactor(linux): remove shellcheck warnings and reformat (#14572) + +## 19.0.106 alpha 2025-08-27 + +* fix(windows): check if automatic updates off before prompt (#14295) +* refactor(linux): use bash testing framework for packaging tests (#14603) +* maint(resources): use correct version for PR title for history cherry-pick (#14610) +* refactor(common): fix shellcheck warnings (#14580) + +## 19.0.105 alpha 2025-08-25 + +* maint(linux): ignore generate patch directories (#14600) + +## 19.0.104 alpha 2025-08-21 + +* fix(android): Update sentry-android dependency to 8.19.1 for 16 KB page alignment (#14559) +* maint(android): Cleanup builder_describe_outputs for configure action (#14566) +* fix(windows): emit nunit report path correctly and update TeamCity links (#14564) +* maint(common): rename and move node-related script functions into node.inc.sh (#14568) + +## 19.0.103 alpha 2025-08-20 + +* fix(android): Show system OSK when physical keyboard connected on Android API 36 (#14538) +* fix(android): Add "Show On-screen keyboard" preference to display OSK when physical keyboard is connected (#14539) +* fix(android): Add FirstVoices preference to show on-screen keyboard (#14546) +* fix(linux): normalize language tags from keyboard (#14542) +* maint(common): fix running docker on Windows (#14154) +* maint(mac): use mac_ prefix for functions in mac.inc.sh (#14562) +* maint(android): publish symbols to Sentry on release buildLevel (#14490) +* maint(linux): add missing `--help.keyman.com` option (#14571) +* docs(web): improve documentation for SearchSpace and SearchNode (#14555) + +## 19.0.102 alpha 2025-08-19 + +* maint(mac): skip unnecessary network steps when buildLevel is build (#14467) +* maint(common): cleanup and reorganize ci scripts (#14459) +* refactor(linux): cleanup some code in bash completion script (#14543) +* maint(resources): handle build-bot skip correctly (#14551) +* maint(resources): don't fall back to 'build' action (#14552) +* maint(resources): handle unset build platform in trigger-build-bot (#14554) + +## 19.0.101 alpha 2025-08-18 + +* maint(android): Update Android Target API to 35 (#14395) +* fix(android): Convert Keyman app layout to use ConstraintLayout for handling edge-to-edge (#14396) +* fix(android): Update other activities for edge to edge (#14463) +* fix(android): Update Sample, Test, and FirstVoices apps for edge to edge (#14514) +* maint(common): rename and move node-related script functions into node.inc.sh (#14540) + +## 19.0.100 alpha 2025-08-14 + +* maint(resources): add space to test-bot command (#14528) +* fix(web): improve accuracy of types for suggestions & suggestion-generation (#14517) +* refactor(web): move applySuggestionCasing to standalone exportable method (#14518) +* chore(web): convert worker-thread tests to TS (#14510) + +## 19.0.99 alpha 2025-08-14 + +* maint(web): skip unnecessary steps for `build` buildLevel (#14516) + +## 19.0.98 alpha 2025-08-14 + +* maint(resources): specify `keymanapp/keyman` for gh in automerge (#14524) + +## 19.0.97 alpha 2025-08-14 + +* maint(resources): use GH_TOKEN variable for gh calls (#14522) + +## 19.0.96 alpha 2025-08-14 + +* maint(resources): add build-bot commands to increment-version auto PRs (#14504) +* maint(resources): use GH builtin automerge for auto PRs (#14519) + +## 19.0.95 alpha 2025-08-13 + +* fix(web): use actual legacy model for legacy-model support unit tests (#14460) +* chore(common): Update Crowdin strings for Spanish (#14464) +* fix(android): Get default sizes before initializing keyboard (#14472) +* docs(resources): clarify Build-bot usage (#14282) +* maint(resources): build.sh-style commands for build-bot (#14461) +* chore(android): Update keycode test app dependencies (#14474) +* maint(common): update npm modules brace-expansion to 1.1.12, form-data to 4.0.4 (#14479) +* chore(android): Specify Java language version 21 (#14491) +* maint(ios): skip unnecessary network steps when buildLevel is build (#14466) +* maint(common): rename shellHelperFunctions.sh to utils.inc.sh (#14446) +* maint(common): consolidate builder scripts (#14449) +* maint(common): rename and move node-related script functions into node.inc.sh (#14451) +* maint(common): move typescript script functions into typescript.inc.sh (#14452) +* maint(common): reorganize mac/ios build script includes (#14458) +* docs(resources): add note on builder-basic vs builder-full (#14507) + +## 19.0.94 alpha 2025-08-01 + +* maint(common): use `cpp_std=c++17` in meson project default_options (#14433) + +## 19.0.93 alpha 2025-07-29 + +* test(developer): add unit tests for `isValidUtf8` (#14381) +* maint(linux): fix stopping of test server (#14403) + +## 19.0.92 alpha 2025-07-28 + +* build(deps): bump multer from 2.0.1 to 2.0.2 (#14373) +* refactor(linux): remove shellcheck warnings (#14409) +* refactor(linux): move dbus defines to common include (#14407) +* docs(linux): update readme for test service (#14408) +* fix(windows): add check a fix for registry datatypes (#14358) + ## 19.0.91 alpha 2025-07-26 * maint(windows): fix calls of renamed functions (#14413) @@ -557,6 +810,43 @@ * refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168) * chore: increment to alpha 19.0 (#13187) +## 18.0.241 stable 2025-09-18 + +* maint(resources): use correct version for PR title for history cherry-pick (#14614) +* chore(linux): Update debian changelog (#14589) +* fix(android): Update: Android Target API to 35, Java to 21, and Gradle dependencies (#14584) +* fix(android): Convert Keyman app layouts for edge to edge (#14597) +* fix(android): Fix insets for in-app vs system keyboard (#14642) +* maint(linux): create temporary worktree for packaging (#14599) +* fix(android): Cleanup layouts for Engine, Sample apps, and FirstVoices apps for edge to edge (#14663) +* fix(web): fix error trying to calculate style scaling effects (#14696) +* fix(linux): don't crash if `kmp.json` is missing `keyboards` section (#14714) +* fix(developer): transform slashes to backslashes in paths in Package Editor (#14722) +* chore(linux): remove automatic installation of `onboard-keyman` (#14773) +* docs(linux): update release notes for stable release (#14774) +* fix(linux): don't crash on invalid languages in keyboard (#14771) + +## 18.0.240 stable 2025-08-27 + +* maint(resources): automatically cherry-pick history updates to master (#14596) +* maint(linux): fix lintian warnings (#14591) +* maint(linux): ignore generate patch directories (#14601) + +## 18.0.239 stable 2025-08-22 + +* docs(linux): clarify requirement of patched ibus for Keyman < 18 (#14113) +* chore(linux): Update debian changelog (#14118) +* fix(windows): add values wucUpdateAvailable and wucNotChecked to TRemoteUpdateCheckResult enum (#14208) +* fix(windows): fix download failing when cache.json missing (#14207) +* fix(web): deploy `web/build/publish` folder (#14238) +* fix(ios): restore keyboard scale map for iPad Air 2 (#14306) +* chore(developer): upgrade multer to 2.0.2 (#14417) +* fix(android): cherry-pick getting default sizes before initializing keyboard (#14473) +* fix(android): polyfill globalThis for API 28 devices (#14553) +* fix(android): Update sentry-android dependency to 7.22.6 for 16 KB page alignment (#14560) +* fix(android): Show system OSK when physical keyboard connected (#14547) +* fix(android): Add preference to show on-screen keyboard to Keyman and FirstVoices apps (#14561) + ## 18.0.238 stable 2025-06-04 * fix(developer): ldml: make sure .run() calls compile() for validation (#14071) diff --git a/VERSION.md b/VERSION.md index 145ea8990d..1ba4a26a2c 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -19.0.92 \ No newline at end of file +19.0.129 \ No newline at end of file diff --git a/android/KMAPro/build.gradle b/android/KMAPro/build.gradle index c644d44fca..d02f3eeb92 100644 --- a/android/KMAPro/build.gradle +++ b/android/KMAPro/build.gradle @@ -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' } } diff --git a/android/KMAPro/build.sh b/android/KMAPro/build.sh index fd54215d1c..06370c06d2 100755 --- a/android/KMAPro/build.sh +++ b/android/KMAPro/build.sh @@ -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 diff --git a/android/KMAPro/kMAPro/build.gradle b/android/KMAPro/kMAPro/build.gradle index 864fb36ee9..77ab2fa736 100644 --- a/android/KMAPro/kMAPro/build.gradle +++ b/android/KMAPro/kMAPro/build.gradle @@ -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,19 +164,17 @@ 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 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" diff --git a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java index 2d03311d50..7554e389ca 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java +++ b/android/KMAPro/kMAPro/src/main/java/com/keyman/android/SystemKeyboard.java @@ -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,7 +30,6 @@ 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; @@ -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; } @@ -246,9 +246,10 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven inputViewHeight = inputView.getHeight(); } + int navigationHeight = KMManager.getNavigationBarHeight(this); 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); @@ -264,6 +265,8 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven @Override public void onKeyboardChanged(String newKeyboard) { + // Refresh banner theme + BannerController.setHTMLBanner(this, KeyboardType.KEYBOARD_TYPE_SYSTEM); KMManager.showSystemKeyboard(); } @@ -278,6 +281,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 diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustKeyboardHeightActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustKeyboardHeightActivity.java index cef9ec33fc..cc23b2d2ed 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustKeyboardHeightActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/AdjustKeyboardHeightActivity.java @@ -44,6 +44,9 @@ public class AdjustKeyboardHeightActivity extends BaseActivity { setContentView(R.layout.activity_adjust_keyboard_height); + setupEdgeToEdge(R.id.adjust_keyboard_layout); + setupStatusBarColors(R.color.keyman_blue, android.R.color.white); + Toolbar toolbar = (Toolbar) findViewById(R.id.titlebar); setSupportActionBar(toolbar); ActionBar actionBar = getSupportActionBar(); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/InfoActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/InfoActivity.java index 3f70750118..3cefe9fe7f 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/InfoActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/InfoActivity.java @@ -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); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java index 09a8921e7b..07ba2a7074 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KMPBrowserActivity.java @@ -79,6 +79,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); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java index 7113e0a982..bda56856b2 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsActivity.java @@ -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); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java index 59ac59e2f8..4629336635 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsFragment.java @@ -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; @@ -157,6 +159,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 +225,8 @@ public class KeymanSettingsFragment extends PreferenceFragmentCompat { screen.addPreference(adjustKeyboardHeight); screen.addPreference(adjustLongpressDelay); + + screen.addPreference(oskWithPhysicalKeyboardPreference); screen.addPreference(spacebarTextPreference); screen.addPreference(hapticFeedbackPreference); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsInstallActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsInstallActivity.java index 71a8f7c355..728a6b2d15 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsInstallActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/KeymanSettingsInstallActivity.java @@ -7,7 +7,6 @@ import java.util.ArrayList; import java.util.HashMap; import androidx.appcompat.app.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.widget.Toolbar; import android.content.Context; @@ -24,11 +23,12 @@ import android.os.Bundle; import android.widget.SimpleAdapter; import android.widget.TextView; +import com.keyman.engine.BaseActivity; 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> installOptionList = null; private static Typeface titleFont = null; @@ -45,6 +45,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); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java index 96abc83f8a..8db40bb3de 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguageSettingsActivity.java @@ -25,10 +25,10 @@ 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 com.keyman.engine.BaseActivity; import com.keyman.engine.KeyboardPickerActivity; import com.keyman.engine.KMManager; import com.keyman.engine.ModelPickerActivity; @@ -43,7 +43,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 +66,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); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguagesSettingsActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguagesSettingsActivity.java index 6e21d04f7b..79c845fa6f 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguagesSettingsActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/LanguagesSettingsActivity.java @@ -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); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java index 40b41b6176..b422d7e881 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/MainActivity.java @@ -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); @@ -622,10 +625,18 @@ public class MainActivity extends BaseActivity implements OnKeyboardEventListene if (resourceId > 0) statusBarHeight = getResources().getDimensionPixelSize(resourceId); + // Navigation bar height + int navigationBarHeight = 0; + resourceId = getResources().getIdentifier("navigation_bar_height", "dimen", "android"); + if (resourceId > 0) { + navigationBarHeight = getResources().getDimensionPixelSize(resourceId); + } + 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() { diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java index ee76265a97..28517ef04a 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PackageActivity.java @@ -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; diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PreferencesManager.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PreferencesManager.java new file mode 100644 index 0000000000..73abd79ecc --- /dev/null +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/PreferencesManager.java @@ -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"; +} diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectLanguageActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectLanguageActivity.java index 892ac820cf..ffaf1e28ae 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectLanguageActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectLanguageActivity.java @@ -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"); diff --git a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectPackageActivity.java b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectPackageActivity.java index 9b05332182..0b6a3ac9a7 100644 --- a/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectPackageActivity.java +++ b/android/KMAPro/kMAPro/src/main/java/com/tavultesoft/kmapro/SelectPackageActivity.java @@ -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> 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); diff --git a/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_keyboard_height.xml b/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_keyboard_height.xml index 7a53c88321..0cb7a262d2 100644 --- a/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_keyboard_height.xml +++ b/android/KMAPro/kMAPro/src/main/res/layout/activity_adjust_keyboard_height.xml @@ -1,9 +1,11 @@ - + android:layout_height="match_parent" + android:orientation="vertical"> @@ -15,7 +17,7 @@ android:layout_marginStart="@dimen/activity_horizontal_margin" android:text="@string/drag_the_keyboard" android:textSize="@dimen/adjust_keyboard_height_label_textsize" - /> + app:layout_constraintTop_toBottomOf="@id/titlebar" /> + app:layout_constraintTop_toBottomOf="@id/drag_the_keyboard" /> - - - - + diff --git a/android/KMEA/app/src/main/res/layout/activity_list_layout.xml b/android/KMEA/app/src/main/res/layout/activity_list_layout.xml index 51c506fc28..c865d841b6 100644 --- a/android/KMEA/app/src/main/res/layout/activity_list_layout.xml +++ b/android/KMEA/app/src/main/res/layout/activity_list_layout.xml @@ -1,13 +1,22 @@ - - + - + - + diff --git a/android/KMEA/app/src/main/res/layout/language_settings_list_layout.xml b/android/KMEA/app/src/main/res/layout/language_settings_list_layout.xml index 42ae9b1511..84f6f189bb 100644 --- a/android/KMEA/app/src/main/res/layout/language_settings_list_layout.xml +++ b/android/KMEA/app/src/main/res/layout/language_settings_list_layout.xml @@ -1,12 +1,15 @@ - - + + android:layout_above="@+id/modeling_settings_layout" + android:layout_below="@id/list_appbar" + app:layout_constraintTop_toBottomOf="@id/list_appbar"/> - - - - - - - + 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"/> + android:orientation="vertical" + app:layout_constraintVertical_bias="1.0" + app:layout_constraintBottom_toBottomOf="parent"> --> - + diff --git a/android/KMEA/app/src/main/res/layout/languages_settings_list_layout.xml b/android/KMEA/app/src/main/res/layout/languages_settings_list_layout.xml index cfd94dc922..052db1cf85 100644 --- a/android/KMEA/app/src/main/res/layout/languages_settings_list_layout.xml +++ b/android/KMEA/app/src/main/res/layout/languages_settings_list_layout.xml @@ -1,31 +1,25 @@ - - + - - - - - + android:layout_below="@+id/list_appbar" + app:layout_constraintTop_toBottomOf="@id/list_appbar" + app:layout_constraintStart_toStartOf="parent"/> + 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"/> - - + diff --git a/android/KMEA/app/src/main/res/layout/list_layout.xml b/android/KMEA/app/src/main/res/layout/list_layout.xml index aeea32ab35..0bd7dfd224 100644 --- a/android/KMEA/app/src/main/res/layout/list_layout.xml +++ b/android/KMEA/app/src/main/res/layout/list_layout.xml @@ -1,15 +1,20 @@ - - + + + android:clipToPadding="false" + android:paddingBottom="?attr/actionBarSize" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent"/> diff --git a/android/KMEA/app/src/main/res/layout/list_toolbar.xml b/android/KMEA/app/src/main/res/layout/list_toolbar.xml index fe200ce6a2..fcfd16377f 100644 --- a/android/KMEA/app/src/main/res/layout/list_toolbar.xml +++ b/android/KMEA/app/src/main/res/layout/list_toolbar.xml @@ -1,16 +1,20 @@ - + + android:layout_height="wrap_content" + app:layout_constraintTop_toTopOf="parent"> + app:layout_constraintTop_toBottomOf="@id/list_toolbar" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" /> diff --git a/android/KMEA/app/src/main/res/layout/titlebar.xml b/android/KMEA/app/src/main/res/layout/titlebar.xml index 64b91a0fee..9166846600 100644 --- a/android/KMEA/app/src/main/res/layout/titlebar.xml +++ b/android/KMEA/app/src/main/res/layout/titlebar.xml @@ -6,7 +6,9 @@ android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:layout_alignParentTop="true" - app:popupTheme="@style/ThemeOverlay.AppCompat.Light"> + app:popupTheme="@style/ThemeOverlay.AppCompat.Light" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent"> Error en el teclado %1$s:%2$s para %3$s idioma. - Keyman requires WebView to be installed.\" + Keyman requiere que WebView esté instalado.\" - Keyman requires WebView to be enabled.\" + Keyman requiere que WebView esté habilitado.\" - Install WebView + Instalar WebView - Enable WebView + Habilitar WebView Comprobando si hay un diccionario asociado para descargar No se puede conectar al servidor Keyman para comprobar si hay un diccionario asociado para descargar @@ -124,7 +124,7 @@ ¡Falló al acceder al servidor! - DownloadManager disabled - cannot check for updates + Descargador deshabilitado: no se pueden comprobar actualizaciones "¡Todos los recursos están actualizados!" diff --git a/android/KMEA/build.gradle b/android/KMEA/build.gradle index 4ca999ea60..af95b22c8e 100644 --- a/android/KMEA/build.gradle +++ b/android/KMEA/build.gradle @@ -7,7 +7,6 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:8.9.2' // io.sentry:sentry-android-gradle-plugin not available for library project - classpath 'io.sentry:sentry-android:7.8.0' classpath 'name.remal:gradle-plugins:1.9.2' } } diff --git a/android/KMEA/build.sh b/android/KMEA/build.sh index 4d4ea7f8e3..f4d7705815 100755 --- a/android/KMEA/build.sh +++ b/android/KMEA/build.sh @@ -4,11 +4,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" # ################################ Main script ################################ diff --git a/android/README.md b/android/README.md index db1eb592b0..76ebf90b22 100644 --- a/android/README.md +++ b/android/README.md @@ -34,10 +34,10 @@ analytics for Debug are associated with an App Bundle ID ### Compiling From Command Line 1. Launch a command prompt and cd to the directory **keyman/android** -2. Run the top level build script `./build.sh configure build:engine build:app --debug` which will: - * Compile KMEA (and its KMW dependency) +2. Run the top level build script `./build.sh configure build:engine build:app` which will: + * Compile debug version of KMEA (and its KMW dependency) * Download default keyboard and dictionary resources as needed - * Compile KMAPro + * Compile debug version of KMAPro 3. The APK will be found in **keyman/android/KMAPro/kMAPro/build/outputs/apk/debug/keyman-\${version}.apk** where `${version}` is the current version number. @@ -98,10 +98,10 @@ There are two included sample projects that can be modified to test a keyboard. **android/Samples/KMSample2** app provides prompts for setting KMSample2 as a system level keyboard. Both sample apps include a default Tamil keyboard and sample dictionary. -Building these projects follow the same steps as KMAPro: +Building the debug versions of these projects follow the same steps as KMAPro: 1. cd to the desired KMSample directory -2. `./build.sh configure build --debug` +2. `./build.sh configure build` 3. Open Android Studio to run the app ### Tests: KeyboardHarness @@ -114,14 +114,14 @@ Building these projects follow the same steps as KMAPro: * Build the keyboardharness.kmp keyboard package 3. Add the keyboard in *android/Tests/KeyboardHarness/app/src/main/java/com/keyman/android/tests/keyboardHarness/MainActivity.java* 4. cd to android/Tests/KeyboardHarness/ -5. `./build.sh configure build --debug` +5. `./build.sh configure build` 6. Open Android Studio to run the app -------------------------------------------------------------- ## How to Build Keyman Engine for Android 1. Open a terminal or Git Bash prompt and go to the Android project folder (e.g. `cd ~/keyman/android/`) -2. Run `./build.sh build:engine --debug` +2. Run `./build.sh build:engine` Keyman Engine for Android library (**keyman-engine.aar**) is now ready to be imported in any project. @@ -135,7 +135,7 @@ Keyman Engine for Android library (**keyman-engine.aar**) is now ready to be imp 4. Check that the `android{}` object, includes the following: ```gradle android { - compileSdkVersion 34 + compileSdkVersion 35 // Don't compress kmp files so they can be copied via AssetManager aaptOptions { @@ -159,6 +159,7 @@ repositories { 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' api (name:'keyman-engine', ext:'aar') implementation 'androidx.preference:preference:1.2.1' diff --git a/android/Samples/KMSample1/app/build.gradle b/android/Samples/KMSample1/app/build.gradle index cc92dee298..ae4ff55d7e 100644 --- a/android/Samples/KMSample1/app/build.gradle +++ b/android/Samples/KMSample1/app/build.gradle @@ -2,8 +2,14 @@ plugins { id 'com.android.application' } +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + android { - compileSdk 34 + compileSdk 35 namespace="com.keyman.kmsample1" // Don't compress kmp files so they can be copied via AssetManager @@ -14,7 +20,7 @@ android { defaultConfig { applicationId "com.keyman.kmsample1" minSdkVersion 21 - targetSdkVersion 34 + targetSdkVersion 35 versionCode 1 versionName "1.0" } @@ -24,12 +30,15 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + } + } } repositories { - flatDir { - dirs 'libs' - } google() } @@ -37,7 +46,8 @@ repositories { 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' - api(name: 'keyman-engine', ext: 'aar') + implementation files('libs/keyman-engine.aar') implementation 'androidx.preference:preference:1.2.1' } diff --git a/android/Samples/KMSample1/app/src/main/java/com/keyman/kmsample1/MainActivity.java b/android/Samples/KMSample1/app/src/main/java/com/keyman/kmsample1/MainActivity.java index f56064b459..add6bc5d63 100644 --- a/android/Samples/KMSample1/app/src/main/java/com/keyman/kmsample1/MainActivity.java +++ b/android/Samples/KMSample1/app/src/main/java/com/keyman/kmsample1/MainActivity.java @@ -1,13 +1,16 @@ package com.keyman.kmsample1; -import androidx.appcompat.app.AppCompatActivity; - import android.content.Context; import android.content.res.Configuration; +import android.graphics.Point; import android.os.Bundle; +import android.util.TypedValue; import android.view.Menu; import android.view.MenuItem; +import androidx.constraintlayout.widget.ConstraintLayout; + +import com.keyman.engine.BaseActivity; import com.keyman.engine.data.Keyboard; import com.keyman.engine.KMKeyboardDownloaderActivity; import com.keyman.engine.KMManager; @@ -19,10 +22,12 @@ import java.util.HashMap; import java.util.List; import java.util.Map; -public class MainActivity extends AppCompatActivity implements OnKeyboardEventListener, OnKeyboardDownloadEventListener { +public class MainActivity extends BaseActivity implements OnKeyboardEventListener, OnKeyboardDownloadEventListener { public static Context context; + private ConstraintLayout constraintLayout; private KMTextView textView; + private int lastOrientation = Configuration.ORIENTATION_UNDEFINED; @Override protected void onCreate(Bundle savedInstanceState) { @@ -34,6 +39,12 @@ public class MainActivity extends AppCompatActivity implements OnKeyboardEventLi KMManager.initialize(this, KeyboardType.KEYBOARD_TYPE_INAPP); setContentView(R.layout.activity_main); + constraintLayout = findViewById(R.id.constraintLayout); + setupEdgeToEdge(R.id.constraintLayout); + setupStatusBarColors( + android.R.color.white, // Color for top status bar + android.R.color.darker_gray); // Color for bottom navigation bar + textView = (KMTextView) findViewById(R.id.kmTextView); // Add a custom keyboard @@ -91,6 +102,18 @@ public class MainActivity extends AppCompatActivity implements OnKeyboardEventLi protected void onResume() { super.onResume(); KMManager.onResume(); + + // onConfigurationChanged() only triggers when device is rotated while app is in foreground + // This handles when device is rotated while app is in background + // using KMManager.getOrientation() since getConfiguration().orientation is unreliable #10241 + int newOrientation = KMManager.getOrientation(context); + if (newOrientation != lastOrientation) { + lastOrientation = newOrientation; + Configuration newConfig = this.getResources().getConfiguration(); + KMManager.onConfigurationChanged(newConfig); + } + resizeTextView(textView.isKeyboardVisible()); + KMManager.addKeyboardEventListener(this); KMKeyboardDownloaderActivity.addKeyboardDownloadEventListener(this); } @@ -108,6 +131,10 @@ public class MainActivity extends AppCompatActivity implements OnKeyboardEventLi @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); + KMManager.onConfigurationChanged(newConfig); + + resizeTextView(textView.isKeyboardVisible()); + lastOrientation = newConfig.orientation; } @Override @@ -123,11 +150,44 @@ public class MainActivity extends AppCompatActivity implements OnKeyboardEventLi @Override public void onKeyboardShown() { // Handle Keyman keyboard shown event here if needed + resizeTextView(true); } @Override public void onKeyboardDismissed() { // Handle Keyman keyboard dismissed event here if needed + resizeTextView(false); + } + + public void resizeTextView(boolean isKeyboardVisible) { + int bannerHeight = 0; + int keyboardHeight = 0; + if (isKeyboardVisible) { + bannerHeight = KMManager.getBannerHeight(this); + keyboardHeight = KMManager.getKeyboardHeight(this); + } + + TypedValue outValue = new TypedValue(); + 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 *** + int statusBarHeight = 0; + int resourceId = getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) + statusBarHeight = getResources().getDimensionPixelSize(resourceId); + + // Navigation bar height + int navigationBarHeight = 0; + resourceId = getResources().getIdentifier("navigation_bar_height", "dimen", "android"); + if (resourceId > 0) { + navigationBarHeight = getResources().getDimensionPixelSize(resourceId); + } + + Point size = KMManager.getWindowSize(context); + int screenHeight = size.y; + textView.setHeight( + screenHeight - statusBarHeight - actionBarHeight - bannerHeight - keyboardHeight - navigationBarHeight); } @Override diff --git a/android/Samples/KMSample1/app/src/main/res/layout/activity_main.xml b/android/Samples/KMSample1/app/src/main/res/layout/activity_main.xml index 06de0d7f24..6ec32a3923 100644 --- a/android/Samples/KMSample1/app/src/main/res/layout/activity_main.xml +++ b/android/Samples/KMSample1/app/src/main/res/layout/activity_main.xml @@ -1,7 +1,11 @@ - + android:scrollbars="vertical" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent"> - + diff --git a/android/Samples/KMSample1/build.sh b/android/Samples/KMSample1/build.sh index 172ef6c5e7..d1d41bc200 100755 --- a/android/Samples/KMSample1/build.sh +++ b/android/Samples/KMSample1/build.sh @@ -4,11 +4,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" ################################ Main script ################################ @@ -23,7 +22,7 @@ builder_describe "Build KMSample1 app for Android." \ "configure" \ "build" \ "test" \ - ":app KMSample1" + ":app KMSample1" # parse before describe_outputs to check debug flags builder_parse "$@" @@ -39,7 +38,6 @@ fi builder_describe_outputs \ - configure /android/Samples/KMSample1/app/libs/keyman-engine.aar \ build:app /android/Samples/KMSample1/app/build/outputs/apk/$CONFIG/$ARTIFACT diff --git a/android/Samples/KMSample2/app/build.gradle b/android/Samples/KMSample2/app/build.gradle index cd2f4d6225..a148a7ab19 100644 --- a/android/Samples/KMSample2/app/build.gradle +++ b/android/Samples/KMSample2/app/build.gradle @@ -2,8 +2,14 @@ plugins { id 'com.android.application' } +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + android { - compileSdk 34 + compileSdk 35 namespace="com.keyman.kmsample2" // Don't compress kmp files so they can be copied via AssetManager @@ -14,7 +20,7 @@ android { defaultConfig { applicationId "com.keyman.kmsample2" minSdkVersion 21 - targetSdkVersion 34 + targetSdkVersion 35 versionCode 1 versionName "1.0" } @@ -24,19 +30,23 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + + sourceSets { + main { + jniLibs.srcDirs = ['libs'] + } + } } 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' - api (name:'keyman-engine', ext:'aar') + implementation files('libs/keyman-engine.aar') implementation 'androidx.preference:preference:1.2.1' } diff --git a/android/Samples/KMSample2/app/src/main/java/com/keyman/kmsample2/MainActivity.java b/android/Samples/KMSample2/app/src/main/java/com/keyman/kmsample2/MainActivity.java index 4f9fa0a45f..6099997520 100644 --- a/android/Samples/KMSample2/app/src/main/java/com/keyman/kmsample2/MainActivity.java +++ b/android/Samples/KMSample2/app/src/main/java/com/keyman/kmsample2/MainActivity.java @@ -1,6 +1,5 @@ package com.keyman.kmsample2; -import androidx.appcompat.app.AppCompatActivity; import android.content.Context; import android.content.Intent; import android.provider.Settings; @@ -11,13 +10,20 @@ import android.view.View; import android.view.inputmethod.InputMethodManager; import android.widget.Button; -public class MainActivity extends AppCompatActivity { +import com.keyman.engine.BaseActivity; + +public class MainActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { setTheme(R.style.AppTheme); super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + setupEdgeToEdge(R.id.constraintLayout); + setupStatusBarColors( // Inset colors for Android API < 35 + android.R.color.black, // Color for status bar on top + android.R.color.white // Color for navigation bar on bottom + ); Button button1 = (Button) findViewById(R.id.button1); button1.setOnClickListener(new View.OnClickListener() { diff --git a/android/Samples/KMSample2/app/src/main/java/com/keyman/kmsample2/SystemKeyboard.java b/android/Samples/KMSample2/app/src/main/java/com/keyman/kmsample2/SystemKeyboard.java index 438ed237db..9e1185720b 100644 --- a/android/Samples/KMSample2/app/src/main/java/com/keyman/kmsample2/SystemKeyboard.java +++ b/android/Samples/KMSample2/app/src/main/java/com/keyman/kmsample2/SystemKeyboard.java @@ -184,12 +184,14 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven Point size = KMManager.getWindowSize(getApplicationContext()); int inputViewHeight = 0; - if (inputView != null) + if (inputView != null) { inputViewHeight = inputView.getHeight(); + } + int navigationHeight = KMManager.getNavigationBarHeight(this); 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); @@ -215,6 +217,20 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven // Handle Keyman keyboard dismissed event here if needed } + @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; + }; + + // TODO: Implement IME setting similar to Gboard "Show on-screen keyboard" with physical keyboard + // (default false) + return true; + } + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (event.getAction() == KeyEvent.ACTION_DOWN) { diff --git a/android/Samples/KMSample2/app/src/main/res/layout/activity_main.xml b/android/Samples/KMSample2/app/src/main/res/layout/activity_main.xml index 349c927903..f4e9e590a9 100644 --- a/android/Samples/KMSample2/app/src/main/res/layout/activity_main.xml +++ b/android/Samples/KMSample2/app/src/main/res/layout/activity_main.xml @@ -1,31 +1,50 @@ - + + android:paddingRight="@dimen/activity_horizontal_margin" + android:paddingBottom="@dimen/activity_vertical_margin" + tools:context=".MainActivity"> - + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:text="@string/description" + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" />