diff --git a/README.md b/README.md
index 3aa9219..030f7b1 100644
--- a/README.md
+++ b/README.md
@@ -41,7 +41,6 @@ You could easily use your own streams in this app, the data set is located in `b
* Playback resumption seems iffy, need to take a better look. See https://android-developers.googleblog.com/2020/08/playing-nicely-with-media-controls.html and the implementation of `onGetLibraryRoot()` and `onGetChildren` in `StreamService`
* CastPlayer implementation in Media3 does not implement the required API to show live song updates unfortunately. Need to revisit in the future
* Landscape UI not implemented
-* Search for Google Assistant/Android Auto not implemented, see: https://developer.android.com/training/cars/media#support_voice
Want to help? Open a PR! Be sure to add Detekt via:
diff --git a/app/build.gradle b/app/build.gradle
index 57f8f0a..0a086be 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -14,8 +14,8 @@ android {
minSdkVersion 26
//noinspection OldTargetApi
targetSdkVersion 33
- versionCode 190
- versionName "2.1.0"
+ versionCode 192
+ versionName "2.2.1"
}
buildTypes {
release {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 799a4c3..f1aee06 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -45,12 +45,18 @@
+
+
+
+
+
diff --git a/app/src/main/java/com/nielsmasdorp/nederadio/data/stream/AndroidStreamManager.kt b/app/src/main/java/com/nielsmasdorp/nederadio/data/stream/AndroidStreamManager.kt
index a7b3aa3..0527200 100644
--- a/app/src/main/java/com/nielsmasdorp/nederadio/data/stream/AndroidStreamManager.kt
+++ b/app/src/main/java/com/nielsmasdorp/nederadio/data/stream/AndroidStreamManager.kt
@@ -81,10 +81,8 @@ class AndroidStreamManager(
if (currentStreamIndex != -1) {
val currentStream = streams[currentStreamIndex]
val currentStreamInUse = controller.currentMediaItem
- if (controller.mediaItemCount == 0) {
+ if (currentStreamInUse?.mediaId != currentStream.mediaId) {
controller.setMediaItems(streams, currentStreamIndex, 0L)
- } else if (currentStreamInUse?.mediaId != currentStream.mediaId) {
- controller.seekTo(currentStreamIndex, 0L)
}
}
}
diff --git a/app/src/main/java/com/nielsmasdorp/nederadio/playback/StreamService.kt b/app/src/main/java/com/nielsmasdorp/nederadio/playback/StreamService.kt
index 2f46068..895b0e9 100644
--- a/app/src/main/java/com/nielsmasdorp/nederadio/playback/StreamService.kt
+++ b/app/src/main/java/com/nielsmasdorp/nederadio/playback/StreamService.kt
@@ -28,7 +28,6 @@ import com.nielsmasdorp.nederadio.R
import com.nielsmasdorp.nederadio.domain.settings.GetLastPlayedId
import com.nielsmasdorp.nederadio.domain.stream.SetActiveStream
import com.nielsmasdorp.nederadio.playback.library.StreamLibrary
-import com.nielsmasdorp.nederadio.playback.library.StreamLibrary.Companion.STATIONS_ITEM_ID
import com.nielsmasdorp.nederadio.ui.NederadioActivity
import com.nielsmasdorp.nederadio.util.connectedDeviceName
import com.nielsmasdorp.nederadio.util.moveToFront
@@ -38,6 +37,7 @@ import kotlinx.coroutines.flow.*
import kotlinx.coroutines.guava.future
import org.koin.android.ext.android.inject
import java.util.concurrent.TimeUnit
+import kotlin.math.max
/**
* @author Niels Masdorp (NielsMasdorp)
@@ -106,6 +106,7 @@ class StreamService :
EXTRAS_KEY_CONTENT_STYLE_PLAYABLE,
EXTRAS_VALUE_CONTENT_STYLE_CATEGORY_GRID_ITEM
)
+ putBoolean(MEDIA_SEARCH_SUPPORTED, true)
}
val newParams = LibraryParams.Builder()
.setExtras(extras)
@@ -152,6 +153,37 @@ class StreamService :
}
}
+ override fun onSearch(
+ session: MediaLibrarySession,
+ browser: MediaSession.ControllerInfo,
+ query: String,
+ params: LibraryParams?
+ ): ListenableFuture> {
+ return serviceScope.future {
+ // Ignore extras since we have only top level streams without genres etc.
+ val results = streamLibrary.browsableContent.first().search(query = query)
+ mediaSession.notifySearchResultChanged(browser, query, results.size, params)
+ LibraryResult.ofVoid()
+ }
+ }
+
+ override fun onGetSearchResult(
+ session: MediaLibrarySession,
+ browser: MediaSession.ControllerInfo,
+ query: String,
+ page: Int,
+ pageSize: Int,
+ params: LibraryParams?
+ ): ListenableFuture>> {
+ return serviceScope.future {
+ // Ignore extras since we have only top level streams without genres etc.
+ val results = streamLibrary.browsableContent.first().search(query = query)
+ val fromIndex = max((page - 1) * pageSize, results.size - 1)
+ val toIndex = max(fromIndex + pageSize, results.size)
+ LibraryResult.ofItemList(results.subList(fromIndex, toIndex), params)
+ }
+ }
+
/**
* Casting has started, switch to [CastPlayer]
*/
@@ -263,17 +295,29 @@ class StreamService :
val streams = streamLibrary
.browsableContent
.first()
- .getChildren(nodeId = STATIONS_ITEM_ID)
- if (mediaItems.size == 1) { // User has selected an item in Android Auto
- val item = mediaItems[0]
- // Replace single items by all items with selected item as first item
- streams
- .toMutableList()
- .apply { moveToFront { it.mediaId == item.mediaId } }
+ if (mediaItems.size == 1) {
+ val singleItem = mediaItems[0]
+ if (singleItem.mediaId.isBlank() &&
+ singleItem.requestMetadata.searchQuery != null
+ ) {
+ // User has preformed a voice search in Android Auto
+ streams.search(query = singleItem.requestMetadata.searchQuery)
+ .toMutableList()
+ } else {
+ // User has selected an item in Android Auto
+ // It is expected behavior, but might get solved in the future
+ // That the rest of the queue is removed by Android Auto
+ // So we have to recreate the queue with the selected item in front
+ streams
+ .getAllPlayableItems()
+ .toMutableList()
+ .apply { moveToFront { it.mediaId == singleItem.mediaId } }
+ }
} else {
// Just use the [MediaItem] from the content library since the URI exists there
+ val allContent = streams.getAllPlayableItems()
mediaItems.map { mediaItem ->
- streams.find { it.mediaId == mediaItem.mediaId }!!
+ allContent.find { it.mediaId == mediaItem.mediaId }!!
}.toMutableList()
}
}
@@ -295,7 +339,8 @@ class StreamService :
val intent = Intent(this, NederadioActivity::class.java)
val requestCode = 0
- val immutableFlag = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) FLAG_IMMUTABLE else requestCode
+ val immutableFlag =
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) FLAG_IMMUTABLE else requestCode
val pendingIntent = PendingIntent.getActivity(
this,
requestCode,
@@ -384,6 +429,8 @@ class StreamService :
companion object {
+ const val MEDIA_SEARCH_SUPPORTED = "android.media.browse.SEARCH_SUPPORTED"
+
const val START_TIMER_COMMAND = "start_timer"
const val START_TIMER_COMMAND_VALUE_KEY = "start_timer_value"
const val TIMER_UPDATED_COMMAND = "timer_updated"
diff --git a/app/src/main/java/com/nielsmasdorp/nederadio/playback/library/Tree.kt b/app/src/main/java/com/nielsmasdorp/nederadio/playback/library/Tree.kt
index e78ae80..b51581a 100644
--- a/app/src/main/java/com/nielsmasdorp/nederadio/playback/library/Tree.kt
+++ b/app/src/main/java/com/nielsmasdorp/nederadio/playback/library/Tree.kt
@@ -45,6 +45,11 @@ class Tree(val rootNode: MediaItemNode) {
}
}
+ /**
+ * Return all playable items
+ */
+ fun getAllPlayableItems() = getChildren(nodeId = STATIONS_ITEM_ID)
+
/**
* Return a single playable item for a given id
*/
@@ -56,4 +61,18 @@ class Tree(val rootNode: MediaItemNode) {
}
error("Unknown id: $itemId!")
}
+
+ /**
+ * Return a list of items based on a search query
+ */
+ fun search(query: String?): List {
+ return getChildren(nodeId = STATIONS_ITEM_ID).let { results ->
+ if (query.isNullOrBlank()) {
+ // Return shuffled full list when no query is supplied
+ results.shuffled()
+ } else {
+ results.filter { it.mediaMetadata.artist?.contains(query) == true }
+ }
+ }
+ }
}