mirror of
https://github.com/Michatec/Radio.git
synced 2026-05-31 04:22:40 +02:00
feat(cast): implement Google Cast support and expanded controller activity
This commit is contained in:
@@ -31,11 +31,6 @@
|
|||||||
android:usesCleartextTraffic="true"
|
android:usesCleartextTraffic="true"
|
||||||
tools:targetApi="33">
|
tools:targetApi="33">
|
||||||
|
|
||||||
<!-- GOOGLE CAST SUPPORT -->
|
|
||||||
<meta-data
|
|
||||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
|
||||||
android:value="com.michatec.radio.CastOptionsProvider" />
|
|
||||||
|
|
||||||
<!-- ANDROID AUTO SUPPORT -->
|
<!-- ANDROID AUTO SUPPORT -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.gms.car.application"
|
android:name="com.google.android.gms.car.application"
|
||||||
@@ -44,6 +39,17 @@
|
|||||||
android:name="com.google.android.gms.car.notification.SmallIcon"
|
android:name="com.google.android.gms.car.notification.SmallIcon"
|
||||||
android:resource="@mipmap/ic_launcher" />
|
android:resource="@mipmap/ic_launcher" />
|
||||||
|
|
||||||
|
<!-- GOOGLE CAST SUPPORT -->
|
||||||
|
<receiver android:name="androidx.mediarouter.media.MediaTransferReceiver" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||||
|
android:value="com.michatec.radio.CastOptionsProvider" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ExpandedControllerActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:launchMode="singleTask" />
|
||||||
|
|
||||||
<!-- Main activity for radio station playback on phone and TV -->
|
<!-- Main activity for radio station playback on phone and TV -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ class CastOptionsProvider : OptionsProvider {
|
|||||||
.build()
|
.build()
|
||||||
val mediaOptions = CastMediaOptions.Builder()
|
val mediaOptions = CastMediaOptions.Builder()
|
||||||
.setNotificationOptions(notificationOptions)
|
.setNotificationOptions(notificationOptions)
|
||||||
|
.setExpandedControllerActivityClassName(ExpandedControllerActivity::class.java.name)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
return CastOptions.Builder()
|
return CastOptions.Builder()
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package com.michatec.radio
|
||||||
|
|
||||||
|
import android.view.Menu
|
||||||
|
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||||
|
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
|
||||||
|
|
||||||
|
class ExpandedControllerActivity : ExpandedControllerActivity() {
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
|
super.onCreateOptionsMenu(menu)
|
||||||
|
menuInflater.inflate(R.menu.expanded_controller, menu)
|
||||||
|
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.media_route_menu_item)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -402,6 +402,7 @@ class PlayerFragment : Fragment(),
|
|||||||
|
|
||||||
/* Releases MediaController */
|
/* Releases MediaController */
|
||||||
private fun releaseController() {
|
private fun releaseController() {
|
||||||
|
controller?.removeListener(playerListener)
|
||||||
MediaController.releaseFuture(controllerFuture)
|
MediaController.releaseFuture(controllerFuture)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import android.os.CountDownTimer
|
|||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
import androidx.localbroadcastmanager.content.LocalBroadcastManager
|
||||||
import androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT
|
import androidx.media.MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT
|
||||||
|
import androidx.media3.cast.CastPlayer
|
||||||
import androidx.media3.common.*
|
import androidx.media3.common.*
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import androidx.media3.datasource.HttpDataSource
|
import androidx.media3.datasource.HttpDataSource
|
||||||
@@ -45,6 +46,8 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
|
|||||||
|
|
||||||
/* Main class variables */
|
/* Main class variables */
|
||||||
private lateinit var player: Player
|
private lateinit var player: Player
|
||||||
|
private lateinit var exoPlayer: ExoPlayer
|
||||||
|
private lateinit var castPlayer: CastPlayer
|
||||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||||
private lateinit var sleepTimer: CountDownTimer
|
private lateinit var sleepTimer: CountDownTimer
|
||||||
var sleepTimerTimeRemaining: Long = 0L
|
var sleepTimerTimeRemaining: Long = 0L
|
||||||
@@ -93,6 +96,8 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
|
|||||||
PreferencesHelper.saveIsPlaying(false)
|
PreferencesHelper.saveIsPlaying(false)
|
||||||
player.removeListener(playerListener)
|
player.removeListener(playerListener)
|
||||||
player.release()
|
player.release()
|
||||||
|
exoPlayer.release()
|
||||||
|
castPlayer.release()
|
||||||
mediaLibrarySession.release()
|
mediaLibrarySession.release()
|
||||||
// unregister preference change listener
|
// unregister preference change listener
|
||||||
PreferencesHelper.unregisterPreferenceChangeListener(this)
|
PreferencesHelper.unregisterPreferenceChangeListener(this)
|
||||||
@@ -134,7 +139,7 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val exoPlayer: ExoPlayer = ExoPlayer.Builder(this, renderersFactory).apply {
|
exoPlayer = ExoPlayer.Builder(this, renderersFactory).apply {
|
||||||
setAudioAttributes(audioAttributes, true)
|
setAudioAttributes(audioAttributes, true)
|
||||||
setHandleAudioBecomingNoisy(true)
|
setHandleAudioBecomingNoisy(true)
|
||||||
setLoadControl(createDefaultLoadControl(bufferSizeMultiplier))
|
setLoadControl(createDefaultLoadControl(bufferSizeMultiplier))
|
||||||
@@ -147,7 +152,10 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
|
|||||||
exoPlayer.addAnalyticsListener(analyticsListener)
|
exoPlayer.addAnalyticsListener(analyticsListener)
|
||||||
exoPlayer.addListener(playerListener)
|
exoPlayer.addListener(playerListener)
|
||||||
|
|
||||||
// manually add seek to next and seek to previous since headphones issue them and they are translated to next and previous station
|
// Initialize CastPlayer
|
||||||
|
castPlayer = CastPlayer.Builder(this).setLocalPlayer(exoPlayer).build()
|
||||||
|
|
||||||
|
// manually add seek to next and seek to previous since headphones issue them, and they are translated to next and previous station
|
||||||
player = object : ForwardingPlayer(exoPlayer) {
|
player = object : ForwardingPlayer(exoPlayer) {
|
||||||
override fun getAvailableCommands(): Player.Commands {
|
override fun getAvailableCommands(): Player.Commands {
|
||||||
return super.getAvailableCommands().buildUpon().add(COMMAND_SEEK_TO_NEXT)
|
return super.getAvailableCommands().buildUpon().add(COMMAND_SEEK_TO_NEXT)
|
||||||
@@ -543,7 +551,7 @@ class PlayerService : MediaLibraryService(), SharedPreferences.OnSharedPreferenc
|
|||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Custom LoadErrorHandlingPolicy that network drop outs
|
* Custom LoadErrorHandlingPolicy that network drop-outs
|
||||||
*/
|
*/
|
||||||
private val loadErrorHandlingPolicy: DefaultLoadErrorHandlingPolicy = object: DefaultLoadErrorHandlingPolicy() {
|
private val loadErrorHandlingPolicy: DefaultLoadErrorHandlingPolicy = object: DefaultLoadErrorHandlingPolicy() {
|
||||||
override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo): Long {
|
override fun getRetryDelayMsFor(loadErrorInfo: LoadErrorHandlingPolicy.LoadErrorInfo): Long {
|
||||||
|
|||||||
@@ -638,15 +638,13 @@ object CollectionHelper {
|
|||||||
}.build()
|
}.build()
|
||||||
// build MediaMetadata
|
// build MediaMetadata
|
||||||
val mediaMetadata = MediaMetadata.Builder().apply {
|
val mediaMetadata = MediaMetadata.Builder().apply {
|
||||||
|
setTitle(station.name)
|
||||||
setArtist(station.name)
|
setArtist(station.name)
|
||||||
//setTitle(station.name)
|
|
||||||
|
|
||||||
// Set artwork URI for casting (TV needs a public URL)
|
// Set artwork URI for casting (remote devices need a public URL)
|
||||||
val artworkUrl = station.remoteImageLocation.ifEmpty {
|
if (station.remoteImageLocation.isNotEmpty()) {
|
||||||
// Placeholder PNG image for stations without remote image
|
setArtworkUri(station.remoteImageLocation.toUri())
|
||||||
"https://raw.githubusercontent.com/google/material-design-icons/master/png/av/radio/materialicons/48dp/2x/baseline_radio_black_48dp.png"
|
|
||||||
}
|
}
|
||||||
setArtworkUri(artworkUrl.toUri())
|
|
||||||
|
|
||||||
/* check for "file://" prevents a crash when an old backup was restored */
|
/* check for "file://" prevents a crash when an old backup was restored */
|
||||||
if (station.image.isNotEmpty() && station.image.startsWith("file://")) {
|
if (station.image.isNotEmpty() && station.image.startsWith("file://")) {
|
||||||
@@ -656,6 +654,7 @@ object CollectionHelper {
|
|||||||
}
|
}
|
||||||
setIsBrowsable(false)
|
setIsBrowsable(false)
|
||||||
setIsPlayable(true)
|
setIsPlayable(true)
|
||||||
|
setMediaType(MediaMetadata.MEDIA_TYPE_RADIO_STATION)
|
||||||
}.build()
|
}.build()
|
||||||
// build MediaItem and return it
|
// build MediaItem and return it
|
||||||
return MediaItem.Builder().apply {
|
return MediaItem.Builder().apply {
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/media_route_menu_item"
|
||||||
|
android:title="@string/media_route_menu_title"
|
||||||
|
app:actionProviderClass="androidx.mediarouter.app.MediaRouteActionProvider"
|
||||||
|
app:showAsAction="always" />
|
||||||
|
|
||||||
|
</menu>
|
||||||
@@ -27,4 +27,10 @@
|
|||||||
<!-- Don't show light status bar -->
|
<!-- Don't show light status bar -->
|
||||||
<item name="android:statusBarColor">@android:color/transparent</item>
|
<item name="android:statusBarColor">@android:color/transparent</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="CustomCastExpandedControllerStyle" parent="CastExpandedController">
|
||||||
|
<item name="android:windowBackground">#FFFFFF</item>
|
||||||
|
<item name="castBackground">#FFFFFF</item>
|
||||||
|
<item name="castButtonColor">#FF495D92</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -157,4 +157,5 @@
|
|||||||
|
|
||||||
<!-- Extras -->
|
<!-- Extras -->
|
||||||
<string name="loading">Loading...</string>
|
<string name="loading">Loading...</string>
|
||||||
|
<string name="media_route_menu_title">Cast</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -74,4 +74,10 @@
|
|||||||
<item name="cornerSizeBottomRight">16dp</item>
|
<item name="cornerSizeBottomRight">16dp</item>
|
||||||
<item name="cornerFamilyBottomRight">rounded</item>
|
<item name="cornerFamilyBottomRight">rounded</item>
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
<style name="CustomCastExpandedControllerStyle" parent="CastExpandedController">
|
||||||
|
<item name="android:windowBackground">#FFFFFF</item>
|
||||||
|
<item name="castBackground">#FFFFFF</item>
|
||||||
|
<item name="castButtonColor">#FF495D92</item>
|
||||||
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
+1
-1
@@ -7,5 +7,5 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tasks.register('clean', Delete) {
|
tasks.register('clean', Delete) {
|
||||||
delete rootProject.buildDir()
|
delete layout.buildDirectory
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user