From b67ee7267d8132b224d85d228e21f6c9a102717c Mon Sep 17 00:00:00 2001 From: Marcos Vinicius Cardoso dos Santos Date: Sat, 22 Aug 2026 23:12:18 -0300 Subject: [PATCH 01/36] feat: auto-start and harden legacy connections --- .github/workflows/main.yml | 89 +++-- PRODUCT.md | 49 +++ README.md | 45 ++- app/build.gradle | 16 +- .../java/it/smg/hu/ui/HomeDashboardTest.java | 26 ++ app/src/main/AndroidManifest.xml | 11 +- .../main/java/it/smg/hu/ODAApplication.java | 6 +- .../main/java/it/smg/hu/config/Settings.java | 4 +- .../it/smg/hu/manager/ConnectionManager.java | 145 ++++++++ .../it/smg/hu/manager/ConnectionPolicy.java | 85 +++++ .../it/smg/hu/manager/ConnectionState.java | 11 + .../smg/hu/manager/HondaConnectManager.java | 39 ++- .../java/it/smg/hu/manager/HondaPlatform.java | 31 ++ .../java/it/smg/hu/manager/USBManager.java | 60 +++- .../smg/hu/manager/UsbPermissionReceiver.java | 26 ++ .../java/it/smg/hu/manager/WIFIManager.java | 15 +- .../java/it/smg/hu/projection/AudioCodec.java | 41 ++- .../it/smg/hu/projection/InputDevice.java | 4 +- .../smg/hu/projection/NativeVideoOutput.java | 12 +- .../java/it/smg/hu/service/ODAService.java | 57 +++- .../main/java/it/smg/hu/ui/MainActivity.java | 246 +++++--------- .../java/it/smg/hu/ui/PlayerActivity.java | 44 ++- .../java/it/smg/hu/ui/main/HomeFragment.java | 315 ++++++++---------- app/src/main/res/drawable/button_primary.xml | 5 + .../main/res/drawable/button_secondary.xml | 5 + app/src/main/res/drawable/panel_surface.xml | 6 + .../main/res/drawable/status_badge_active.xml | 5 + .../main/res/drawable/status_badge_error.xml | 5 + .../main/res/drawable/status_badge_idle.xml | 5 + app/src/main/res/layout/home_fragment.xml | 283 ++++++++-------- app/src/main/res/values/colors.xml | 13 +- app/src/main/res/values/strings.xml | 30 +- .../smg/hu/manager/ConnectionPolicyTest.java | 36 ++ 33 files changed, 1173 insertions(+), 597 deletions(-) create mode 100644 PRODUCT.md create mode 100644 app/src/androidTest/java/it/smg/hu/ui/HomeDashboardTest.java create mode 100644 app/src/main/java/it/smg/hu/manager/ConnectionManager.java create mode 100644 app/src/main/java/it/smg/hu/manager/ConnectionPolicy.java create mode 100644 app/src/main/java/it/smg/hu/manager/ConnectionState.java create mode 100644 app/src/main/java/it/smg/hu/manager/HondaPlatform.java create mode 100644 app/src/main/java/it/smg/hu/manager/UsbPermissionReceiver.java create mode 100644 app/src/main/res/drawable/button_primary.xml create mode 100644 app/src/main/res/drawable/button_secondary.xml create mode 100644 app/src/main/res/drawable/panel_surface.xml create mode 100644 app/src/main/res/drawable/status_badge_active.xml create mode 100644 app/src/main/res/drawable/status_badge_error.xml create mode 100644 app/src/main/res/drawable/status_badge_idle.xml create mode 100644 app/src/test/java/it/smg/hu/manager/ConnectionPolicyTest.java diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 04fc3932..5da012e7 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,57 +2,50 @@ name: Android CI on: push: - tags: - - '*' + branches: [main] + tags: ['*'] + pull_request: + branches: [main] + +permissions: + contents: read jobs: build: 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: Build and test + run: | + chmod +x gradlew + ./gradlew test :common:build assembleRelease + - name: Upload release APK + if: startsWith(github.ref, 'refs/tags/') + uses: actions/upload-artifact@v4 + with: + name: release-apk + path: app/build/outputs/apk/release/HondaAppCenter_A1.apk - - 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: release-apk + path: release + - uses: softprops/action-gh-release@v2 + with: + files: release/HondaAppCenter_A1.apk diff --git a/PRODUCT.md b/PRODUCT.md new file mode 100644 index 00000000..bbbee5f9 --- /dev/null +++ b/PRODUCT.md @@ -0,0 +1,49 @@ +# Product + + + +## 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 without a manual Start action. + +## 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 auto-start is the normal flow after Android grants device access; Android system permissions cannot be bypassed without privileged installation. +- 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. +- 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. diff --git a/README.md b/README.md index 6e87d77c..4b70be68 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,46 @@ # 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 +## Connection behavior -## How to build +- USB is the primary path. After Android grants access to the phone and associates OpenDroidAuto as its handler, the app switches the phone to AOAP mode and opens Android Auto automatically. +- Selecting **Exit** stops the session and keeps the dashboard open. Reconnect the USB cable to start a new automatic session. +- 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 retries short connection failures automatically. +- 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. -./gradlew :common:assemble -./gradlew assembleRelease +## 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. + +## Build -## Generate apk +Install JDK 17, Android SDK platform 34, and NDK `17.2.4988734`, then run: +```sh +./gradlew test ./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. + +## Diagnostics + +The home dashboard reports permission, AOAP switching, connection, error, and retry states. Enable external logs only when diagnosing a hardware problem; protocol logging can contain sensitive connection data. + +## License + +GPL-3.0. See [LICENSE](LICENSE). diff --git a/app/build.gradle b/app/build.gradle index 8957b5b6..4f2defde 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -40,6 +40,18 @@ android { } } + signingConfigs { + release { + def storePath = System.getenv("ODA_RELEASE_STORE_FILE") + if (storePath != null && !storePath.trim().isEmpty()) { + storeFile file(storePath) + storePassword System.getenv("ODA_RELEASE_STORE_PASSWORD") + keyAlias System.getenv("ODA_RELEASE_KEY_ALIAS") + keyPassword System.getenv("ODA_RELEASE_KEY_PASSWORD") + } + } + } + buildTypes { debug { packagingOptions { @@ -50,7 +62,7 @@ android { } release { - signingConfig debug.signingConfig + signingConfig signingConfigs.release.storeFile != null ? signingConfigs.release : signingConfigs.debug minifyEnabled true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' @@ -86,4 +98,4 @@ dependencies { testImplementation libs.junit androidTestImplementation libs.ext.junit androidTestImplementation libs.espresso.core -} \ No newline at end of file +} diff --git a/app/src/androidTest/java/it/smg/hu/ui/HomeDashboardTest.java b/app/src/androidTest/java/it/smg/hu/ui/HomeDashboardTest.java new file mode 100644 index 00000000..c6159955 --- /dev/null +++ b/app/src/androidTest/java/it/smg/hu/ui/HomeDashboardTest.java @@ -0,0 +1,26 @@ +package it.smg.hu.ui; + +import androidx.test.core.app.ActivityScenario; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import it.smg.hu.R; + +import static androidx.test.espresso.Espresso.onView; +import static androidx.test.espresso.assertion.ViewAssertions.matches; +import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed; +import static androidx.test.espresso.matcher.ViewMatchers.withId; + +@RunWith(AndroidJUnit4.class) +public class HomeDashboardTest { + @Test + public void dashboardShowsConnectionStatusWithoutPhone() { + try (ActivityScenario ignored = ActivityScenario.launch(MainActivity.class)) { + onView(withId(R.id.statusTitle)).check(matches(isDisplayed())); + onView(withId(R.id.statusBadge)).check(matches(isDisplayed())); + onView(withId(R.id.settingsBtn)).check(matches(isDisplayed())); + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 818ea726..fcf74f0d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -34,7 +34,7 @@ android:hardwareAccelerated="true" android:theme="@style/Theme.AppCompat"> - + @@ -49,9 +49,16 @@ - + + + + + + + + diff --git a/app/src/main/java/it/smg/hu/ODAApplication.java b/app/src/main/java/it/smg/hu/ODAApplication.java index 422ee352..88b9de19 100644 --- a/app/src/main/java/it/smg/hu/ODAApplication.java +++ b/app/src/main/java/it/smg/hu/ODAApplication.java @@ -7,6 +7,8 @@ import it.smg.hu.config.ODALog; import it.smg.hu.config.Settings; import it.smg.hu.manager.HondaConnectManager; +import it.smg.hu.manager.HondaPlatform; +import it.smg.hu.manager.ConnectionManager; import it.smg.hu.manager.USBManager; import it.smg.hu.manager.WIFIManager; @@ -51,6 +53,8 @@ public void onCreate() { // if (Log.isDebug()) Log.d(TAG, "Initialized jni: " + Runtime.handle()); + HondaPlatform.init(getApplicationContext()); + ConnectionManager.init(getApplicationContext()); USBManager.init(getApplicationContext()); WIFIManager.init(getApplicationContext()); AppBadge.init(getApplicationContext()); @@ -58,4 +62,4 @@ public void onCreate() { HondaConnectManager.init(getApplicationContext()); } } -} \ No newline at end of file +} diff --git a/app/src/main/java/it/smg/hu/config/Settings.java b/app/src/main/java/it/smg/hu/config/Settings.java index cddcd300..bbbe94be 100644 --- a/app/src/main/java/it/smg/hu/config/Settings.java +++ b/app/src/main/java/it/smg/hu/config/Settings.java @@ -10,6 +10,7 @@ import it.smg.libs.common.ILog; import it.smg.libs.aasdk.configuration.ICarConfiguration; +import it.smg.hu.manager.HondaPlatform; //import it.smg.hu.oda.service.input.KeyCode; //import it.smg.hu.oda.service.sensor.IDayNightSensor; @@ -562,7 +563,8 @@ public void threads(int threads){ } public boolean hondaIntegrationEnabled(){ - return SP.getBoolean(ADVANCED_ENABLE_HONDA_INTEGRATION, ADVANCED_ENABLE_HONDA_INTEGRATION_DEFAULT_VALUE); + return SP.getBoolean(ADVANCED_ENABLE_HONDA_INTEGRATION, ADVANCED_ENABLE_HONDA_INTEGRATION_DEFAULT_VALUE) + && HondaPlatform.isAvailable(); } public void hondaIntegrationEnabled(boolean integration){ diff --git a/app/src/main/java/it/smg/hu/manager/ConnectionManager.java b/app/src/main/java/it/smg/hu/manager/ConnectionManager.java new file mode 100644 index 00000000..40110c76 --- /dev/null +++ b/app/src/main/java/it/smg/hu/manager/ConnectionManager.java @@ -0,0 +1,145 @@ +package it.smg.hu.manager; + +import android.content.Context; +import android.content.Intent; +import android.os.Handler; +import android.os.Looper; + +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +/** Coordinates USB and Wi-Fi session state for the activities and foreground service. */ +public final class ConnectionManager { + public static final String ACTION_STATE_CHANGED = "it.smg.hu.CONNECTION_STATE_CHANGED"; + public static final String ACTION_RETRY = "it.smg.hu.CONNECTION_RETRY"; + public static final String EXTRA_STATE = "state"; + public static final String EXTRA_MODE = "mode"; + public static final String EXTRA_MESSAGE = "message"; + public static final String EXTRA_ATTEMPT = "attempt"; + + private static ConnectionManager instance_; + + private final ConnectionPolicy policy_ = new ConnectionPolicy(); + private final LocalBroadcastManager broadcastManager_; + private final Handler handler_ = new Handler(Looper.getMainLooper()); + private Runnable pendingRetry_; + + private ConnectionManager(Context context) { + broadcastManager_ = LocalBroadcastManager.getInstance(context.getApplicationContext()); + } + + public static void init(Context context) { + if (instance_ == null) { + instance_ = new ConnectionManager(context); + } + } + + public static ConnectionManager instance() { + return instance_; + } + + public ConnectionState state() { + return policy_.state(); + } + + public String mode() { + return policy_.mode(); + } + + public boolean isAutoStartAllowed() { + return policy_.isAutoStartAllowed(); + } + + public void transportAvailable(String mode, String message) { + cancelRetry(); + policy_.transportAvailable(mode); + publish(message); + } + + public void permissionPending(String message) { + cancelRetry(); + policy_.permissionPending(); + publish(message); + } + + public void switchingToAoap(String message) { + cancelRetry(); + policy_.switchingToAoap(); + publish(message); + } + + public void connecting(String mode, String message) { + policy_.connecting(mode); + publish(message); + } + + public void active(String message) { + cancelRetry(); + policy_.active(); + publish(message); + } + + public void userExited(String message) { + cancelRetry(); + policy_.userExited(); + publish(message); + } + + public void detached(String message) { + cancelRetry(); + policy_.detached(); + publish(message); + } + + public void failed(final String message) { + if (policy_.failed()) { + publish(message + " Retrying…"); + final int attempt = policy_.retryCount(); + final long delayMs = 1000L << (attempt - 1); + pendingRetry_ = new Runnable() { + @Override + public void run() { + pendingRetry_ = null; + if (!policy_.isAutoStartAllowed() || policy_.mode() == null) { + return; + } + Intent retry = new Intent(ACTION_RETRY); + retry.putExtra(EXTRA_MODE, policy_.mode()); + retry.putExtra(EXTRA_ATTEMPT, attempt); + broadcastManager_.sendBroadcast(retry); + } + }; + handler_.postDelayed(pendingRetry_, delayMs); + } else { + publish(message); + } + } + + public void retryNow() { + if (policy_.mode() == null || !policy_.isAutoStartAllowed()) { + return; + } + cancelRetry(); + policy_.manualRetry(); + publish("Retrying connection"); + Intent retry = new Intent(ACTION_RETRY); + retry.putExtra(EXTRA_MODE, policy_.mode()); + retry.putExtra(EXTRA_ATTEMPT, 0); + broadcastManager_.sendBroadcast(retry); + } + + private void publish(String message) { + Intent intent = new Intent(ACTION_STATE_CHANGED); + intent.putExtra(EXTRA_STATE, policy_.state().name()); + intent.putExtra(EXTRA_MODE, policy_.mode()); + intent.putExtra(EXTRA_MESSAGE, message); + intent.putExtra(EXTRA_ATTEMPT, policy_.retryCount()); + broadcastManager_.sendBroadcast(intent); + } + + private void cancelRetry() { + if (pendingRetry_ != null) { + handler_.removeCallbacks(pendingRetry_); + pendingRetry_ = null; + } + } +} diff --git a/app/src/main/java/it/smg/hu/manager/ConnectionPolicy.java b/app/src/main/java/it/smg/hu/manager/ConnectionPolicy.java new file mode 100644 index 00000000..c88c5ca3 --- /dev/null +++ b/app/src/main/java/it/smg/hu/manager/ConnectionPolicy.java @@ -0,0 +1,85 @@ +package it.smg.hu.manager; + +/** Pure connection policy so retry and exit behavior can be tested without Android hardware. */ +public final class ConnectionPolicy { + public static final int MAX_RETRIES = 3; + + private ConnectionState state = ConnectionState.IDLE; + private String mode; + private int retryCount; + private boolean autoStartSuppressed; + + public ConnectionState state() { + return state; + } + + public String mode() { + return mode; + } + + public int retryCount() { + return retryCount; + } + + public boolean isAutoStartAllowed() { + return !autoStartSuppressed; + } + + public void transportAvailable(String connectionMode) { + if (state == ConnectionState.EXITED && connectionMode != null && connectionMode.equals(mode)) { + return; + } + mode = connectionMode; + retryCount = 0; + autoStartSuppressed = false; + state = ConnectionState.IDLE; + } + + public void permissionPending() { + state = ConnectionState.PERMISSION_PENDING; + } + + public void switchingToAoap() { + state = ConnectionState.AOAP_SWITCHING; + } + + public void connecting(String connectionMode) { + if (connectionMode != null) { + mode = connectionMode; + } + state = ConnectionState.CONNECTING; + } + + public void active() { + retryCount = 0; + state = ConnectionState.ACTIVE; + } + + public void manualRetry() { + if (!autoStartSuppressed) { + retryCount = 0; + state = ConnectionState.CONNECTING; + } + } + + public void userExited() { + autoStartSuppressed = true; + state = ConnectionState.EXITED; + } + + public void detached() { + mode = null; + retryCount = 0; + autoStartSuppressed = false; + state = ConnectionState.IDLE; + } + + public boolean failed() { + state = ConnectionState.ERROR; + if (autoStartSuppressed || mode == null || retryCount >= MAX_RETRIES) { + return false; + } + retryCount++; + return true; + } +} diff --git a/app/src/main/java/it/smg/hu/manager/ConnectionState.java b/app/src/main/java/it/smg/hu/manager/ConnectionState.java new file mode 100644 index 00000000..1c891e43 --- /dev/null +++ b/app/src/main/java/it/smg/hu/manager/ConnectionState.java @@ -0,0 +1,11 @@ +package it.smg.hu.manager; + +public enum ConnectionState { + IDLE, + PERMISSION_PENDING, + AOAP_SWITCHING, + CONNECTING, + ACTIVE, + EXITED, + ERROR +} diff --git a/app/src/main/java/it/smg/hu/manager/HondaConnectManager.java b/app/src/main/java/it/smg/hu/manager/HondaConnectManager.java index 120a671c..c1eb3c70 100644 --- a/app/src/main/java/it/smg/hu/manager/HondaConnectManager.java +++ b/app/src/main/java/it/smg/hu/manager/HondaConnectManager.java @@ -115,7 +115,7 @@ public void onServiceConnected(ComponentName name, IBinder service) { registerSteeringMenuCallback(); - if (pControl_.authType != Constants.AUTH_TYPE_PREINSTALL){ + if (!isPreinstall()){ notifySteeringMenuDispMode(1); } @@ -171,7 +171,7 @@ public void onServiceDisconnected(ComponentName name) { } public int mediaAudioStream(ChannelId audioChannel){ - if (pControl_.authType == Constants.AUTH_TYPE_PREINSTALL){ + if (isPreinstall()){ switch (audioChannel) { case SPEECH_AUDIO: case SYSTEM_AUDIO: @@ -197,11 +197,11 @@ public void adjustPermission(){ } public void requestAudioFocus(){ - if (Log.isDebug()) Log.d(TAG, "requestAudioFocus -> app with auth " + pControl_.authType); + if (Log.isDebug()) Log.d(TAG, "requestAudioFocus -> app with auth " + authType()); if (Log.isVerbose()) Log.v(TAG, "requestAudioFocus modeMgr audio hasAudioFocus= " + hasAudioFocus_); // If AUTH_TYPE <> preinstall the app has already audio focus - if (pControl_.authType == Constants.AUTH_TYPE_PREINSTALL && !hasAudioFocus_) { + if (isPreinstall() && modeMgrManager_ != null && !hasAudioFocus_) { int idx = settings_.advanced.modeMgrAudioIdx(); int ret; @@ -233,11 +233,11 @@ public void requestAudioFocus(){ } public void releaseAudioFocus(){ - if (Log.isDebug()) Log.d(TAG, "releaseAudioFocus -> app with auth " + pControl_.authType); + if (Log.isDebug()) Log.d(TAG, "releaseAudioFocus -> app with auth " + authType()); if (Log.isVerbose()) Log.v(TAG, "releaseAudioFocus modeMgr audio hasAudioFocus= " + hasAudioFocus_); // If AUTH_TYPE <> preinstall the app has already audio focus - if (pControl_.authType == Constants.AUTH_TYPE_PREINSTALL && hasAudioFocus_) { + if (isPreinstall() && modeMgrManager_ != null && hasAudioFocus_) { int idx = settings_.advanced.modeMgrAudioIdx(); int ret; @@ -263,17 +263,17 @@ public void releaseAudioFocus(){ public void increaseVolume(){ if (Log.isVerbose()) Log.v(TAG, "increaseVolume"); - modeMgrManager_.reqModeMgrSteeringVolCmd(true); + if (modeMgrManager_ != null) modeMgrManager_.reqModeMgrSteeringVolCmd(true); } public void decreaseVolume(){ if (Log.isVerbose()) Log.v(TAG, "decreaseVolume"); - modeMgrManager_.reqModeMgrSteeringVolCmd(false); + if (modeMgrManager_ != null) modeMgrManager_.reqModeMgrSteeringVolCmd(false); } // Used in onCreate public void initialize(){ - if (Log.isDebug()) Log.d(TAG, "initialize -> app with auth " + pControl_.authType); + if (Log.isDebug()) Log.d(TAG, "initialize -> app with auth " + authType()); bindToEcNcService(); bindToWheelService(); @@ -282,9 +282,9 @@ public void initialize(){ // Used in onResume public void initAudioBinding(){ - if (Log.isDebug()) Log.d(TAG, "initAudioBinding -> app with auth " + pControl_.authType); + if (Log.isDebug()) Log.d(TAG, "initAudioBinding -> app with auth " + authType()); // if app has THIRD_PARTY auth will have exclusive audio focus, only bind wheel service - if (pControl_.authType == Constants.AUTH_TYPE_PREINSTALL){ + if (isPreinstall()){ if (Log.isVerbose()) Log.v(TAG, "initAudioBinding -> app auth = preinstall -> register ModeMgr and SW callback"); registerModeMgrCallback(); @@ -304,18 +304,18 @@ public void initAudioBinding(){ // Used in onPause (app in background) public void sendToBackground(){ - if (Log.isDebug()) Log.d(TAG, "sendToBackground -> app with auth " + pControl_.authType + " unregister SW callback"); + if (Log.isDebug()) Log.d(TAG, "sendToBackground -> app with auth " + authType() + " unregister SW callback"); unregisterSteeringMenuCallback(); notifySteeringMenuDispMode(0); } public void endAudioBinding(){ - if (Log.isDebug()) Log.d(TAG, "endAudioBinding -> app with auth " + pControl_.authType); + if (Log.isDebug()) Log.d(TAG, "endAudioBinding -> app with auth " + authType()); stopMicSession(); unbindFromEcNcService(); - if (pControl_.authType == Constants.AUTH_TYPE_PREINSTALL){ + if (isPreinstall()){ if (Log.isDebug()) Log.d(TAG, "endAudioBinding -> auth PREINSTALL -> release audio and unregister modemgr callback"); releaseAudioFocus(); unregisterModeMgrCallback(); @@ -481,6 +481,9 @@ private void unregisterModeMgrCallback() { int idx = settings_.advanced.modeMgrAudioIdx(); if (Log.isVerbose()) Log.v(TAG, "unregisterModeMgrCallback idx " + idx); + if (modeMgrManager_ == null) { + return; + } int ret = modeMgrManager_.unregisterModeMgrCallback(idx); if (Log.isVerbose()) Log.v(TAG, "unregisterModeMgrCallback ret " + ret); @@ -500,6 +503,14 @@ private void registerModeMgrCallback(){ } } + private boolean isPreinstall() { + return pControl_ != null && pControl_.authType == Constants.AUTH_TYPE_PREINSTALL; + } + + private String authType() { + return pControl_ == null ? "unavailable" : String.valueOf(pControl_.authType); + } + // private boolean waitForCond(int timeout){ // boolean res = false; // try { diff --git a/app/src/main/java/it/smg/hu/manager/HondaPlatform.java b/app/src/main/java/it/smg/hu/manager/HondaPlatform.java new file mode 100644 index 00000000..5d8d4297 --- /dev/null +++ b/app/src/main/java/it/smg/hu/manager/HondaPlatform.java @@ -0,0 +1,31 @@ +package it.smg.hu.manager; + +import android.content.Context; + +/** Keeps proprietary Honda APIs out of the generic-device startup path. */ +public final class HondaPlatform { + private static Context context_; + private static Boolean available_; + + private HondaPlatform() {} + + public static void init(Context context) { + context_ = context.getApplicationContext(); + available_ = null; + } + + public static boolean isAvailable() { + if (available_ != null) { + return available_; + } + boolean available = false; + try { + Class.forName("com.fujitsu_ten.displayaudio.whitelist.common.IWhiteList"); + available = context_ != null && context_.getSystemService("ModeMgrService") != null; + } catch (Throwable ignored) { + available = false; + } + available_ = available; + return available; + } +} diff --git a/app/src/main/java/it/smg/hu/manager/USBManager.java b/app/src/main/java/it/smg/hu/manager/USBManager.java index 2e4a6161..18e1f52e 100644 --- a/app/src/main/java/it/smg/hu/manager/USBManager.java +++ b/app/src/main/java/it/smg/hu/manager/USBManager.java @@ -23,6 +23,8 @@ public class USBManager { private static final int USB_PID_ACC = 0x2D00; // Accessory 100 private static final int USB_PID_ACC_ADB = 0x2D01; // Accessory + ADB 110 + private static final int USB_PID_ACC_AUDIO = 0x2D04; // Accessory + Audio + private static final int USB_PID_ACC_AUDIO_ADB = 0x2D05; // Accessory + Audio + ADB private static final int USB_VID_GOO = 0x18D1; private static final int ACC_REQ_GET_PROTOCOL = 51; @@ -45,6 +47,7 @@ public class USBManager { private final Context ctx_; private final UsbManager usbManager_; private LibUsbDevice usbDevice_; + private UsbDevice lastDevice_; private final LocalBroadcastManager localBroadcastManager_; private static USBManager instance_; @@ -64,10 +67,21 @@ private USBManager(Context ctx){ localBroadcastManager_ = LocalBroadcastManager.getInstance(ctx_); LibUsb.init(); libUsb_ = LibUsb.instance(); + ConnectionManager.init(ctx_); } public boolean checkDevice(UsbDevice device){ - if (Log.isInfo()) Log.i(TAG, "checkDevice"); + handleUsbAttached(device); + return checkAOAPDevice(device); + } + + public void handleUsbAttached(UsbDevice device){ + if (device == null) { + return; + } + if (Log.isInfo()) Log.i(TAG, "handleUsbAttached " + device); + lastDevice_ = device; + ConnectionManager.instance().transportAvailable("modeUSB", "USB device detected"); boolean deviceHasPermission = usbManager_.hasPermission(device); if (Log.isInfo()) Log.i(TAG, "deviceHasPermission " + deviceHasPermission); @@ -75,24 +89,56 @@ public boolean checkDevice(UsbDevice device){ if (Log.isDebug()) Log.d(TAG, "Request permission"); Intent i = new Intent(ACTION_USB_PERMISSION); PendingIntent permissionIntent = PendingIntent.getBroadcast(ctx_, 0, i, PendingIntent.FLAG_ONE_SHOT); + ConnectionManager.instance().permissionPending("Waiting for USB permission"); usbManager_.requestPermission(device, permissionIntent); + return; } boolean isAoap = checkAOAPDevice(device); if (!isAoap) { if (Log.isInfo()) Log.i(TAG, "Request aoap"); - requestAOAP(device); + if (requestAOAP(device)) { + ConnectionManager.instance().switchingToAoap("Preparing Android Auto USB accessory"); + } else { + ConnectionManager.instance().failed("Could not switch USB device to Android Auto mode"); + } } else { if (Log.isDebug()) Log.d(TAG, "Device is aoap"); usbDevice_ = libUsb_.createDevice(device, usbManager_); + ConnectionManager.instance().transportAvailable("modeUSB", "Android Auto USB accessory ready"); if (Log.isDebug()) Log.d(TAG, "send local broadcast ATTACH_AOAP_DEVICE"); Intent aoapDeviceIntent = new Intent(ATTACH_AOAP_DEVICE); aoapDeviceIntent.putExtra(UsbManager.EXTRA_DEVICE, device); localBroadcastManager_.sendBroadcast(aoapDeviceIntent); } + } + + public void onUsbPermissionResult(UsbDevice device, boolean granted) { + if (device == null) { + ConnectionManager.instance().failed("USB permission result did not include a device"); + return; + } + if (!granted) { + ConnectionManager.instance().failed("USB permission was denied"); + return; + } + handleUsbAttached(device); + } + + public void onUsbDetached(UsbDevice device) { + if (device == null || lastDevice_ == null || device.getDeviceId() == lastDevice_.getDeviceId()) { + usbDevice_ = null; + lastDevice_ = null; + ConnectionManager.instance().detached("USB device disconnected"); + localBroadcastManager_.sendBroadcast(new Intent(DETACH_AOAP_DEVICE)); + } + } - return isAoap; + public void retryLastDevice() { + if (lastDevice_ != null) { + handleUsbAttached(lastDevice_); + } } public LibUsbDevice aoapDevice(){ @@ -100,7 +146,7 @@ public LibUsbDevice aoapDevice(){ } private boolean checkAOAPDevice(UsbDevice device){ - if (device.getVendorId() == USB_VID_GOO && (device.getProductId() == USB_PID_ACC || device.getProductId() == USB_PID_ACC_ADB)) { + if (device.getVendorId() == USB_VID_GOO && (device.getProductId() == USB_PID_ACC || device.getProductId() == USB_PID_ACC_ADB || device.getProductId() == USB_PID_ACC_AUDIO || device.getProductId() == USB_PID_ACC_AUDIO_ADB)) { if (Log.isInfo()) Log.i(TAG, "Found aop device"); return true; } @@ -176,7 +222,7 @@ public boolean requestAOAP(UsbDevice device){ if (Log.isDebug()) Log.d(TAG, "Send Version " + VERSION); buffer = (VERSION + "\0").getBytes(); - len = usbConnection.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, ACC_REQ_SEND_STRING, 0, 0, buffer, buffer.length, 10000); + len = usbConnection.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, ACC_REQ_SEND_STRING, 0, 3, buffer, buffer.length, 10000); if (len != buffer.length) { Log.e(TAG, "Error sending Version, len= " + len + " expected= " + buffer.length); return false; @@ -184,7 +230,7 @@ public boolean requestAOAP(UsbDevice device){ if (Log.isDebug()) Log.d(TAG, "Send URI " + URI); buffer = (URI + "\0").getBytes(); - len = usbConnection.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, ACC_REQ_SEND_STRING, 0, 0, buffer, buffer.length, 10000); + len = usbConnection.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, ACC_REQ_SEND_STRING, 0, 4, buffer, buffer.length, 10000); if (len != buffer.length) { Log.e(TAG, "Error sending Manufacter, len= " + len + " expected= " + buffer.length); return false; @@ -192,7 +238,7 @@ public boolean requestAOAP(UsbDevice device){ if (Log.isDebug()) Log.d(TAG, "Send Serial " + SERIAL); buffer = (SERIAL + "\0").getBytes(); - len = usbConnection.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, ACC_REQ_SEND_STRING, 0, 0, buffer, buffer.length, 10000); + len = usbConnection.controlTransfer(UsbConstants.USB_DIR_OUT | UsbConstants.USB_TYPE_VENDOR, ACC_REQ_SEND_STRING, 0, 5, buffer, buffer.length, 10000); if (len != buffer.length) { Log.e(TAG, "Error sending Serial, len= " + len + " expected= " + buffer.length); return false; diff --git a/app/src/main/java/it/smg/hu/manager/UsbPermissionReceiver.java b/app/src/main/java/it/smg/hu/manager/UsbPermissionReceiver.java new file mode 100644 index 00000000..b95c9a1a --- /dev/null +++ b/app/src/main/java/it/smg/hu/manager/UsbPermissionReceiver.java @@ -0,0 +1,26 @@ +package it.smg.hu.manager; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.hardware.usb.UsbDevice; +import android.hardware.usb.UsbManager; + +/** Receives permission and detach broadcasts even while MainActivity is not visible. */ +public class UsbPermissionReceiver extends BroadcastReceiver { + @Override + public void onReceive(Context context, Intent intent) { + USBManager manager = USBManager.instance(); + if (manager == null || intent == null) { + return; + } + String action = intent.getAction(); + if (USBManager.ACTION_USB_PERMISSION.equals(action)) { + UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + boolean granted = intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false); + manager.onUsbPermissionResult(device, granted); + } else if (UsbManager.ACTION_USB_DEVICE_DETACHED.equals(action)) { + manager.onUsbDetached(intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)); + } + } +} diff --git a/app/src/main/java/it/smg/hu/manager/WIFIManager.java b/app/src/main/java/it/smg/hu/manager/WIFIManager.java index 03382e1a..b0a5f0eb 100644 --- a/app/src/main/java/it/smg/hu/manager/WIFIManager.java +++ b/app/src/main/java/it/smg/hu/manager/WIFIManager.java @@ -13,6 +13,7 @@ import java.net.UnknownHostException; import it.smg.libs.common.Log; +import it.smg.hu.config.Settings; public class WIFIManager { @@ -57,6 +58,9 @@ private String networkSSID(){ private String ipAddress(){ WifiManager wifi = (WifiManager) ctx_.getSystemService(Context.WIFI_SERVICE); DhcpInfo d = wifi.getDhcpInfo(); + if (d == null || d.gateway == 0) { + return null; + } byte[] addressBytes = {(byte) (0xff & d.gateway), (byte) (0xff & (d.gateway >> 8)), @@ -73,20 +77,28 @@ private String ipAddress(){ private boolean isConnected(){ ConnectivityManager connManager = (ConnectivityManager) ctx_.getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo wifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); - return wifi.isConnected(); + return wifi != null && wifi.isConnected(); } public void checkNetwork(){ synchronized (this) { + if (!Settings.instance().advanced.enableWiFi()) { + return; + } boolean isConnected = isConnected(); String networkSSID_; if (isConnected) { ipAddress_ = ipAddress(); networkSSID_ = networkSSID(); + if (ipAddress_ == null) { + ConnectionManager.instance().failed("Phone hotspot has no gateway yet"); + return; + } if (Log.isDebug()) Log.d(TAG, "Network with ip: " + ipAddress_); if (Log.isDebug()) Log.d(TAG, "Network with SSID: " + networkSSID_); + ConnectionManager.instance().transportAvailable("modeWifi", "Phone hotspot ready: " + networkSSID_); Intent connectWifiIntent = new Intent(CONNECT_WIFI); connectWifiIntent.putExtra(EXTRA_SSID, networkSSID_); localBroadcastManager_.sendBroadcast(connectWifiIntent); @@ -103,6 +115,7 @@ public void checkNetwork(){ Intent disconnectWifiIntent = new Intent(DISCONNECT_WIFI); localBroadcastManager_.sendBroadcast(disconnectWifiIntent); + ConnectionManager.instance().detached("Phone hotspot disconnected"); } } } diff --git a/app/src/main/java/it/smg/hu/projection/AudioCodec.java b/app/src/main/java/it/smg/hu/projection/AudioCodec.java index 4a72945e..332c351a 100644 --- a/app/src/main/java/it/smg/hu/projection/AudioCodec.java +++ b/app/src/main/java/it/smg/hu/projection/AudioCodec.java @@ -5,8 +5,9 @@ import android.media.AudioTrack; import java.nio.ByteBuffer; -import java.util.Queue; -import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import it.smg.libs.common.Log; @@ -22,7 +23,7 @@ public class AudioCodec implements IAudioCodec, Runnable { private final int sampleSize_; protected AtomicBoolean running_; - private final Queue queue_; + private final BlockingQueue queue_; private Thread codecThread_; public AudioCodec(String name, int streamType, int sampleRate, int channelConfig, int sampleSize){ @@ -33,7 +34,7 @@ public AudioCodec(String name, int streamType, int sampleRate, int channelConfig sampleSize_ = sampleSize; running_ = new AtomicBoolean(false); - queue_ = new ConcurrentLinkedQueue<>(); + queue_ = new ArrayBlockingQueue<>(32); } private static int channels2num(int channels){ @@ -67,22 +68,30 @@ public static int sampleSizeFromInt(int sampleSize){ @Override public void write(ByteBuffer buffer, long timestamp) { if (Log.isVerbose()) Log.v(TAG, "buffer size: " + buffer.limit()); - final int size = buffer.limit(); + ByteBuffer source = buffer.slice(); + final int size = source.remaining(); + if (size <= 0) { + return; + } final byte[] data = new byte[size]; - buffer.get(data); + source.get(data); - queue_.add(data); + if (!queue_.offer(data)) { + queue_.poll(); + queue_.offer(data); + } } @Override public void start() { if (Log.isInfo()) Log.i(TAG, "Start"); + if (running_.getAndSet(true)) { + return; + } codecThread_ = new Thread(this); codecThread_.setName(name_); codecThread_.start(); - - running_.set(true); } @Override @@ -93,7 +102,7 @@ public void stop() { if (codecThread_ != null){ try { - codecThread_.join(); + codecThread_.join(1000); if (Log.isDebug()) Log.d(TAG + "_" + codecThread_.getName(), "thread joined"); } catch (InterruptedException ignored) {} codecThread_ = null; @@ -113,6 +122,11 @@ public void run() { if (audioTrack_ == null) { try { int bufferSize_ = AudioTrack.getMinBufferSize(sampleRate_, channels2num(channelConfig_), sampleSizeFromInt(sampleSize_)); + if (bufferSize_ <= 0) { + Log.e(TAG, "Invalid AudioTrack buffer size " + bufferSize_); + running_.set(false); + return; + } if (Log.isInfo()) Log.i(TAG, "Buffer size " + bufferSize_ + "*2"); audioTrack_ = new AudioTrack(streamType_, sampleRate_, channels2num(channelConfig_), sampleSizeFromInt(sampleSize_), bufferSize_ * 3, AudioTrack.MODE_STREAM); } catch (Exception e) { @@ -130,7 +144,12 @@ public void run() { if (Log.isVerbose()) Log.v(TAG + "_" + codecThread_.getName(), "running thread"); while (running_.get()) { - byte[] data = queue_.poll(); + byte[] data = null; + try { + data = queue_.poll(50, TimeUnit.MILLISECONDS); + } catch (InterruptedException ignored) { + Thread.currentThread().interrupt(); + } if (data != null){ audioTrack_.write(data, 0, data.length); } diff --git a/app/src/main/java/it/smg/hu/projection/InputDevice.java b/app/src/main/java/it/smg/hu/projection/InputDevice.java index 091e7b5d..a6361fe9 100644 --- a/app/src/main/java/it/smg/hu/projection/InputDevice.java +++ b/app/src/main/java/it/smg/hu/projection/InputDevice.java @@ -185,11 +185,11 @@ public boolean onKey(View v, int keyCode, KeyEvent event) { final int plusKeyCode = Settings.instance().keymap.key(KeymapFragment.KeyMap.PLUS.keyName() + "_code"); final int minusKeyCode = Settings.instance().keymap.key(KeymapFragment.KeyMap.MINUS.keyName() + "_code"); - if (keyCode == plusKeyCode){ + if (keyCode == plusKeyCode && Settings.instance().advanced.hondaIntegrationEnabled()){ if (event.getAction() == KeyEvent.ACTION_DOWN) { // increase/decrease volume only on action down to prevent double increase/decrease HondaConnectManager.instance().increaseVolume(); } - } else if (keyCode == minusKeyCode) { + } else if (keyCode == minusKeyCode && Settings.instance().advanced.hondaIntegrationEnabled()) { if (event.getAction() == KeyEvent.ACTION_DOWN) { // increase/decrease volume only on action down to prevent double increase/decrease HondaConnectManager.instance().decreaseVolume(); } diff --git a/app/src/main/java/it/smg/hu/projection/NativeVideoOutput.java b/app/src/main/java/it/smg/hu/projection/NativeVideoOutput.java index 7b9dd36b..44a9c777 100644 --- a/app/src/main/java/it/smg/hu/projection/NativeVideoOutput.java +++ b/app/src/main/java/it/smg/hu/projection/NativeVideoOutput.java @@ -20,8 +20,8 @@ public class NativeVideoOutput extends VideoOutput implements Runnable { private static final String TAG = "NativeVideoOutput"; private MediaCodec codec_; - private boolean configured_; - private boolean running_; + private volatile boolean configured_; + private volatile boolean running_; private Thread codecThread_; public NativeVideoOutput(SurfaceView surfaceView){ @@ -58,9 +58,9 @@ public boolean init() { format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, 655360); codec_.configure(format, surface, null, 0); configured_ = true; + running_ = true; codec_.start(); codecThread_.start(); - running_ = true; return true; } catch (IOException e) { return false; @@ -97,7 +97,9 @@ public void stop() { running_ = false; configured_ = false; try { - codecThread_.join(); + if (codecThread_ != null) { + codecThread_.join(1000); + } } catch (InterruptedException ignored) {} codec_.flush(); @@ -113,7 +115,7 @@ public void run() { MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); while (running_) { if (configured_) { - int index = codec_.dequeueOutputBuffer(info, 0); + int index = codec_.dequeueOutputBuffer(info, 10000); if (index >= 0) { if (Log.isVerbose()) Log.v(TAG, "outputBufferIndex: " + index); ByteBuffer buffer = null; diff --git a/app/src/main/java/it/smg/hu/service/ODAService.java b/app/src/main/java/it/smg/hu/service/ODAService.java index e27fd410..31828a11 100644 --- a/app/src/main/java/it/smg/hu/service/ODAService.java +++ b/app/src/main/java/it/smg/hu/service/ODAService.java @@ -15,6 +15,7 @@ import it.smg.hu.config.Settings; import it.smg.hu.manager.HondaConnectManager; +import it.smg.hu.manager.ConnectionManager; import it.smg.hu.manager.USBManager; import it.smg.hu.manager.WIFIManager; import it.smg.hu.projection.InputDevice; @@ -52,7 +53,9 @@ public class ODAService extends Service implements IAndroidAutoEntityEventHandle private Handler mainHandler_; - private boolean isRunning_; + private volatile boolean isRunning_; + private volatile boolean stopRequested_; + private String currentMode_; public ODAService() {} @@ -72,19 +75,30 @@ public void onCreate() { } public void startUsb(SurfaceView surfaceView, InputDevice.OnKeyHolder keyHolder){ + if (isRunning_) { + return; + } + currentMode_ = MODE_USB; + stopRequested_ = false; + ConnectionManager.instance().connecting(MODE_USB, "Connecting through USB"); startThread_ = new Thread(() -> { Looper.prepare(); if (usbManager_.aoapDevice() != null) { - isRunning_ = true; - if (Log.isVerbose()) Log.v(TAG, "aoap device available, start in usb mode"); try { LibUsbDevice device = usbManager_.aoapDevice(); if (device.open()) { if (Log.isInfo()) Log.i(TAG, "device opened"); androidAutoEntity_ = AndroidAutoEntityFactory.create(this, device, surfaceView, keyHolder); + if (stopRequested_) { + androidAutoEntity_.delete(); + androidAutoEntity_ = null; + return; + } androidAutoEntity_.start(this); + isRunning_ = true; + ConnectionManager.instance().active("Android Auto is connected through USB"); } else { Log.e(TAG, "Error in open usb device"); onAndroidAutoQuitOnError("USB OPEN DEVICE", -1); @@ -96,6 +110,8 @@ public void startUsb(SurfaceView surfaceView, InputDevice.OnKeyHolder keyHolder) // onAndroidAutoQuit(); return; } + } else { + ConnectionManager.instance().failed("Android Auto USB accessory is no longer available"); } if (Log.isInfo()) Log.i(TAG, "start usb thead completed"); }); @@ -103,22 +119,36 @@ public void startUsb(SurfaceView surfaceView, InputDevice.OnKeyHolder keyHolder) } public void startWifi(SurfaceView surfaceView, InputDevice.OnKeyHolder keyHolder){ + if (isRunning_) { + return; + } + currentMode_ = MODE_WIFI; + stopRequested_ = false; + ConnectionManager.instance().connecting(MODE_WIFI, "Connecting through phone hotspot"); startThread_ = new Thread(() -> { Looper.prepare(); try { String ipAddress = wifiManager_.getIpAddress(); if (ipAddress != null) { - isRunning_ = true; - // if (Log.isInfo()) Log.i(TAG, "Connect to ip " + ipAddress); TCPEndpoint tcpEndpoint = new TCPEndpoint(ipAddress); androidAutoEntity_ = AndroidAutoEntityFactory.create(this, tcpEndpoint, surfaceView, keyHolder); + if (stopRequested_) { + androidAutoEntity_.delete(); + androidAutoEntity_ = null; + return; + } androidAutoEntity_.start(this); + isRunning_ = true; + ConnectionManager.instance().active("Android Auto is connected through Wi-Fi"); + } else { + ConnectionManager.instance().failed("Phone hotspot gateway is not ready"); } if (Log.isInfo()) Log.i(TAG, "start wifi thead completed"); } catch (TCPConnectException e){ Log.e(TAG, "TCP Connection error", e); + ConnectionManager.instance().failed("Could not reach Android Auto on the phone hotspot"); stop(); } }); @@ -126,7 +156,9 @@ public void startWifi(SurfaceView surfaceView, InputDevice.OnKeyHolder keyHolder } public void shutdown(){ - androidAutoEntity_.shutdown(); + if (androidAutoEntity_ != null) { + androidAutoEntity_.shutdown(); + } } public void releaseFocus(){ @@ -136,12 +168,17 @@ public void releaseFocus(){ } public void gainFocus(){ - androidAutoEntity_.gainFocus(); + if (androidAutoEntity_ != null) { + androidAutoEntity_.gainFocus(); + } } public void stop(){ - if (!isRunning_) { + stopRequested_ = true; + if (!isRunning_ && androidAutoEntity_ == null) { if (Log.isInfo()) Log.i(TAG, "service not running, already stopped?"); + localBroadcastManager_.sendBroadcast(new Intent(ODAService.STOP_ACTION)); + stopService(new Intent(this, ODAService.class)); return; } @@ -163,6 +200,7 @@ public void stop(){ } startThread_ = null; + currentMode_ = null; Intent stopIntent = new Intent(ODAService.STOP_ACTION); localBroadcastManager_.sendBroadcast(stopIntent); @@ -178,6 +216,7 @@ public IBinder onBind(Intent intent) { public void onDestroy() { if (Log.isDebug()) Log.d(TAG, "onDestroy"); + super.onDestroy(); } @Override @@ -200,6 +239,7 @@ public int onStartCommand(Intent intent, int flags, int startId) { @Keep @Override public void onAndroidAutoQuit() { + ConnectionManager.instance().detached("Android Auto session ended"); stop(); } @@ -207,6 +247,7 @@ public void onAndroidAutoQuit() { @Override public void onAndroidAutoQuitOnError(String error, int nativeErrorCode){ Log.e(TAG, "closing with error " + error + "(" + nativeErrorCode + ")"); + ConnectionManager.instance().failed(error); mainHandler_.post(() -> { Toast.makeText(this, "Closed due to " + error + " error", Toast.LENGTH_LONG).show(); diff --git a/app/src/main/java/it/smg/hu/ui/MainActivity.java b/app/src/main/java/it/smg/hu/ui/MainActivity.java index 64a79775..fb4e28d3 100644 --- a/app/src/main/java/it/smg/hu/ui/MainActivity.java +++ b/app/src/main/java/it/smg/hu/ui/MainActivity.java @@ -1,226 +1,160 @@ package it.smg.hu.ui; -import android.app.Activity; import android.content.BroadcastReceiver; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.ServiceConnection; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; import android.net.ConnectivityManager; import android.os.Bundle; -import android.os.Handler; -import android.os.IBinder; -import android.widget.Toast; import androidx.fragment.app.FragmentActivity; import androidx.localbroadcastmanager.content.LocalBroadcastManager; -import it.smg.hu.service.ODAService; import it.smg.hu.R; +import it.smg.hu.manager.ConnectionManager; +import it.smg.hu.manager.ConnectionState; import it.smg.hu.manager.USBManager; import it.smg.hu.manager.WIFIManager; +import it.smg.hu.service.ODAService; import it.smg.hu.ui.main.HomeFragment; -import it.smg.hu.ui.notification.AppBadge; -import it.smg.hu.ui.notification.NotificationFactory; -import it.smg.libs.common.Log; +/** Connection host. It launches the player only after a transport is ready. */ public class MainActivity extends FragmentActivity { - public static final int SETTINGS_ACTIVITY_REQUEST = 12345; - private final static String TAG = "MainActivity"; - private USBManager usbManager_; private WIFIManager wifiManager_; private LocalBroadcastManager localBroadcastManager_; - - private Intent newUsbDeviceIntent_ = null; - private boolean isOpenFromMain_ = false; - private boolean isOpenFromUsb_ = false; - private boolean isServiceBound_ = false; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - if (Log.isDebug()) Log.d(TAG, "onCreate, intent: " + getIntent()); - if (Log.isDebug()) Log.d(TAG, "onCreate, intent action: " + getIntent().getAction()); - - isOpenFromMain_ = false; - isOpenFromUsb_ = false; - - localBroadcastManager_ = LocalBroadcastManager.getInstance(this); - usbManager_ = USBManager.instance(); - wifiManager_ = WIFIManager.instance(); - - setContentView(R.layout.activity_main); - - Intent intent = getIntent(); - if (Log.isInfo()) Log.i(TAG, "intent action: " + intent.getAction()); - - if ( UsbManager.ACTION_USB_DEVICE_ATTACHED.equalsIgnoreCase(intent.getAction())) { - if (Log.isInfo()) Log.i(TAG, "new usb device intent: " + intent); - newUsbDeviceIntent_ = intent; - getWindow().setLayout(0, 0); - isOpenFromMain_ = false; - isOpenFromUsb_ = true; - } else if (Intent.ACTION_MAIN.equalsIgnoreCase(intent.getAction())) { - HomeFragment f = new HomeFragment(); - Bundle args = new Bundle(); - args.putBoolean(HomeFragment.AUTOSTART, true); - f.setArguments(args); - - getSupportFragmentManager() - .beginTransaction() - .replace(R.id.home_content, f) - .commit(); - - isOpenFromMain_ = true; - isOpenFromUsb_ = false; - } - - } - - private void registerReceiver(){ - IntentFilter wifiFilter = new IntentFilter(); - wifiFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION); - registerReceiver(wifiReceiver, wifiFilter); - - IntentFilter filter = new IntentFilter(); - filter.addAction(USBManager.ATTACH_AOAP_DEVICE); - localBroadcastManager_.registerReceiver(usbReceiver, filter); - } + private boolean receiversRegistered_; + private boolean playerLaunchPending_; private final BroadcastReceiver wifiReceiver = new BroadcastReceiver() { - private final static String TAG = "MainActivity-WifiReceiver"; @Override public void onReceive(Context context, Intent intent) { - if (Log.isDebug()) Log.d(TAG, "Received action: " + intent.getAction()); if (wifiManager_ != null) { wifiManager_.checkNetwork(); } } }; - private final BroadcastReceiver usbReceiver = new BroadcastReceiver() { + private final BroadcastReceiver connectionReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { - final String TAG = "MainActivity-UsbReceiver"; - - if (Log.isDebug()) Log.d(TAG, intent.getAction()); - if (isOpenFromUsb_) { - if (intent.getAction().equals(USBManager.ATTACH_AOAP_DEVICE)) { - if (Log.isDebug()) Log.d(TAG, "open popup activity"); - NotificationFactory.instance().notifyStartRequest(); - -// MainActivity.this.finish(); + String action = intent.getAction(); + if (USBManager.ATTACH_AOAP_DEVICE.equals(action)) { + launchPlayer(ODAService.MODE_USB); + } else if (WIFIManager.CONNECT_WIFI.equals(action)) { + launchPlayer(ODAService.MODE_WIFI); + } else if (ConnectionManager.ACTION_RETRY.equals(action)) { + launchPlayer(intent.getStringExtra(ConnectionManager.EXTRA_MODE)); + } else if (ConnectionManager.ACTION_STATE_CHANGED.equals(action)) { + String state = intent.getStringExtra(ConnectionManager.EXTRA_STATE); + if (ConnectionState.ERROR.name().equals(state) || ConnectionState.IDLE.name().equals(state) + || ConnectionState.EXITED.name().equals(state)) { + playerLaunchPending_ = false; } } } }; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + usbManager_ = USBManager.instance(); + wifiManager_ = WIFIManager.instance(); + localBroadcastManager_ = LocalBroadcastManager.getInstance(this); + + setContentView(R.layout.activity_main); + if (savedInstanceState == null) { + getSupportFragmentManager().beginTransaction() + .replace(R.id.home_content, new HomeFragment()) + .commit(); + } + handleUsbIntent(getIntent()); + } + @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); - if (Log.isDebug()) Log.d(TAG, "onNewIntent, action " + intent.getAction()); - if ( UsbManager.ACTION_USB_DEVICE_ATTACHED.equalsIgnoreCase(intent.getAction())) { - if (Log.isInfo()) Log.i(TAG, "attached new device, keep intent"); - newUsbDeviceIntent_ = intent; - } + setIntent(intent); + handleUsbIntent(intent); } @Override protected void onResume() { super.onResume(); - if (Log.isDebug()) Log.d(TAG, "onResume"); - - Intent odaServiceIntent = new Intent(this, ODAService.class); - startService(odaServiceIntent); - - registerReceiver(); - - Intent intent = newUsbDeviceIntent_; - if (Log.isDebug()) Log.d(TAG, "onResume intent: " + intent); - if (intent != null) { - if (UsbManager.ACTION_USB_DEVICE_ATTACHED.equalsIgnoreCase(intent.getAction())) { - UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); - if (Log.isDebug()) Log.d(TAG, "attached device " + device); - if (device != null) { - usbManager_.checkDevice(device); - } - } else if (USBManager.ACTION_USB_PERMISSION.equalsIgnoreCase(intent.getAction())) { - UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); - if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) { - if (Log.isDebug()) Log.d(TAG, "permission granted for device " + device); - if (device != null) { - usbManager_.checkDevice(device); - } - } else { - if (Log.isWarn()) Log.w(TAG, "permission negated for device " + device + " do nothing"); - } - } + registerConnectionReceivers(); + handleUsbIntent(getIntent()); + if (wifiManager_ != null) { + wifiManager_.checkNetwork(); } - -// NotificationFactory.init(getApplicationContext()); -// NotificationFactory.instance().notifyStartRequest(); - AppBadge.instance().dismiss(); - - newUsbDeviceIntent_ = null; } @Override protected void onPause() { + unregisterConnectionReceivers(); super.onPause(); - unregisterReceiver(wifiReceiver); - - localBroadcastManager_.unregisterReceiver(usbReceiver); - if (Log.isDebug()) Log.d(TAG, "onPause"); } - @Override - protected void onStart() { - if (Log.isDebug()) Log.d(TAG, "onStart"); - super.onStart(); + private void handleUsbIntent(Intent intent) { + if (intent == null || !UsbManager.ACTION_USB_DEVICE_ATTACHED.equals(intent.getAction()) || usbManager_ == null) { + return; + } + UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE); + if (device != null) { + usbManager_.handleUsbAttached(device); + } } - @Override - protected void onStop() { - if (Log.isDebug()) Log.d(TAG, "onStop"); - super.onStop(); + private void registerConnectionReceivers() { + if (receiversRegistered_) { + return; + } + registerReceiver(wifiReceiver, new IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)); + IntentFilter localFilter = new IntentFilter(); + localFilter.addAction(USBManager.ATTACH_AOAP_DEVICE); + localFilter.addAction(WIFIManager.CONNECT_WIFI); + localFilter.addAction(ConnectionManager.ACTION_RETRY); + localFilter.addAction(ConnectionManager.ACTION_STATE_CHANGED); + localBroadcastManager_.registerReceiver(connectionReceiver, localFilter); + receiversRegistered_ = true; } - @Override - protected void onDestroy() { - super.onDestroy(); - if (Log.isDebug()) Log.d(TAG, "onDestroy"); + private void unregisterConnectionReceivers() { + if (!receiversRegistered_) { + return; + } + unregisterReceiver(wifiReceiver); + localBroadcastManager_.unregisterReceiver(connectionReceiver); + receiversRegistered_ = false; } - private boolean doubleBackToExitPressedOnce = false; - - public void exitApp(){ - Intent service = new Intent(this, ODAService.class); - stopService(service); - - Log.shutdown(); + private void launchPlayer(String mode) { + if (mode == null || playerLaunchPending_ || PlayerActivity.isActive() + || !ConnectionManager.instance().isAutoStartAllowed()) { + return; + } + if (ODAService.MODE_WIFI.equals(mode) && usbManager_ != null && usbManager_.aoapDevice() != null) { + return; + } + playerLaunchPending_ = true; + Intent player = new Intent(this, PlayerActivity.class); + player.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + player.putExtra("mode", mode); + startActivity(player); + } - finish(); -// android.os.Process.killProcess(android.os.Process.myPid()); - return; + /** Called by the home screen and PlayerActivity to prevent a reconnect loop. */ + public void exitSession() { + playerLaunchPending_ = false; + ConnectionManager.instance().userExited("Android Auto stopped. Reconnect the cable to start again."); + stopService(new Intent(this, ODAService.class)); } @Override public void onBackPressed() { - if (doubleBackToExitPressedOnce) { - super.onBackPressed(); - exitApp(); - return; - } - - this.doubleBackToExitPressedOnce = true; - Toast.makeText(this, "Please click BACK again to exit", Toast.LENGTH_SHORT).show(); - - new Handler().postDelayed(() -> doubleBackToExitPressedOnce=false, 2000); + exitSession(); } -} \ No newline at end of file +} diff --git a/app/src/main/java/it/smg/hu/ui/PlayerActivity.java b/app/src/main/java/it/smg/hu/ui/PlayerActivity.java index f71d3e4a..ffaa25e6 100644 --- a/app/src/main/java/it/smg/hu/ui/PlayerActivity.java +++ b/app/src/main/java/it/smg/hu/ui/PlayerActivity.java @@ -21,6 +21,7 @@ import it.smg.hu.R; import it.smg.hu.config.Settings; import it.smg.hu.manager.HondaConnectManager; +import it.smg.hu.manager.ConnectionManager; import it.smg.hu.projection.InputDevice; import it.smg.hu.service.ODAService; import it.smg.hu.ui.notification.AppBadge; @@ -56,7 +57,7 @@ protected void onCreate(Bundle savedInstanceState) { isRunning_ = savedInstanceState.getBoolean("isRunning"); } else { Bundle b = getIntent().getExtras(); - startMode_ = b.getString("mode"); + startMode_ = b == null ? null : b.getString("mode"); } if (Log.isDebug()) Log.d(TAG, "start mode: " + startMode_); @@ -92,12 +93,7 @@ protected void onSaveInstanceState(Bundle outState) { @Override public void onBackPressed() { if (Log.isDebug()) Log.d(TAG, "onBackPressed"); - - if (Settings.instance().advanced.hondaIntegrationEnabled()){ - HondaConnectManager.instance().endAudioBinding(); - } - - odaService_.stop(); + exitSession(); } @Override @@ -105,11 +101,7 @@ public boolean onKeyLongPress(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK) { if (Log.isInfo()) Log.i(TAG, "Back button long pressed"); - if (Settings.instance().advanced.hondaIntegrationEnabled()){ - HondaConnectManager.instance().endAudioBinding(); - } - - odaService_.stop(); + exitSession(); return true; } return super.onKeyLongPress(keyCode, event); @@ -139,7 +131,9 @@ protected void onResume() { protected void onPause() { if (Log.isDebug()) Log.d(TAG, "onPause"); super.onPause(); - odaService_.releaseFocus(); + if (odaService_ != null) { + odaService_.releaseFocus(); + } if (isServiceBound_) { unbindService(this); @@ -151,7 +145,10 @@ protected void onPause() { AppBadge.instance().show(); - localBroadcastManager_.unregisterReceiver(localReceiver_); + if (localReceiver_ != null) { + localBroadcastManager_.unregisterReceiver(localReceiver_); + localReceiver_ = null; + } isActive_ = false; } @@ -207,7 +204,9 @@ public void surfaceChanged(SurfaceHolder holder, int format, int width, int heig @Override public void surfaceDestroyed(SurfaceHolder holder) { Log.d(TAG, "surfaceDestroyed"); - odaService_.releaseFocus(); + if (odaService_ != null) { + odaService_.releaseFocus(); + } } @Override @@ -231,6 +230,12 @@ private void start(){ HondaConnectManager.instance().adjustPermission(); } + if (startMode_ == null) { + ConnectionManager.instance().failed("No connection mode was provided"); + finish(); + return; + } + switch (startMode_){ case ODAService.MODE_USB: odaService_.startUsb(surfaceView_, this); @@ -276,4 +281,13 @@ public void onReceive(Context context, Intent intent) { public static boolean isActive(){ return isActive_; } + + private void exitSession() { + ConnectionManager.instance().userExited("Android Auto stopped. Reconnect the cable to start again."); + if (odaService_ != null) { + odaService_.stop(); + } else { + finish(); + } + } } diff --git a/app/src/main/java/it/smg/hu/ui/main/HomeFragment.java b/app/src/main/java/it/smg/hu/ui/main/HomeFragment.java index 0589ac6e..a6ff2bbf 100644 --- a/app/src/main/java/it/smg/hu/ui/main/HomeFragment.java +++ b/app/src/main/java/it/smg/hu/ui/main/HomeFragment.java @@ -1,12 +1,9 @@ package it.smg.hu.ui.main; -import android.app.AlertDialog; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.hardware.usb.UsbDevice; -import android.hardware.usb.UsbManager; import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; @@ -20,208 +17,178 @@ import it.smg.hu.R; import it.smg.hu.config.Settings; +import it.smg.hu.manager.ConnectionManager; +import it.smg.hu.manager.ConnectionState; import it.smg.hu.manager.USBManager; import it.smg.hu.manager.WIFIManager; import it.smg.hu.service.ODAService; import it.smg.hu.ui.MainActivity; -import it.smg.hu.ui.PlayerActivity; import it.smg.hu.ui.SettingsActivity; -import it.smg.libs.aasdk.usb.LibUsbDevice; -import it.smg.libs.common.Log; +/** The stationary connection dashboard. The normal USB flow never requires a Start button. */ public class HomeFragment extends Fragment { - - private static final String TAG = "HomeFragment"; - - public static final String AUTOSTART = "enableAutostart"; - - private ImageView usbIconConnection_; - private TextView txtUsbIconConnection_; - private ImageView wifiIconConnection_; - private TextView txtWifiIconConnection_; - private Button startBtn_; - private Button startUsbBtn_; - - private BroadcastReceiver localReceiver_; - private LocalBroadcastManager localBroadcastManager_; - + private TextView statusBadge_; + private TextView statusTitle_; + private TextView statusDetail_; + private TextView usbStatus_; + private TextView wifiStatus_; + private ImageView indicator_; + private Button retryButton_; private USBManager usbManager_; - private WIFIManager WIFIManager_; + private WIFIManager wifiManager_; + private LocalBroadcastManager broadcasts_; + private ConnectionState renderedState_; - private Settings settings_; + private final BroadcastReceiver receiver_ = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (ConnectionManager.ACTION_STATE_CHANGED.equals(action)) { + String name = intent.getStringExtra(ConnectionManager.EXTRA_STATE); + ConnectionState state = name == null ? ConnectionState.IDLE : ConnectionState.valueOf(name); + renderState(state, intent.getStringExtra(ConnectionManager.EXTRA_MESSAGE), + intent.getStringExtra(ConnectionManager.EXTRA_MODE)); + } else if (USBManager.ATTACH_AOAP_DEVICE.equals(action)) { + usbStatus_.setText(R.string.home_usb_ready); + } else if (USBManager.DETACH_AOAP_DEVICE.equals(action)) { + usbStatus_.setText(R.string.home_usb_waiting); + } else if (WIFIManager.CONNECT_WIFI.equals(action)) { + String ssid = intent.getStringExtra(WIFIManager.EXTRA_SSID); + wifiStatus_.setText(getString(R.string.home_wifi_ready, ssid == null ? "" : ssid)); + } else if (WIFIManager.DISCONNECT_WIFI.equals(action)) { + renderWifiAvailability(); + } + } + }; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { - if (Log.isDebug()) Log.d(TAG, "onCreateView"); - - super.onCreateView(inflater, container, savedInstanceState); View view = inflater.inflate(R.layout.home_fragment, container, false); - - settings_ = Settings.instance(); - localBroadcastManager_ = LocalBroadcastManager.getInstance(getContext()); - usbManager_ = USBManager.instance(); - WIFIManager_ = WIFIManager.instance(); - - if (usbManager_.aoapDevice() == null){ - usbManager_.searchForAoapDevice(); - } - - initUI(view); - + wifiManager_ = WIFIManager.instance(); + broadcasts_ = LocalBroadcastManager.getInstance(requireContext()); + + statusBadge_ = view.findViewById(R.id.statusBadge); + statusTitle_ = view.findViewById(R.id.statusTitle); + statusDetail_ = view.findViewById(R.id.statusDetail); + usbStatus_ = view.findViewById(R.id.usbStatus); + wifiStatus_ = view.findViewById(R.id.wifiStatus); + indicator_ = view.findViewById(R.id.connectionIndicator); + retryButton_ = view.findViewById(R.id.retryBtn); + + view.findViewById(R.id.settingsBtn).setOnClickListener(v -> + startActivityForResult(new Intent(getContext(), SettingsActivity.class), MainActivity.SETTINGS_ACTIVITY_REQUEST)); + view.findViewById(R.id.exitBtn).setOnClickListener(v -> ((MainActivity) requireActivity()).exitSession()); + retryButton_.setOnClickListener(v -> retryConnection()); + + renderWifiAvailability(); + renderState(ConnectionManager.instance().state(), null, ConnectionManager.instance().mode()); return view; } - private void initUI(View view){ - usbIconConnection_ = view.findViewById(R.id.usbIcon); - usbIconConnection_.setImageResource(R.drawable.usb_dis); - txtUsbIconConnection_ = view.findViewById(R.id.txtUsbIcon); - txtUsbIconConnection_.setText(""); - - Button settingsBtn = view.findViewById(R.id.settingsBtn); - - wifiIconConnection_ = view.findViewById(R.id.wifiIcon); - wifiIconConnection_.setImageResource(R.drawable.wifi_dis); - txtWifiIconConnection_ = view.findViewById(R.id.txtWifiIcon); - txtWifiIconConnection_.setText(""); - startBtn_ = view.findViewById(R.id.startBtn); - startBtn_.setEnabled(false); - startBtn_.setAlpha(0.5f); - - if (settings_.advanced.enableWiFi()) { - startBtn_.setVisibility(View.VISIBLE); - wifiIconConnection_.setVisibility(View.VISIBLE); - txtWifiIconConnection_.setVisibility(View.VISIBLE); - } else { - startBtn_.setVisibility(View.GONE); - wifiIconConnection_.setVisibility(View.GONE); - txtWifiIconConnection_.setVisibility(View.GONE); - } - - startUsbBtn_ = view.findViewById(R.id.startUsbBtn); - startUsbBtn_.setEnabled(false); - startUsbBtn_.setAlpha(0.5f); - Button exit = view.findViewById(R.id.exitBtn); - - settingsBtn.setOnClickListener(c -> { - Intent i = new Intent(getContext(), SettingsActivity.class); - startActivityForResult(i, MainActivity.SETTINGS_ACTIVITY_REQUEST); - }); - - startBtn_.setOnClickListener(c -> { - if (Log.isInfo()) Log.i(TAG, "Starting in WiFi mode"); - start(ODAService.MODE_WIFI); - }); - - startUsbBtn_.setOnClickListener(c -> { - if (Log.isInfo()) Log.i(TAG, "Starting in USB mode"); - start(ODAService.MODE_USB); - }); - - exit.setOnClickListener(c -> { - // TODO: fix - ((MainActivity) getActivity()).exitApp(); - }); - - usbIconConnection_.setOnClickListener(v -> { - AlertDialog dialog = new DeviceListDialog(getActivity()); - dialog.show(); - }); - - initUsbDeviceIcon(); - } - - private void initUsbDeviceIcon(){ - if (usbManager_.aoapDevice() != null){ - LibUsbDevice device = usbManager_.aoapDevice(); - if (Log.isDebug()) Log.d(TAG, "current aoap device " + device); - usbIconConnection_.setImageResource(R.drawable.usb); - txtUsbIconConnection_.setText(device.product); - startUsbBtn_.setEnabled(true); - startUsbBtn_.setAlpha(1f); - } + @Override + public void onStart() { + super.onStart(); + IntentFilter filter = new IntentFilter(); + filter.addAction(ConnectionManager.ACTION_STATE_CHANGED); + filter.addAction(USBManager.ATTACH_AOAP_DEVICE); + filter.addAction(USBManager.DETACH_AOAP_DEVICE); + filter.addAction(WIFIManager.CONNECT_WIFI); + filter.addAction(WIFIManager.DISCONNECT_WIFI); + broadcasts_.registerReceiver(receiver_, filter); } - public void start(String mode){ - Intent i = new Intent(getContext(), PlayerActivity.class); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); - i.putExtra("mode", mode); - startActivity(i); + @Override + public void onStop() { + broadcasts_.unregisterReceiver(receiver_); + super.onStop(); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { - if (Log.isDebug()) Log.d(TAG, "onActivityResult " + resultCode); - if (requestCode == MainActivity.SETTINGS_ACTIVITY_REQUEST){ - - if (settings_.advanced.enableWiFi()) { - startBtn_.setVisibility(View.VISIBLE); - wifiIconConnection_.setVisibility(View.VISIBLE); - txtWifiIconConnection_.setVisibility(View.VISIBLE); - } else { - startBtn_.setVisibility(View.GONE); - wifiIconConnection_.setVisibility(View.GONE); - txtWifiIconConnection_.setVisibility(View.GONE); + super.onActivityResult(requestCode, resultCode, data); + if (requestCode == MainActivity.SETTINGS_ACTIVITY_REQUEST) { + renderWifiAvailability(); + if (wifiManager_ != null) { + wifiManager_.checkNetwork(); } - - initUsbDeviceIcon(); } - super.onActivityResult(requestCode, resultCode, data); } - @Override - public void onStart() { - super.onStart(); - if (Log.isDebug()) Log.d(TAG, "onStart"); - - if (Log.isDebug()) Log.d(TAG, "Registering LocalReceiver"); - IntentFilter localFilter = new IntentFilter(); - localFilter.addAction(USBManager.ATTACH_AOAP_DEVICE); - localFilter.addAction(USBManager.DETACH_AOAP_DEVICE); - localFilter.addAction(WIFIManager.CONNECT_WIFI); - localFilter.addAction(WIFIManager.DISCONNECT_WIFI); - localReceiver_ = new LocalReceiver(); - LocalBroadcastManager localBroadcastManager = LocalBroadcastManager.getInstance(getActivity()); - localBroadcastManager.registerReceiver(localReceiver_, localFilter); + private void retryConnection() { + String mode = ConnectionManager.instance().mode(); + if (ODAService.MODE_USB.equals(mode) && usbManager_ != null) { + usbManager_.retryLastDevice(); + } else { + ConnectionManager.instance().retryNow(); + } } - @Override - public void onStop() { - super.onStop(); - if (Log.isDebug()) Log.d(TAG, "onStop"); - LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(localReceiver_); + private void renderWifiAvailability() { + if (Settings.instance().advanced.enableWiFi()) { + wifiStatus_.setText(R.string.home_wifi_waiting); + } else { + wifiStatus_.setText(R.string.home_wifi_off); + } } - private class LocalReceiver extends BroadcastReceiver { - private final static String TAG = "HomeFragment-LocalReceiver"; - - @Override - public void onReceive(Context context, Intent intent) { - if (Log.isDebug()) Log.d(TAG, "received action " + intent.getAction()); - if (USBManager.DETACH_AOAP_DEVICE.equalsIgnoreCase(intent.getAction())){ - usbIconConnection_.setImageResource(R.drawable.usb_dis); - txtUsbIconConnection_.setText(""); - startUsbBtn_.setEnabled(false); - startUsbBtn_.setAlpha(0.5f); - } else if (USBManager.ATTACH_AOAP_DEVICE.equalsIgnoreCase(intent.getAction())){ - LibUsbDevice device = usbManager_.aoapDevice(); - usbIconConnection_.setImageResource(R.drawable.usb); - txtUsbIconConnection_.setText(device.product); - startUsbBtn_.setEnabled(true); - startUsbBtn_.setAlpha(1f); - } else if (WIFIManager.CONNECT_WIFI.equalsIgnoreCase(intent.getAction())){ - String ssid = intent.getStringExtra(WIFIManager.EXTRA_SSID); - wifiIconConnection_.setImageResource(R.drawable.wifi); - txtWifiIconConnection_.setText(ssid); - startBtn_.setEnabled(true); - startBtn_.setAlpha(1f); - } else if (WIFIManager.DISCONNECT_WIFI.equalsIgnoreCase(intent.getAction())){ - wifiIconConnection_.setImageResource(R.drawable.wifi_dis); - txtWifiIconConnection_.setText(""); - startBtn_.setEnabled(false); - startBtn_.setAlpha(0.5f); - } + private void renderState(ConnectionState state, String message, String mode) { + renderedState_ = state; + retryButton_.setVisibility(state == ConnectionState.ERROR ? View.VISIBLE : View.GONE); + String detail = message; + int badge = R.drawable.status_badge_idle; + int icon = R.drawable.usb_dis; + + switch (state) { + case PERMISSION_PENDING: + statusBadge_.setText(R.string.home_status_permission); + statusTitle_.setText(R.string.home_permission_title); + detail = fallback(detail, R.string.home_permission_detail); + break; + case AOAP_SWITCHING: + statusBadge_.setText(R.string.home_status_connecting); + statusTitle_.setText(R.string.home_connecting_title); + detail = fallback(detail, R.string.home_aoap_detail); + break; + case CONNECTING: + statusBadge_.setText(R.string.home_status_connecting); + statusTitle_.setText(R.string.home_connecting_title); + detail = fallback(detail, R.string.home_connecting_detail); + break; + case ACTIVE: + badge = R.drawable.status_badge_active; + icon = R.drawable.usb; + statusBadge_.setText(R.string.home_status_connected); + statusTitle_.setText(R.string.home_connected_title); + detail = fallback(detail, R.string.home_connected_detail); + break; + case EXITED: + statusBadge_.setText(R.string.home_status_stopped); + statusTitle_.setText(R.string.home_stopped_title); + detail = fallback(detail, R.string.home_stopped_detail); + break; + case ERROR: + badge = R.drawable.status_badge_error; + statusBadge_.setText(R.string.home_status_error); + statusTitle_.setText(R.string.home_error_title); + detail = fallback(detail, R.string.home_error_detail); + break; + case IDLE: + default: + statusBadge_.setText(R.string.home_status_waiting); + statusTitle_.setText(R.string.home_waiting_title); + detail = fallback(detail, R.string.home_waiting_detail); + break; + } + statusBadge_.setBackgroundResource(badge); + indicator_.setImageResource(icon); + statusDetail_.setText(detail); + if (ODAService.MODE_USB.equals(mode) && usbManager_ != null && usbManager_.aoapDevice() != null) { + usbStatus_.setText(R.string.home_usb_ready); } } -} \ No newline at end of file + private String fallback(String detail, int fallbackResource) { + return detail == null || detail.trim().isEmpty() ? getString(fallbackResource) : detail; + } +} diff --git a/app/src/main/res/drawable/button_primary.xml b/app/src/main/res/drawable/button_primary.xml new file mode 100644 index 00000000..8fe30bb4 --- /dev/null +++ b/app/src/main/res/drawable/button_primary.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/button_secondary.xml b/app/src/main/res/drawable/button_secondary.xml new file mode 100644 index 00000000..376fb7cf --- /dev/null +++ b/app/src/main/res/drawable/button_secondary.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/panel_surface.xml b/app/src/main/res/drawable/panel_surface.xml new file mode 100644 index 00000000..5be6fa78 --- /dev/null +++ b/app/src/main/res/drawable/panel_surface.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/status_badge_active.xml b/app/src/main/res/drawable/status_badge_active.xml new file mode 100644 index 00000000..2273df5f --- /dev/null +++ b/app/src/main/res/drawable/status_badge_active.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/status_badge_error.xml b/app/src/main/res/drawable/status_badge_error.xml new file mode 100644 index 00000000..f4a12492 --- /dev/null +++ b/app/src/main/res/drawable/status_badge_error.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/status_badge_idle.xml b/app/src/main/res/drawable/status_badge_idle.xml new file mode 100644 index 00000000..feb15eb1 --- /dev/null +++ b/app/src/main/res/drawable/status_badge_idle.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout/home_fragment.xml b/app/src/main/res/layout/home_fragment.xml index a9c52ef7..39bb1cfc 100644 --- a/app/src/main/res/layout/home_fragment.xml +++ b/app/src/main/res/layout/home_fragment.xml @@ -2,170 +2,171 @@ + android:background="@color/oda_surface" + android:padding="24dp"> - - + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:orientation="horizontal"> + - - - - + android:text="@string/app_name" + android:textColor="@color/oda_text_primary" + android:textSize="26sp" + android:textStyle="bold" /> - - - + + - + android:background="@drawable/status_badge_idle" + android:paddingLeft="14dp" + android:paddingTop="8dp" + android:paddingRight="14dp" + android:paddingBottom="8dp" + android:text="@string/home_status_waiting" + android:textColor="@color/oda_text_primary" + android:textSize="14sp" + android:textStyle="bold" /> + android:padding="26dp"> + android:id="@+id/connectionIndicator" + android:layout_width="64dp" + android:layout_height="64dp" + android:contentDescription="@string/home_connection_status" + android:src="@drawable/usb_dis" /> + + + + + android:layout_marginTop="18dp" + android:orientation="horizontal"> + + + + + +