Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions .github/workflows/brrrboard-apk-only.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
name: brrrBoard — APK Only (Audit) # independent APK delivery path

on:
push:
branches:
- 'brrrboard-audit-*'

permissions:
contents: write

jobs:
apk:
runs-on: ubuntu-latest
timeout-minutes: 25
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Restore persistent debug signing key
shell: bash
run: |
set -euo pipefail
test -f .github/debug.keystore
mkdir -p "$HOME/.android"
cp .github/debug.keystore "$HOME/.android/debug.keystore"
chmod 600 "$HOME/.android/debug.keystore"

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: temurin

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- name: Build debug APK
shell: bash
run: |
set -euo pipefail
chmod +x gradlew
./gradlew --no-daemon --stacktrace assembleDebug

- name: Verify APK and signing identity
shell: bash
run: |
set -euo pipefail
APK="$(find app/build/outputs/apk/debug -maxdepth 1 -type f -name '*-debug.apk' | head -n 1)"
test -n "$APK"
test -s "$APK"
APKSIGNER="$(find "$ANDROID_HOME/build-tools" -type f -name apksigner | sort -V | tail -n 1)"
CERT_OUTPUT="$("$APKSIGNER" verify --print-certs "$APK")"
EXPECTED_CERT="$(keytool -list -v -keystore .github/debug.keystore -storepass android -alias androiddebugkey | sed -n 's/^.*SHA256:[[:space:]]*//p' | head -n 1 | tr -d ':' | tr '[:lower:]' '[:upper:]')"
ACTUAL_CERT="$(printf '%s\n' "$CERT_OUTPUT" | sed -n 's/^.*SHA-256 digest:[[:space:]]*//p' | head -n 1 | tr -d ':' | tr '[:lower:]' '[:upper:]')"
test -n "$EXPECTED_CERT"
test -n "$ACTUAL_CERT"
test "$EXPECTED_CERT" = "$ACTUAL_CERT"
SHA256="$(sha256sum "$APK" | cut -d' ' -f1)"
echo "APK=$APK" >> "$GITHUB_ENV"
echo "SHA256=$SHA256" >> "$GITHUB_ENV"
echo "APK_SIZE=$(stat -c '%s' "$APK")" >> "$GITHUB_ENV"
echo "APK: $APK"
echo "SHA-256: $SHA256"

- name: Upload APK artifact
uses: actions/upload-artifact@v4
with:
name: brrrBoard-debug-apk-${{ github.sha }}
path: ${{ env.APK }}
if-no-files-found: error
retention-days: 14

- name: Publish audit release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
SHORT_SHA="${GITHUB_SHA::7}"
STAMP="$(date -u +'%Y%m%d-%H%M%S')"
TAG="brrrboard-audit-${STAMP}-${SHORT_SHA}"
gh release create "$TAG" "$APK" --repo "$GITHUB_REPOSITORY" --target "$GITHUB_SHA" --title "brrrBoard Audit APK — ${SHORT_SHA}" --notes "APK-only audit build. Commit: $GITHUB_SHA. SHA-256: $SHA256. Built with the tracked persistent debug signing key." --prerelease
123 changes: 123 additions & 0 deletions .github/workflows/brrrboard-build-debug.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: brrrBoard — Build Debug APK

on:
push:
branches:
- main
- 'brrrboard-audit-*'
workflow_dispatch:

permissions:
contents: write

concurrency:
group: brrrBoard-debug-${{ github.ref }}
cancel-in-progress: true

jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 45

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Restore persistent debug signing key
shell: bash
run: |
set -euo pipefail
test -f .github/debug.keystore
mkdir -p "$HOME/.android"
cp .github/debug.keystore "$HOME/.android/debug.keystore"
chmod 600 "$HOME/.android/debug.keystore"

- name: Set up JDK 21
uses: actions/setup-java@v4
with:
java-version: '21' # Robolectric SDK 36 requires JDK 21
distribution: temurin

- name: Set up Gradle
uses: gradle/actions/setup-gradle@v4

- name: Build debug APK
shell: bash
run: |
set -euo pipefail
chmod +x gradlew
./gradlew --no-daemon --stacktrace assembleDebug

- name: Verify APK and signing identity
shell: bash
run: |
set -euo pipefail
APK="$(find app/build/outputs/apk/debug -maxdepth 1 -type f -name '*-debug.apk' | head -n 1)"
test -n "$APK"
test -s "$APK"

APKSIGNER="$(find "$ANDROID_HOME/build-tools" -type f -name apksigner | sort -V | tail -n 1)"
CERT_OUTPUT="$("$APKSIGNER" verify --print-certs "$APK")"
printf '%s\n' "$CERT_OUTPUT"

