Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
95 commits
Select commit Hold shift + click to select a range
84d05a3
Align the 26-key letter hints with the reference skin
Slacker-LLC Oct 14, 2026
108be29
Align key weights, the nine-key column and candidate spacing with the…
Slacker-LLC Oct 14, 2026
559c711
Align the nine-key reading rail with the reference filter list
Slacker-LLC Oct 14, 2026
5cc0440
Type the zero through the space bar in the rail stress case
Slacker-LLC Oct 14, 2026
1ccb9ef
Skip the stale-capture clipboard tests while this app is the active IME
Slacker-LLC Oct 14, 2026
ffe8db9
Allow a side-by-side development build through openimeAppIdSuffix
Slacker-LLC Oct 14, 2026
5ed5710
Rank candidates that spell out every typed letter above fragment matches
Slacker-LLC Oct 14, 2026
f921b1e
Offer the reference skin's URL marks in the nine-key rail
Slacker-LLC Oct 14, 2026
3781cfb
Draw the first candidate in the accent, not on a filled tile
Slacker-LLC Oct 14, 2026
174aac8
Draw the expanded page the way the reference ships it
Slacker-LLC Oct 14, 2026
6bdaae6
Record the alignment work in the change log
Slacker-LLC Oct 14, 2026
94616ec
Ship the reference's seven-key bottom row and its @# nine-key cell
Slacker-LLC Oct 14, 2026
0538ce1
Split the pre-edit into the reference's two colours
Slacker-LLC Oct 14, 2026
98e3806
Lay the candidate strip out as the reference does
Slacker-LLC Oct 14, 2026
147a2a0
Give the idle toolbar the reference's pill keys
Slacker-LLC Oct 14, 2026
bc1c806
Lay the number page out as the reference's own number layout
Slacker-LLC Oct 14, 2026
530311f
Give the stroke board the reference's nine-cell grid
Slacker-LLC Oct 14, 2026
852f792
Put the idle toolbar in the reference's order and its own scrolling s…
Slacker-LLC Oct 14, 2026
1bdf76c
Move the symbol page's categories into the reference's bottom strip
Slacker-LLC Oct 14, 2026
a771496
Give the symbol page its own 123 and delete keys
Slacker-LLC Oct 14, 2026
d18ad19
List the typed symbols in the symbol board's toolbar
Slacker-LLC Oct 14, 2026
54138e4
Draw the launcher pages on the reference's tile grid
Slacker-LLC Oct 14, 2026
6b6a199
Meet the floating keyboard with the reference's tip banner
Slacker-LLC Oct 14, 2026
45bb047
Cap the pre-edit at the reference's half of the page
Slacker-LLC Oct 14, 2026
1d51bc1
Wear the reference's quick-input row for our own suggestions
Slacker-LLC Oct 14, 2026
59b03c7
Point every new citation at its own line
Slacker-LLC Oct 14, 2026
111604f
Ride the suggestion's origin glyph above its text
Slacker-LLC Oct 14, 2026
dac5435
Point two candidate-strip citations at the lines they quote
Slacker-LLC Oct 14, 2026
c2ba905
Measure the origin glyph from the line it marks
Slacker-LLC Oct 14, 2026
167d9f7
Settle the sixteen instrumented failures the alignment left
Slacker-LLC Oct 14, 2026
6b5be72
Draw every key cap with its own class's reference margins
Slacker-LLC Oct 14, 2026
2c51a43
Round every key and the long-press bubble the way the skin does
Slacker-LLC Oct 14, 2026
cd34267
Shrink a one-line mark to its key instead of trusting autosize
Slacker-LLC Oct 14, 2026
110b2c8
Read the rail in the one font and margin the skin gives it
Slacker-LLC Oct 14, 2026
ae1e6f2
Read every key face at the size the reference gives it
Slacker-LLC Oct 14, 2026
a0ccbab
Split the composing strip the way the reference splits its own
Slacker-LLC Oct 14, 2026
fe9c987
Let the key fills trade places on press, and put the shadow back unde…
Slacker-LLC Oct 14, 2026
f9ceba7
Fill each pressed surface with the reference's own colour
Slacker-LLC Oct 14, 2026
b7a1546
Give the corner hints, idle toolbar and strip split their own colours
Slacker-LLC Oct 14, 2026
7097ccf
Let the dark skin's first candidate be plain, and log the day's ports
Slacker-LLC Oct 14, 2026
693a019
Press tiles and toolbar keys onto their own page, not into the text
Slacker-LLC Oct 14, 2026
0711907
Give the number page's return key the skin's theme colour
Slacker-LLC Oct 14, 2026
1ad3427
Read the switch page and the English board from their own classes
Slacker-LLC Oct 14, 2026
6d2aa7d
Draw the nine-key and stroke bottom rows on white keys
Slacker-LLC Oct 14, 2026
a3bc8c1
Draw the panel header's close key as the reference's circle
Slacker-LLC Oct 14, 2026
20078dd
Record the reference surfaces checked and left unported
Slacker-LLC Oct 14, 2026
f717f6f
Paint the expanded candidates page from its own classes
Slacker-LLC Oct 14, 2026
0bb3ce0
Note the rail's custom-symbol class among the unported ones
Slacker-LLC Oct 15, 2026
9311ff7
Keep the board area's reference side inset
Slacker-LLC Oct 15, 2026
993f240
Give the expanded candidates page its own side inset
Slacker-LLC Oct 15, 2026
daf9556
Record why the landscape faces keep one compactness factor
Slacker-LLC Oct 15, 2026
997ffda
Record the colour sweep and the accent-key label rule
Slacker-LLC Oct 15, 2026
6a39de1
Revert the portrait board's reference side inset
Slacker-LLC Oct 15, 2026
61e15b2
Add the reference's pinyin display setting: candidate bar or input box
Slacker-LLC Oct 15, 2026
4f994f6
Record the reference's candidate size scale
Slacker-LLC Oct 15, 2026
27c9d63
Keep the best syllable paths, not the first ones
Slacker-LLC Oct 15, 2026
441d487
Reach the word table for the reading the user typed
Slacker-LLC Oct 15, 2026
8078ba9
Lead the candidate bar with the reading when no word spells it
Slacker-LLC Oct 15, 2026
cbe6909
Return the committed text and the candidate bar to the volume run
Slacker-LLC Oct 15, 2026
f221ef9
Read the sentence words off the lattice, one position at a time
Slacker-LLC Oct 15, 2026
31ddfce
Spell the syllables the reference spells with v
Slacker-LLC Oct 15, 2026
9d6f480
Lead the 26-key bar with the words the user picked before
Slacker-LLC Oct 15, 2026
33dc757
Let one initial and one final fuzzy pair apply together
Slacker-LLC Oct 15, 2026
c204091
Let a fuzzy spelling be one the catalogue has no syllable for
Slacker-LLC Oct 15, 2026
39b674d
Carry the four fuzzy pairs the reference has beyond the eleven
Slacker-LLC Oct 15, 2026
3f42ad8
Show a committed word the emoji the reference gives it
Slacker-LLC Oct 15, 2026
6865cd4
Record why the stroke candidates are not in the reference's order
Slacker-LLC Oct 15, 2026
4830658
Call the association setting what the reference calls it
Slacker-LLC Oct 15, 2026
ac44805
Name the double-pinyin schemes the way the reference does
Slacker-LLC Oct 15, 2026
12b51e2
Turn on the wubi four-code commit the reference defaults to
Slacker-LLC Oct 15, 2026
fcae0dd
Start the six fuzzy pairs the reference starts
Slacker-LLC Oct 15, 2026
55d979a
Read the words the reference reads differently
Slacker-LLC Oct 15, 2026
8614395
Read the letter-hint order from the reference on P and L
Slacker-LLC Oct 15, 2026
ea7e255
Record the two differences the preference sweep leaves
Slacker-LLC Oct 15, 2026
46e0348
Keep one long-press order on every key
Slacker-LLC Oct 15, 2026
efa79cf
Give the floating card a touchable area again
Slacker-LLC Oct 15, 2026
143b2b2
Stop re-measuring the keyboard on every frame
Slacker-LLC Oct 15, 2026
ca2cf02
Let the top zone claim the gestures that land on bare space
Slacker-LLC Oct 15, 2026
21771a7
Write the floating window only when the card really moves
Slacker-LLC Oct 15, 2026
8e859fb
Stop a press with no finger from blocking every later rebuild
Slacker-LLC Oct 15, 2026
9034005
Preview the keyboard height while dragging and save it once
Slacker-LLC Oct 15, 2026
423de18
Record the four touch-path fixes in the changelog
Slacker-LLC Oct 15, 2026
d4f0089
Find the top zone's blank band next to the icon strip
Slacker-LLC Oct 15, 2026
bc97f37
Insert a line break on a long press of a send key
Slacker-LLC Oct 15, 2026
8754912
Split a clipboard entry into words on a long press
Slacker-LLC Oct 15, 2026
1cb1fc4
Recommend the poem line the composition spells
Slacker-LLC Oct 15, 2026
86ebaab
Port four more reference input behaviours
Slacker-LLC Oct 15, 2026
dfac7b1
Add the 快捷格式输入 settings and wire the batch into the service
Slacker-LLC Oct 15, 2026
3070e33
Give nine-key the reference's reading list and its digit-level fuzzy
Slacker-LLC Oct 15, 2026
0f5a71e
Restore the original keyboard layouts and return key styling
Slacker-LLC Oct 15, 2026
3304d29
Match the settings accent, centre the launcher pages and accept the o…
Slacker-LLC Oct 15, 2026
7808c86
Release 1.0.0
Slacker-LLC Oct 15, 2026
ad2a73c
Merge remote-tracking branch 'origin/main' into align/letter-hints
Slacker-LLC Oct 15, 2026
342d5cb
Stop the floating-insets test from calling a hidden framework method
Slacker-LLC Oct 15, 2026
fb3cdda
Let the release's click run before a pending row rebuild
Slacker-LLC Oct 15, 2026
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
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -257,11 +257,11 @@ jobs:
channel_flags=(--prerelease)
latest_flag=--latest=false
fi
# The full and lite APKs are the assets; their SHA-256 and the signing certificate
# The -llm and plain APKs are the assets; their SHA-256 and the signing certificate
# are in the release notes, and the licenses ship inside the APK.
gh release create "$GITHUB_REF_NAME" \
release-files/openIME-*-arm64-release.apk \
release-files/openIME-*-arm64-lite-release.apk \
release-files/openIME-llm-v*.apk \
release-files/openIME-v*.apk \
--repo "$GITHUB_REPOSITORY" \
--verify-tag \
--draft \
Expand All @@ -270,9 +270,9 @@ jobs:
--notes-file release-files/RELEASE_NOTES.md

