Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improving Automotive search #681

Merged
merged 2 commits into from
Jan 9, 2023
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
([#583](https://github.com/Automattic/pocket-casts-android/pull/583)).
* Add categories to recommendations screen
([#675](https://github.com/Automattic/pocket-casts-android/pull/675)).
* Improved the Android Automotive search
([#681](https://github.com/Automattic/pocket-casts-android/pull/681)).

7.29
-----
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,6 @@ abstract class EpisodeDao {
@Query("SELECT * FROM episodes WHERE podcast_id = :podcastUuid ORDER BY published_date DESC, added_date DESC LIMIT 1")
abstract fun findLatestRx(podcastUuid: String): Maybe<Episode>

@Query("SELECT * FROM episodes WHERE podcast_id = :podcastUuid AND playing_status != 2 ORDER BY published_date DESC LIMIT 10")
abstract fun findPodcastEpisodesForMediaBrowserSearch(podcastUuid: String): List<Episode>

@Query("SELECT * FROM episodes WHERE (download_task_id IS NOT NULL OR episode_status == :downloadEpisodeStatusEnum OR (episode_status == :failedEpisodeStatusEnum AND last_download_attempt_date > :failedDownloadCutoff AND archived == 0)) ORDER BY last_download_attempt_date DESC")
abstract fun observeDownloadingEpisodesIncludingFailed(failedDownloadCutoff: Long, failedEpisodeStatusEnum: EpisodeStatusEnum = EpisodeStatusEnum.DOWNLOAD_FAILED, downloadEpisodeStatusEnum: EpisodeStatusEnum = EpisodeStatusEnum.DOWNLOADED): Flowable<List<Episode>>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,7 @@ class MediaSessionManager(
* ‘Listen to [filter name] in Pocket Casts’
* ‘Listen to Up Next in Pocket Casts’
* ‘Play Up Next in Pocket Casts’
* ‘Play New Releasees Next in Pocket Casts’
* ‘Play New Releases Next in Pocket Casts’
*/
private fun performPlayFromSearch(searchTerm: String?) {
Timber.d("performSearch $searchTerm")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import au.com.shiftyjelly.pocketcasts.models.entity.Episode
import au.com.shiftyjelly.pocketcasts.models.entity.Podcast
import au.com.shiftyjelly.pocketcasts.models.to.FolderItem
import au.com.shiftyjelly.pocketcasts.models.to.SubscriptionStatus
import au.com.shiftyjelly.pocketcasts.models.type.PodcastsSortType
import au.com.shiftyjelly.pocketcasts.preferences.Settings
import au.com.shiftyjelly.pocketcasts.repositories.extensions.id
import au.com.shiftyjelly.pocketcasts.repositories.notification.NotificationDrawer
Expand Down Expand Up @@ -66,7 +67,6 @@ const val RECENT_ROOT = "__RECENT__"
const val SUGGESTED_ROOT = "__SUGGESTED__"
const val FOLDER_ROOT_PREFIX = "__FOLDER__"

private const val MAX_SEARCH_RESULT_COUNT = 10
private const val MEDIA_SEARCH_SUPPORTED = "android.media.browse.SEARCH_SUPPORTED"
private const val CONTENT_STYLE_SUPPORTED = "android.media.browse.CONTENT_STYLE_SUPPORTED"

Expand Down Expand Up @@ -569,86 +569,40 @@ open class PlaybackService : MediaBrowserServiceCompat(), CoroutineScope {
override fun onSearch(query: String, extras: Bundle?, result: Result<List<MediaBrowserCompat.MediaItem>>) {
result.detach()
launch {
result.sendResult(performSearch(query))
result.sendResult(podcastSearch(query))
}
}

/**
* Voice search.
* "Play Up Next"
* "Play Material"
* "Play Material in Pocket Casts"
* Search for local and remote podcasts.
* Returning an empty list displays "No media available for browsing here"
* Returning null displays "Something went wrong". There is no way to display our own error message.
*/
private fun performSearch(searchTerm: String): List<MediaBrowserCompat.MediaItem> {
val searchResults = mutableListOf<MediaBrowserCompat.MediaItem>()

val query = searchTerm.trim { it <= ' ' }.lowercase()
if (query.startsWith("up next") || query.startsWith("next episode") || query.startsWith("next podcast")) {
val upNextEpisodes = playbackManager.upNextQueue.queueEpisodes.take(10)
for (episode in upNextEpisodes) {
if (episode is Episode) {
podcastManager.findPodcastByUuid(episode.podcastUuid)?.let { parentPodcast ->
addToResultsIfNotExists(AutoConverter.convertEpisodeToMediaItem(context = this, episode = episode, parentPodcast = parentPodcast), searchResults)
}
} else {
// TODO: UserEpisode
}
}

return searchResults
}

val options = MediaSessionManager.calculateSearchQueryOptions(query)
for (option in options) {
val matchingPodcast = podcastManager.searchPodcastByTitle(option) ?: continue
val latestEpisodeUuid = matchingPodcast.latestEpisodeUuid
val latestEpisode = if (latestEpisodeUuid == null) null else episodeManager.findByUuid(latestEpisodeUuid)

if (latestEpisode != null) {
addToResultsIfNotExists(AutoConverter.convertEpisodeToMediaItem(context = this, episode = latestEpisode, parentPodcast = matchingPodcast), searchResults)
if (searchResults.size >= MAX_SEARCH_RESULT_COUNT) {
return searchResults
}
}

// add a few of the latest episodes from this podcast
episodeManager.findPodcastEpisodesForMediaBrowserSearch(matchingPodcast.uuid).forEach { episode ->
addToResultsIfNotExists(AutoConverter.convertEpisodeToMediaItem(context = this, episode = episode, parentPodcast = matchingPodcast), searchResults)
}
if (searchResults.size >= MAX_SEARCH_RESULT_COUNT) {
return searchResults
}
}

for (option in options) {
episodeManager.findFirstBySearchQuery(option)?.let { firstMatch ->
podcastManager.findPodcastByUuid(firstMatch.podcastUuid)?.let { parentPodcast ->
addToResultsIfNotExists(AutoConverter.convertEpisodeToMediaItem(context = this, episode = firstMatch, parentPodcast = parentPodcast), searchResults)
}

if (searchResults.size >= MAX_SEARCH_RESULT_COUNT) {
return searchResults
}
private suspend fun podcastSearch(term: String): List<MediaBrowserCompat.MediaItem>? {
val termCleaned = term.trim()
// search for local podcasts
val localPodcasts = podcastManager.findSubscribedNoOrder()
.filter { it.title.contains(termCleaned, ignoreCase = true) || it.author.contains(termCleaned, ignoreCase = true) }
.sortedBy { PodcastsSortType.cleanStringForSort(it.title) }
// search for podcasts on the server
val serverPodcasts = try {
// only search the server if the term is over one character long
if (termCleaned.length <= 1) {
emptyList()
} else {
serverManager.searchForPodcastsSuspend(searchTerm = term, resources = resources).searchResults
}
}

if (searchResults.isEmpty()) { // Try the server
serverManager.searchForPodcastsRx(query).blockingGet().searchResults.forEach { podcast ->
val mediaItem = convertPodcastToMediaItem(podcast = podcast, context = this)
addToResultsIfNotExists(mediaItem, searchResults)
} catch (ex: Exception) {
Timber.e(ex)
// display the error message when the server call fails only if there is no local podcasts to display
if (localPodcasts.isEmpty()) {
return null
}
emptyList()
}

return searchResults
}

private fun addToResultsIfNotExists(item: MediaBrowserCompat.MediaItem?, searchResults: MutableList<MediaBrowserCompat.MediaItem>) {
item ?: return

for (existingItem in searchResults) {
if (existingItem.mediaId == item.mediaId) return
}

searchResults.add(item)
// merge the local and remote podcasts
val podcasts = (localPodcasts + serverPodcasts).distinctBy { it.uuid }
// convert podcasts to the media browser format
return podcasts.mapNotNull { podcast -> convertPodcastToMediaItem(context = this, podcast = podcast) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ interface EpisodeManager {
fun findLatestUnfinishedEpisodeByPodcast(podcast: Podcast): Episode?
fun findLatestEpisodeToPlay(): Episode?
fun observeEpisodesByPodcastOrderedRx(podcast: Podcast): Flowable<List<Episode>>
fun findPodcastEpisodesForMediaBrowserSearch(podcastUuid: String): List<Episode>
fun observeEpisodesWhere(queryAfterWhere: String): Flowable<List<Episode>>
fun observeDownloadingEpisodes(): LiveData<List<Episode>>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,10 +173,6 @@ class EpisodeManagerImpl @Inject constructor(
}
}

override fun findPodcastEpisodesForMediaBrowserSearch(podcastUuid: String): List<Episode> {
return episodeDao.findPodcastEpisodesForMediaBrowserSearch(podcastUuid)
}

override fun findEpisodesWhere(queryAfterWhere: String): List<Episode> {
return episodeDao.findEpisodes(SimpleSQLiteQuery("SELECT episodes.* FROM episodes JOIN podcasts ON episodes.podcast_id = podcasts.uuid WHERE podcasts.subscribed = 1 AND $queryAfterWhere"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package au.com.shiftyjelly.pocketcasts.servers

import android.content.res.Resources
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.text.TextUtils
import au.com.shiftyjelly.pocketcasts.localization.R
import au.com.shiftyjelly.pocketcasts.localization.helper.LocaliseHelper
import au.com.shiftyjelly.pocketcasts.models.entity.Podcast
import au.com.shiftyjelly.pocketcasts.models.to.Share
import au.com.shiftyjelly.pocketcasts.preferences.Settings
Expand All @@ -13,6 +16,7 @@ import au.com.shiftyjelly.pocketcasts.servers.model.AuthResultModel
import au.com.shiftyjelly.pocketcasts.utils.log.LogBuffer
import io.reactivex.Single
import io.reactivex.SingleEmitter
import kotlinx.coroutines.suspendCancellableCoroutine
import okhttp3.Call
import okhttp3.Callback
import okhttp3.FormBody
Expand All @@ -28,6 +32,8 @@ import java.io.IOException
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException

@Singleton
open class ServerManager @Inject constructor(
Expand Down Expand Up @@ -119,6 +125,39 @@ open class ServerManager @Inject constructor(
)
}

suspend fun searchForPodcastsSuspend(searchTerm: String, resources: Resources): PodcastSearch {
if (searchTerm.isEmpty()) {
return PodcastSearch()
}
return suspendCancellableCoroutine { continuation ->
searchForPodcasts(
searchTerm = searchTerm,
callback = object : ServerCallback<PodcastSearch> {
override fun dataReturned(result: PodcastSearch?) {
if (result == null) {
continuation.resumeWithException(Exception(resources.getString(R.string.error_search_failed)))
} else {
continuation.resume(result)
}
}

override fun onFailed(
errorCode: Int,
userMessage: String?,
serverMessageId: String?,
serverMessage: String?,
throwable: Throwable?,
) {
val message = LocaliseHelper.serverMessageIdToMessage(serverMessageId, resources::getString)
?: userMessage
?: resources.getString(R.string.error_search_failed)
continuation.resumeWithException(throwable ?: Exception(message))
}
}
)
}
}

open fun searchForPodcastsRx(searchTerm: String): Single<PodcastSearch> {
return convertCallToRx { emitter ->
searchForPodcasts(searchTerm, getRxServerCallback(emitter))
Expand Down