EXPECTED_CERT="$(keytool -list -v -keystore .github/debug.keystore -storepass android -alias androiddebugkey | sed -n 's/^.*SHA256:[[:space:]]*//p' | head -n 1 | tr -d ':' | tr '[:lower:]' '[:upper:]')"
ACTUAL_CERT="$(printf '%s\n' "$CERT_OUTPUT" | sed -n 's/^.*SHA-256 digest:[[:space:]]*//p' | head -n 1 | tr -d ':' | tr '[:lower:]' '[:upper:]')"

test -n "$EXPECTED_CERT"
test -n "$ACTUAL_CERT"
test "$EXPECTED_CERT" = "$ACTUAL_CERT"

SHA256="$(sha256sum "$APK" | cut -d' ' -f1)"
echo "APK=$APK" >> "$GITHUB_ENV"
echo "SHA256=$SHA256" >> "$GITHUB_ENV"
echo "APK_SIZE=$(stat -c '%s' "$APK")" >> "$GITHUB_ENV"

echo "APK: $APK"
echo "SHA-256: $SHA256"
ls -lh "$APK"

- name: Upload debug APK artifact
uses: actions/upload-artifact@v4
with:
name: brrrBoard-debug-apk-${{ github.sha }}
path: ${{ env.APK }}
if-no-files-found: error
retention-days: 14

- name: Run unit tests
id: unit_tests
continue-on-error: true
shell: bash
run: |
set -euo pipefail
./gradlew --no-daemon --stacktrace testRunTests

- name: Fail workflow when tests fail
if: steps.unit_tests.outcome == 'failure'
shell: bash
run: |
echo "Unit tests failed; APK was still built, verified, and published for audit/testing."
exit 1

