Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
b67ee72
feat: auto-start and harden legacy connections
usmarcos Aug 23, 2026
63ff3a2
ci: publish APK artifact for every build
usmarcos Aug 23, 2026
aa4512b
fix: make auto-start opt-in and preserve connection errors
usmarcos Aug 23, 2026
d7b0612
feat: modernize settings dashboard
usmarcos Aug 23, 2026
bd798c4
fix: keep Android Auto manual and restore stable settings
usmarcos Aug 23, 2026
2fd6be0
feat: rebuild settings navigation for legacy displays
usmarcos Aug 23, 2026
d6d0489
refactor: stabilize legacy projection and settings UI
usmarcos Aug 23, 2026
28aa851
fix: follow head unit locale for Portuguese resources
usmarcos Aug 23, 2026
2152ffb
fix: harden projection startup and video lifecycle
usmarcos Aug 23, 2026
d6e84e1
feat: add persistent themes and complete legacy UI polish
usmarcos Aug 23, 2026
341a836
fix: restore Android 4.0.4 USB and projection stability
usmarcos Aug 23, 2026
4630868
fix: restore legacy projection and simplify automotive UI
usmarcos Aug 23, 2026
f637235
fix: define legacy startup timer constant
usmarcos Aug 23, 2026
9de2ca4
fix: prevent silent crash on AOAP negotiation failure
usmarcos Aug 23, 2026
caa5185
fix: add missing Context import in Runtime.java
usmarcos Aug 23, 2026
d6334aa
fix: restore default crash reporting and capture native crashes
usmarcos Aug 23, 2026
94cf52b
fix: guard NotificationFactory/AppBadge nulls on cold start and fix l…
usmarcos Aug 23, 2026
62aaaea
fix: eliminate native USB crashes on disconnect/reconnect (SEGV)
usmarcos Aug 23, 2026
903e273
ci: skip unit/instrumented tests to speed up APK builds
usmarcos Aug 23, 2026
4ce55ce
fix: make native session teardown actually wait, and cancel USB trans…
usmarcos Aug 23, 2026
c8f3a53
fix: stop overlapping Android Auto sessions and unwedge the service
usmarcos Aug 23, 2026
75a8b86
fix: harden A/V codec teardown on the legacy (API 15) projection path
usmarcos Aug 23, 2026
654ea71
fix: reap leaked logcat processes and stop negotiating AOAP with stor…
usmarcos Aug 23, 2026
a89a6ef
ci: verify unit tests and lint in a parallel job
usmarcos Aug 23, 2026
d8c9452
ci: report lint instead of gating on it
usmarcos Aug 23, 2026
24b3adc
ci: capture lint output so a failing lint task can be diagnosed
usmarcos Aug 24, 2026
cf95b54
ci: set pipefail so a failing lint is not reported as a pass
usmarcos Aug 24, 2026
995d85c
ci: give the check job the NDK setup lint needs
usmarcos Aug 24, 2026
9f07b66
build: let lint analyse the library modules too
usmarcos Aug 24, 2026
c9b2856
fix: preserve immediate USB completions
usmarcos Aug 24, 2026
60ce1de
Fix USB recovery and projection teardown
usmarcos Aug 24, 2026
e0ca4bc
fix: recover USB sessions cleanly
usmarcos Aug 25, 2026
ade596b
Merge pull request #1 from usmarcos/codex/fix-usb-recovery-feedback
usmarcos Aug 25, 2026
310a687
fix: defer native session teardown
usmarcos Aug 25, 2026
48371f8
fix: stabilize USB reconnect and app exit
usmarcos Aug 26, 2026
334f0d3
Merge branch 'pr' into main
MaKi983 Sep 30, 2026
6fe7c77
Update main.yml
MaKi983 Sep 30, 2026
df964af
Update Settings.java
MaKi983 Sep 30, 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
174 changes: 126 additions & 48 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,57 +2,135 @@ name: Android CI

