Skip to content

Commit

Permalink
Show the Car Info app without Notifications app
Browse files Browse the repository at this point in the history
  • Loading branch information
hufman committed Sep 24, 2024
1 parent b4dda91 commit ea49ba1
Show file tree
Hide file tree
Showing 11 changed files with 434 additions and 197 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import org.junit.runner.RunWith
import org.mockito.kotlin.*
import me.hufman.androidautoidrive.notifications.CarNotification
import me.hufman.androidautoidrive.notifications.CarNotificationControllerIntent
import me.hufman.androidautoidrive.carapp.notifications.PhoneNotifications
import me.hufman.androidautoidrive.carapp.notifications.NotificationApp
import me.hufman.androidautoidrive.notifications.NotificationUpdaterControllerIntent
import org.awaitility.Awaitility.await

Expand All @@ -36,7 +36,7 @@ class InstrumentedTestNotificationApp {
val appContext = InstrumentationRegistry.getInstrumentation().targetContext

// prepare to listen to updates from the phone
val mockListener = mock<PhoneNotifications.PhoneNotificationListener> {}
val mockListener = mock<NotificationApp.PhoneNotificationListener> {}
val updateListener = NotificationUpdaterControllerIntent.Receiver(mockListener)
val updateReceiver = object: BroadcastReceiver() {
override fun onReceive(p0: Context?, p1: Intent?) {
Expand Down
5 changes: 5 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -1066,6 +1066,11 @@
<action android:name="me.hufman.androidautoidrive.carconnection.service" />
</intent-filter>
</service>
<service android:name=".carapp.carinfo.CarInfoAppService" android:exported="false">
<intent-filter>
<action android:name="me.hufman.androidautoidrive.carconnection.service" />
</intent-filter>
</service>
<service android:name=".carapp.carinfo.CarInformationDiscoveryService" android:exported="false">
<intent-filter>
<action android:name="me.hufman.androidautoidrive.carconnection.service" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
package me.hufman.androidautoidrive.carapp

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Handler
import android.util.Log
import androidx.core.content.ContextCompat
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIApplication
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIEvent
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIModel
Expand All @@ -26,22 +32,20 @@ enum class ReadoutState(val value: Int) {
}
}
}
class ReadoutController(val name: String, val speechEvent: RHMIEvent.ActionEvent, val commandEvent: RHMIEvent.ActionEvent) {
val speechList = speechEvent.getAction()?.asLinkAction()?.getLinkModel()?.asRaListModel()!!
val commandList = commandEvent.getAction()?.asLinkAction()?.getLinkModel()?.asRaListModel()!!

companion object {
fun build(app: RHMIApplication, name: String): ReadoutController {
val events = app.events.values.filterIsInstance<RHMIEvent.ActionEvent>().filter {
it.getAction()?.asLinkAction()?.actionType == "readout"
}
if (events.size != 2) {
throw IllegalArgumentException("UI Description is missing 2 readout events")
}
return ReadoutController(name, events[0], events[1])
}
}
interface ReadoutCommands {
fun readout(name: String, lines: Iterable<String>)
fun cancel(name: String)
}

/**
* Implements a high-level wrapper around the car's Readout system
* A client would instantiate a ReadoutController with a given name and command interface
* and can trigger readout and cancel commands
* The client should also forward onTTSEvents to update the isActive flag,
* and the isActive flag shows that the car is currently reading out this named ReadoutController
*/
class ReadoutController(val name: String, val readoutCommands: ReadoutCommands) {
var currentState: ReadoutState = ReadoutState.UNDEFINED
var currentName: String = ""
var currentBlock: Int? = null
Expand All @@ -56,21 +60,102 @@ class ReadoutController(val name: String, val speechEvent: RHMIEvent.ActionEvent
}

fun readout(lines: Iterable<String>) {
val data = RHMIModel.RaListModel.RHMIListConcrete(2)
data.addRow(arrayOf(lines.joinToString(".\n"), name))
Log.d(TAG, "Starting readout from $name: ${data[0][0]}")
speechList.value = data
speechEvent.triggerEvent()
// Log.d(TAG, "Starting readout from $name")
readoutCommands.readout(name, lines)
}

fun cancel() {
if (!isActive) {
return
}
Log.d(TAG, "Cancelling $name readout")
// Log.d(TAG, "Cancelling $name readout")
readoutCommands.cancel(name)
}
}

class ReadoutCommandsRHMI(val speechEvent: RHMIEvent.ActionEvent, val commandEvent: RHMIEvent.ActionEvent): ReadoutCommands {
companion object {
fun build(app: RHMIApplication): ReadoutCommandsRHMI {
val events = app.events.values.filterIsInstance<RHMIEvent.ActionEvent>().filter {
it.getAction()?.asLinkAction()?.actionType == "readout"
}
if (events.size != 2) {
throw IllegalArgumentException("UI Description is missing 2 readout events")
}
return ReadoutCommandsRHMI(events[0], events[1])
}
}

val speechList = speechEvent.getAction()?.asLinkAction()?.getLinkModel()?.asRaListModel()!!
val commandList = commandEvent.getAction()?.asLinkAction()?.getLinkModel()?.asRaListModel()!!

override fun readout(name: String, lines: Iterable<String>) {
val data = RHMIModel.RaListModel.RHMIListConcrete(2)
data.addRow(arrayOf(lines.joinToString(".\n"), name))
Log.d(TAG, "Starting rhmi readout from $name: ${data[0][0]}")
speechList.value = data
speechEvent.triggerEvent()
}

override fun cancel(name: String) {
Log.d(TAG, "Cancelling rhmi readout from $name")
val data = RHMIModel.RaListModel.RHMIListConcrete(2)
data.addRow(arrayOf("STR_READOUT_STOP", name))
commandList.value = data
commandEvent.triggerEvent()
}
}

class ReadoutCommandsSender(val context: Context): ReadoutCommands {
override fun readout(name: String, lines: Iterable<String>) {
val intent = Intent(ReadoutCommandsReceiver.INTENT_READOUT)
.setPackage(context.packageName)
.putExtra(ReadoutCommandsReceiver.EXTRA_COMMAND, ReadoutCommandsReceiver.EXTRA_COMMAND_LINES)
.putExtra(ReadoutCommandsReceiver.EXTRA_COMMAND_NAME, name)
.putExtra(ReadoutCommandsReceiver.EXTRA_COMMAND_LINES, lines.toList().toTypedArray())
context.sendBroadcast(intent)
}

override fun cancel(name: String) {
val intent = Intent(ReadoutCommandsReceiver.INTENT_READOUT)
.setPackage(context.packageName)
.putExtra(ReadoutCommandsReceiver.EXTRA_COMMAND, ReadoutCommandsReceiver.EXTRA_COMMAND_CANCEL)
.putExtra(ReadoutCommandsReceiver.EXTRA_COMMAND_NAME, name)
context.sendBroadcast(intent)
}
}

class ReadoutCommandsReceiver(val commands: ReadoutCommands): BroadcastReceiver() {
companion object {
const val INTENT_READOUT = "me.hufman.androidautoidrive.READOUT_COMMAND"
const val EXTRA_COMMAND = "READOUT"
const val EXTRA_COMMAND_NAME = "READOUT_NAME"
const val EXTRA_COMMAND_LINES = "READOUT_LINES"
const val EXTRA_COMMAND_CANCEL = "READOUT_CANCEL"
}
override fun onReceive(context: Context?, intent: Intent?) {
intent ?: return
val command = intent.getStringExtra(EXTRA_COMMAND)
val name = intent.getStringExtra(EXTRA_COMMAND_NAME) ?: return
val lines = intent.getStringArrayExtra(EXTRA_COMMAND_LINES)
if (command == EXTRA_COMMAND_LINES && lines != null) {
commands.readout(name, lines.toList())
}
if (command == EXTRA_COMMAND_CANCEL) {
commands.cancel(name)
}
}

fun register(context: Context, handler: Handler) {
ContextCompat.registerReceiver(context, this, IntentFilter(INTENT_READOUT), null, handler, ContextCompat.RECEIVER_NOT_EXPORTED)
}

fun unregister(context: Context) {
try {
context.unregisterReceiver(this)
} catch (e: IllegalArgumentException) {
// duplicate unregister
}
}

}
Original file line number Diff line number Diff line change
@@ -1,35 +1,54 @@
package me.hufman.androidautoidrive.carapp.notifications
package me.hufman.androidautoidrive.carapp.carinfo

import android.annotation.SuppressLint
import android.content.res.Resources
import android.content.res.Resources.NotFoundException
import android.os.Handler
import android.util.Log
import com.google.gson.Gson
import de.bmw.idrive.BMWRemoting
import de.bmw.idrive.BMWRemotingServer
import de.bmw.idrive.BaseBMWRemotingClient
import io.bimmergestalt.idriveconnectkit.CDS
import io.bimmergestalt.idriveconnectkit.IDriveConnection
import io.bimmergestalt.idriveconnectkit.RHMIUtils.rhmi_setResourceCached
import io.bimmergestalt.idriveconnectkit.android.CarAppResources
import io.bimmergestalt.idriveconnectkit.android.IDriveConnectionStatus
import io.bimmergestalt.idriveconnectkit.android.security.SecurityAccess
import io.bimmergestalt.idriveconnectkit.rhmi.*
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIApplication
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIApplicationEtch
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIApplicationIdempotent
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIApplicationSynchronized
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIComponent
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIEvent
import io.bimmergestalt.idriveconnectkit.rhmi.RHMIState
import io.bimmergestalt.idriveconnectkit.rhmi.deserialization.loadFromXML
import kotlinx.coroutines.android.asCoroutineDispatcher
import me.hufman.androidautoidrive.AppSettings
import me.hufman.androidautoidrive.BuildConfig
import me.hufman.androidautoidrive.CarInformation
import me.hufman.androidautoidrive.R
import me.hufman.androidautoidrive.carapp.*
import me.hufman.androidautoidrive.carapp.carinfo.CarDetailedInfo
import me.hufman.androidautoidrive.carapp.AMCategory
import me.hufman.androidautoidrive.carapp.FocusTriggerController
import me.hufman.androidautoidrive.carapp.L
import me.hufman.androidautoidrive.carapp.RHMIActionAbort
import me.hufman.androidautoidrive.carapp.RHMIApplicationSwappable
import me.hufman.androidautoidrive.carapp.ReadoutCommands
import me.hufman.androidautoidrive.carapp.ReadoutCommandsRHMI
import me.hufman.androidautoidrive.carapp.ReadoutController
import me.hufman.androidautoidrive.carapp.TTSState
import me.hufman.androidautoidrive.carapp.carinfo.views.CarDetailedView
import me.hufman.androidautoidrive.carapp.carinfo.views.CategoryView
import me.hufman.androidautoidrive.cds.*
import me.hufman.androidautoidrive.carapp.notifications.TAG
import me.hufman.androidautoidrive.cds.CDSConnectionEtch
import me.hufman.androidautoidrive.cds.CDSDataProvider
import me.hufman.androidautoidrive.cds.CDSEventHandler
import me.hufman.androidautoidrive.cds.CDSMetrics
import me.hufman.androidautoidrive.cds.flow
import me.hufman.androidautoidrive.cds.onPropertyChangedEvent
import me.hufman.androidautoidrive.cds.subscriptions
import me.hufman.androidautoidrive.utils.Utils

class ReadoutApp(val iDriveConnectionStatus: IDriveConnectionStatus, val securityAccess: SecurityAccess, val carAppAssets: CarAppResources, val unsignedCarAppAssets: CarAppResources, val handler: Handler, val resources: Resources, val appSettings: AppSettings) {
class CarInfoApp(val iDriveConnectionStatus: IDriveConnectionStatus, val securityAccess: SecurityAccess,
val carAppAssets: CarAppResources, val unsignedCarAppAssets: CarAppResources,
val handler: Handler, val resources: Resources, val appSettings: AppSettings) {
private val coroutineContext = handler.asCoroutineDispatcher()
val carConnection: BMWRemotingServer
var rhmiHandle: Int = -1
Expand All @@ -39,11 +58,11 @@ class ReadoutApp(val iDriveConnectionStatus: IDriveConnectionStatus, val securit
val focusTriggerController: FocusTriggerController
val infoState: CarDetailedView
val categoryState: CategoryView
val readoutController: ReadoutController
val readoutCommands: ReadoutCommands

init {
val cdsData = CDSDataProvider()
val listener = ReadoutAppListener(cdsData)
val listener = CarInfoAppListener(cdsData)
carConnection = IDriveConnection.getEtchConnection(iDriveConnectionStatus.host ?: "127.0.0.1", iDriveConnectionStatus.port ?: 8003, listener)
val readoutCert = carAppAssets.getAppCertificate(iDriveConnectionStatus.brand ?: "")?.readBytes() as ByteArray
val sas_challenge = carConnection.sas_certificate(readoutCert)
Expand All @@ -62,7 +81,7 @@ class ReadoutApp(val iDriveConnectionStatus: IDriveConnectionStatus, val securit
recreateRhmiApp()
}

this.readoutController = ReadoutController.build(carApp, "NotificationReadout")
this.readoutCommands = ReadoutCommandsRHMI.build(carApp)

val carInfo = CarInformation().also {
cdsData.flow.defaultIntervalLimit = 100
Expand All @@ -76,28 +95,20 @@ class ReadoutApp(val iDriveConnectionStatus: IDriveConnectionStatus, val securit
initWidgets()
}

// register for readout updates
// register for car info updates
cdsData.setConnection(CDSConnectionEtch(carConnection))
cdsData.subscriptions[CDS.HMI.TTS] = {
val state = try {
Gson().fromJson(it["TTSState"], TTSState::class.java)
} catch (e: Exception) { null }
if (state != null) {
readoutController.onTTSEvent(state)
}
}

// set up the AM icon in the "Addressbook"/Communications section
amHandle = carConnection.am_create("0", "\u0000\u0000\u0000\u0000\u0000\u0002\u0000\u0000".toByteArray())
carConnection.am_addAppEventHandler(amHandle, "me.hufman.androidautoidrive.notification.readout")
carConnection.am_addAppEventHandler(amHandle, "me.hufman.androidautoidrive.notification.readout") // the old name, which might be in people's bookmarks
createAmApp()
}

/** creates the app in the car */
fun createRhmiApp(): RHMIApplication {
// create the app in the car
rhmiHandle = carConnection.rhmi_create(null, BMWRemoting.RHMIMetaData("me.hufman.androidautoidrive.notification.readout", BMWRemoting.VersionInfo(0, 1, 0),
"me.hufman.androidautoidrive.notification.readout", "me.hufman"))
"me.hufman.androidautoidrive.notification.readout", "me.hufman"))
carConnection.rhmi_setResourceCached(rhmiHandle, BMWRemoting.RHMIResourceType.DESCRIPTION, carAppAssets.getUiDescription())
if (BuildConfig.SEND_UNSIGNED_RESOURCES) {
try {
Expand Down Expand Up @@ -142,16 +153,16 @@ class ReadoutApp(val iDriveConnectionStatus: IDriveConnectionStatus, val securit
} else {
Utils.convertPngToGrayscale(resources.openRawResource(R.drawable.ic_carinfo_common).readBytes())
}
} catch (e: NotFoundException) { "" }
} catch (e: Resources.NotFoundException) { "" }

val amInfo = mutableMapOf<Int, Any>(
0 to 145, // basecore version
1 to name, // app name
2 to carAppImage,
3 to AMCategory.VEHICLE_INFORMATION.value, // section
4 to true,
5 to 800, // weight
8 to infoState.state.id // mainstateId
0 to 145, // basecore version
1 to name, // app name
2 to carAppImage,
3 to AMCategory.VEHICLE_INFORMATION.value, // section
4 to true,
5 to 800, // weight
8 to infoState.state.id // mainstateId
)
// language translations, dunno which one is which
for (languageCode in 101..123) {
Expand All @@ -163,7 +174,7 @@ class ReadoutApp(val iDriveConnectionStatus: IDriveConnectionStatus, val securit
}
}

inner class ReadoutAppListener(val cdsEventHandler: CDSEventHandler): BaseBMWRemotingClient() {
inner class CarInfoAppListener(val cdsEventHandler: CDSEventHandler): BaseBMWRemotingClient() {
var server: BMWRemotingServer? = null
var app: RHMIApplication? = null

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package me.hufman.androidautoidrive.carapp.carinfo

import android.util.Log
import io.bimmergestalt.idriveconnectkit.android.CarAppAssetResources
import me.hufman.androidautoidrive.AppSettingsViewer
import me.hufman.androidautoidrive.MainService
import me.hufman.androidautoidrive.carapp.CarAppService
import me.hufman.androidautoidrive.carapp.ReadoutCommandsReceiver

class CarInfoAppService: CarAppService() {

var carApp: CarInfoApp? = null
var readoutController: ReadoutCommandsReceiver? = null

override fun shouldStartApp(): Boolean = true

override fun onCarStart() {
Log.i(MainService.TAG, "Starting car info app")

val handler = handler!!
val carApp = CarInfoApp(iDriveConnectionStatus, securityAccess,
CarAppAssetResources(applicationContext, "news"),
CarAppAssetResources(applicationContext, "carinfo_unsigned"),
handler, applicationContext.resources, AppSettingsViewer()
)
this.carApp = carApp
readoutController = ReadoutCommandsReceiver(carApp.readoutCommands)
readoutController?.register(this, handler)
}

override fun onCarStop() {
carApp?.disconnect()
carApp = null
readoutController?.unregister(this)
}
}
Loading

0 comments on commit ea49ba1

Please sign in to comment.