Skip to content

Commit

Permalink
Merge pull request #40 from NielsMasdorp/feature/search
Browse files Browse the repository at this point in the history
Android Auto and Google Assistant voice search
  • Loading branch information
Niels Masdorp authored May 4, 2023
2 parents ad48450 + 3d17ffe commit 91ec57a
Show file tree
Hide file tree
Showing 6 changed files with 85 additions and 16 deletions.
1 change: 0 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,18 @@
<activity
android:name=".ui.NederadioActivity"
android:exported="true"
android:label="@string/app_name"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.media.action.MEDIA_PLAY_FROM_SEARCH" />

<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -152,6 +153,37 @@ class StreamService :
}
}

override fun onSearch(
session: MediaLibrarySession,
browser: MediaSession.ControllerInfo,
query: String,
params: LibraryParams?
): ListenableFuture<LibraryResult<Void>> {
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<LibraryResult<ImmutableList<MediaItem>>> {
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]
*/
Expand Down Expand Up @@ -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()
}
}
Expand All @@ -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,
Expand Down Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand All @@ -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<MediaItem> {
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 }
}
}
}
}

0 comments on commit 91ec57a

Please sign in to comment.