on:
push:
tags:
- '*'
branches: ['**']
tags: ['*']
pull_request:
branches: [main]

permissions:
contents: read

jobs:
# Gates the APK build so a failing unit test cannot produce a releasable
# artifact. Lint remains advisory while the legacy API backlog is triaged.
check:
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
# Lint pulls in the native build (the app depends on aasdk, which consumes
# common through prefab), so this job needs the same NDK setup as the build
# job. Without it lint died in configureNdkBuildDebug before it ever got to
# run a single check.
- name: Install NDK
run: echo "y" | $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --install "ndk;17.2.4988734" --sdk_root=${ANDROID_SDK_ROOT}
- name: Install legacy NDK dependency
run: sudo apt-get update && sudo apt-get install -y libtinfo5
- name: Unit tests
run: |
chmod +x gradlew
./gradlew testDebugUnitTest
# Builds common's native output first so aasdk's prefab import resolves.
- name: Build native dependencies for lint
run: ./gradlew :common:build
# Reported, not enforced. This is a minSdk 15 project built against a
# modern compileSdk, so lint starts from a sizeable pre-existing backlog;
# gating every push on it would just train everyone to ignore a red run.
# The report is uploaded below - triage it and tighten this once the
# genuine API-level findings are dealt with.
- name: Lint
continue-on-error: true
# Output is captured because a lint task that dies before writing its
# report leaves nothing behind to diagnose from. pipefail matters here:
# without it the step reports the exit code of tee and a failing lint
# looks like a pass.
run: |
set -o pipefail
./gradlew :app:lintDebug --stacktrace 2>&1 | tee lint-output.log
- name: Upload reports
if: always()
uses: actions/upload-artifact@v4
with:
name: check-reports
path: |
**/build/reports/tests/**
**/build/reports/lint-results*.*
**/build/intermediates/lint*/**/*.xml
lint-output.log
if-no-files-found: warn
retention-days: 14

build:
needs: check
runs-on: ubuntu-22.04
permissions: write-all
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Install NDK
run: echo "y" | $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --install "ndk;17.2.4988734" --sdk_root=${ANDROID_SDK_ROOT}
- name: Install legacy NDK dependency
run: sudo apt-get update && sudo apt-get install -y libtinfo5
- name: Configure stable release signing
env:
ODA_RELEASE_KEYSTORE_BASE64: ${{ secrets.ODA_RELEASE_KEYSTORE_BASE64 }}
ODA_RELEASE_STORE_PASSWORD_SECRET: ${{ secrets.ODA_RELEASE_STORE_PASSWORD }}
ODA_RELEASE_KEY_ALIAS_SECRET: ${{ secrets.ODA_RELEASE_KEY_ALIAS }}
ODA_RELEASE_KEY_PASSWORD_SECRET: ${{ secrets.ODA_RELEASE_KEY_PASSWORD }}
run: |
if [ -n "$ODA_RELEASE_KEYSTORE_BASE64" ] && [ -n "$ODA_RELEASE_STORE_PASSWORD_SECRET" ] && [ -n "$ODA_RELEASE_KEY_ALIAS_SECRET" ] && [ -n "$ODA_RELEASE_KEY_PASSWORD_SECRET" ]; then
KEYSTORE_PATH="$RUNNER_TEMP/opendroidauto-release.jks"
printf '%s' "$ODA_RELEASE_KEYSTORE_BASE64" | base64 --decode > "$KEYSTORE_PATH"
echo "ODA_RELEASE_STORE_FILE=$KEYSTORE_PATH" >> "$GITHUB_ENV"
echo "ODA_RELEASE_STORE_PASSWORD=$ODA_RELEASE_STORE_PASSWORD_SECRET" >> "$GITHUB_ENV"
echo "ODA_RELEASE_KEY_ALIAS=$ODA_RELEASE_KEY_ALIAS_SECRET" >> "$GITHUB_ENV"
echo "ODA_RELEASE_KEY_PASSWORD=$ODA_RELEASE_KEY_PASSWORD_SECRET" >> "$GITHUB_ENV"
echo "ODA_STABLE_SIGNING=true" >> "$GITHUB_ENV"
else
echo "Stable release signing secrets are unavailable; this run will use debug signing."
echo "ODA_STABLE_SIGNING=false" >> "$GITHUB_ENV"
fi
- name: Require stable signing for repository builds
if: github.event_name == 'push'
run: test "$ODA_STABLE_SIGNING" = "true"
- name: Build
run: |
chmod +x gradlew
./gradlew :common:build assembleRelease -x test -x testDebugUnitTest -x testReleaseUnitTest -x assembleDebugAndroidTest
- name: Verify APK signature
run: |
APKSIGNER_PATH=$(find "$ANDROID_SDK_ROOT/build-tools" -name apksigner -type f | sort -V | tail -n 1)
CERT_OUTPUT=$("$APKSIGNER_PATH" verify --verbose --print-certs app/build/outputs/apk/release/HondaAppCenter_A1.apk)
printf '%s\n' "$CERT_OUTPUT"
#if [ "$ODA_STABLE_SIGNING" = "true" ]; then
# printf '%s' "$CERT_OUTPUT" | tr '[:upper:]' '[:lower:]' | grep -F "ddf49d726ddbb5a7ff5fea219daa27666994e2f337f7e338eecdf9c4abb73cb6"
#fi
- name: Upload release APK
uses: actions/upload-artifact@v4
with:
name: OpenDroidAuto-APK
path: app/build/outputs/apk/release/HondaAppCenter_A1.apk
if-no-files-found: error
retention-days: 14