names="$(gh release view "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" --json assets --jq '.assets[].name' | sort)"
wanted="$(printf 'openIME-%s-arm64-lite-release.apk\nopenIME-%s-arm64-release.apk\n' "$GITHUB_REF_NAME" "$GITHUB_REF_NAME" | sort)"
wanted="$(printf 'openIME-%s.apk\nopenIME-llm-%s.apk\n' "$GITHUB_REF_NAME" "$GITHUB_REF_NAME" | sort)"
if [[ "$names" != "$wanted" ]]; then
echo "::error::Expected the full and the lite APK as release assets; the draft was left unpublished."
echo "::error::Expected the -llm and the plain APK as release assets; the draft was left unpublished."
echo "found:"
echo "$names"
exit 1
Expand Down
97 changes: 97 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,12 @@ The app has no `INTERNET` permission, so your text cannot leave the phone.
## Install

1. Open [Releases](https://github.com/Slacker-LLC/openIME/releases).
Download the latest `openIME-v*-arm64-release.apk`.
Download `openIME-llm-v*.apk` for the build that carries the offline voice
models, or `openIME-v*.apk` for the smaller build that has none.
2. Check the SHA-256 checksum that the release notes show:

```bash
sha256sum openIME-v*-arm64-release.apk
sha256sum openIME-llm-v*.apk
```

3. Install the APK.
Expand Down
4 changes: 2 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,12 @@ openIME 是一款独立的 Android 系统输入法。拼音候选、用户词库

## 下载与安装

1. 在 [Releases](https://github.com/Slacker-LLC/openIME/releases) 下载最新的 `openIME-v*-arm64-release.apk`。
1. 在 [Releases](https://github.com/Slacker-LLC/openIME/releases) 下载:带离线语音模型的 `openIME-llm-v*.apk`,或是不含模型的小体积版本 `openIME-v*.apk`。
发布包仅支持 `arm64-v8a` 设备(绝大多数近年的 Android 手机),系统要求 Android 8.0(API 26)及以上。
2. 对照发布说明里的 SHA-256 校验后再安装:

```bash
sha256sum openIME-v*-arm64-release.apk
sha256sum openIME-llm-v*.apk
```

3. 打开 openIME,按引导启用输入法并切换到 openIME。
Expand Down
3 changes: 2 additions & 1 deletion THIRD_PARTY_NOTICES.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,14 @@ This file covers only the third-party components.
| sherpa-onnx Android runtime | `app/libs/sherpa-onnx-1.13.6.aar` | https://github.com/k2-fsa/sherpa-onnx | Apache-2.0 | The APK contains `assets/licenses/sherpa-onnx-Apache-2.0.txt`. |
| Streaming Paraformer bilingual (Chinese and English) INT8 model | `app/src/main/assets/models/voice/bilingual-paraformer/` | https://huggingface.co/csukuangfj/sherpa-onnx-streaming-paraformer-bilingual-zh-en | Apache-2.0 | The APK contains only `encoder.int8.onnx`, `decoder.int8.onnx` and `tokens.txt`. `assets/licenses/paraformer-model-Apache-2.0.txt` is included. `manifest.json` pins the model ID, the version and the file hashes. |
| CT-Transformer punctuation INT8 model | `app/src/main/assets/models/voice/punctuation/` | https://modelscope.cn/models/iic/punc_ct-transformer_zh-cn-common-vocab272727-pytorch and https://github.com/k2-fsa/sherpa-onnx/releases/tag/punctuation-models | Apache-2.0 | We pin the `2024-04-12-int8` export. The manifest lists the file hashes. The APK contains `ct-transformer-Apache-2.0.txt`. |
| Apache Commons Compress | `org.apache.commons:commons-compress:1.27.1` (Maven) | https://commons.apache.org/proper/commons-compress/ | Apache-2.0 | Imported to read the bzip2 archives the official sherpa-onnx voice-model releases ship. The APK contains `assets/licenses/commons-compress-Apache-2.0.txt` (the artifact's own `META-INF/NOTICE.txt` and `META-INF/LICENSE.txt`). |
| Microsoft Fluent Emoji | `app/src/main/assets/emoji/fluent/` | https://github.com/microsoft/fluentui-emoji | MIT | The APK has only the emoji images that the app uses. It contains `assets/licenses/fluent-emoji-MIT.txt`. |

## Checks before a release

- Do not delete `LICENSE`, `COPYING`, `AUTHORS` or `NOTICE` files from vendored source directories.
- Keep the license texts in `app/src/main/assets/licenses/` in the APK.
This includes Rime Ice, librime, OpenCC, Snappy, sherpa-onnx, the Paraformer model and Fluent Emoji.
This includes Rime Ice, librime, OpenCC, Snappy, sherpa-onnx, Apache Commons Compress, the Paraformer model and Fluent Emoji.
- When you update the voice runtime or a model, check the license of the new version.
Do not rely on the earlier result.
- When the dictionary source or its pinned commit changes, update this file, [docs/LICENSING.md](docs/LICENSING.md) and the license files in the APK.
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.0.10-beta.1
1.0.0
8 changes: 7 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ android {
ndkVersion = "27.0.12077973"

defaultConfig {
applicationId = "llc.slacker.openime"
// A side-by-side development build (the owner's phone runs the .dev
// variant) passes -PopenimeAppIdSuffix=.dev; release builds never do.
applicationId = "llc.slacker.openime" + ((findProperty("openimeAppIdSuffix") as String?) ?: "")
minSdk = 26
targetSdk = 36
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
Expand Down Expand Up @@ -212,6 +214,10 @@ androidComponents {

dependencies {
implementation(files("libs/sherpa-onnx-1.13.6.aar"))
// The official sherpa-onnx voice-model archives ship as tar.bz2, and the
// platform ships no bzip2 decoder (`java.util.zip` has zip/gzip only), so
// the import needs one from here.
implementation("org.apache.commons:commons-compress:1.27.1")
testImplementation("junit:junit:4.13.2")
testImplementation("org.json:json:20260814")
androidTestImplementation("androidx.test.ext:junit:1.3.0")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ import llc.slacker.openime.voice.VoiceRecognitionEvents
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNotSame
import org.junit.Assert.assertNull
import org.junit.Assert.assertSame
import org.junit.Assert.assertTrue
import org.junit.Assume.assumeTrue
import org.junit.Test
import org.junit.runner.RunWith
import java.lang.reflect.Proxy
Expand Down Expand Up @@ -653,10 +655,11 @@ class AuditInteractionInstrumentedTest {
keyboard.showPanel(Panel.TOOLS)
val card = keyboard.findViewWithTag<View>("tool:表情")
assertTrue("Tool card must be keyboard-focusable", card.isFocusable)
// The column is the target; its icon tile mirrors the column's state.
val tile = (card as ViewGroup).getChildAt(0)
assertTrue("Tool tile must expose pressed and focus feedback", tile.background is StateListDrawable)
assertTrue("Tool tile must follow the card's pressed state", tile.isDuplicateParentStateEnabled)
// The whole cell is the reference's own tile: it carries the face and
// the press, and its glyph mirrors the tile's state.
assertTrue("Tool tile must expose pressed and focus feedback", card.background is StateListDrawable)
val icon = (card as ViewGroup).getChildAt(0)
assertTrue("Tool glyph must follow the tile's pressed state", icon.isDuplicateParentStateEnabled)
assertEquals("表情", card.contentDescription)
val label = card.getChildAt(1)
assertTrue(
Expand Down Expand Up @@ -919,7 +922,8 @@ class AuditInteractionInstrumentedTest {
@Test
fun modeSwitchDismissesLongPressChoicePopup() = withKeyboard { harness, _, keyboard ->
harness.awaitMain {
// 1 is the nine-key segmentation key; its long press offers @ # /.
// The nine-key 1 cell is the @# key: its long press offers the
// reference's own symbol list.
keyboard.setMode(KeyboardMode.PINYIN_9, notifyListener = false)
val segment = keyboard.findViewWithTag<View>("key-9:1")
val baseline = keyboard.childCount
Expand Down Expand Up @@ -1174,6 +1178,7 @@ class AuditInteractionInstrumentedTest {
var downTime = 0L
var released = false
var spaceViewSurvivedRebuildPoll = false
var starvedPastLongPress = false
lateinit var spaceKey: View
val releaseDelayMs = minOf(
100L,
Expand Down Expand Up @@ -1201,6 +1206,12 @@ class AuditInteractionInstrumentedTest {
// Give the deferred row rebuild poll several main-loop turns, but
// release before Android's own long-click can consume this tap.
keyboard.postDelayed({
// 机器卡顿时这个回调会晚到:一旦越过长按阈值,平台就不再把它当点击
// (`View.onTouchEvent` 置了 `mHasPerformedLongPress` 后不再发 click),
// 这颗键按设计就是语音而空格。这条断言在那种时间线下没有意义,跳过而不是
// 报错——否则 CI 越忙越"红"。
starvedPastLongPress =
SystemClock.uptimeMillis() - downTime >= ViewConfiguration.getLongPressTimeout().toLong()
spaceViewSurvivedRebuildPoll =
keyboard.findViewWithTag<View>("key-space") === spaceKey
val point = keyPoint(keyboard, "key-space")
Expand All @@ -1210,6 +1221,10 @@ class AuditInteractionInstrumentedTest {
true
}
harness.awaitMain(timeoutMs = 3_000L) { if (released) true else null }
assumeTrue(
"The main thread ran past the long-press deadline, so this press is a voice hold, not a tap",
!starvedPastLongPress,
)
harness.awaitMain {
assertTrue(
"The Space view must remain attached while the rebuild poll sees the active touch",
Expand All @@ -1222,6 +1237,65 @@ class AuditInteractionInstrumentedTest {
assertEquals("A rebuild during the press must not swallow the space", 1, recorder.spaces)
}

@Test
fun aPressLeftWithoutAFingerOnlyDefersWhileTheFingerIsStillDown() = withKeyboard { harness, _, keyboard ->
// Half by half: the deferral (see rebuildWhileSpaceIsHeldDoesNotSwallowTheSpace)
// must protect a real gesture and must not outlive it. A press left behind by
// an earlier gesture — the rebuild that swallowed it, or a view swapped out
// mid-gesture — used to defer every later rebuild and poll for it forever.
val releaseDelayMs = minOf(
100L,
(ViewConfiguration.getLongPressTimeout().toLong() / 2L).coerceAtLeast(1L),
)
lateinit var staleSpace: View
harness.awaitMain {
staleSpace = requireNotNull(keyboard.findViewWithTag("key-space"))
staleSpace.isPressed = true
true
}
harness.awaitMain {
keyboard.setMode(KeyboardMode.ENGLISH_26, notifyListener = false)
val rebuilt = keyboard.findViewWithTag<View>("key-space")
assertNotNull("The mode switch must rebuild the rows of the new layout", rebuilt)
assertNotSame(
"A press with no finger on the screen must not defer this rebuild",
staleSpace,
rebuilt,
)
assertFalse("The deferred rebuild must let go of the stale key", staleSpace.isAttachedToWindow)
true
}

// A finger really on the screen: the deferral still holds, and the rebuild
// lands as soon as that finger lifts.
var released = false
var downTime = 0L
lateinit var heldSpace: View
harness.awaitMain {
val point = keyPoint(keyboard, "key-space")
heldSpace = requireNotNull(keyboard.findViewWithTag("key-space"))
downTime = SystemClock.uptimeMillis()
pointers(keyboard, downTime, MotionEvent.ACTION_DOWN, listOf(point))
assertTrue("Space must own the active press", heldSpace.isPressed)
keyboard.setMode(KeyboardMode.PINYIN_26, notifyListener = false)
assertSame(
"A live finger must still defer the rebuild",
heldSpace,
keyboard.findViewWithTag<View>("key-space"),
)
keyboard.postDelayed({
pointers(keyboard, downTime, MotionEvent.ACTION_UP, listOf(point))
released = true
}, releaseDelayMs)
true
}
harness.awaitMain(timeoutMs = 3_000L) { if (released) true else null }
// The rebuild poll runs on a 40ms cadence; give it a few turns.
harness.awaitMain(timeoutMs = 3_000L) {
keyboard.findViewWithTag<View>("key-space")?.takeIf { it !== heldSpace }
}
}

@Test
fun backspaceTapDeletesExactlyOnceAndCancelDeletesNothing() = withKeyboard { harness, recorder, keyboard ->
harness.awaitMain {
Expand Down
Loading
Loading