- name: Publish GitHub release
if: github.ref_name != 'main' || steps.unit_tests.outcome == 'success'
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
VERSION_NAME="$(sed -n 's/.*versionName = "\([^"]*\)".*/\1/p' app/build.gradle.kts | head -n 1)"
SHORT_SHA="${GITHUB_SHA::7}"
STAMP="$(date -u +'%Y%m%d-%H%M%S')"

if [ "$GITHUB_REF_NAME" = "main" ]; then
TAG="brrrboard-debug-${STAMP}-${SHORT_SHA}"
TITLE="brrrBoard Debug — ${VERSION_NAME} — ${SHORT_SHA}"
gh release create "$TAG" "$APK" --repo "$GITHUB_REPOSITORY" --target "$GITHUB_SHA" --title "$TITLE" --notes "Verified debug APK. Commit: $GITHUB_SHA. SHA-256: $SHA256. Uses the persistent debug signing key so later debug builds can update the same debug installation."
else
TAG="brrrboard-test-${STAMP}-${SHORT_SHA}"
TITLE="brrrBoard Test Build — ${SHORT_SHA}"
gh release create "$TAG" "$APK" --repo "$GITHUB_REPOSITORY" --target "$GITHUB_SHA" --title "$TITLE" --notes "Audit/test build. Commit: $GITHUB_SHA. SHA-256: $SHA256." --prerelease
fi
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ abstract class GitCommitCountValueSource : ValueSource<Int, ValueSourceParameter
abstract val execOperations: ExecOperations

override fun obtain(): Int {
val timeBasedCode = (System.currentTimeMillis() / 60_000L)
val timeBasedCode = (System.currentTimeMillis() / 1_000L)
.coerceAtMost(Int.MAX_VALUE.toLong())
.toInt()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,7 @@ object SubtypeSettings {
}

private fun loadResourceSubtypes(resources: Resources) {
resourceSubtypesByLocale.clear()
getResourceSubtypes(resources).forEach {
resourceSubtypesByLocale.getOrPut(it.locale()) { ArrayList(2) }.add(it)
}
Expand Down
6 changes: 5 additions & 1 deletion app/src/test/java/helium314/keyboard/SubtypeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@ class SubtypeTest {

@Test fun subtypeStaysEnabledOnEdits() {
val prefs = latinIME.prefs()
prefs.edit().putString(Settings.PREF_ADDITIONAL_SUBTYPES, "").apply() // clear it for convenience
prefs.edit()
.putString(Settings.PREF_ADDITIONAL_SUBTYPES, "")
.putString(Settings.PREF_ENABLED_SUBTYPES, "")
.putString(Settings.PREF_SELECTED_SUBTYPE, "")
.apply()

// edit enabled resource subtype
val from = SubtypeSettings.getResourceSubtypesForLocale("es".constructLocale()).first()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import org.robolectric.annotation.Config
import kotlin.system.measureTimeMillis
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertTrue

// todo: actually this test could/should be significantly expanded...
@RunWith(RobolectricTestRunner::class)
Expand Down Expand Up @@ -205,7 +206,7 @@ class StringUtilsTest {
val brokenDetectionAtStart = listOf("〰️", "〽️", "©️", "®️", "#️⃣", "*️⃣", "0️⃣", "1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "㊗️", "㊙️")
allEmojis.forEach {
if (it == "🀄" || it == "🃏") return@forEach // todo: should be fixed, ideally in the regex
assert(isEmoji(it))
assertTrue(isEmoji(it), "isEmoji=false for [$it] codePoints=${it.codePoints().toArray().joinToString(","){ "U+" + it.toString(16).uppercase() }}")
assert(StringUtils.mightBeEmoji(it.codePointBefore(it.length)))
if (it !in brokenDetectionAtStart)
assert(StringUtils.mightBeEmoji(it.codePointAt(0)))
Expand Down
108 changes: 108 additions & 0 deletions docs/EXPERIMENT_LOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# brrrBoard Experiment & Fix History

This file is the project's durable engineering ledger.

**Rule:** every attempted bug fix, UI experiment, CI repair, rollback, or workaround that is not obvious from the final code should be recorded here, including failed attempts. Never delete a failed experiment; append the outcome instead.

## Entry format

- **Date / build:** YYYY-MM-DD / version or commit
- **Problem:** exact symptom
- **Hypothesis:** suspected root cause
- **Change attempted:** files/functions changed
- **Result:** PASS / FAIL / PARTIAL / REVERTED
- **Evidence:** CI run, APK SHA-256, device/model, logcat, screenshot, etc.
- **Next step:** what should be tried next

## 2026-10-07 — CI / APK recovery audit

- **Problem:** main had no active APK workflow; multiple temporary workflows had been created, patched, and then deleted during repeated CI repairs.
- **Hypothesis:** build/release infrastructure had become more fragile than the application code and could not reliably produce a current test APK.
- **Change attempted:** restore one canonical debug-build workflow with full checkout, JDK 17, Gradle setup, persistent debug signing, unit tests, APK verification, artifact upload, and GitHub release publication.
- **Result:** IN PROGRESS — this branch is the validation vehicle.
- **Evidence:** main's latest commits removed `brrrboard-ci-test.yml`, `brrrboard-release.yml`, `brrrboard-signing-bootstrap.yml`, `build-debug-apk.yml`, and `release-apk.yml` after repeated workflow repairs.
- **Next step:** keep the new workflow only if the build/test/signing verification passes.

## 2026-10-07 — Debug versionCode collision hardening

- **Problem:** debug `versionCode` used minute-resolution wall-clock time as its fallback.
- **Hypothesis:** two builds produced within the same minute can receive the same `versionCode`, causing Android update/install failures even when the APK is signed correctly.
- **Change attempted:** increase the fallback clock resolution from minutes to seconds while retaining the git-revision-count floor.
- **Result:** IN PROGRESS — requires CI build verification and device update verification.
- **Evidence:** `GitCommitCountValueSource` previously used `System.currentTimeMillis() / 60_000L`.
- **Next step:** install two successive debug APKs over the same package and verify that the second installs as an update without uninstalling.

## 2026-10-05 — Debug settings crash

- **Problem:** opening Debug settings crashed with a Compose nested-scroll measurement exception.
- **Root cause confirmed:** a `LazyColumn` was nested inside another vertically scrolling `LazyColumn`.
- **Fix:** render Debug settings directly in the parent `ColumnScope`; make filtering injectable.
- **Result:** FIXED.
- **Evidence:** commit `9c022e9977894c0d4e35b0451f07769843f3977d`.

## 2026-10-05 — Klipy GIF search returned zero results

- **Problem:** GIF searches such as cat/dog/hello returned no results.
- **Root cause confirmed:** GIF requests were filtered with `png`, although the GIF endpoint supports gif/webp/jpg/mp4/webm.
- **Fix:** use `gif,webp` for GIF searches while preserving `webp,gif,png` for stickers.
- **Result:** FIXED.
- **Evidence:** commit `65af63a1bd30c566ae2da432dd5916d1f0715be5`.

## 2026-10-05 — In-keyboard search lifecycle / panel regressions

- **Problem:** emoji/Klipy search panels could be destroyed or flicker during keyboard element switches.
- **Fix:** guard panel teardown, preserve search-mode state, add transition-state tracking, rebind search listeners/pointer trackers, and fix Klipy Compose lifecycle initialization.
- **Result:** FIXED in source; runtime verification still matters on physical devices.
- **Evidence:** commit `b08e9170dcc618b7a05d920b1b2cb2dfc49a5695`.

## 2026-08-16 — Frosted Glass compatibility crashes

- **Problem:** blur effects crashed on older Android versions and some non-bitmap drawable paths could fail.
- **Fix:** gate native blur to Android 12+, harden gradient drawable cropping, validate decoded background bitmaps, and add theme logging/fallbacks.
- **Result:** FIXED in source.
- **Evidence:** commit `4f2daef1d378e4b7ad9f8cd851dbabf3c5ad3b3`.

## 2026-06-14 to 2026-06-25 — InputConnection / resize / blur stabilization

- The project went through several related input-lag, cursor-query, resize-overlay, and native-blur fixes.
- **Important:** treat these as regression-sensitive areas; any change here should be accompanied by typing, cursor-movement, resize, and blur tests on a real device.
- **Evidence:** commits `a7dde8aea5d83e5c8a739b44b9e5f251dc624e9f`, `c967f4f891c873fdffa368fa99fd5974379b5724`, `f7b11443feb7a5457c955c6c0c539c18c5185d6d`, `7998fad4ebd8b0499d638c89d0e5ea5888dd2a05`.

## How to record a failed experiment

Use the template above even when the change is abandoned. A failed experiment is useful because it prevents repeating the same hypothesis without new evidence.

## 2026-10-07 — CI unit-test environment failure (JDK 17 vs Android SDK 36)

- **Problem:** the canonical build reached `:app:testDebugUnitTest` but 10/17 tests failed before test bodies ran.
- **Root cause confirmed:** Robolectric's SDK 36 support requires Java 21; the workflow was installing Java 17. The failures all surfaced as `UnsupportedOperationException` from `DefaultSdkProvider.java:170`.
- **Change attempted:** first workflow revision intentionally used JDK 17 because the project compiles source/target at Java 17; CI exposed the separate Robolectric runtime requirement.
- **Result:** FAIL — this was a CI/toolchain configuration bug, not evidence that those ten tests are functionally broken.
- **Evidence:** GitHub Actions run `37573697017`; 17 tests completed, 10 failed; failed classes included ParserTest, SubtypeTest, XLinkTest, SparkleDustViewTest, ClipboardImageHistoryClipTest, InputLogicTest, StringUtilsTest, SuggestTest, KlipyHistoryDaoTest, and ToolbarUtilsTest. All reported `UnsupportedOperationException at DefaultSdkProvider.java:170`.
- **Next step:** rerun the complete build with JDK 21; keep Java/Kotlin source compatibility at 17 unless the compiler itself requires a change.


## 2026-10-07 — APK-only delivery validation

- **Problem:** the normal workflow was blocked by the unit-test gate and GitHub Actions became congested by an in-progress run.
- **Change:** added `.github/workflows/brrrboard-apk-only.yml` so APK packaging/signing/upload is independently verifiable and not blocked by tests.
- **Result:** APK **BUILD + SIGNING + ARTIFACT UPLOAD PASS**; GitHub Release publication failed only because the Actions integration received HTTP 403 for the Releases endpoint.
- **Evidence:** workflow run `37575456832`; APK `FrostKeys_2.5.8-debug.apk`; SHA-256 `1b594eff45b0f9b41c290e4c234cd3a77f46229565de70a07d53d6c66ae9873a`.
- **Next step:** test this APK on the target phone; keep release publication separate until repository Actions permissions allow Releases.

## 2026-10-07 — Subtype cache/test-state investigation

- **Problem:** `SubtypeTest` failed with `IllegalArgumentException` from `.single()` when resource/enabled subtype collections contained more than one entry.
- **Change:** clear `resourceSubtypesByLocale` before rebuilding resource subtype caches; isolate enabled/selected subtype preferences in the test.
- **Result:** APK compiles with the changes; the full regression workflow has not yet completed after these changes, so this is **PROVISIONAL**, not marked fully fixed.

## 2026-10-07 — Emoji detector diagnostic

- **Problem:** `StringUtilsTest.isEmojiDetectsAllAvailableEmojis` still failed under `testRunTests`.
- **Change:** replaced the silent JVM assertion with an assertion that prints the exact failing emoji and code points.
- **Result:** diagnostic patch committed; final detector correction awaits the next successful test run.

## 2026-10-07 — CI workflow permission finding

- **Problem:** `gh release create` returned HTTP 403 `Resource not accessible by integration` even though the workflow has `contents: write`.
- **Result:** GitHub Actions artifact upload works; Release API publication is currently blocked by the connected integration's permissions.
Loading
Loading