- name: set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle

- name: Install NDK
run: echo "y" | $ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager --install "ndk;17.2.4988734" --sdk_root=${ANDROID_SDK_ROOT}

- name: Install missing packages
run: sudo apt-get install libtinfo5

- name: Grant execute permission for gradlew
run: chmod +x gradlew

- name: Setup Gradle Cache
uses: gradle/gradle-build-action@v2
with:
gradle-home-cache-cleanup: true

- name: Build common module
run: ./gradlew :common:build

- name: Build with Gradle
run: ./gradlew assembleRelease

- name: Create Release
id: create_release
uses: softprops/action-gh-release@v2
with:
draft: false
prerelease: false

- name: Upload Release Asset
id: upload-release-asset
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: app/build/outputs/apk/release/HondaAppCenter_A1.apk
asset_name: HondaAppCenter_A1.apk
asset_content_type: application/zip
release:
needs: build
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
name: OpenDroidAuto-APK
path: release
- uses: softprops/action-gh-release@v2
with:
files: release/HondaAppCenter_A1.apk
5 changes: 2 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,8 @@ captures/
.idea/navEditor.xml

# Keystore files
# Uncomment the following lines if you do not want to check your keystore files in.
#*.jks
#*.keystore
*.jks
*.keystore

# External native build folder generated in Android Studio 2.2 and later
.externalNativeBuild
Expand Down
54 changes: 54 additions & 0 deletions PRODUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Product

<!-- impeccable:product-schema 1 -->

## Platform

android

## Users

Drivers and installers using legacy Android ARM32 multimedia head units, primarily Honda Connect units and compatible generic devices.

## Product Purpose

OpenDroidAuto turns a legacy head unit into an Android Auto receiver through USB AOAP or a phone hotspot. Success is a reliable, low-distraction connection that starts manually only after the transport is ready.

## Positioning

Unlike a generic Android Auto client, the product preserves support for Android 4.x Honda hardware while safely falling back when Honda/Fujitsu system services are unavailable.

## Operating Context

The interface is used in a landscape car display, often at 800x480, while the vehicle is stationary or being driven. A saved phone hotspot may be used for wireless Android Auto.

## Capabilities and Constraints

