Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -101,4 +103,4 @@
},
"owner": "ghenry22"
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1429,6 +1436,55 @@ class MusicService : HeadlessJsMediaService() {
}
return super.onSetRating(session, controller, rating)
}

override fun onGetLibraryRoot(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
params: LibraryParams?
): ListenableFuture<LibraryResult<MediaItem>> {
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<LibraryResult<ImmutableList<MediaItem>>> {
val future = SettableFuture.create<LibraryResult<ImmutableList<MediaItem>>>()

if (parentId == "root") {
val rootMenu = mutableListOf<MediaItem>()
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 {
Expand Down
26 changes: 26 additions & 0 deletions modules/substreamer-auto/android/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
2 changes: 2 additions & 0 deletions modules/substreamer-auto/android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -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<String, SettableFuture<LibraryResult<ImmutableList<MediaItem>>>>()

fun requestDataFromTS(parentId: String, future: SettableFuture<LibraryResult<ImmutableList<MediaItem>>>) {
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<MediaItem>()
// 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)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
9 changes: 9 additions & 0 deletions modules/substreamer-auto/expo-module.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"platforms": ["apple", "android", "web"],
"apple": {
"modules": ["SubstreamerAutoModule"]
},
"android": {
"modules": ["com.ghenry22.substream2.auto.SubstreamerAutoModule"]
}
}
5 changes: 5 additions & 0 deletions modules/substreamer-auto/index.ts
Original file line number Diff line number Diff line change
@@ -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';
23 changes: 23 additions & 0 deletions modules/substreamer-auto/ios/SubstreamerAuto.podspec
Original file line number Diff line number Diff line change
@@ -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
48 changes: 48 additions & 0 deletions modules/substreamer-auto/ios/SubstreamerAutoModule.swift
Original file line number Diff line number Diff line change
@@ -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")
}
}
}
38 changes: 38 additions & 0 deletions modules/substreamer-auto/ios/SubstreamerAutoView.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
19 changes: 19 additions & 0 deletions modules/substreamer-auto/src/SubstreamerAuto.types.ts
Original file line number Diff line number Diff line change
@@ -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<ViewStyle>;
};
12 changes: 12 additions & 0 deletions modules/substreamer-auto/src/SubstreamerAutoModule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NativeModule, requireNativeModule } from 'expo';

import { SubstreamerAutoModuleEvents } from './SubstreamerAuto.types';

declare class SubstreamerAutoModule extends NativeModule<SubstreamerAutoModuleEvents> {
PI: number;
hello(): string;
setValueAsync(value: string): Promise<void>;
}

// This call loads the native module object from the JSI.
export default requireNativeModule<SubstreamerAutoModule>('SubstreamerAuto');
19 changes: 19 additions & 0 deletions modules/substreamer-auto/src/SubstreamerAutoModule.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { registerWebModule, NativeModule } from 'expo';

import { ChangeEventPayload } from './SubstreamerAuto.types';

type SubstreamerAutoModuleEvents = {
onChange: (params: ChangeEventPayload) => void;
}

class SubstreamerAutoModule extends NativeModule<SubstreamerAutoModuleEvents> {
PI = Math.PI;
async setValueAsync(value: string): Promise<void> {
this.emit('onChange', { value });
}
hello() {
return 'Hello world! 👋';
}
};

export default registerWebModule(SubstreamerAutoModule, 'SubstreamerAutoModule');
11 changes: 11 additions & 0 deletions modules/substreamer-auto/src/SubstreamerAutoView.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { requireNativeView } from 'expo';
import * as React from 'react';

import { SubstreamerAutoViewProps } from './SubstreamerAuto.types';

const NativeView: React.ComponentType<SubstreamerAutoViewProps> =
requireNativeView('SubstreamerAuto');

export default function SubstreamerAutoView(props: SubstreamerAutoViewProps) {
return <NativeView {...props} />;
}
Loading