feat: 发布4×4候选与Android手表应用内键盘组件

这个提交包含在:
sydxky 于 2026-11-14 13:55:53 +08:00 提交
1 父节点 82aeeba816
当前提交 87aa966a51
86 个文件已更改
+2697 -2975

没有与搜索条件相匹配的文件

+51
查看文件
@@ -0,0 +1,51 @@
"""Losslessly build the Android SQLite asset from the licensed original V5 trie."""
import argparse,hashlib,json,pathlib,sqlite3
APP=pathlib.Path(__file__).resolve().parents[1]
def build(rtos):
source=rtos/'third_party/crabKeyboard/pinyinDictV5_20260416'
files={p.name:p.read_bytes()for p in source.glob('*.dat')};entries={}
assert len(files)==41,'Expected the complete upstream V5 dictionary'
def number(text):
result=0
for ch in text:assert 33<=ord(ch)<=126;result=result*94+ord(ch)-33
return result
def walk(key,depth,position,length):
for item in files[f'key{depth}.dat'][position:position+length].decode('ascii').split('\x1e'):
if not item:continue
offset,size=map(number,item[1:].split('\x1f'))
if item[0]=='_':entries[key]=[word for word in files[f'value{depth}.dat'][offset:offset+size].decode('utf-8').split(' ')if word]
else:walk(key+item[0],depth+1,offset,size)
walk('',0,0,len(files['key0.dat']))
frequency_path=rtos/'third_party/jieba/dict.txt'
frequencies={}
for line in frequency_path.read_text(encoding='utf-8').splitlines():
parts=line.split(' ')
if len(parts)>=2 and parts[1].isdigit():frequencies[parts[0]]=int(parts[1])
groups9=['abc','def','ghi','jkl','mno','pqrs','tuv','wxyz'];groups14=['qw','er','ty','ui','op','as','df','gh','jk','l','zx','cv','bn','m']
map9={ch:str(i+2)for i,group in enumerate(groups9)for ch in group}
map14={ch:'0123456789abcd'[i]for i,group in enumerate(groups14)for ch in group}
target=APP/'keyboard/src/main/assets/dictionary.db';target.parent.mkdir(parents=True,exist_ok=True)
temporary=target.with_suffix('.tmp')
if temporary.exists():temporary.unlink()
db=sqlite3.connect(temporary)
db.executescript('PRAGMA page_size=4096; PRAGMA journal_mode=OFF; CREATE TABLE entries(id INTEGER PRIMARY KEY,pinyin TEXT NOT NULL UNIQUE,code9 TEXT NOT NULL,code14 TEXT NOT NULL); CREATE TABLE words(entry INTEGER NOT NULL,ordinal INTEGER NOT NULL,word TEXT NOT NULL,frequency INTEGER NOT NULL,PRIMARY KEY(entry,ordinal)) WITHOUT ROWID;')
for index,(key,words)in enumerate(sorted(entries.items()),1):
db.execute('INSERT INTO entries VALUES(?,?,?,?)',(index,key,''.join(map9[ch]for ch in key),''.join(map14[ch]for ch in key)))
db.executemany('INSERT INTO words VALUES(?,?,?,?)',[(index,ordinal,word,frequencies.get(word,0))for ordinal,word in enumerate(words)])
db.executescript('CREATE INDEX entries_code9 ON entries(code9);CREATE INDEX entries_code14 ON entries(code14);ANALYZE;PRAGMA user_version=1;')
db.commit()
restored={}
for key,word in db.execute('SELECT e.pinyin,w.word FROM entries e JOIN words w ON w.entry=e.id ORDER BY e.pinyin,w.ordinal'):restored.setdefault(key,[]).append(word)
assert restored==entries,'Full dictionary round-trip mismatch'
assert db.execute('PRAGMA integrity_check').fetchone()[0]=='ok';db.close();temporary.replace(target)
report={'keys':len(entries),'candidates':sum(map(len,entries.values())),'longest_word':max(len(word)for words in entries.values()for word in words),'sha256':hashlib.sha256(target.read_bytes()).hexdigest(),'bytes':target.stat().st_size,'round_trip':True,'source_files':{name:hashlib.sha256(data).hexdigest()for name,data in sorted(files.items())},'jieba_sha256':hashlib.sha256(frequency_path.read_bytes()).hexdigest()}
(APP/'keyboard/src/main/assets/dictionary-manifest.json').write_text(json.dumps(report,ensure_ascii=False,indent=2)+'\n',encoding='utf-8')
print(json.dumps({k:v for k,v in report.items()if k!='source_files'},ensure_ascii=False))
if __name__=='__main__':
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--rtos-source',type=pathlib.Path)
args=parser.parse_args();rtos=args.rtos_source
if rtos is None:
rtos=next((p for p in [APP.parent/'com.sydxky.rtos.inputmethod',APP.parent/'RTOS/com.sydxky.rtos.inputmethod']if p.exists()),None)
assert rtos is not None,'Provide the licensed RTOS source with --rtos-source';build(rtos)
+75
查看文件
@@ -0,0 +1,75 @@
"""Capture public screenshots by operating the actual embedded keyboard UI."""
import argparse
from pathlib import Path
import re
import subprocess
import time
import xml.etree.ElementTree as ET
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--serial', required=True)
parser.add_argument('--output', type=Path, required=True)
args = parser.parse_args()
args.output.mkdir(parents=True, exist_ok=True)
def adb(*command):
return subprocess.check_output(['adb', '-s', args.serial, *command], timeout=30)
def tap(value, description=False):
adb('shell', 'uiautomator', 'dump', '/sdcard/crab-capture.xml')
tree = ET.fromstring(adb('exec-out', 'cat', '/sdcard/crab-capture.xml'))
for node in tree.iter('node'):
if node.get('content-desc' if description else 'text') == value:
x1, y1, x2, y2 = map(int, re.findall(r'\d+', node.get('bounds')))
adb('shell', 'input', 'tap', str((x1 + x2)//2), str((y1 + y2)//2))
time.sleep(.22)
return
raise RuntimeError('Missing UI control: ' + value)
def shot(name):
time.sleep(2)
(args.output / (name + '.png')).write_bytes(adb('exec-out', 'screencap', '-p'))
adb('shell', 'input', 'keyevent', '224')
adb('shell', 'wm', 'dismiss-keyguard')
adb('shell', 'am', 'force-stop', 'com.sydxky.crabinput')
adb('shell', 'am', 'start', '-n', 'com.sydxky.crabinput/.MainActivity')
time.sleep(3)
shot('android-home')
tap('demo_text', True)
tap('26键')
shot('android-qwerty')
for letter in 'shi':
tap(letter)
tap('更多候选', True)
shot('android-candidates-16')
tap('返回键盘', True)
# Begin a clean composition by closing and reopening the real editor.
tap('收起键盘', True)
tap('demo_text', True)
tap('14键')
shot('android-fourteen')
for key in ['BN', 'UI', 'GH', 'AS', 'OP']:
tap(key)
tap('更多候选', True)
shot('android-phrases')
tap('你好')
tap('收起键盘', True)
shot('android-result')
adb('shell', 'am', 'start', '-n', 'com.sydxky.crabinput.host/.HostActivity')
time.sleep(2)
tap('fixture_editor', True)
tap('26键')
for letter in 'nihao':
tap(letter)
tap('你好')
tap('收起键盘', True)
shot('android-host')
adb('shell', 'rm', '-f', '/sdcard/crab-capture.xml')
print('Captured 7 real-device UI states; no system IME changes.')
if __name__ == '__main__':
main()
+39
查看文件
@@ -0,0 +1,39 @@
"""Install the release and run embedded-component instrumentation on one explicit device."""
import argparse,hashlib,json,pathlib,subprocess,time
APP=pathlib.Path(__file__).resolve().parents[1]
def main():
parser=argparse.ArgumentParser(description=__doc__);parser.add_argument('--serial',required=True);parser.add_argument('--output',type=pathlib.Path,required=True)
args=parser.parse_args();args.output.mkdir(parents=True,exist_ok=True)
def adb(*command,binary=False,timeout=30,retries=3):
last=None
for attempt in range(retries):
result=subprocess.run(['adb','-s',args.serial,*command],stdout=subprocess.PIPE,stderr=subprocess.PIPE,timeout=timeout)
if result.returncode==0:return result.stdout if binary else result.stdout.decode('utf-8',errors='replace').strip()
last=(result.stdout+result.stderr).decode('utf-8',errors='replace').strip()
if command[0]!='install'and'device' not in last.lower()and'closed' not in last.lower():break
print('Device connection interrupted; retrying this command.',flush=True);time.sleep(1)
raise RuntimeError(last)
apk=APP/'app/build/outputs/apk/release/app-release.apk';test=APP/'app/build/outputs/apk/androidTest/release/app-release-androidTest.apk'
assert apk.is_file()and test.is_file()
model=adb('shell','getprop','ro.product.model');sdk=adb('shell','getprop','ro.build.version.sdk')
original=args.output/'original-ime.txt'
if not original.exists():original.write_text(adb('shell','settings','get','secure','default_input_method')+'\n',encoding='utf-8')
host=APP/'host/build/outputs/apk/debug/host-debug.apk'
for path in [apk,test,host]:print(adb('install','-r',str(path),timeout=90),flush=True)
before=adb('shell','settings','get','secure','default_input_method')
print('Default system IME retained; testing the embedded keyboard only.',flush=True)
adb('shell','input','keyevent','224');adb('shell','wm','dismiss-keyguard')
# The instrumentation is bounded short acceptance, not a stress/soak run.
command=['adb','-s',args.serial,'shell','am','instrument','-w','-r','com.sydxky.crabinput.test/androidx.test.runner.AndroidJUnitRunner']
result=subprocess.run(command,stdout=subprocess.PIPE,stderr=subprocess.STDOUT,timeout=240)
output=result.stdout.decode('utf-8',errors='replace');(args.output/'instrumentation.txt').write_text(output,encoding='utf-8');print(output,flush=True)
passed=result.returncode==0 and 'OK (8 tests)'in output and 'FAILURES!!!'not in output
report={'model':model,'sdk':sdk,'version':'1.2.0','apk_sha256':hashlib.sha256(apk.read_bytes()).hexdigest(),'instrumentation_tests':8,'embedded_component':True,'system_ime_unchanged':adb('shell','settings','get','secure','default_input_method')==before,'passed':passed}
(args.output/'device-validation.json').write_text(json.dumps(report,indent=2)+'\n',encoding='utf-8')
assert passed and report['system_ime_unchanged'],'See instrumentation.txt; failed/unfinished cases are not accepted'
adb('shell','am','start','-n','com.sydxky.crabinput/.MainActivity');time.sleep(.5)
(args.output/'home.png').write_bytes(adb('exec-out','screencap','-p',binary=True))
print(json.dumps(report),flush=True)
if __name__=='__main__':main()