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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@
# App build outputs
/app/build/
/app/release/
# Local release staging: signed AABs and R8 mapping files for Play upload.
/release/
/glass-hud/build/
/glass-hud/local.properties

Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ A modern Android application for connecting to and monitoring Xiaomi/Ninebot M36

## 📸 Screenshots

<p align="center">
<img src="doc/play-store/promo/demo.gif" width="280" alt="Demo Ride walkthrough: dashboard, details and glasses display settings">
</p>

| Dashboard | Details | Glasses Display | Settings |
| :-------: | :-----: | :-------------: | :------: |
| <img src="doc/play-store/en-US/01_dashboard.png" width="200" alt="Dashboard"> | <img src="doc/play-store/en-US/02_details.png" width="200" alt="Scooter details"> | <img src="doc/play-store/en-US/03_glasses_display.png" width="200" alt="Glasses display settings"> | <img src="doc/play-store/en-US/04_settings.png" width="200" alt="Settings"> |
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -418,8 +418,8 @@ android {
// glass-hud so both APKs share one support matrix.
minSdk = 28
targetSdk = 36
versionCode = 9
versionName = "1.5.0"
versionCode = 10
versionName = "1.5.1"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
137 changes: 69 additions & 68 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -1,68 +1,69 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.

# ============================================
# Missing Annotation Classes (Google Tink / Crypto)
# ============================================
-dontwarn com.google.errorprone.annotations.**
-dontwarn javax.annotation.**
-dontwarn javax.annotation.concurrent.**
-dontwarn org.checkerframework.**
-dontwarn com.google.crypto.tink.**

# Keep Tink classes used by AndroidX Security Crypto
-keep class com.google.crypto.tink.** { *; }
-keepclassmembers class com.google.crypto.tink.** { *; }

# ============================================
# JNA (Java Native Access) - Required for Rust FFI
# ============================================
-dontwarn com.sun.jna.**
-keep class com.sun.jna.** { *; }
-keepclassmembers class * extends com.sun.jna.** { public *; }

# ============================================
# Kotlin Coroutines
# ============================================
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepclassmembers class kotlinx.coroutines.** {
volatile <fields>;
}
-keep class kotlinx.coroutines.android.** { *; }

# ============================================
# Jetpack Compose
# ============================================
-keep class androidx.compose.** { *; }
-dontwarn androidx.compose.**

# ============================================
# AndroidX Security Crypto
# ============================================
-keep class androidx.security.crypto.** { *; }

# ============================================
# Application Classes - Keep native interface
# ============================================
-keep class com.m365bleapp.** { *; }

# ============================================
# General Android
# ============================================
-keepattributes *Annotation*
-keepattributes SourceFile,LineNumberTable
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider

# ============================================
# Remove Verbose Logging in Release Build
# ============================================
-assumenosideeffects class android.util.Log {
public static int v(...);
public static int d(...);
}
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.
#
# Keep rules here must be NARROW. This file used to keep androidx.compose.**,
# com.m365bleapp.**, Tink, security-crypto and coroutines-android wholesale,
# which left most of the DEX unshrunk, unobfuscated and unoptimized: Play
# Console reported 13% on all three and flags anything below 25%. Every one
# of those libraries ships its own consumer rules; a blanket `-keep` here only
# overrides them with "touch nothing".

# ============================================
# Missing Annotation Classes (Google Tink / Crypto)
# ============================================
-dontwarn com.google.errorprone.annotations.**
-dontwarn javax.annotation.**
-dontwarn javax.annotation.concurrent.**
-dontwarn org.checkerframework.**
-dontwarn com.google.crypto.tink.**

# Tink (via AndroidX Security Crypto) parses its keysets with shaded
# protobuf-lite, which reads message fields reflectively.
-keepclassmembers class * extends com.google.crypto.tink.shaded.protobuf.GeneratedMessageLite {
<fields>;
}

# ============================================
# JNA (Java Native Access)
# ============================================
# JNA binds Java methods to native symbols by name at runtime.
-dontwarn com.sun.jna.**
-keep class com.sun.jna.** { *; }
-keepclassmembers class * extends com.sun.jna.** { public *; }

# ============================================
# Rust FFI (ninebot-ffi)
# ============================================
# The native library exports Java_com_m365bleapp_ffi_M365Native_<method>, so
# the class name and the native method names must survive. The Rust side does
# not call back into Java (no FindClass / CallMethod), so nothing else in the
# app is reached by name from native code.
-keep class com.m365bleapp.ffi.M365Native {
native <methods>;
}

# ============================================
# Kotlin Coroutines
# ============================================
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
-keepclassmembers class kotlinx.coroutines.** {
volatile <fields>;
}

# ============================================
# General Android
# ============================================
# Readable stack traces in Play Console (deobfuscated with mapping.txt).
-keepattributes *Annotation*
-keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile

# ============================================
# Remove Verbose Logging in Release Build
# ============================================
-assumenosideeffects class android.util.Log {
public static int v(...);
public static int d(...);
}
37 changes: 35 additions & 2 deletions app/src/main/java/com/m365bleapp/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ package com.m365bleapp

import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.os.Bundle
import android.view.WindowManager
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.core.view.WindowCompat
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
Expand All @@ -31,7 +33,7 @@ class MainActivity : ComponentActivity() {
}

override fun onCreate(savedInstanceState: Bundle?) {
enableEdgeToEdge()
drawEdgeToEdge()
super.onCreate(savedInstanceState)

repository = ScooterRepository.getInstance(applicationContext)
Expand All @@ -52,6 +54,37 @@ class MainActivity : ComponentActivity() {
}
}

/**
* Edge-to-edge on every supported API level, without androidx.activity's
* `enableEdgeToEdge()`.
*
* That helper sets `LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES` on API 28-29
* (its `EdgeToEdgeApi28`), and because minSdk is 28 that code path is in
* the release DEX. Play Console flags the constant as an API deprecated in
* Android 15, whatever device actually runs it.
*
* The rest of what the helper did is covered here or elsewhere:
* - bar colours: transparent via the window theme (themes.xml);
* - icon appearance: M365BleAppTheme sets light icons (dark-only app);
* - cutout: ALWAYS on API 30+, which is also what the helper used there.
* On API 28-29 the platform DEFAULT already lets a portrait window that
* extends under the status bar draw into the cutout.
*/
private fun drawEdgeToEdge() {
WindowCompat.setDecorFitsSystemWindows(window, false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
window.attributes = window.attributes.apply {
layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Otherwise the system paints a translucent scrim behind the
// gesture/button navigation bar on API 29-34.
window.isStatusBarContrastEnforced = false
window.isNavigationBarContrastEnforced = false
}
}

override fun onDestroy() {
super.onDestroy()
// Only disconnect if the activity is actually finishing (user pressed back, etc.)
Expand Down
4 changes: 4 additions & 0 deletions doc/README_zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@

## 📸 屏幕截图

<p align="center">
<img src="play-store/promo/demo.gif" width="280" alt="模拟骑行操作演示:仪表盘、详细信息与眼镜显示设置">
</p>

| 仪表盘 | 详细信息 | 眼镜显示 | 设置 |
| :----: | :------: | :------: | :--: |
| <img src="play-store/zh-CN/01_dashboard.png" width="200" alt="仪表盘"> | <img src="play-store/zh-CN/02_details.png" width="200" alt="滑板车详细信息"> | <img src="play-store/zh-CN/03_glasses_display.png" width="200" alt="眼镜显示设置"> | <img src="play-store/zh-CN/04_settings.png" width="200" alt="设置"> |
Expand Down
4 changes: 4 additions & 0 deletions doc/README_zh-TW.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@

## 📸 螢幕截圖

<p align="center">
<img src="play-store/promo/demo.gif" width="280" alt="模擬騎乘操作示範:儀表板、詳細資訊與眼鏡顯示設定">
</p>

| 儀表板 | 詳細資訊 | 眼鏡顯示 | 設定 |
| :----: | :------: | :------: | :--: |
| <img src="play-store/zh-TW/01_dashboard.png" width="200" alt="儀表板"> | <img src="play-store/zh-TW/02_details.png" width="200" alt="滑板車詳細資訊"> | <img src="play-store/zh-TW/03_glasses_display.png" width="200" alt="眼鏡顯示設定"> | <img src="play-store/zh-TW/04_settings.png" width="200" alt="設定"> |
Expand Down
67 changes: 67 additions & 0 deletions doc/RELEASE_NOTES_v1.5.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# v1.5.1 — Play Console fixes, full translations | Play Console 修正、翻譯補齊

Phone app `com.m365bleapp` versionCode 10, glasses app `com.m365hud.glass`
versionCode 7. Both report versionName 1.5.1.

手機端 `com.m365bleapp` versionCode 10,眼鏡端 `com.m365hud.glass` versionCode 7,
versionName 皆為 1.5.1。

---

## Play Console issues addressed | 已處理的 Play Console 問題

| Issue | Fix |
| :--- | :--- |
| DEX optimization / obfuscation / shrinking at 13% (threshold 25%) | Blanket R8 keep rules removed. Phone DEX 22.5 MB → 2.5 MB, 91% of classes now renamed; glasses 95%. |
| Deprecated `LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES` (`b.r.a`) | Came from androidx.activity `enableEdgeToEdge()`; the app now sets up edge-to-edge itself and never uses that mode. |
| Edge-to-edge may not display for all users | Same change. |

| 問題 | 修正 |
| :--- | :--- |
| DEX 最佳化/模糊化/縮減僅 13%(門檻 25%) | 移除過寬的 R8 keep 規則。手機端 DEX 22.5 MB → 2.5 MB,91% 類別已混淆;眼鏡端 95%。 |
| 已淘汰的 `LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES`(`b.r.a`) | 來源為 androidx.activity 的 `enableEdgeToEdge()`;改為自行處理無邊框,不再使用此模式。 |
| 無邊框畫面可能無法向所有使用者顯示 | 同上。 |

## Other changes | 其他變更

- All 10 translations completed (58–72 missing strings each). | 補齊 10 種語言翻譯(每種缺 58–72 條)。
- Arabic now lays out right-to-left. | 阿拉伯文改為由右至左版面。
- Motor lock buttons no longer break in Russian / Ukrainian. | 修正俄文/烏克蘭文的馬達鎖定按鈕版面。
- Demo Ride switch in Settings reflects its state. | 設定中的模擬騎乘開關會正確顯示狀態。
- Refreshed store screenshots for 11 languages and a promo video. | 更新 11 種語言商店截圖與宣傳影片。

## Play "What's new" text | Play「最新異動」文字

en-US:

```
• Fully translated into all 11 supported languages
• Arabic now displays right-to-left
• Fixed motor lock buttons in Russian and Ukrainian
• Smaller, faster app (optimized release build)
• Better display on Android 15 edge-to-edge screens
```

zh-TW:

```
• 11 種語言翻譯全數補齊
• 阿拉伯文改為由右至左顯示
• 修正俄文、烏克蘭文的馬達鎖定按鈕版面
• 安裝檔更小、執行更有效率
• 改善 Android 15 無邊框畫面的顯示
```

## Build | 建置

Built in WSL (Rust toolchain) with the release key
(`SHA256 7C:A3:A3:F7:…`, same as v1.5.0), so it updates the existing Play
listing. `libninebot_ffi.so` is present for all four ABIs with 16 KB
`PT_LOAD` alignment. No locally configured Rokid credential appears in either
bundle. Upload the matching `mapping-*.txt` to Play Console for readable
crash reports.

於 WSL(含 Rust 工具鏈)以正式金鑰建置(`SHA256 7C:A3:A3:F7:…`,與 v1.5.0 相同),
可直接更新既有 Play 上架。四種 ABI 皆含 16 KB 對齊的 `libninebot_ffi.so`,兩個
bundle 內皆無本機設定的 Rokid 憑證。請一併上傳對應的 `mapping-*.txt` 至 Play Console,
以取得可讀的當機報告。
Binary file added doc/play-store/promo/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added doc/play-store/promo/promo_1920x1080.mp4
Binary file not shown.
4 changes: 2 additions & 2 deletions glass-hud/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ android {
// Aligned with the phone app so a joint release ships two APKs that
// report the same versionName. versionCode must strictly increase for
// sideloaded updates to install over an existing build.
versionCode = 6
versionName = "1.5.0"
versionCode = 7
versionName = "1.5.1"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
Expand Down
13 changes: 4 additions & 9 deletions glass-hud/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.kts.

# ============================================
# BLE related classes
# ============================================
-keep class android.bluetooth.** { *; }

# ============================================
# Compose
# ============================================
-keep class androidx.compose.** { *; }
# No blanket keeps for android.bluetooth.** (framework classes are never in
# the DEX, so the rule did nothing) or androidx.compose.** (Compose ships its
# own consumer rules; keeping all of it left most of the DEX unshrunk and
# unobfuscated, which Play Console flags below 25%).

# ============================================
# Rokid CXR SDK -- REQUIRED, do not comment out
Expand Down
Loading