- Keep Android 4.x, `armeabi-v7a`, target SDK 15, and NDK 17.2 compatibility.
- USB starts manually from the dashboard after Android grants device access; Android system permissions cannot be bypassed without privileged installation.
- USB preparation runs asynchronously with bounded retries and an AOAP re-enumeration watchdog; it must never block the Android 4.x UI thread.
- Release APKs use one stable certificate so updates preserve package identity and the Android USB default association.
- Start in a high-contrast light theme and persist the driver-selected light or dark appearance across restarts.
- Use Portuguese resources for any Portuguese system locale and English as the fallback for every other language.
- Wi-Fi uses a hotspot already saved by the Android system; Wi-Fi Direct and in-app password provisioning are out of scope.
- Honda integration stays available when compatible and must not crash generic devices.

## Brand Commitments

OpenDroidAuto remains recognisable as an automotive utility: concise, high-contrast, calm, and legible at a glance.

## Evidence on Hand

The app ships a legacy logo and current connection assets in `app/src/main/res/drawable/`. Hardware-specific behavior requires validation on an actual head unit and phone.

## Product Principles

- Connect first; ask only when the operating system requires it.
- Make connection state and recovery obvious without distracting the driver.
- Preserve the working Honda path while degrading safely elsewhere.
- Projection lifecycle changes must be based on the proven manual-start implementation; transport hardening stays outside that lifecycle unless hardware evidence requires otherwise.
- Prefer bounded retries and clear recovery over hidden loops.

## Accessibility & Inclusion

Use landscape-friendly high contrast, readable type, and at least 48dp touch targets for actions available while parked.
54 changes: 47 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,55 @@
# OpenDroidAuto

## Debugging
Android Auto receiver for legacy Android ARM32 head units, including Honda Connect units and compatible generic multimedia systems.

For debugging, hondaIntegrationEnabled() must be false, otherwise you will get an error on startup
Current hotfix version: **2.6.2**.

## How to build
## Connection behavior

./gradlew :common:assemble
./gradlew assembleRelease
- USB is the primary path. When the phone reaches AOAP mode, use **START ANDROID AUTO** on the dashboard to begin the session.
- If the USB stack becomes stale, use **RECOVER USB**. It closes the stale handle, rescans the bus and recreates the AOAP device without resetting the physical USB port.
- Selecting **Exit** stops the session and keeps the dashboard open. The same cable can be used to press Start again.
- Wi-Fi is optional. Enable it in settings only after the phone hotspot is already saved by Android on the head unit. The app uses the hotspot gateway on TCP port 5277 and reports failures for manual recovery.
- The dashboard and Settings support a persistent light/dark selector. A fresh installation starts in light mode; Portuguese multimedia systems use Portuguese interface text and every other locale falls back to English.
- Android controls USB permission. A non-rooted head unit can still show the system dialog on a first connection or after a device is disconnected; the app does not bypass that permission.
- Version 2.6.1 introduces a stable release certificate. Uninstall the older debug-signed APK once before installing 2.6.1; future stable-signed updates can then preserve the app identity and USB default association.
- Version 2.6.2 restores the proven legacy projection lifecycle, adds bounded native USB transfer retries and simplifies the dashboard/Settings layout for the Honda display viewport.
- Stable release certificate SHA-256: `DD:F4:9D:72:6D:DB:B5:A7:FF:5F:EA:21:9D:AA:27:66:69:94:E2:F3:37:F7:E3:38:EE:CD:F9:C4:AB:B7:3C:B6`.

## Compatibility

- Android 4.x and `armeabi-v7a` remain the supported baseline.
- Honda integration is used only when Fujitsu/Honda system services are present. Generic head units fall back to standard Android audio and input behavior.
- Android Auto Wi-Fi Direct is not implemented. Use a saved phone hotspot.

## Generate apk
## Build

Install JDK 17, Android SDK platform 34, and NDK `17.2.4988734`, then run:

