diff --git a/.github/workflows/release-mobile-lab-control.yml b/.github/workflows/release-mobile-lab-control.yml new file mode 100644 index 0000000..8e2761b --- /dev/null +++ b/.github/workflows/release-mobile-lab-control.yml @@ -0,0 +1,55 @@ +name: release Mobile Lab Control + +on: + push: + tags: + - "mobile-lab-control-v*" + +permissions: + contents: write + +jobs: + release-apk: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "17" + - name: Verify Android build inputs + run: | + test -f "$ANDROID_SDK_ROOT/platforms/android-35/android.jar" + test -x "$ANDROID_SDK_ROOT/build-tools/35.0.0/aapt2" + test -x "$ANDROID_SDK_ROOT/build-tools/35.0.0/apksigner" + - name: Restore release signing key + env: + KEYSTORE_B64: ${{ secrets.MOBILE_LAB_CONTROL_KEYSTORE_B64 }} + run: | + test -n "$KEYSTORE_B64" + umask 077 + printf '%s' "$KEYSTORE_B64" | base64 --decode > "$RUNNER_TEMP/mobile-lab-control-release.jks" + - name: Build release APK + env: + ANDROID_RELEASE_KEYSTORE: ${{ runner.temp }}/mobile-lab-control-release.jks + ANDROID_RELEASE_KEY_ALIAS: ${{ secrets.MOBILE_LAB_CONTROL_KEY_ALIAS }} + ANDROID_KEYSTORE_PASSWORD: ${{ secrets.MOBILE_LAB_CONTROL_KEYSTORE_PASSWORD }} + ANDROID_KEY_PASSWORD: ${{ secrets.MOBILE_LAB_CONTROL_KEY_PASSWORD }} + run: | + version=${GITHUB_REF_NAME#mobile-lab-control-v} + IFS=. read -r major minor patch <<< "$version" + version_code=$((major * 10000 + minor * 100 + patch)) + VERSION_NAME="$version" VERSION_CODE="$version_code" BUILD_MODE=release apps/mobile-lab-control/build.sh + cd apps/mobile-lab-control/build + sha256sum "mobile-lab-control-$version.apk" > "mobile-lab-control-$version.apk.sha256" + - name: Publish GitHub Release + env: + GH_TOKEN: ${{ github.token }} + run: | + version=${GITHUB_REF_NAME#mobile-lab-control-v} + apk="apps/mobile-lab-control/build/mobile-lab-control-$version.apk" + checksum="$apk.sha256" + gh release create "$GITHUB_REF_NAME" "$apk" "$checksum" \ + --verify-tag \ + --title "Mobile Lab Control $version" \ + --notes "Installable release-signed APK and SHA-256 checksum for the three independent Happy, llama.cpp, and model-profile widgets. The app has no Internet permission. / 三个独立 Happy、llama.cpp、模型配置微件的正式签名 APK 与 SHA-256;App 不申请联网权限。" diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 17d0b31..61750aa 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -12,5 +12,35 @@ jobs: repository: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - run: ./scripts/verify.sh + + android-app: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: "17" + - name: Verify Android build inputs + run: | + test -f "$ANDROID_SDK_ROOT/platforms/android-35/android.jar" + test -x "$ANDROID_SDK_ROOT/build-tools/35.0.0/aapt2" + test -x "$ANDROID_SDK_ROOT/build-tools/35.0.0/apksigner" + - name: Build debug-signed verification APK + run: BUILD_MODE=debug apps/mobile-lab-control/build.sh + - name: Verify package permissions and signature + run: | + apk=apps/mobile-lab-control/build/mobile-lab-control-0.4.0-debug.apk + permissions=$("$ANDROID_SDK_ROOT/build-tools/35.0.0/aapt2" dump permissions "$apk") + printf '%s\n' "$permissions" + ! grep -q 'android.permission.INTERNET' <<< "$permissions" + grep -q 'com.termux.permission.RUN_COMMAND' <<< "$permissions" + "$ANDROID_SDK_ROOT/build-tools/35.0.0/apksigner" verify --verbose "$apk" + - uses: actions/upload-artifact@v4 + with: + name: mobile-lab-control-debug-verification + path: apps/mobile-lab-control/build/mobile-lab-control-0.4.0-debug.apk + if-no-files-found: error + retention-days: 7 diff --git a/.gitignore b/.gitignore index 9f0fced..554329a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ .env secrets/ evidence/private/ +apps/mobile-lab-control/build/ +*.jks +*.keystore diff --git a/README.md b/README.md index fef7a39..98f2a65 100644 --- a/README.md +++ b/README.md @@ -31,14 +31,17 @@ OpenCode TUI/Web ---------------------+-> provider profile -> model API loopback OpenAI-compatible API for lightweight offline tasks. - Switching model profiles is explicit, keeps secrets outside committed configuration, and never changes service lifecycle implicitly. -- Termux:Widget exposes three separate status-bearing controls for Happy, - llama.cpp, and model configuration. +- The offline `Mobile Lab Control` app exposes three independent native widgets + for Happy, llama.cpp, and model profiles. Termux:Widget remains an opt-in + terminal fallback. ## Repository layout - `skills/deploy-android-ai-stack/` — install and operate the runtime. - `skills/validate-android-ai-stack/` — bounded, evidence-based acceptance checks. +- `apps/mobile-lab-control/` — dependency-free Android AppWidget source and + reproducible build script. - `config/profiles/` — safe provider-profile templates. - `docs/` — architecture and the verified OnePlus 8T case. - `manifests/` — facts captured from tested devices, with secret-free values. @@ -52,7 +55,9 @@ OpenCode TUI/Web ---------------------+-> provider profile -> model API [SKILL.md](skills/deploy-android-ai-stack/SKILL.md). 3. Create a provider profile from one of `config/profiles/*.example` and put the token in the referenced private file. -4. Use `$validate-android-ai-stack` to run zero-call checks first, then opt in +4. Install the native control bridge and a checksum-verified APK from + [GitHub Releases](https://github.com/toolazytoname/android-ai-stack/releases). +5. Use `$validate-android-ai-stack` to run zero-call checks first, then opt in to at most one model call per client. For an on-phone checklist, use the bilingual @@ -77,6 +82,7 @@ fallback have both been exercised. Exact versions and evidence are recorded in | Phone-local Happy | exercised end to end | lifecycle wrapper included | | Provider switching | exercised on the private device config | redacted generic profiles included | | llama.cpp local model | Qwen3 0.6B Q8, API and `local-lite` passed | authenticated loopback lifecycle and bounded checker included | +| Native control widgets | three independent widgets exercised with zero model calls | source, allow-list bridge, bilingual UI, and release APK included | The final phone acceptance also exercised the control split: profile changes left running services untouched, Happy reported `STALE` when its startup diff --git a/README.zh-CN.md b/README.zh-CN.md index 8ade261..897d293 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -9,8 +9,9 @@ Claude Code、Happy agent 与 Happy server 都运行在手机上;模型推理可以像 电脑一样,通过 URL/token 使用云 API、私有中转或可选的本地小模型。 -本仓只发布可复用的部署、模型切换和验收经验,不包含真实 token、私人 -endpoint、CC Switch 数据库、Happy 上游源码、签名 APK 或模型权重。 +本仓只发布可复用的部署、模型切换和验收经验,不包含真实 Token、私人 +Endpoint、CC Switch 数据库、Happy 上游源码或模型权重。正式签名 APK 只作为 +GitHub Release 资产发布,不进入 Git 历史。 ## 架构 @@ -28,13 +29,14 @@ OpenCode TUI/Web ------------------------+-> provider profile -> 模型 API - 本地推理也是可选扩展:小型 llama.cpp server 只监听 loopback,适合离线 摘要、分类、脱敏和断网兜底。 - 模型 profile 只保存路由信息;凭据始终位于独立的 mode `0600` 文件。 -- 配置切换不会隐式启停服务;Termux:Widget 分别提供 Happy、llama.cpp、Model - 三个带状态入口。 +- 离线 `Mobile Lab Control` App 分别提供 Happy、llama.cpp、模型配置三个 + 原生微件;Termux:Widget 只保留为可选终端回退。 ## 仓库内容 - `skills/deploy-android-ai-stack/`:安装、模型切换和本地服务生命周期。 - `skills/validate-android-ai-stack/`:有调用预算的真实验收。 +- `apps/mobile-lab-control/`:无第三方依赖的 Android AppWidget 源码与可复建脚本。 - `config/profiles/`:直连、中转、本地 OpenAI-compatible 模板。 - `docs/`:架构、OnePlus 8T 案例和手机端速查教程。 - `manifests/`:不含身份信息的设备观察记录。 @@ -46,8 +48,11 @@ OpenCode TUI/Web ------------------------+-> provider profile -> 模型 API 2. 按 `$deploy-android-ai-stack` 安装固定版本的上游组件和本仓 wrapper。 3. 复制 `config/opencode.example.json`,从模板创建 mode `0600` 的 profile 和 token 文件。 -4. 先执行 `quick`、`tui`、`web-start` 等零模型调用检查。 -5. 前置全通过后,再分别允许 OpenCode 与 Claude 各做一次短请求。 +4. 安装原生控制桥接,并从 + [GitHub Releases](https://github.com/toolazytoname/android-ai-stack/releases) + 下载和核对 APK。 +5. 先执行 `quick`、`tui`、`web-start` 等零模型调用检查。 +6. 前置全通过后,再分别允许 OpenCode 与 Claude 各做一次短请求。 适合直接放在手机上边看边操作的版本见 [手机速查教程](docs/phone-quickstart.zh-CN.md)。 @@ -61,6 +66,7 @@ OpenCode TUI/Web ------------------------+-> provider profile -> 模型 API | 手机本地 Happy | 已做端到端验证 | 提供显式启停 wrapper | | Provider 切换 | 私有设备配置已验证 | 提供脱敏通用 profile | | llama.cpp 本地模型 | Qwen3 0.6B Q8 的 API 与 `local-lite` 已通过 | 提供带认证的 loopback wrapper 与限次 checker | +| 原生控制微件 | 三个独立微件已完成零模型调用真机验收 | 提供源码、白名单桥接、双语 UI 与 Release APK | 最终手机验收也覆盖了控制面解耦:切换 profile 不改变正在运行的服务,Happy 启动时 profile 与当前配置不一致时明确显示 `STALE`,两个服务都能在不改变 diff --git a/SECURITY.md b/SECURITY.md index 03aa3a2..10c1a24 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -25,5 +25,15 @@ Prefer text assertions and redacted command output. Capture screenshots only when a GUI state cannot be proved otherwise. Run `scripts/verify.sh` before a commit; it detects common secret-shaped strings and accidental model files. +## APK release identity + +Installable Mobile Lab Control APKs belong in GitHub Releases, never in source +history. Release APKs must match the public certificate in +`apps/mobile-lab-control/release-certificate.pem`; its SHA-256 certificate +fingerprint is +`D0:2C:A4:E7:A4:FB:74:7A:E4:46:92:1A:DD:19:B1:1D:83:65:EE:3E:29:30:A4:CA:2B:25:94:AB:CE:74:70:45`. +Do not publish a debug-signed APK as a product release. Keep the private key +outside Git and back it up separately from GitHub Actions secrets. + Report a suspected vulnerability privately through GitHub's security advisory feature rather than a public issue. diff --git a/apps/mobile-lab-control/AndroidManifest.xml b/apps/mobile-lab-control/AndroidManifest.xml new file mode 100644 index 0000000..5ee8f7d --- /dev/null +++ b/apps/mobile-lab-control/AndroidManifest.xml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/apps/mobile-lab-control/README.md b/apps/mobile-lab-control/README.md new file mode 100644 index 0000000..07a30c2 --- /dev/null +++ b/apps/mobile-lab-control/README.md @@ -0,0 +1,56 @@ +# Mobile Lab Control + +[English](README.md) · [简体中文](README.zh-CN.md) + +An offline Android control surface for the Android AI Stack. It exposes three +independent home-screen widgets: + +- **Happy** — status, start/stop, restart, and refresh; +- **llama.cpp** — status, start/stop, memory release, and refresh; +- **model profiles** — current readable profile plus one-tap configuration + switching without changing either service lifecycle. + +The app has no Internet permission, reads no token, holds no root privilege, +and accepts no free-form command. It can invoke only +`$PREFIX/bin/mobile-lab-widget-action` through Termux's documented +[`RUN_COMMAND` interface](https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent). +The Termux bridge independently validates the same allow-list. + +## Build + +Install Android platform 35, build-tools 35.0.0, and a JDK, then run: + +```bash +BUILD_MODE=debug ./build.sh +``` + +Supported modes are `debug`, `unsigned`, and `release`. Release signing reads +the keystore path, alias, and passwords from environment variables; no key or +password belongs in Git. Published APKs are attached to GitHub Releases rather +than committed to repository history. + +The public release certificate is +[`release-certificate.pem`](release-certificate.pem). Its SHA-256 certificate +fingerprint is +`D0:2C:A4:E7:A4:FB:74:7A:E4:46:92:1A:DD:19:B1:1D:83:65:EE:3E:29:30:A4:CA:2B:25:94:AB:CE:74:70:45`. +Verify future APKs against this identity as well as the attached file checksum. + +## Runtime setup + +1. Install the Android AI Stack operator scripts in Termux. +2. Review the external-command boundary, then run + `install-control-bridge --enable-external-apps`. +3. Install the APK and grant `com.termux.permission.RUN_COMMAND` when Android + prompts. +4. Open the app and add Happy, llama.cpp, and model-profile widgets separately. + +Copy [`config/widget-profiles.example`](../../config/widget-profiles.example) to +`~/.config/android-ai-stack/widget-profiles`, keep it mode `0600`, and replace +only the redacted profile names and labels. The bridge never reads provider +tokens or CC Switch databases. + +## Upgrade identity + +An APK signed with a debug key cannot be upgraded in place to the public +release-signed build. Remove the debug build once, install the release build, +and re-add the widgets. Future release-signed versions then upgrade normally. diff --git a/apps/mobile-lab-control/README.zh-CN.md b/apps/mobile-lab-control/README.zh-CN.md new file mode 100644 index 0000000..99cfaab --- /dev/null +++ b/apps/mobile-lab-control/README.zh-CN.md @@ -0,0 +1,47 @@ +# Mobile Lab Control + +[English](README.md) · [简体中文](README.zh-CN.md) + +这是 Android AI Stack 的离线原生控制面,提供三个彼此独立的桌面微件: + +- **Happy**:状态、启动/关闭、重启与刷新; +- **llama.cpp**:状态、启动/关闭、释放模型内存与刷新; +- **模型配置**:显示当前可读名称,一键切换配置,但不改变两个服务的启停状态。 + +App 不申请联网权限、不读取 Token、不持有 root,也没有任意命令输入。它只通过 +Termux 官方 [`RUN_COMMAND` 接口](https://github.com/termux/termux-app/wiki/RUN_COMMAND-Intent) +调用固定脚本 +`$PREFIX/bin/mobile-lab-widget-action`,Termux 桥接层会再次校验同一套白名单。 + +## 构建 + +准备 Android platform 35、build-tools 35.0.0 与 JDK,然后执行: + +```bash +BUILD_MODE=debug ./build.sh +``` + +构建支持 `debug`、`unsigned` 和 `release`。正式签名只从环境变量读取 keystore +路径、alias 与密码,密钥和密码都不能进入 Git。可安装 APK 通过 GitHub +Release 发布,不提交进源码历史。 + +公开 Release 证书保存在 +[`release-certificate.pem`](release-certificate.pem),证书 SHA-256 指纹为 +`D0:2C:A4:E7:A4:FB:74:7A:E4:46:92:1A:DD:19:B1:1D:83:65:EE:3E:29:30:A4:CA:2B:25:94:AB:CE:74:70:45`。 +后续 APK 除核对附件 SHA-256 外,也应核对这一签名身份。 + +## 手机端配置 + +1. 在 Termux 安装 Android AI Stack 操作脚本; +2. 理解外部命令边界后执行 `install-control-bridge --enable-external-apps`; +3. 安装 APK,并在 Android 提示时授予 `com.termux.permission.RUN_COMMAND`; +4. 打开 App,分别添加 Happy、llama.cpp、模型配置三个微件。 + +把 [`config/widget-profiles.example`](../../config/widget-profiles.example) 复制到 +`~/.config/android-ai-stack/widget-profiles`,保持 mode `0600`,只填写脱敏的 +Profile 名称和展示名。桥接层不会读取 Provider Token 或 CC Switch 数据库。 + +## 升级签名说明 + +使用 debug key 签名的旧版本不能直接覆盖升级到公开 release 签名版本。首次迁移 +需要卸载 debug 版、安装 release 版并重新添加微件;之后的正式版本可以正常覆盖升级。 diff --git a/apps/mobile-lab-control/build.sh b/apps/mobile-lab-control/build.sh new file mode 100755 index 0000000..9a7a69a --- /dev/null +++ b/apps/mobile-lab-control/build.sh @@ -0,0 +1,108 @@ +#!/usr/bin/env bash +set -euo pipefail + +project_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +sdk_root=${ANDROID_SDK_ROOT:-${ANDROID_HOME:-/opt/homebrew/share/android-commandlinetools}} +platform_api=${ANDROID_PLATFORM_API:-35} +build_tools_version=${ANDROID_BUILD_TOOLS_VERSION:-35.0.0} +version_name=${VERSION_NAME:-0.4.0} +version_code=${VERSION_CODE:-400} +build_mode=${BUILD_MODE:-debug} + +platform="$sdk_root/platforms/android-$platform_api/android.jar" +build_tools="$sdk_root/build-tools/$build_tools_version" +build_dir="$project_dir/build" +compiled_dir="$build_dir/compiled" +generated_dir="$build_dir/generated" +classes_dir="$build_dir/classes" +dex_dir="$build_dir/dex" +unsigned_apk="$build_dir/mobile-lab-control-unsigned.apk" +aligned_apk="$build_dir/mobile-lab-control-aligned.apk" + +case "$build_mode" in + debug|release|unsigned) ;; + *) printf 'BUILD_MODE must be debug, release, or unsigned\n' >&2; exit 2 ;; +esac + +for required in "$platform" "$build_tools/aapt2" "$build_tools/zipalign" \ + "$build_tools/apksigner" "$build_tools/d8"; do + [ -e "$required" ] || { + printf 'missing Android build dependency: %s\n' "$required" >&2 + exit 1 + } +done +command -v javac >/dev/null 2>&1 || { printf 'missing javac\n' >&2; exit 1; } + +if [ -d "$build_dir" ]; then + find "$build_dir" -mindepth 1 -delete +fi +mkdir -p "$compiled_dir" "$generated_dir" "$classes_dir" "$dex_dir" + +"$build_tools/aapt2" compile --dir "$project_dir/res" -o "$compiled_dir/resources.zip" +"$build_tools/aapt2" link \ + -I "$platform" \ + --manifest "$project_dir/AndroidManifest.xml" \ + --java "$generated_dir" \ + --min-sdk-version 26 \ + --target-sdk-version 35 \ + --version-code "$version_code" \ + --version-name "$version_name" \ + -o "$unsigned_apk" \ + "$compiled_dir/resources.zip" + +find "$project_dir/src" "$generated_dir" -type f -name '*.java' -print0 | \ + xargs -0 javac -encoding UTF-8 -source 8 -target 8 -classpath "$platform" -d "$classes_dir" + +"$build_tools/d8" --min-api 26 --output "$dex_dir" \ + $(find "$classes_dir" -type f -name '*.class' -print) +(cd "$dex_dir" && zip -q -u "$unsigned_apk" classes.dex) +"$build_tools/zipalign" -f 4 "$unsigned_apk" "$aligned_apk" + +case "$build_mode" in + unsigned) + final_apk="$build_dir/mobile-lab-control-$version_name-unsigned.apk" + cp "$aligned_apk" "$final_apk" + ;; + debug) + keystore=${ANDROID_DEBUG_KEYSTORE:-$HOME/.android/debug.keystore} + if [ ! -s "$keystore" ]; then + mkdir -p "$(dirname "$keystore")" + keytool -genkeypair -noprompt \ + -keystore "$keystore" \ + -storepass android \ + -keypass android \ + -alias androiddebugkey \ + -keyalg RSA \ + -keysize 2048 \ + -validity 10000 \ + -dname 'CN=Android Debug,O=Android,C=US' + fi + final_apk="$build_dir/mobile-lab-control-$version_name-debug.apk" + "$build_tools/apksigner" sign \ + --ks "$keystore" \ + --ks-key-alias androiddebugkey \ + --ks-pass pass:android \ + --key-pass pass:android \ + --out "$final_apk" \ + "$aligned_apk" + ;; + release) + : "${ANDROID_RELEASE_KEYSTORE:?set ANDROID_RELEASE_KEYSTORE}" + : "${ANDROID_RELEASE_KEY_ALIAS:?set ANDROID_RELEASE_KEY_ALIAS}" + : "${ANDROID_KEYSTORE_PASSWORD:?set ANDROID_KEYSTORE_PASSWORD}" + : "${ANDROID_KEY_PASSWORD:?set ANDROID_KEY_PASSWORD}" + final_apk="$build_dir/mobile-lab-control-$version_name.apk" + "$build_tools/apksigner" sign \ + --ks "$ANDROID_RELEASE_KEYSTORE" \ + --ks-key-alias "$ANDROID_RELEASE_KEY_ALIAS" \ + --ks-pass env:ANDROID_KEYSTORE_PASSWORD \ + --key-pass env:ANDROID_KEY_PASSWORD \ + --out "$final_apk" \ + "$aligned_apk" + ;; +esac + +if [ "$build_mode" != unsigned ]; then + "$build_tools/apksigner" verify --verbose "$final_apk" +fi +printf 'built: %s\n' "$final_apk" diff --git a/apps/mobile-lab-control/release-certificate.pem b/apps/mobile-lab-control/release-certificate.pem new file mode 100644 index 0000000..a8f700a --- /dev/null +++ b/apps/mobile-lab-control/release-certificate.pem @@ -0,0 +1,30 @@ +-----BEGIN CERTIFICATE----- +MIIFKTCCAxGgAwIBAgIIWA6hUZEPrYAwDQYJKoZIhvcNAQEMBQAwQjELMAkGA1UE +BhMCQ04xFjAUBgNVBAoTDXRvb2xhenl0b25hbWUxGzAZBgNVBAMTEk1vYmlsZSBM +YWIgQ29udHJvbDAgFw0yNjA4MDcxMzM0MzlaGA8yMDUxMDgwMTEzMzQzOVowQjEL +MAkGA1UEBhMCQ04xFjAUBgNVBAoTDXRvb2xhenl0b25hbWUxGzAZBgNVBAMTEk1v +YmlsZSBMYWIgQ29udHJvbDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ANPnfEtX4tG5xh47Pkd/jrQtk4+7v/k/D6v2cWMR42iKc12ABRgCSIsBQmtXYcWi +bZvpQ63qBWtfZzlsMBckuVIas3sKNfYV7zxtsxI9cewwfeBRavBqBd8TuZOpCiHU +sevFex7KTLDRoJZPBqClabtC6GDwg69JD6HoLQgGTy4Z9UZay6wSFnlol3g5fTgW +Jz31hGY2CjK16JSS6HangpAGAaaacybrX9wkqwVpyiwe6tIlA94OBIhh81WdVhVp +leJIU74vYMu9wFNiOD6queBWOi2R0Ma57d71w4XkX6sUDQYkL0AYqbacw0w4OIGT +tpfOqZWlNy8BdEeSoofzpm1p/ZQKq7D7xIUR8iwj5CAutOlMbcIgguKiNLNnkMzm +I2jcOKpCmauQV16NIvmamUNaxY8p6eVolcL84lso03u3mT0fIPkKnEnhbn3MpY7I +6geiLwhFqn5u8YGc59RmsWahoXfIFr2u1nqu12ffMJatRo0COujzxuDPzu99eyWs +yCpz2zXLfJ/bCTRNI1P43vYb/qbjeIuto0d/yNbfsIInVaOh5Vr7pxje3Mpu7A3A +QW6OuBNWR0Rnrf34u3y7iAgbBQFN94Eoe+qv8zHzsP+WOldN77aHmtrM42BecoOj +FZmEgW+9+7EODemBiuED6catwJJw2JZY86aZ2siTd97XAgMBAAGjITAfMB0GA1Ud +DgQWBBSTJ/yQQSqc0Cfo5Oeuc/hfWoLPZDANBgkqhkiG9w0BAQwFAAOCAgEAZMrL +5WXoXIWvTUwiAnV/IQzYUN5sVBNDB4a+EE37k0Vd3jn0KQ2ewKKrstHvFjjQcyAb +5iN7sclvGrqcduSFFZZ+UqkjbKwI4RvcaJ98zuBzxeQADt0JlO2BNYout0UGDgyd +KCJOyXZ+gczdjXvRL9GCwgeNzYWMXJXhjodrFBePoW7fJyCKi2kjnRMiBQrcqeX4 +5O83UlZs8SB2vkO+INP/MAcLKzys3xCX9qEBW2vxezhbEApRq1FQ9fgcHCLNmvYK +jsvKMHlEgt+mt42+PXHxTyhkOrK2AKcENFJKG9230l48VLsiZZFlYBXSJx5GDYvA +yPoPdp+vScQ6fUTHWcef0fk65qgFpwPyyTv25gFLw18ofkPX15kJW9QlN3hstQcs +0vfJ6qqNz9u8y0YE3/o2n6M95w+nAxmIQFDC8P4YeFd05dQHfeSYjxDl3tGdXq7q +4axpSIxrLR4erRFW0c1/Qa+xugGnTRIf/eRuU7aHx5/gAYV57kt7+pdZU1O6ouhf +ULPn7vFpmH+hvesYGrQneYYYxNaMmaXmRubdx1YISzDsm3GoO/GxtTMNsU6FsHr6 +VC/5P4ymvmbJZ1sOwzpE6NPNf2c7JswsTlEc/WF6SIco65rsVmrVMHs7tx/1iQPb +D/biDZeukiO5Fo6KKCtmwjV47/+6d/MNF0axViQ= +-----END CERTIFICATE----- diff --git a/apps/mobile-lab-control/res/drawable/button_background.xml b/apps/mobile-lab-control/res/drawable/button_background.xml new file mode 100644 index 0000000..048c942 --- /dev/null +++ b/apps/mobile-lab-control/res/drawable/button_background.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/apps/mobile-lab-control/res/drawable/ic_mobile_lab.xml b/apps/mobile-lab-control/res/drawable/ic_mobile_lab.xml new file mode 100644 index 0000000..b65d2a7 --- /dev/null +++ b/apps/mobile-lab-control/res/drawable/ic_mobile_lab.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/apps/mobile-lab-control/res/drawable/widget_background.xml b/apps/mobile-lab-control/res/drawable/widget_background.xml new file mode 100644 index 0000000..1b5c5ca --- /dev/null +++ b/apps/mobile-lab-control/res/drawable/widget_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/apps/mobile-lab-control/res/layout/activity_main.xml b/apps/mobile-lab-control/res/layout/activity_main.xml new file mode 100644 index 0000000..d41023b --- /dev/null +++ b/apps/mobile-lab-control/res/layout/activity_main.xml @@ -0,0 +1,103 @@ + + + + + + + + + + + +