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 } + } + } + } }