```sh
./gradlew test
./gradlew assembleDebugAndroidTest
./gradlew assembleRelease
You will find the apk in app/build/outputs/apk/release
```

The APK is created at `app/build/outputs/apk/release/HondaAppCenter_A1.apk`.

For a production-signed APK, set these environment variables before the release build:

```text
ODA_RELEASE_STORE_FILE
ODA_RELEASE_STORE_PASSWORD
ODA_RELEASE_KEY_ALIAS
ODA_RELEASE_KEY_PASSWORD
```

Without them, the legacy debug-signing behavior is retained for local installation.
GitHub Actions decodes `ODA_RELEASE_KEYSTORE_BASE64` and supplies the other three signing values from repository secrets. Keep an offline backup of the release keystore and password; losing them prevents future in-place APK updates.

## Diagnostics

The home dashboard reports permission, AOAP switching, connection and native error details. Enable external logs only when diagnosing a hardware problem; protocol logging can contain sensitive connection data.

## License

GPL-3.0. See [LICENSE](LICENSE).
29 changes: 22 additions & 7 deletions aasdk/src/main/cpp/libaasdk-jni/jni/it_smg_libs_aasdk_Runtime.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -37,14 +37,29 @@ void JRuntime::stopIOServiceWorkers() {
}

if(Log::isDebug()) Log_d("stop all threads");
auto iter = threadPool_.begin();
while (iter != threadPool_.end()){
iter->detach();
if (iter->joinable()){
if(Log::isDebug()) Log_d("join thread");
iter->join();
// detach() used to run first, which makes joinable() false and meant the
// join below never executed: stopIOServiceWorkers() returned while workers
// were still inside ioService_.run(). The caller then went straight on to
// destroy the messenger, cryptor and transport those threads were using,
// which is exactly the use-after-free that surfaced as a SEGV inside
// boost::asio's strand_service. ioService_.stop() above has already been
// requested, so run() unwinds promptly and these joins are short.
// Teardown can be reached from a native quit callback, which runs on one of
// these very worker threads. Joining ourselves would throw
// resource_deadlock_would_occur (or hang), so that one thread is detached and
// every other is genuinely awaited.
const auto currentThreadId = std::this_thread::get_id();
for (auto& thread : threadPool_){
if (!thread.joinable()){
continue;
}
threadPool_.erase(iter);
if (thread.get_id() == currentThreadId){
if(Log::isWarn()) Log_w("skipping join of the calling worker thread");
thread.detach();
continue;
}
if(Log::isDebug()) Log_d("join thread");
thread.join();
}

if(Log::isDebug()) Log_d("clear threadpool");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ void JLibUsbDevice::close() {
libUsbDevice_->close();
}

int JLibUsbDevice::resetDevice() {
return libUsbDevice_->resetDevice();
}

int JLibUsbDevice::deviceDescriptor(libusb_device_descriptor &descriptor) {
return libUsbDevice_->deviceDescriptor(descriptor);
}
Expand Down Expand Up @@ -119,4 +123,11 @@ Java_it_smg_libs_aasdk_usb_LibUsbDevice_nativeSerial(JNIEnv *env, jobject thiz)
jbyteArray data = env->NewByteArray(size);
env->SetByteArrayRegion (data, 0, size, reinterpret_cast<const jbyte *>(jlibUsbDevice->serial()));
return data;
}
}

extern "C"
JNIEXPORT jint JNICALL
Java_it_smg_libs_aasdk_usb_LibUsbDevice_nativeResetDevice(JNIEnv *env, jobject thiz) {
JLibUsbDevice::Pointer jlibUsbDevice = JLibUsbDevice::getJLibUsbDevice(env, thiz);
return jlibUsbDevice->resetDevice();
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class JLibUsbDevice : JNIBase {

int open(int fd);
void close();
int resetDevice();
int deviceDescriptor(libusb_device_descriptor &descriptor);
LibUsbDevice::Pointer getLibUsbDevice();

Expand Down
Loading