diff --git a/app.json b/app.json index 1ab564a2..3f18d04a 100644 --- a/app.json +++ b/app.json @@ -63,6 +63,8 @@ "./plugins/large-heap", "./plugins/fresco-race-suppression", "./plugins/set-gradle-version", + "./plugins/with-android-auto.js", + "./plugins/with-automotive-xml.js", "expo-sqlite", "./modules/expo-ssl-trust/plugin", "./modules/expo-backup-exclusions/plugin", @@ -101,4 +103,4 @@ }, "owner": "ghenry22" } -} +} \ No newline at end of file diff --git a/modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt b/modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt index c15d94b5..b8535939 100644 --- a/modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt +++ b/modules/react-native-track-player/android/src/main/java/com/doublesymmetry/trackplayer/service/MusicService.kt @@ -52,6 +52,13 @@ import kotlinx.coroutines.flow.flow import timber.log.Timber import java.util.concurrent.TimeUnit import kotlin.system.exitProcess +import androidx.media3.common.MediaMetadata +import androidx.media3.session.LibraryResult +import androidx.media3.session.MediaLibraryService.LibraryParams +import androidx.media3.session.MediaLibraryService.MediaLibrarySession +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.Futures +import com.google.common.util.concurrent.SettableFuture @OptIn(UnstableApi::class) @MainThread @@ -1429,6 +1436,55 @@ class MusicService : HeadlessJsMediaService() { } return super.onSetRating(session, controller, rating) } + + override fun onGetLibraryRoot( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + params: LibraryParams? + ): ListenableFuture> { + val rootItem = MediaItem.Builder() + .setMediaId("root") + .setMediaMetadata( + MediaMetadata.Builder() + .setTitle("Substreamer") + .setIsBrowsable(true) + .setIsPlayable(false) + .build() + ) + .build() + return Futures.immediateFuture(LibraryResult.ofItem(rootItem, null)) + } + + override fun onGetChildren( + session: MediaLibrarySession, + browser: MediaSession.ControllerInfo, + parentId: String, + page: Int, + pageSize: Int, + params: LibraryParams? + ): ListenableFuture>> { + val future = SettableFuture.create>>() + + if (parentId == "root") { + val rootMenu = mutableListOf() + future.set(LibraryResult.ofItemList(rootMenu, params)) + } else { + try { + val moduleClass = Class.forName("com.ghenry22.substream2.auto.SubstreamerAutoModule") + val companionInstance = moduleClass.getField("Companion").get(null) + val method = companionInstance.javaClass.getMethod( + "requestDataFromTS", + String::class.java, + SettableFuture::class.java + ) + method.invoke(companionInstance, parentId, future) + } catch (e: Exception) { + e.printStackTrace() + future.setException(e) + } + } + return future + } } private fun getPendingIntentFlags(): Int { diff --git a/modules/substreamer-auto/android/build.gradle b/modules/substreamer-auto/android/build.gradle new file mode 100644 index 00000000..62eb0e75 --- /dev/null +++ b/modules/substreamer-auto/android/build.gradle @@ -0,0 +1,26 @@ +plugins { + id 'com.android.library' + id 'expo-module-gradle-plugin' +} + +group = 'com.ghenry22.substream2.auto' +version = '0.7.10' + +android { + namespace "com.ghenry22.substream2.auto" + defaultConfig { + versionCode 1 + versionName "0.7.10" + } + lintOptions { + abortOnError false + } +} + +dependencies { + implementation project(':expo-modules-core') + + // --- ADD THESE TWO LINES FOR ANDROID AUTO --- + implementation 'androidx.media3:media3-session:1.1.1' + implementation 'com.google.guava:guava:31.1-android' +} \ No newline at end of file diff --git a/modules/substreamer-auto/android/src/main/AndroidManifest.xml b/modules/substreamer-auto/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..bdae66c8 --- /dev/null +++ b/modules/substreamer-auto/android/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + diff --git a/modules/substreamer-auto/android/src/main/java/com/ghenry22/substream2/auto/SubstreamerAutoModule.kt b/modules/substreamer-auto/android/src/main/java/com/ghenry22/substream2/auto/SubstreamerAutoModule.kt new file mode 100644 index 00000000..1d1cba49 --- /dev/null +++ b/modules/substreamer-auto/android/src/main/java/com/ghenry22/substream2/auto/SubstreamerAutoModule.kt @@ -0,0 +1,47 @@ +package com.ghenry22.substream2.auto + +import androidx.media3.common.MediaItem +import androidx.media3.session.LibraryResult +import com.google.common.collect.ImmutableList +import com.google.common.util.concurrent.SettableFuture +import expo.modules.kotlin.modules.Module +import expo.modules.kotlin.modules.ModuleDefinition + +class SubstreamerAutoModule : Module() { + + companion object { + var moduleInstance: SubstreamerAutoModule? = null + // We use SettableFuture to hold the connection open while TS fetches data + val pendingRequests = mutableMapOf>>>() + + fun requestDataFromTS(parentId: String, future: SettableFuture>>) { + pendingRequests[parentId] = future + moduleInstance?.sendEvent("onCarRequestedData", mapOf("parentId" to parentId)) + } + } + + override fun definition() = ModuleDefinition { + Name("SubstreamerAuto") + Events("onCarRequestedData") + + OnCreate { + moduleInstance = this@SubstreamerAutoModule + } + + AsyncFunction("provideChildrenData") { parentId: String, jsonData: String -> + val future = pendingRequests.remove(parentId) + + if (future != null) { + try { + val items = mutableListOf() + // TODO: Parse the 'jsonData' string into Media3 MediaItems here + + // Fulfill the future to send data to the car + future.set(LibraryResult.ofItemList(items, null)) + } catch (e: Exception) { + future.setException(e) + } + } + } + } +} \ No newline at end of file diff --git a/modules/substreamer-auto/android/src/main/java/com/ghenry22/substream2/auto/SubstreamerAutoView.kt b/modules/substreamer-auto/android/src/main/java/com/ghenry22/substream2/auto/SubstreamerAutoView.kt new file mode 100644 index 00000000..3037ac27 --- /dev/null +++ b/modules/substreamer-auto/android/src/main/java/com/ghenry22/substream2/auto/SubstreamerAutoView.kt @@ -0,0 +1,30 @@ +package com.ghenry22.substream2.auto + +import android.content.Context +import android.webkit.WebView +import android.webkit.WebViewClient +import expo.modules.kotlin.AppContext +import expo.modules.kotlin.viewevent.EventDispatcher +import expo.modules.kotlin.views.ExpoView + +class SubstreamerAutoView(context: Context, appContext: AppContext) : ExpoView(context, appContext) { + // Creates and initializes an event dispatcher for the `onLoad` event. + // The name of the event is inferred from the value and needs to match the event name defined in the module. + private val onLoad by EventDispatcher() + + // Defines a WebView that will be used as the root subview. + internal val webView = WebView(context).apply { + layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + webViewClient = object : WebViewClient() { + override fun onPageFinished(view: WebView, url: String) { + // Sends an event to JavaScript. Triggers a callback defined on the view component in JavaScript. + onLoad(mapOf("url" to url)) + } + } + } + + init { + // Adds the WebView to the view hierarchy. + addView(webView) + } +} diff --git a/modules/substreamer-auto/expo-module.config.json b/modules/substreamer-auto/expo-module.config.json new file mode 100644 index 00000000..44871b8a --- /dev/null +++ b/modules/substreamer-auto/expo-module.config.json @@ -0,0 +1,9 @@ +{ + "platforms": ["apple", "android", "web"], + "apple": { + "modules": ["SubstreamerAutoModule"] + }, + "android": { + "modules": ["com.ghenry22.substream2.auto.SubstreamerAutoModule"] + } +} diff --git a/modules/substreamer-auto/index.ts b/modules/substreamer-auto/index.ts new file mode 100644 index 00000000..97f00e06 --- /dev/null +++ b/modules/substreamer-auto/index.ts @@ -0,0 +1,5 @@ +// Reexport the native module. On web, it will be resolved to SubstreamerAutoModule.web.ts +// and on native platforms to SubstreamerAutoModule.ts +export { default } from './src/SubstreamerAutoModule'; +export { default as SubstreamerAutoView } from './src/SubstreamerAutoView'; +export * from './src/SubstreamerAuto.types'; diff --git a/modules/substreamer-auto/ios/SubstreamerAuto.podspec b/modules/substreamer-auto/ios/SubstreamerAuto.podspec new file mode 100644 index 00000000..e9029d3f --- /dev/null +++ b/modules/substreamer-auto/ios/SubstreamerAuto.podspec @@ -0,0 +1,23 @@ +Pod::Spec.new do |s| + s.name = 'SubstreamerAuto' + s.version = '1.0.0' + s.summary = 'A sample project summary' + s.description = 'A sample project description' + s.author = '' + s.homepage = 'https://docs.expo.dev/modules/' + s.platforms = { + :ios => '15.1', + :tvos => '15.1' + } + s.source = { git: '' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + + # Swift/Objective-C compatibility + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + } + + s.source_files = "**/*.{h,m,mm,swift,hpp,cpp}" +end diff --git a/modules/substreamer-auto/ios/SubstreamerAutoModule.swift b/modules/substreamer-auto/ios/SubstreamerAutoModule.swift new file mode 100644 index 00000000..317cc2f6 --- /dev/null +++ b/modules/substreamer-auto/ios/SubstreamerAutoModule.swift @@ -0,0 +1,48 @@ +import ExpoModulesCore + +public class SubstreamerAutoModule: Module { + // Each module class must implement the definition function. The definition consists of components + // that describes the module's functionality and behavior. + // See https://docs.expo.dev/modules/module-api for more details about available components. + public func definition() -> ModuleDefinition { + // Sets the name of the module that JavaScript code will use to refer to the module. Takes a string as an argument. + // Can be inferred from module's class name, but it's recommended to set it explicitly for clarity. + // The module will be accessible from `requireNativeModule('SubstreamerAuto')` in JavaScript. + Name("SubstreamerAuto") + + // Defines constant property on the module. + Constant("PI") { + Double.pi + } + + // Defines event names that the module can send to JavaScript. + Events("onChange") + + // Defines a JavaScript synchronous function that runs the native code on the JavaScript thread. + Function("hello") { + return "Hello world! 👋" + } + + // Defines a JavaScript function that always returns a Promise and whose native code + // is by default dispatched on the different thread than the JavaScript runtime runs on. + AsyncFunction("setValueAsync") { (value: String) in + // Send an event to JavaScript. + self.sendEvent("onChange", [ + "value": value + ]) + } + + // Enables the module to be used as a native view. Definition components that are accepted as part of the + // view definition: Prop, Events. + View(SubstreamerAutoView.self) { + // Defines a setter for the `url` prop. + Prop("url") { (view: SubstreamerAutoView, url: URL) in + if view.webView.url != url { + view.webView.load(URLRequest(url: url)) + } + } + + Events("onLoad") + } + } +} diff --git a/modules/substreamer-auto/ios/SubstreamerAutoView.swift b/modules/substreamer-auto/ios/SubstreamerAutoView.swift new file mode 100644 index 00000000..efff4ea7 --- /dev/null +++ b/modules/substreamer-auto/ios/SubstreamerAutoView.swift @@ -0,0 +1,38 @@ +import ExpoModulesCore +import WebKit + +// This view will be used as a native component. Make sure to inherit from `ExpoView` +// to apply the proper styling (e.g. border radius and shadows). +class SubstreamerAutoView: ExpoView { + let webView = WKWebView() + let onLoad = EventDispatcher() + var delegate: WebViewDelegate? + + required init(appContext: AppContext? = nil) { + super.init(appContext: appContext) + clipsToBounds = true + delegate = WebViewDelegate { url in + self.onLoad(["url": url]) + } + webView.navigationDelegate = delegate + addSubview(webView) + } + + override func layoutSubviews() { + webView.frame = bounds + } +} + +class WebViewDelegate: NSObject, WKNavigationDelegate { + let onUrlChange: (String) -> Void + + init(onUrlChange: @escaping (String) -> Void) { + self.onUrlChange = onUrlChange + } + + func webView(_ webView: WKWebView, didFinish navigation: WKNavigation) { + if let url = webView.url { + onUrlChange(url.absoluteString) + } + } +} diff --git a/modules/substreamer-auto/src/SubstreamerAuto.types.ts b/modules/substreamer-auto/src/SubstreamerAuto.types.ts new file mode 100644 index 00000000..991b73c8 --- /dev/null +++ b/modules/substreamer-auto/src/SubstreamerAuto.types.ts @@ -0,0 +1,19 @@ +import type { StyleProp, ViewStyle } from 'react-native'; + +export type OnLoadEventPayload = { + url: string; +}; + +export type SubstreamerAutoModuleEvents = { + onChange: (params: ChangeEventPayload) => void; +}; + +export type ChangeEventPayload = { + value: string; +}; + +export type SubstreamerAutoViewProps = { + url: string; + onLoad: (event: { nativeEvent: OnLoadEventPayload }) => void; + style?: StyleProp; +}; diff --git a/modules/substreamer-auto/src/SubstreamerAutoModule.ts b/modules/substreamer-auto/src/SubstreamerAutoModule.ts new file mode 100644 index 00000000..58fb50f7 --- /dev/null +++ b/modules/substreamer-auto/src/SubstreamerAutoModule.ts @@ -0,0 +1,12 @@ +import { NativeModule, requireNativeModule } from 'expo'; + +import { SubstreamerAutoModuleEvents } from './SubstreamerAuto.types'; + +declare class SubstreamerAutoModule extends NativeModule { + PI: number; + hello(): string; + setValueAsync(value: string): Promise; +} + +// This call loads the native module object from the JSI. +export default requireNativeModule('SubstreamerAuto'); diff --git a/modules/substreamer-auto/src/SubstreamerAutoModule.web.ts b/modules/substreamer-auto/src/SubstreamerAutoModule.web.ts new file mode 100644 index 00000000..91137634 --- /dev/null +++ b/modules/substreamer-auto/src/SubstreamerAutoModule.web.ts @@ -0,0 +1,19 @@ +import { registerWebModule, NativeModule } from 'expo'; + +import { ChangeEventPayload } from './SubstreamerAuto.types'; + +type SubstreamerAutoModuleEvents = { + onChange: (params: ChangeEventPayload) => void; +} + +class SubstreamerAutoModule extends NativeModule { + PI = Math.PI; + async setValueAsync(value: string): Promise { + this.emit('onChange', { value }); + } + hello() { + return 'Hello world! 👋'; + } +}; + +export default registerWebModule(SubstreamerAutoModule, 'SubstreamerAutoModule'); diff --git a/modules/substreamer-auto/src/SubstreamerAutoView.tsx b/modules/substreamer-auto/src/SubstreamerAutoView.tsx new file mode 100644 index 00000000..1f3df934 --- /dev/null +++ b/modules/substreamer-auto/src/SubstreamerAutoView.tsx @@ -0,0 +1,11 @@ +import { requireNativeView } from 'expo'; +import * as React from 'react'; + +import { SubstreamerAutoViewProps } from './SubstreamerAuto.types'; + +const NativeView: React.ComponentType = + requireNativeView('SubstreamerAuto'); + +export default function SubstreamerAutoView(props: SubstreamerAutoViewProps) { + return ; +} diff --git a/modules/substreamer-auto/src/SubstreamerAutoView.web.tsx b/modules/substreamer-auto/src/SubstreamerAutoView.web.tsx new file mode 100644 index 00000000..e9ce7182 --- /dev/null +++ b/modules/substreamer-auto/src/SubstreamerAutoView.web.tsx @@ -0,0 +1,15 @@ +import * as React from 'react'; + +import { SubstreamerAutoViewProps } from './SubstreamerAuto.types'; + +export default function SubstreamerAutoView(props: SubstreamerAutoViewProps) { + return ( +
+