mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 09:33:33 +00:00
Merge branch 'development' into fix-hardcoded-strings
This commit is contained in:
commit
3824dd882c
11 changed files with 107 additions and 1156 deletions
62
.github/workflows/github_release.yml
vendored
62
.github/workflows/github_release.yml
vendored
|
|
@ -35,12 +35,18 @@ jobs:
|
|||
echo "BUILD_TOOL_VERSION=$BUILD_TOOL_VERSION" >> $GITHUB_ENV
|
||||
echo Last build tool version is: $BUILD_TOOL_VERSION
|
||||
|
||||
- name: Build APK
|
||||
- name: Build All APKs
|
||||
id: build
|
||||
run: bash ./gradlew assembleTempoRelease
|
||||
run: |
|
||||
# Build release variants
|
||||
bash ./gradlew assembleTempoRelease
|
||||
bash ./gradlew assembleNotquitemyRelease
|
||||
# Build debug variants
|
||||
bash ./gradlew assembleTempoDebug
|
||||
bash ./gradlew assembleNotquitemyDebug
|
||||
|
||||
- name: Sign APK
|
||||
id: sign_apk
|
||||
- name: Sign Tempo Release APKs
|
||||
id: sign_tempo_release
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/tempo/release
|
||||
|
|
@ -51,11 +57,17 @@ jobs:
|
|||
env:
|
||||
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
|
||||
|
||||
- name: Make artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
- name: Sign NotQuiteMy Release APKs
|
||||
id: sign_notquitemy_release
|
||||
uses: r0adkll/sign-android-release@v1
|
||||
with:
|
||||
name: app-release-signed
|
||||
path: ${{steps.sign_apk.outputs.signedReleaseFile}}
|
||||
releaseDirectory: app/build/outputs/apk/notquitemy/release
|
||||
signingKeyBase64: ${{ secrets.KEYSTORE_BASE64 }}
|
||||
alias: ${{ secrets.KEY_ALIAS_GITHUB }}
|
||||
keyStorePassword: ${{ secrets.KEYSTORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD_GITHUB }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: ${{ env.BUILD_TOOL_VERSION }}
|
||||
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
|
|
@ -67,12 +79,40 @@ jobs:
|
|||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
|
||||
- name: Upload APK
|
||||
- name: Upload Release APKs
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ${{steps.sign_apk.outputs.signedReleaseFile}}
|
||||
asset_path: ${{steps.sign_tempo_release.outputs.signedReleaseFile}}
|
||||
asset_name: app-tempo-release.apk
|
||||
asset_content_type: application/zip
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
|
||||
- name: Upload NotQuiteMy Release APK
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ github.token }}
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: ${{steps.sign_notquitemy_release.outputs.signedReleaseFile}}
|
||||
asset_name: app-notquitemy-release.apk
|
||||
asset_content_type: application/vnd.android.package-archive
|
||||
|
||||
- name: Upload Debug APKs as artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: debug-apks
|
||||
path: |
|
||||
app/build/outputs/apk/tempo/debug/
|
||||
app/build/outputs/apk/notquitemy/debug/
|
||||
retention-days: 30
|
||||
|
||||
- name: Upload Release APKs as artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-apks
|
||||
path: |
|
||||
${{steps.sign_tempo_release.outputs.signedReleaseFile}}
|
||||
${{steps.sign_notquitemy_release.outputs.signedReleaseFile}}
|
||||
retention-days: 30
|
||||
|
|
@ -40,6 +40,8 @@ class Download(@PrimaryKey override val id: String) : Child(id) {
|
|||
transcodedSuffix = child.transcodedSuffix
|
||||
duration = child.duration
|
||||
bitrate = child.bitrate
|
||||
samplingRate = child.samplingRate
|
||||
bitDepth = child.bitDepth
|
||||
path = child.path
|
||||
isVideo = child.isVideo
|
||||
userRating = child.userRating
|
||||
|
|
|
|||
|
|
@ -191,7 +191,7 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
|
|||
R.string.song_subtitle_formatter,
|
||||
song.getArtist(),
|
||||
MusicUtil.getReadableDurationString(song.getDuration(), false),
|
||||
""
|
||||
MusicUtil.getReadableAudioQualityString(song)
|
||||
)
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -120,6 +120,11 @@ public class MappingUtil {
|
|||
}
|
||||
|
||||
public static MediaItem mapDownload(Child media) {
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putInt("samplingRate", media.getSamplingRate() != null ? media.getSamplingRate() : 0);
|
||||
bundle.putInt("bitDepth", media.getBitDepth() != null ? media.getBitDepth() : 0);
|
||||
|
||||
return new MediaItem.Builder()
|
||||
.setMediaId(media.getId())
|
||||
.setMediaMetadata(
|
||||
|
|
@ -130,12 +135,14 @@ public class MappingUtil {
|
|||
.setReleaseYear(media.getYear() != null ? media.getYear() : 0)
|
||||
.setAlbumTitle(media.getAlbum())
|
||||
.setArtist(media.getArtist())
|
||||
.setExtras(bundle)
|
||||
.setIsBrowsable(false)
|
||||
.setIsPlayable(true)
|
||||
.build()
|
||||
)
|
||||
.setRequestMetadata(
|
||||
new MediaItem.RequestMetadata.Builder()
|
||||
.setExtras(bundle)
|
||||
.setMediaUri(Preferences.preferTranscodedDownload() ? MusicUtil.getTranscodedDownloadUri(media.getId()) : MusicUtil.getDownloadUri(media.getId()))
|
||||
.build()
|
||||
)
|
||||
|
|
|
|||
|
|
@ -69,6 +69,7 @@
|
|||
<string name="download_directory_dialog_positive_button">Descargar</string>
|
||||
<string name="download_directory_dialog_summary">Se descargarán todas las pistas de esta carpeta. Las pistas en las subcarpetas no se descargarán.</string>
|
||||
<string name="download_directory_dialog_title">Descargar las pistas</string>
|
||||
<string name="download_directory_set">Indicar ubicación de descarga</string>
|
||||
<string name="download_info_empty_subtitle">Una vez que descargues una pista, la encontrarás aquí</string>
|
||||
<string name="download_info_empty_title">No hay descargas</string>
|
||||
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s elementos</string>
|
||||
|
|
@ -79,7 +80,10 @@
|
|||
<string name="download_storage_dialog_title">Selecciona una opción de almacenamiento</string>
|
||||
<string name="download_storage_external_dialog_positive_button">Externo</string>
|
||||
<string name="download_storage_internal_dialog_negative_button">Interno</string>
|
||||
<string name="download_storage_directory_dialog_neutral_button">Directorio</string>
|
||||
<string name="download_title_section">Descargas</string>
|
||||
<string name="download_refresh_no_changes">No se han encontrado descargas que falten</string>
|
||||
<string name="download_refresh_button_content_description">Actualizar descargas</string>
|
||||
<string name="downloaded_bottom_sheet_add_to_queue">Añadir a la cola</string>
|
||||
<string name="downloaded_bottom_sheet_play_next">Reproducir siguiente</string>
|
||||
<string name="downloaded_bottom_sheet_remove">Eliminar</string>
|
||||
|
|
@ -88,6 +92,8 @@
|
|||
<string name="error_required">Obligatorio</string>
|
||||
<string name="error_server_prefix">Se necesita un prefijo http o https</string>
|
||||
<string name="exo_download_notification_channel_name">Descargas</string>
|
||||
<string name="exo_controls_heart_on_description">Añadir a favoritos</string>
|
||||
<string name="cast_expanded_controller_loading">Cargando…</string>
|
||||
<string name="filter_info_selection">Selecciona dos o más filtros</string>
|
||||
<string name="filter_title">Filtrar</string>
|
||||
<string name="filter_artist">Filtrar artistas</string>
|
||||
|
|
@ -118,6 +124,7 @@
|
|||
<string name="home_sync_starred_subtitle">Descargar estas pistas usará una gran cantidad de datos</string>
|
||||
<string name="home_sync_starred_title">Parece que hay algunas pistas destacadas para sincronizar</string>
|
||||
<string name="home_sync_starred_albums_subtitle">Los álbumes marcados como favoritos estarán disponibles en el modo sin conexión.</string>
|
||||
<string name="home_sync_starred_artists_subtitle">Has destacado artistas con música que no has descargado</string>
|
||||
<string name="home_title_best_of">Lo mejor de</string>
|
||||
<string name="home_title_discovery">Descubrir</string>
|
||||
<string name="home_title_discovery_shuffle_all_button">Todo en aleatorio</string>
|
||||
|
|
@ -171,6 +178,7 @@
|
|||
<string name="menu_group_by_album">Álbum</string>
|
||||
<string name="menu_group_by_artist">Artista</string>
|
||||
<string name="settings_scan_result">Escaneo: hay %1$d pistas</string>
|
||||
<string name="settings_support_title">Soporte al usuario</string>
|
||||
<string name="settings_image_size">Resolución de la imagen</string>
|
||||
<string name="settings_language">Idioma</string>
|
||||
<string name="settings_system_language">Idioma del sistema</string>
|
||||
|
|
@ -202,6 +210,7 @@
|
|||
<string name="player_playback_speed">%1$.2fx</string>
|
||||
<string name="player_queue_clean_all_button">Limpiar la cola de reproducción</string>
|
||||
<string name="player_queue_save_queue_success">Cola de reproducción guardada</string>
|
||||
<string name="player_lyrics_download_failure">La letra no se puede descargar</string>
|
||||
<string name="player_server_priority">Prioridad del servidor</string>
|
||||
<string name="player_unknown_format">Formato desconocido</string>
|
||||
<string name="player_transcoding">Transcodificando</string>
|
||||
|
|
@ -213,6 +222,7 @@
|
|||
<string name="playlist_chooser_dialog_neutral_button">Crear</string>
|
||||
<string name="playlist_chooser_dialog_title">Añadir a una lista de reproducción</string>
|
||||
<string name="playlist_chooser_dialog_toast_add_failure">Error al añadir a la lista</string>
|
||||
<string name="playlist_chooser_dialog_toast_all_skipped">Todas las pistas se han descartado porque están repetidas</string>
|
||||
<string name="playlist_counted_tracks">%1$d pistas • %2$s</string>
|
||||
<string name="playlist_duration">Duración • %1$s</string>
|
||||
<string name="playlist_editor_dialog_action_delete_toast">Pulsación larga para eliminar</string>
|
||||
|
|
@ -282,6 +292,7 @@
|
|||
<string name="settings_about_summary">Tempo es un cliente de música Subsonic ligero y de código abierto, diseñado nativamente para Android.</string>
|
||||
<string name="settings_about_title">Acerca de</string>
|
||||
<string name="settings_always_on_display">Pantalla siempre activa</string>
|
||||
<string name="settings_allow_playlist_duplicates_summary">Si está habilitada, no se comprobará si hay pistas repetidas cuando se añadan a la lista.</string>
|
||||
<string name="settings_audio_transcode_download_format">Formato de transcodificación</string>
|
||||
<string name="settings_audio_transcode_download_priority_summary">Si está habilitada, Tempo no descargará la pista con las opciones de transcodificación que aparecen a continuación.</string>
|
||||
<string name="settings_audio_transcode_download_priority_title">Dar prioridad a las opciones del servidor usadas para el streaming en las descargas</string>
|
||||
|
|
@ -320,7 +331,9 @@
|
|||
<string name="settings_song_rating_summary">Si está habilitada, muestra la valoración de la pista como barra de 5 estrellas en la página del control de reproducción.\n\n*Requiere reiniciar la aplicación</string>
|
||||
<string name="settings_item_rating">Mostrar valoración de los elementos</string>
|
||||
<string name="settings_queue_syncing_title">Sincronizar cola de reproducción para este usuario</string>
|
||||
<string name="settings_show_mini_shuffle_button_summary">Si está habilitada, muestra el botón de reproducción aleatoria y oculta el botón de «Favoritos» en el minirreproductor</string>
|
||||
<string name="settings_radio">Mostrar emisoras de radio</string>
|
||||
<string name="settings_auto_download_lyrics_summary">Descargar las letras automáticamente cuando estén disponibles para que se puedan mostrar cuando no hay conexión.</string>
|
||||
<string name="settings_replay_gain">Configurar el modo de ganancia de reproducción</string>
|
||||
<string name="settings_rounded_corner">Esquinas redondeadas</string>
|
||||
<string name="settings_rounded_corner_size">Tamaño de las esquinas</string>
|
||||
|
|
@ -372,6 +385,7 @@
|
|||
<string name="settings_theme">Tema</string>
|
||||
<string name="settings_title_data">Datos</string>
|
||||
<string name="settings_title_general">General</string>
|
||||
<string name="settings_title_playlist">Lista de reproducción</string>
|
||||
<string name="settings_title_rating">Valoraciones</string>
|
||||
<string name="settings_title_replay_gain">Ganancia de reproducción</string>
|
||||
<string name="settings_title_scrobble">Rastreo de música (scrobble)</string>
|
||||
|
|
@ -437,7 +451,9 @@
|
|||
<string name="playlist_chooser_dialog_toast_add_success">Se ha añadido a la lista</string>
|
||||
<string name="settings_song_rating">Mostrar valoración de las pistas</string>
|
||||
<string name="home_sync_starred_albums_title">Sincronizar álbumes favoritos</string>
|
||||
<string name="settings_sync_starred_artists_for_offline_use_title">Sincronizar artistas destacados para uso sin conexión</string>
|
||||
<string name="settings_sync_starred_albums_for_offline_use_summary">Si está habilitada, los álbumes favoritos se descargarán para uso sin conexión.</string>
|
||||
<string name="starred_artist_sync_dialog_title">Sincronizar artistas destacados</string>
|
||||
<string name="starred_album_sync_dialog_summary">Descargar los álbumes favoritos puede consumir una gran cantidad de datos.</string>
|
||||
<string name="equalizer_fragment_title">Ecualizador</string>
|
||||
<string name="equalizer_reset">Restablecer</string>
|
||||
|
|
@ -447,4 +463,27 @@
|
|||
<string name="settings_app_equalizer_summary">Abrir el ecualizador integrado</string>
|
||||
<string name="settings_download_folder_cleared">Se ha limpiado la carpeta de descargas.</string>
|
||||
<string name="settings_download_folder_set">Se ha establecido la carpeta de descargas</string>
|
||||
<string name="widget_label">Widget de Tempo</string>
|
||||
<string name="widget_not_playing">En pausa</string>
|
||||
<string name="widget_placeholder_subtitle">Abrir Tempo</string>
|
||||
<string name="widget_time_duration_placeholder">0:00</string>
|
||||
<string name="widget_content_desc_album_art">Portada del álbum</string>
|
||||
<string name="widget_content_desc_play_pause">Reproducir o pausar</string>
|
||||
<string name="widget_content_desc_next">Siguiente pista</string>
|
||||
<string name="widget_content_desc_repeat">Cambiar modo de repetición</string>
|
||||
<string name="widget_content_desc_shuffle">Activar/desactivar aleatorio</string>
|
||||
<string name="widget_content_desc_prev">Pista anterior</string>
|
||||
<string name="download_refresh_no_directory">Establece una carpeta de descarga para actualizar tus descargas</string>
|
||||
<string name="home_sync_starred_artists_title">Sincronizar artistas destacados</string>
|
||||
<string name="player_lyrics_download_content_description">Descargar letras para uso sin conexión</string>
|
||||
<string name="player_lyrics_downloaded_content_description">Letras descargadas para uso sin conexión</string>
|
||||
<string name="player_lyrics_download_success">Letra guardada para uso sin conexión</string>
|
||||
<string name="settings_allow_playlist_duplicates">Permitir añadir pistas repetidas a la lista</string>
|
||||
<string name="settings_support_summary">Participa en las discusiones y el soporte de la comunidad</string>
|
||||
<string name="settings_show_mini_shuffle_button">Mostrar el botón «Aleatorio»</string>
|
||||
<string name="settings_auto_download_lyrics">Descargar automáticamente las letras</string>
|
||||
<string name="starred_artist_sync_dialog_summary">Descargar los artistas destacados podría consumir una gran cantidad de datos.</string>
|
||||
<string name="settings_sync_starred_artists_for_offline_use_summary">Si está habilitada, los artistas destacados se descargarán para uso sin conexión.</string>
|
||||
<string name="widget_time_elapsed_placeholder">0:00</string>
|
||||
<string name="exo_controls_heart_off_description">Eliminar de favoritos</string>
|
||||
</resources>
|
||||
|
|
@ -3,10 +3,16 @@
|
|||
<PreferenceCategory app:title="@string/settings_title_general">
|
||||
<Preference
|
||||
android:layout_height="match_parent"
|
||||
android:key="equalizer"
|
||||
android:key="system_equalizer"
|
||||
android:summary="@string/settings_system_equalizer_summary"
|
||||
android:title="@string/settings_system_equalizer_title" />
|
||||
|
||||
<Preference
|
||||
android:layout_height="match_parent"
|
||||
android:key="app_equalizer"
|
||||
android:summary="@string/settings_app_equalizer_summary"
|
||||
android:title="@string/settings_app_equalizer" />
|
||||
|
||||
<Preference
|
||||
android:key="scan_library"
|
||||
android:title="@string/settings_scan_title" />
|
||||
|
|
|
|||
|
|
@ -1,497 +0,0 @@
|
|||
package com.cappielloantonio.tempo.service
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaItem.SubtitleConfiguration
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.session.LibraryResult
|
||||
import com.cappielloantonio.tempo.repository.AutomotiveRepository
|
||||
import com.cappielloantonio.tempo.util.Preferences.getServerId
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import com.google.common.util.concurrent.SettableFuture
|
||||
|
||||
object MediaBrowserTree {
|
||||
|
||||
private lateinit var automotiveRepository: AutomotiveRepository
|
||||
|
||||
private var treeNodes: MutableMap<String, MediaItemNode> = mutableMapOf()
|
||||
|
||||
private var isInitialized = false
|
||||
|
||||
// Root
|
||||
private const val ROOT_ID = "[rootID]"
|
||||
|
||||
// First level
|
||||
private const val HOME_ID = "[homeID]"
|
||||
private const val LIBRARY_ID = "[libraryID]"
|
||||
private const val OTHER_ID = "[otherID]"
|
||||
|
||||
// Second level HOME_ID
|
||||
private const val MOST_PLAYED_ID = "[mostPlayedID]"
|
||||
private const val LAST_PLAYED_ID = "[lastPlayedID]"
|
||||
private const val RECENTLY_ADDED_ID = "[recentlyAddedID]"
|
||||
private const val RECENT_SONGS_ID = "[recentSongsID]"
|
||||
private const val MADE_FOR_YOU_ID = "[madeForYouID]"
|
||||
private const val STARRED_TRACKS_ID = "[starredTracksID]"
|
||||
private const val STARRED_ALBUMS_ID = "[starredAlbumsID]"
|
||||
private const val STARRED_ARTISTS_ID = "[starredArtistsID]"
|
||||
private const val RANDOM_ID = "[randomID]"
|
||||
|
||||
// Second level LIBRARY_ID
|
||||
private const val FOLDER_ID = "[folderID]"
|
||||
private const val INDEX_ID = "[indexID]"
|
||||
private const val DIRECTORY_ID = "[directoryID]"
|
||||
private const val PLAYLIST_ID = "[playlistID]"
|
||||
|
||||
// Second level OTHER_ID
|
||||
private const val PODCAST_ID = "[podcastID]"
|
||||
private const val RADIO_ID = "[radioID]"
|
||||
|
||||
private const val ALBUM_ID = "[albumID]"
|
||||
private const val ARTIST_ID = "[artistID]"
|
||||
|
||||
private class MediaItemNode(val item: MediaItem) {
|
||||
private val children: MutableList<MediaItem> = ArrayList()
|
||||
|
||||
fun addChild(childID: String) {
|
||||
this.children.add(treeNodes[childID]!!.item)
|
||||
}
|
||||
|
||||
fun getChildren(): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||
val listenableFuture = SettableFuture.create<LibraryResult<ImmutableList<MediaItem>>>()
|
||||
val libraryResult = LibraryResult.ofItemList(children, null)
|
||||
|
||||
listenableFuture.set(libraryResult)
|
||||
|
||||
return listenableFuture
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMediaItem(
|
||||
title: String,
|
||||
mediaId: String,
|
||||
isPlayable: Boolean,
|
||||
isBrowsable: Boolean,
|
||||
mediaType: @MediaMetadata.MediaType Int,
|
||||
subtitleConfigurations: List<SubtitleConfiguration> = mutableListOf(),
|
||||
album: String? = null,
|
||||
artist: String? = null,
|
||||
genre: String? = null,
|
||||
sourceUri: Uri? = null,
|
||||
imageUri: Uri? = null
|
||||
): MediaItem {
|
||||
val metadata =
|
||||
MediaMetadata.Builder()
|
||||
.setAlbumTitle(album)
|
||||
.setTitle(title)
|
||||
.setArtist(artist)
|
||||
.setGenre(genre)
|
||||
.setIsBrowsable(isBrowsable)
|
||||
.setIsPlayable(isPlayable)
|
||||
.setArtworkUri(imageUri)
|
||||
.setMediaType(mediaType)
|
||||
.build()
|
||||
|
||||
return MediaItem.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setSubtitleConfigurations(subtitleConfigurations)
|
||||
.setMediaMetadata(metadata)
|
||||
.setUri(sourceUri)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun initialize(automotiveRepository: AutomotiveRepository) {
|
||||
this.automotiveRepository = automotiveRepository
|
||||
|
||||
if (isInitialized) return
|
||||
|
||||
isInitialized = true
|
||||
|
||||
// Root level
|
||||
|
||||
treeNodes[ROOT_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Root Folder",
|
||||
mediaId = ROOT_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
// First level
|
||||
|
||||
treeNodes[HOME_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Home",
|
||||
mediaId = HOME_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[LIBRARY_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Library",
|
||||
mediaId = LIBRARY_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[OTHER_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Other",
|
||||
mediaId = OTHER_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[ROOT_ID]!!.addChild(HOME_ID)
|
||||
treeNodes[ROOT_ID]!!.addChild(LIBRARY_ID)
|
||||
treeNodes[ROOT_ID]!!.addChild(OTHER_ID)
|
||||
|
||||
// Second level HOME_ID
|
||||
|
||||
treeNodes[MOST_PLAYED_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Most played",
|
||||
mediaId = MOST_PLAYED_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[LAST_PLAYED_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Last played",
|
||||
mediaId = LAST_PLAYED_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[RECENTLY_ADDED_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Recently added",
|
||||
mediaId = RECENTLY_ADDED_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[RECENT_SONGS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Recent songs",
|
||||
mediaId = RECENT_SONGS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[MADE_FOR_YOU_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Made for you",
|
||||
mediaId = MADE_FOR_YOU_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[STARRED_TRACKS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Starred tracks",
|
||||
mediaId = STARRED_TRACKS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[STARRED_ALBUMS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Starred albums",
|
||||
mediaId = STARRED_ALBUMS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ALBUMS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[STARRED_ARTISTS_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Starred artists",
|
||||
mediaId = STARRED_ARTISTS_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_ARTISTS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[RANDOM_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Random",
|
||||
mediaId = RANDOM_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[HOME_ID]!!.addChild(MOST_PLAYED_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(LAST_PLAYED_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(RECENTLY_ADDED_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(RECENT_SONGS_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(MADE_FOR_YOU_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(STARRED_TRACKS_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(STARRED_ALBUMS_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(STARRED_ARTISTS_ID)
|
||||
treeNodes[HOME_ID]!!.addChild(RANDOM_ID)
|
||||
|
||||
// Second level LIBRARY_ID
|
||||
|
||||
treeNodes[FOLDER_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Folders",
|
||||
mediaId = FOLDER_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_MIXED
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[PLAYLIST_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Playlists",
|
||||
mediaId = PLAYLIST_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PLAYLISTS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[LIBRARY_ID]!!.addChild(FOLDER_ID)
|
||||
treeNodes[LIBRARY_ID]!!.addChild(PLAYLIST_ID)
|
||||
|
||||
// Second level OTHER_ID
|
||||
|
||||
treeNodes[PODCAST_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Podcasts",
|
||||
mediaId = PODCAST_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_PODCASTS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[RADIO_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Radio stations",
|
||||
mediaId = RADIO_ID,
|
||||
isPlayable = false,
|
||||
isBrowsable = true,
|
||||
mediaType = MediaMetadata.MEDIA_TYPE_FOLDER_RADIO_STATIONS
|
||||
)
|
||||
)
|
||||
|
||||
treeNodes[OTHER_ID]!!.addChild(PODCAST_ID)
|
||||
treeNodes[OTHER_ID]!!.addChild(RADIO_ID)
|
||||
}
|
||||
|
||||
fun getRootItem(): MediaItem {
|
||||
return treeNodes[ROOT_ID]!!.item
|
||||
}
|
||||
|
||||
fun getChildren(
|
||||
id: String
|
||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||
return when (id) {
|
||||
ROOT_ID -> treeNodes[ROOT_ID]?.getChildren()!!
|
||||
HOME_ID -> treeNodes[HOME_ID]?.getChildren()!!
|
||||
LIBRARY_ID -> treeNodes[LIBRARY_ID]?.getChildren()!!
|
||||
OTHER_ID -> treeNodes[OTHER_ID]?.getChildren()!!
|
||||
|
||||
MOST_PLAYED_ID -> automotiveRepository.getAlbums(id, "frequent", 100)
|
||||
LAST_PLAYED_ID -> automotiveRepository.getAlbums(id, "recent", 100)
|
||||
RECENTLY_ADDED_ID -> automotiveRepository.getAlbums(id, "newest", 100)
|
||||
RECENT_SONGS_ID -> automotiveRepository.getRecentlyPlayedSongs(getServerId(),100)
|
||||
MADE_FOR_YOU_ID -> automotiveRepository.getStarredArtists(id)
|
||||
STARRED_TRACKS_ID -> automotiveRepository.starredSongs
|
||||
STARRED_ALBUMS_ID -> automotiveRepository.getStarredAlbums(id)
|
||||
STARRED_ARTISTS_ID -> automotiveRepository.getStarredArtists(id)
|
||||
RANDOM_ID -> automotiveRepository.getRandomSongs(100)
|
||||
FOLDER_ID -> automotiveRepository.getMusicFolders(id)
|
||||
PLAYLIST_ID -> automotiveRepository.getPlaylists(id)
|
||||
PODCAST_ID -> automotiveRepository.getNewestPodcastEpisodes(100)
|
||||
RADIO_ID -> automotiveRepository.internetRadioStations
|
||||
|
||||
else -> {
|
||||
if (id.startsWith(MOST_PLAYED_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(
|
||||
id.removePrefix(
|
||||
MOST_PLAYED_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(LAST_PLAYED_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(
|
||||
id.removePrefix(
|
||||
LAST_PLAYED_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(RECENTLY_ADDED_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(
|
||||
id.removePrefix(
|
||||
RECENTLY_ADDED_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(MADE_FOR_YOU_ID)) {
|
||||
return automotiveRepository.getMadeForYou(
|
||||
id.removePrefix(
|
||||
MADE_FOR_YOU_ID
|
||||
),
|
||||
20
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(STARRED_ALBUMS_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(
|
||||
id.removePrefix(
|
||||
STARRED_ALBUMS_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(STARRED_ARTISTS_ID)) {
|
||||
return automotiveRepository.getArtistAlbum(
|
||||
STARRED_ALBUMS_ID,
|
||||
id.removePrefix(
|
||||
STARRED_ARTISTS_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(FOLDER_ID)) {
|
||||
return automotiveRepository.getIndexes(
|
||||
INDEX_ID,
|
||||
id.removePrefix(
|
||||
FOLDER_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(INDEX_ID)) {
|
||||
return automotiveRepository.getDirectories(
|
||||
DIRECTORY_ID,
|
||||
id.removePrefix(
|
||||
INDEX_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(DIRECTORY_ID)) {
|
||||
return automotiveRepository.getDirectories(
|
||||
DIRECTORY_ID,
|
||||
id.removePrefix(
|
||||
DIRECTORY_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(PLAYLIST_ID)) {
|
||||
return automotiveRepository.getPlaylistSongs(
|
||||
id.removePrefix(
|
||||
PLAYLIST_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(ALBUM_ID)) {
|
||||
return automotiveRepository.getAlbumTracks(
|
||||
id.removePrefix(
|
||||
ALBUM_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (id.startsWith(ARTIST_ID)) {
|
||||
return automotiveRepository.getArtistAlbum(
|
||||
ALBUM_ID,
|
||||
id.removePrefix(
|
||||
ARTIST_ID
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/androidx/media/issues/156
|
||||
fun getItems(mediaItems: List<MediaItem>): List<MediaItem> {
|
||||
val updatedMediaItems = ArrayList<MediaItem>()
|
||||
|
||||
mediaItems.forEach {
|
||||
if (it.localConfiguration?.uri != null) {
|
||||
updatedMediaItems.add(it)
|
||||
} else {
|
||||
val sessionMediaItem = automotiveRepository.getSessionMediaItem(it.mediaId)
|
||||
|
||||
if (sessionMediaItem != null) {
|
||||
var toAdd = automotiveRepository.getMetadatas(sessionMediaItem.timestamp!!)
|
||||
val index = toAdd.indexOfFirst { mediaItem -> mediaItem.mediaId == it.mediaId }
|
||||
|
||||
toAdd = toAdd.subList(index, toAdd.size)
|
||||
|
||||
updatedMediaItems.addAll(toAdd)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return updatedMediaItems
|
||||
}
|
||||
|
||||
fun search(query: String): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||
return automotiveRepository.search(
|
||||
query,
|
||||
ALBUM_ID,
|
||||
ARTIST_ID
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,202 +0,0 @@
|
|||
package com.cappielloantonio.tempo.service
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.session.CommandButton
|
||||
import androidx.media3.session.LibraryResult
|
||||
import androidx.media3.session.MediaLibraryService
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.SessionCommand
|
||||
import androidx.media3.session.SessionResult
|
||||
import com.cappielloantonio.tempo.R
|
||||
import com.cappielloantonio.tempo.repository.AutomotiveRepository
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
|
||||
open class MediaLibrarySessionCallback(
|
||||
context: Context,
|
||||
automotiveRepository: AutomotiveRepository
|
||||
) :
|
||||
MediaLibraryService.MediaLibrarySession.Callback {
|
||||
|
||||
init {
|
||||
MediaBrowserTree.initialize(automotiveRepository)
|
||||
}
|
||||
|
||||
private val shuffleCommandButtons: List<CommandButton> = listOf(
|
||||
CommandButton.Builder()
|
||||
.setDisplayName(context.getString(R.string.exo_controls_shuffle_on_description))
|
||||
.setSessionCommand(
|
||||
SessionCommand(
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON, Bundle.EMPTY
|
||||
)
|
||||
).setIconResId(R.drawable.exo_icon_shuffle_off).build(),
|
||||
|
||||
CommandButton.Builder()
|
||||
.setDisplayName(context.getString(R.string.exo_controls_shuffle_off_description))
|
||||
.setSessionCommand(
|
||||
SessionCommand(
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF, Bundle.EMPTY
|
||||
)
|
||||
).setIconResId(R.drawable.exo_icon_shuffle_on).build()
|
||||
)
|
||||
|
||||
private val repeatCommandButtons: List<CommandButton> = listOf(
|
||||
CommandButton.Builder()
|
||||
.setDisplayName(context.getString(R.string.exo_controls_repeat_off_description))
|
||||
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF, Bundle.EMPTY))
|
||||
.setIconResId(R.drawable.exo_icon_repeat_off)
|
||||
.build(),
|
||||
CommandButton.Builder()
|
||||
.setDisplayName(context.getString(R.string.exo_controls_repeat_one_description))
|
||||
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE, Bundle.EMPTY))
|
||||
.setIconResId(R.drawable.exo_icon_repeat_one)
|
||||
.build(),
|
||||
CommandButton.Builder()
|
||||
.setDisplayName(context.getString(R.string.exo_controls_repeat_all_description))
|
||||
.setSessionCommand(SessionCommand(CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL, Bundle.EMPTY))
|
||||
.setIconResId(R.drawable.exo_icon_repeat_all)
|
||||
.build()
|
||||
)
|
||||
|
||||
private val customLayoutCommandButtons: List<CommandButton> =
|
||||
shuffleCommandButtons + repeatCommandButtons
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
val mediaNotificationSessionCommands =
|
||||
MediaSession.ConnectionResult.DEFAULT_SESSION_AND_LIBRARY_COMMANDS.buildUpon()
|
||||
.also { builder ->
|
||||
(shuffleCommandButtons + repeatCommandButtons).forEach { commandButton ->
|
||||
commandButton.sessionCommand?.let { builder.add(it) }
|
||||
}
|
||||
}.build()
|
||||
|
||||
fun buildCustomLayout(player: Player): ImmutableList<CommandButton> {
|
||||
val shuffle = shuffleCommandButtons[if (player.shuffleModeEnabled) 1 else 0]
|
||||
val repeat = when (player.repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> repeatCommandButtons[1]
|
||||
Player.REPEAT_MODE_ALL -> repeatCommandButtons[2]
|
||||
else -> repeatCommandButtons[0]
|
||||
}
|
||||
return ImmutableList.of(shuffle, repeat)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onConnect(
|
||||
session: MediaSession, controller: MediaSession.ControllerInfo
|
||||
): MediaSession.ConnectionResult {
|
||||
if (session.isMediaNotificationController(controller) || session.isAutomotiveController(
|
||||
controller
|
||||
) || session.isAutoCompanionController(controller)
|
||||
) {
|
||||
val customLayout = buildCustomLayout(session.player)
|
||||
|
||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session)
|
||||
.setAvailableSessionCommands(mediaNotificationSessionCommands)
|
||||
.setCustomLayout(customLayout).build()
|
||||
}
|
||||
|
||||
return MediaSession.ConnectionResult.AcceptedResultBuilder(session).build()
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCustomCommand(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
customCommand: SessionCommand,
|
||||
args: Bundle
|
||||
): ListenableFuture<SessionResult> {
|
||||
when (customCommand.customAction) {
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON -> session.player.shuffleModeEnabled = true
|
||||
CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF -> session.player.shuffleModeEnabled = false
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL,
|
||||
CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE -> {
|
||||
val nextMode = when (session.player.repeatMode) {
|
||||
Player.REPEAT_MODE_ONE -> Player.REPEAT_MODE_ALL
|
||||
Player.REPEAT_MODE_OFF -> Player.REPEAT_MODE_ONE
|
||||
else -> Player.REPEAT_MODE_OFF
|
||||
}
|
||||
session.player.repeatMode = nextMode
|
||||
}
|
||||
else -> return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED))
|
||||
}
|
||||
|
||||
session.setCustomLayout(
|
||||
session.mediaNotificationControllerInfo!!,
|
||||
buildCustomLayout(session.player)
|
||||
)
|
||||
|
||||
return Futures.immediateFuture(SessionResult(SessionResult.RESULT_SUCCESS))
|
||||
}
|
||||
|
||||
override fun onGetLibraryRoot(
|
||||
session: MediaLibraryService.MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
params: MediaLibraryService.LibraryParams?
|
||||
): ListenableFuture<LibraryResult<MediaItem>> {
|
||||
return Futures.immediateFuture(LibraryResult.ofItem(MediaBrowserTree.getRootItem(), params))
|
||||
}
|
||||
|
||||
override fun onGetChildren(
|
||||
session: MediaLibraryService.MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
parentId: String,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
params: MediaLibraryService.LibraryParams?
|
||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||
return MediaBrowserTree.getChildren(parentId)
|
||||
}
|
||||
|
||||
override fun onAddMediaItems(
|
||||
mediaSession: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
mediaItems: List<MediaItem>
|
||||
): ListenableFuture<List<MediaItem>> {
|
||||
return super.onAddMediaItems(
|
||||
mediaSession,
|
||||
controller,
|
||||
MediaBrowserTree.getItems(mediaItems)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onSearch(
|
||||
session: MediaLibraryService.MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
query: String,
|
||||
params: MediaLibraryService.LibraryParams?
|
||||
): ListenableFuture<LibraryResult<Void>> {
|
||||
session.notifySearchResultChanged(browser, query, 60, params)
|
||||
return Futures.immediateFuture(LibraryResult.ofVoid())
|
||||
}
|
||||
|
||||
override fun onGetSearchResult(
|
||||
session: MediaLibraryService.MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
query: String,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
params: MediaLibraryService.LibraryParams?
|
||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||
return MediaBrowserTree.search(query)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON =
|
||||
"android.media3.session.demo.SHUFFLE_ON"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_OFF =
|
||||
"android.media3.session.demo.SHUFFLE_OFF"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_OFF =
|
||||
"android.media3.session.demo.REPEAT_OFF"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ONE =
|
||||
"android.media3.session.demo.REPEAT_ONE"
|
||||
private const val CUSTOM_COMMAND_TOGGLE_REPEAT_MODE_ALL =
|
||||
"android.media3.session.demo.REPEAT_ALL"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,363 +0,0 @@
|
|||
package com.cappielloantonio.tempo.service
|
||||
|
||||
import android.app.PendingIntent.FLAG_IMMUTABLE
|
||||
import android.app.PendingIntent.FLAG_UPDATE_CURRENT
|
||||
import android.app.TaskStackBuilder
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.media3.cast.CastPlayer
|
||||
import androidx.media3.cast.SessionAvailabilityListener
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.Tracks
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.exoplayer.DefaultLoadControl
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.session.MediaLibraryService
|
||||
import androidx.media3.session.MediaSession.ControllerInfo
|
||||
import com.cappielloantonio.tempo.repository.AutomotiveRepository
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity
|
||||
import com.cappielloantonio.tempo.util.Constants
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil
|
||||
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
|
||||
import com.cappielloantonio.tempo.util.Preferences
|
||||
import com.cappielloantonio.tempo.util.ReplayGainUtil
|
||||
import com.cappielloantonio.tempo.widget.WidgetUpdateManager
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
|
||||
@UnstableApi
|
||||
class MediaService : MediaLibraryService(), SessionAvailabilityListener {
|
||||
private lateinit var automotiveRepository: AutomotiveRepository
|
||||
private lateinit var player: ExoPlayer
|
||||
private lateinit var castPlayer: CastPlayer
|
||||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private lateinit var librarySessionCallback: MediaLibrarySessionCallback
|
||||
lateinit var equalizerManager: EqualizerManager
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getEqualizerManager(): EqualizerManager {
|
||||
return this@MediaService.equalizerManager
|
||||
}
|
||||
}
|
||||
|
||||
private val binder = LocalBinder()
|
||||
|
||||
companion object {
|
||||
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER"
|
||||
}
|
||||
private val widgetUpdateHandler = Handler(Looper.getMainLooper())
|
||||
private var widgetUpdateScheduled = false
|
||||
private val widgetUpdateRunnable = object : Runnable {
|
||||
override fun run() {
|
||||
if (!player.isPlaying) {
|
||||
widgetUpdateScheduled = false
|
||||
return
|
||||
}
|
||||
updateWidget()
|
||||
widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
initializeRepository()
|
||||
initializePlayer()
|
||||
initializeCastPlayer()
|
||||
initializeMediaLibrarySession()
|
||||
initializePlayerListener()
|
||||
initializeEqualizerManager()
|
||||
|
||||
setPlayer(
|
||||
null,
|
||||
if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable) castPlayer else player
|
||||
)
|
||||
}
|
||||
|
||||
override fun onGetSession(controllerInfo: ControllerInfo): MediaLibrarySession {
|
||||
return mediaLibrarySession
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
val player = mediaLibrarySession.player
|
||||
|
||||
if (!player.playWhenReady || player.mediaItemCount == 0) {
|
||||
stopSelf()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
equalizerManager.release()
|
||||
stopWidgetUpdates()
|
||||
releasePlayer()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
// Check if the intent is for our custom equalizer binder
|
||||
if (intent?.action == ACTION_BIND_EQUALIZER) {
|
||||
return binder
|
||||
}
|
||||
// Otherwise, handle it as a normal MediaLibraryService connection
|
||||
return super.onBind(intent)
|
||||
}
|
||||
|
||||
private fun initializeRepository() {
|
||||
automotiveRepository = AutomotiveRepository()
|
||||
}
|
||||
|
||||
private fun initializePlayer() {
|
||||
player = ExoPlayer.Builder(this)
|
||||
.setRenderersFactory(getRenderersFactory())
|
||||
.setMediaSourceFactory(DynamicMediaSourceFactory(this))
|
||||
.setAudioAttributes(AudioAttributes.DEFAULT, true)
|
||||
.setHandleAudioBecomingNoisy(true)
|
||||
.setWakeMode(C.WAKE_MODE_NETWORK)
|
||||
.setLoadControl(initializeLoadControl())
|
||||
.build()
|
||||
|
||||
player.shuffleModeEnabled = Preferences.isShuffleModeEnabled()
|
||||
player.repeatMode = Preferences.getRepeatMode()
|
||||
}
|
||||
|
||||
private fun initializeEqualizerManager() {
|
||||
equalizerManager = EqualizerManager()
|
||||
val audioSessionId = player.audioSessionId
|
||||
if (equalizerManager.attachToSession(audioSessionId)) {
|
||||
val enabled = Preferences.isEqualizerEnabled()
|
||||
equalizerManager.setEnabled(enabled)
|
||||
|
||||
val bands = equalizerManager.getNumberOfBands()
|
||||
val savedLevels = Preferences.getEqualizerBandLevels(bands)
|
||||
for (i in 0 until bands) {
|
||||
equalizerManager.setBandLevel(i.toShort(), savedLevels[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeCastPlayer() {
|
||||
if (GoogleApiAvailability.getInstance()
|
||||
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
||||
) {
|
||||
castPlayer = CastPlayer(CastContext.getSharedInstance(this))
|
||||
castPlayer.setSessionAvailabilityListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeMediaLibrarySession() {
|
||||
val sessionActivityPendingIntent =
|
||||
TaskStackBuilder.create(this).run {
|
||||
addNextIntent(Intent(this@MediaService, MainActivity::class.java))
|
||||
getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
librarySessionCallback = createLibrarySessionCallback()
|
||||
mediaLibrarySession =
|
||||
MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||
.setSessionActivity(sessionActivityPendingIntent)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createLibrarySessionCallback(): MediaLibrarySessionCallback {
|
||||
return MediaLibrarySessionCallback(this, automotiveRepository)
|
||||
}
|
||||
|
||||
private fun initializePlayerListener() {
|
||||
player.addListener(object : Player.Listener {
|
||||
override fun onMediaItemTransition(mediaItem: MediaItem?, reason: Int) {
|
||||
if (mediaItem == null) return
|
||||
|
||||
if (reason == Player.MEDIA_ITEM_TRANSITION_REASON_SEEK || reason == Player.MEDIA_ITEM_TRANSITION_REASON_AUTO) {
|
||||
MediaManager.setLastPlayedTimestamp(mediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onTracksChanged(tracks: Tracks) {
|
||||
ReplayGainUtil.setReplayGain(player, tracks)
|
||||
val currentMediaItem = player.currentMediaItem
|
||||
if (currentMediaItem != null && currentMediaItem.mediaMetadata.extras != null) {
|
||||
MediaManager.scrobble(currentMediaItem, false)
|
||||
}
|
||||
|
||||
if (player.currentMediaItemIndex + 1 == player.mediaItemCount)
|
||||
MediaManager.continuousPlay(player.currentMediaItem)
|
||||
}
|
||||
|
||||
override fun onIsPlayingChanged(isPlaying: Boolean) {
|
||||
if (!isPlaying) {
|
||||
MediaManager.setPlayingPausedTimestamp(
|
||||
player.currentMediaItem,
|
||||
player.currentPosition
|
||||
)
|
||||
} else {
|
||||
MediaManager.scrobble(player.currentMediaItem, false)
|
||||
}
|
||||
if (isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
} else {
|
||||
stopWidgetUpdates()
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPlaybackStateChanged(playbackState: Int) {
|
||||
super.onPlaybackStateChanged(playbackState)
|
||||
|
||||
if (!player.hasNextMediaItem() &&
|
||||
playbackState == Player.STATE_ENDED &&
|
||||
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
|
||||
) {
|
||||
MediaManager.scrobble(player.currentMediaItem, true)
|
||||
MediaManager.saveChronology(player.currentMediaItem)
|
||||
}
|
||||
updateWidget()
|
||||
}
|
||||
|
||||
override fun onPositionDiscontinuity(
|
||||
oldPosition: Player.PositionInfo,
|
||||
newPosition: Player.PositionInfo,
|
||||
reason: Int
|
||||
) {
|
||||
super.onPositionDiscontinuity(oldPosition, newPosition, reason)
|
||||
|
||||
if (reason == Player.DISCONTINUITY_REASON_AUTO_TRANSITION) {
|
||||
if (oldPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
|
||||
MediaManager.scrobble(oldPosition.mediaItem, true)
|
||||
MediaManager.saveChronology(oldPosition.mediaItem)
|
||||
}
|
||||
|
||||
if (newPosition.mediaItem?.mediaMetadata?.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC) {
|
||||
MediaManager.setLastPlayedTimestamp(newPosition.mediaItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
|
||||
Preferences.setShuffleModeEnabled(shuffleModeEnabled)
|
||||
mediaLibrarySession.setCustomLayout(
|
||||
librarySessionCallback.buildCustomLayout(player)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onRepeatModeChanged(repeatMode: Int) {
|
||||
Preferences.setRepeatMode(repeatMode)
|
||||
mediaLibrarySession.setCustomLayout(
|
||||
librarySessionCallback.buildCustomLayout(player)
|
||||
)
|
||||
}
|
||||
})
|
||||
if (player.isPlaying) {
|
||||
scheduleWidgetUpdates()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateWidget() {
|
||||
val mi = player.currentMediaItem
|
||||
val title = mi?.mediaMetadata?.title?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("title")
|
||||
val artist = mi?.mediaMetadata?.artist?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("artist")
|
||||
val album = mi?.mediaMetadata?.albumTitle?.toString()
|
||||
?: mi?.mediaMetadata?.extras?.getString("album")
|
||||
val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId")
|
||||
|
||||
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
|
||||
WidgetUpdateManager.updateFromState(
|
||||
this,
|
||||
title ?: "",
|
||||
artist ?: "",
|
||||
album ?: "",
|
||||
coverId,
|
||||
player.isPlaying,
|
||||
player.shuffleModeEnabled,
|
||||
player.repeatMode,
|
||||
position,
|
||||
duration
|
||||
)
|
||||
}
|
||||
|
||||
private fun scheduleWidgetUpdates() {
|
||||
if (widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.postDelayed(widgetUpdateRunnable, WIDGET_UPDATE_INTERVAL_MS)
|
||||
widgetUpdateScheduled = true
|
||||
}
|
||||
|
||||
private fun stopWidgetUpdates() {
|
||||
if (!widgetUpdateScheduled) return
|
||||
widgetUpdateHandler.removeCallbacks(widgetUpdateRunnable)
|
||||
widgetUpdateScheduled = false
|
||||
}
|
||||
|
||||
private fun initializeLoadControl(): DefaultLoadControl {
|
||||
return DefaultLoadControl.Builder()
|
||||
.setBufferDurationsMs(
|
||||
(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||
(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
|
||||
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun getQueueFromPlayer(player: Player): List<MediaItem> {
|
||||
val queue = mutableListOf<MediaItem>()
|
||||
for (i in 0 until player.mediaItemCount) {
|
||||
queue.add(player.getMediaItemAt(i))
|
||||
}
|
||||
return queue
|
||||
}
|
||||
|
||||
private fun setPlayer(oldPlayer: Player?, newPlayer: Player) {
|
||||
if (oldPlayer === newPlayer) return
|
||||
|
||||
oldPlayer?.stop()
|
||||
mediaLibrarySession.player = newPlayer
|
||||
}
|
||||
|
||||
private fun releasePlayer() {
|
||||
if (this::castPlayer.isInitialized) castPlayer.setSessionAvailabilityListener(null)
|
||||
if (this::castPlayer.isInitialized) castPlayer.release()
|
||||
player.release()
|
||||
mediaLibrarySession.release()
|
||||
automotiveRepository.deleteMetadata()
|
||||
}
|
||||
|
||||
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
|
||||
|
||||
override fun onCastSessionAvailable() {
|
||||
val currentQueue = getQueueFromPlayer(player)
|
||||
val currentIndex = player.currentMediaItemIndex
|
||||
val currentPosition = player.currentPosition
|
||||
val isPlaying = player.playWhenReady
|
||||
|
||||
setPlayer(player, castPlayer)
|
||||
|
||||
castPlayer.setMediaItems(currentQueue, currentIndex, currentPosition)
|
||||
castPlayer.playWhenReady = isPlaying
|
||||
castPlayer.prepare()
|
||||
}
|
||||
|
||||
override fun onCastSessionUnavailable() {
|
||||
val currentQueue = getQueueFromPlayer(castPlayer)
|
||||
val currentIndex = castPlayer.currentMediaItemIndex
|
||||
val currentPosition = castPlayer.currentPosition
|
||||
val isPlaying = castPlayer.playWhenReady
|
||||
|
||||
setPlayer(castPlayer, player)
|
||||
|
||||
player.setMediaItems(currentQueue, currentIndex, currentPosition)
|
||||
player.playWhenReady = isPlaying
|
||||
player.prepare()
|
||||
}
|
||||
}
|
||||
|
||||
private const val WIDGET_UPDATE_INTERVAL_MS = 1000L
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
package com.cappielloantonio.tempo.ui.fragment;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.databinding.FragmentToolbarBinding;
|
||||
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory;
|
||||
|
||||
@UnstableApi
|
||||
public class ToolbarFragment extends Fragment {
|
||||
private static final String TAG = "ToolbarFragment";
|
||||
|
||||
private FragmentToolbarBinding bind;
|
||||
private MainActivity activity;
|
||||
|
||||
public ToolbarFragment() {
|
||||
// Required empty public constructor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(@NonNull Menu menu, @NonNull MenuInflater inflater) {
|
||||
super.onCreateOptionsMenu(menu, inflater);
|
||||
inflater.inflate(R.menu.main_page_menu, menu);
|
||||
CastButtonFactory.setUpMediaRouteButton(requireContext(), menu, R.id.media_route_menu_item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
activity = (MainActivity) getActivity();
|
||||
|
||||
bind = FragmentToolbarBinding.inflate(inflater, container, false);
|
||||
View view = bind.getRoot();
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
if (item.getItemId() == R.id.action_search) {
|
||||
activity.navController.navigate(R.id.searchFragment);
|
||||
return true;
|
||||
} else if (item.getItemId() == R.id.action_settings) {
|
||||
activity.navController.navigate(R.id.settingsFragment);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
package com.cappielloantonio.tempo.util;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
|
||||
public class Flavors {
|
||||
public static void initializeCastContext(Context context) {
|
||||
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) == ConnectionResult.SUCCESS)
|
||||
CastContext.getSharedInstance(context);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue