Migrate AutoMediaBrowser
This commit is contained in:
parent
2f7f47783a
commit
dd65a12b53
|
@ -21,6 +21,7 @@ multidex = "2.0.1"
|
|||
room = "2.4.0"
|
||||
kotlin = "1.6.10"
|
||||
kotlinxCoroutines = "1.6.0-native-mt"
|
||||
kotlinxGuava = "1.6.0"
|
||||
viewModelKtx = "2.3.0"
|
||||
|
||||
retrofit = "2.9.0"
|
||||
|
@ -74,6 +75,7 @@ media3session = { module = "androidx.media3:media3-session", version.r
|
|||
kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
|
||||
kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
|
||||
kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" }
|
||||
kotlinxGuava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "kotlinxGuava"}
|
||||
retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
|
||||
gsonConverter = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
|
||||
jacksonConverter = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" }
|
||||
|
|
|
@ -112,6 +112,7 @@ dependencies {
|
|||
|
||||
implementation libs.kotlinStdlib
|
||||
implementation libs.kotlinxCoroutines
|
||||
implementation libs.kotlinxGuava
|
||||
implementation libs.koinAndroid
|
||||
implementation libs.okhttpLogging
|
||||
implementation libs.fastScroll
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -30,7 +30,7 @@ class CachedDataSource(
|
|||
) : BaseDataSource(false) {
|
||||
|
||||
class Factory(
|
||||
var upstreamDataSourceFactory: DataSource.Factory
|
||||
private var upstreamDataSourceFactory: DataSource.Factory
|
||||
) : DataSource.Factory {
|
||||
|
||||
private var eventListener: EventListener? = null
|
||||
|
@ -112,16 +112,16 @@ class CachedDataSource(
|
|||
}
|
||||
|
||||
override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
|
||||
if (cachePath != null) {
|
||||
return if (cachePath != null) {
|
||||
try {
|
||||
return readInternal(buffer, offset, length)
|
||||
readInternal(buffer, offset, length)
|
||||
} catch (e: IOException) {
|
||||
throw HttpDataSource.HttpDataSourceException.createForIOException(
|
||||
e, Util.castNonNull(dataSpec), HttpDataSource.HttpDataSourceException.TYPE_READ
|
||||
)
|
||||
}
|
||||
} else {
|
||||
return upstreamDataSource.read(buffer, offset, length)
|
||||
upstreamDataSource.read(buffer, offset, length)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -64,20 +64,6 @@ class LegacyPlaylistManager : KoinComponent {
|
|||
currentPlaying = mediaItemCache[item?.mediaMetadata?.mediaUri.toString()]
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clearIncomplete() {
|
||||
val iterator = _playlist.iterator()
|
||||
var changedPlaylist = false
|
||||
while (iterator.hasNext()) {
|
||||
val downloadFile = iterator.next()
|
||||
if (!downloadFile.isCompleteFileAvailable) {
|
||||
iterator.remove()
|
||||
changedPlaylist = true
|
||||
}
|
||||
}
|
||||
if (changedPlaylist) playlistUpdateRevision++
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clearPlaylist() {
|
||||
_playlist.clear()
|
||||
|
|
|
@ -1,247 +0,0 @@
|
|||
/*
|
||||
* Copyright 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.moire.ultrasonic.playback
|
||||
|
||||
import android.content.res.AssetManager
|
||||
import android.net.Uri
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.MediaMetadata
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_MIXED
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_NONE
|
||||
import androidx.media3.common.MediaMetadata.FOLDER_TYPE_PLAYLISTS
|
||||
import com.google.common.collect.ImmutableList
|
||||
import org.json.JSONObject
|
||||
|
||||
/**
|
||||
* A sample media catalog that represents media items as a tree.
|
||||
*
|
||||
* It fetched the data from {@code catalog.json}. The root's children are folders containing media
|
||||
* items from the same album/artist/genre.
|
||||
*
|
||||
* Each app should have their own way of representing the tree. MediaItemTree is used for
|
||||
* demonstration purpose only.
|
||||
*/
|
||||
object MediaItemTree {
|
||||
private var treeNodes: MutableMap<String, MediaItemNode> = mutableMapOf()
|
||||
private var titleMap: MutableMap<String, MediaItemNode> = mutableMapOf()
|
||||
private var isInitialized = false
|
||||
private const val ROOT_ID = "[rootID]"
|
||||
private const val ALBUM_ID = "[albumID]"
|
||||
private const val GENRE_ID = "[genreID]"
|
||||
private const val ARTIST_ID = "[artistID]"
|
||||
private const val ALBUM_PREFIX = "[album]"
|
||||
private const val GENRE_PREFIX = "[genre]"
|
||||
private const val ARTIST_PREFIX = "[artist]"
|
||||
private const val ITEM_PREFIX = "[item]"
|
||||
|
||||
private class MediaItemNode(val item: MediaItem) {
|
||||
private val children: MutableList<MediaItem> = ArrayList()
|
||||
|
||||
fun addChild(childID: String) {
|
||||
this.children.add(treeNodes[childID]!!.item)
|
||||
}
|
||||
|
||||
fun getChildren(): List<MediaItem> {
|
||||
return ImmutableList.copyOf(children)
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildMediaItem(
|
||||
title: String,
|
||||
mediaId: String,
|
||||
isPlayable: Boolean,
|
||||
@MediaMetadata.FolderType folderType: Int,
|
||||
album: String? = null,
|
||||
artist: String? = null,
|
||||
genre: String? = null,
|
||||
sourceUri: Uri? = null,
|
||||
imageUri: Uri? = null,
|
||||
): MediaItem {
|
||||
// TODO(b/194280027): add artwork
|
||||
val metadata =
|
||||
MediaMetadata.Builder()
|
||||
.setAlbumTitle(album)
|
||||
.setTitle(title)
|
||||
.setArtist(artist)
|
||||
.setGenre(genre)
|
||||
.setFolderType(folderType)
|
||||
.setIsPlayable(isPlayable)
|
||||
.setArtworkUri(imageUri)
|
||||
.build()
|
||||
return MediaItem.Builder()
|
||||
.setMediaId(mediaId)
|
||||
.setMediaMetadata(metadata)
|
||||
.setUri(sourceUri)
|
||||
.build()
|
||||
}
|
||||
|
||||
fun initialize(assets: AssetManager) {
|
||||
if (isInitialized) return
|
||||
isInitialized = true
|
||||
// create root and folders for album/artist/genre.
|
||||
treeNodes[ROOT_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Root Folder",
|
||||
mediaId = ROOT_ID,
|
||||
isPlayable = false,
|
||||
folderType = FOLDER_TYPE_MIXED
|
||||
)
|
||||
)
|
||||
treeNodes[ALBUM_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Album Folder",
|
||||
mediaId = ALBUM_ID,
|
||||
isPlayable = false,
|
||||
folderType = FOLDER_TYPE_MIXED
|
||||
)
|
||||
)
|
||||
treeNodes[ARTIST_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Artist Folder",
|
||||
mediaId = ARTIST_ID,
|
||||
isPlayable = false,
|
||||
folderType = FOLDER_TYPE_MIXED
|
||||
)
|
||||
)
|
||||
treeNodes[GENRE_ID] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = "Genre Folder",
|
||||
mediaId = GENRE_ID,
|
||||
isPlayable = false,
|
||||
folderType = FOLDER_TYPE_MIXED
|
||||
)
|
||||
)
|
||||
treeNodes[ROOT_ID]!!.addChild(ALBUM_ID)
|
||||
treeNodes[ROOT_ID]!!.addChild(ARTIST_ID)
|
||||
treeNodes[ROOT_ID]!!.addChild(GENRE_ID)
|
||||
|
||||
// Here, parse the json file in asset for media list.
|
||||
// We use a file in asset for demo purpose
|
||||
// val jsonObject = JSONObject(loadJSONFromAsset(assets))
|
||||
// val mediaList = jsonObject.getJSONArray("media")
|
||||
//
|
||||
// // create subfolder with same artist, album, etc.
|
||||
// for (i in 0 until mediaList.length()) {
|
||||
// addNodeToTree(mediaList.getJSONObject(i))
|
||||
// }
|
||||
}
|
||||
|
||||
private fun addNodeToTree(mediaObject: JSONObject) {
|
||||
|
||||
val id = mediaObject.getString("id")
|
||||
val album = mediaObject.getString("album")
|
||||
val title = mediaObject.getString("title")
|
||||
val artist = mediaObject.getString("artist")
|
||||
val genre = mediaObject.getString("genre")
|
||||
val sourceUri = Uri.parse(mediaObject.getString("source"))
|
||||
val imageUri = Uri.parse(mediaObject.getString("image"))
|
||||
// key of such items in tree
|
||||
val idInTree = ITEM_PREFIX + id
|
||||
val albumFolderIdInTree = ALBUM_PREFIX + album
|
||||
val artistFolderIdInTree = ARTIST_PREFIX + artist
|
||||
val genreFolderIdInTree = GENRE_PREFIX + genre
|
||||
|
||||
treeNodes[idInTree] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = title,
|
||||
mediaId = idInTree,
|
||||
isPlayable = true,
|
||||
album = album,
|
||||
artist = artist,
|
||||
genre = genre,
|
||||
sourceUri = sourceUri,
|
||||
imageUri = imageUri,
|
||||
folderType = FOLDER_TYPE_NONE
|
||||
)
|
||||
)
|
||||
|
||||
titleMap[title.lowercase()] = treeNodes[idInTree]!!
|
||||
|
||||
if (!treeNodes.containsKey(albumFolderIdInTree)) {
|
||||
treeNodes[albumFolderIdInTree] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = album,
|
||||
mediaId = albumFolderIdInTree,
|
||||
isPlayable = true,
|
||||
folderType = FOLDER_TYPE_PLAYLISTS
|
||||
)
|
||||
)
|
||||
treeNodes[ALBUM_ID]!!.addChild(albumFolderIdInTree)
|
||||
}
|
||||
treeNodes[albumFolderIdInTree]!!.addChild(idInTree)
|
||||
|
||||
// add into artist folder
|
||||
if (!treeNodes.containsKey(artistFolderIdInTree)) {
|
||||
treeNodes[artistFolderIdInTree] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = artist,
|
||||
mediaId = artistFolderIdInTree,
|
||||
isPlayable = true,
|
||||
folderType = FOLDER_TYPE_PLAYLISTS
|
||||
)
|
||||
)
|
||||
treeNodes[ARTIST_ID]!!.addChild(artistFolderIdInTree)
|
||||
}
|
||||
treeNodes[artistFolderIdInTree]!!.addChild(idInTree)
|
||||
|
||||
// add into genre folder
|
||||
if (!treeNodes.containsKey(genreFolderIdInTree)) {
|
||||
treeNodes[genreFolderIdInTree] =
|
||||
MediaItemNode(
|
||||
buildMediaItem(
|
||||
title = genre,
|
||||
mediaId = genreFolderIdInTree,
|
||||
isPlayable = true,
|
||||
folderType = FOLDER_TYPE_PLAYLISTS
|
||||
)
|
||||
)
|
||||
treeNodes[GENRE_ID]!!.addChild(genreFolderIdInTree)
|
||||
}
|
||||
treeNodes[genreFolderIdInTree]!!.addChild(idInTree)
|
||||
}
|
||||
|
||||
fun getItem(id: String): MediaItem? {
|
||||
return treeNodes[id]?.item
|
||||
}
|
||||
|
||||
fun getRootItem(): MediaItem {
|
||||
return treeNodes[ROOT_ID]!!.item
|
||||
}
|
||||
|
||||
fun getChildren(id: String): List<MediaItem>? {
|
||||
return treeNodes[id]?.getChildren()
|
||||
}
|
||||
|
||||
fun getRandomItem(): MediaItem {
|
||||
var curRoot = getRootItem()
|
||||
while (curRoot.mediaMetadata.folderType != FOLDER_TYPE_NONE) {
|
||||
val children = getChildren(curRoot.mediaId)!!
|
||||
curRoot = children.random()
|
||||
}
|
||||
return curRoot
|
||||
}
|
||||
|
||||
fun getItemFromTitle(title: String): MediaItem? {
|
||||
return titleMap[title]?.item
|
||||
}
|
||||
}
|
|
@ -50,7 +50,6 @@ internal class MediaNotificationProvider(context: Context) :
|
|||
context,
|
||||
NOTIFICATION_CHANNEL_ID
|
||||
)
|
||||
// TODO(b/193193926): Filter actions depending on the player's available commands.
|
||||
// Skip to previous action.
|
||||
builder.addAction(
|
||||
actionFactory.createMediaAction(
|
||||
|
|
|
@ -18,8 +18,6 @@ package org.moire.ultrasonic.playback
|
|||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import androidx.media3.common.AudioAttributes
|
||||
import androidx.media3.common.C
|
||||
import androidx.media3.common.C.CONTENT_TYPE_MUSIC
|
||||
|
@ -29,13 +27,8 @@ import androidx.media3.datasource.DataSource
|
|||
import androidx.media3.exoplayer.DefaultRenderersFactory
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
|
||||
import androidx.media3.session.LibraryResult
|
||||
import androidx.media3.session.MediaLibraryService
|
||||
import androidx.media3.session.MediaSession
|
||||
import androidx.media3.session.SessionResult
|
||||
import com.google.common.collect.ImmutableList
|
||||
import com.google.common.util.concurrent.Futures
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import org.moire.ultrasonic.activity.NavigationActivity
|
||||
|
@ -48,94 +41,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
|||
private lateinit var mediaLibrarySession: MediaLibrarySession
|
||||
private lateinit var dataSourceFactory: DataSource.Factory
|
||||
|
||||
private val librarySessionCallback = CustomMediaLibrarySessionCallback()
|
||||
|
||||
companion object {
|
||||
private const val SEARCH_QUERY_PREFIX_COMPAT = "androidx://media3-session/playFromSearch"
|
||||
private const val SEARCH_QUERY_PREFIX = "androidx://media3-session/setMediaUri"
|
||||
}
|
||||
|
||||
private inner class CustomMediaLibrarySessionCallback :
|
||||
MediaLibrarySession.MediaLibrarySessionCallback {
|
||||
override fun onGetLibraryRoot(
|
||||
session: MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
params: LibraryParams?
|
||||
): ListenableFuture<LibraryResult<MediaItem>> {
|
||||
return Futures.immediateFuture(
|
||||
LibraryResult.ofItem(
|
||||
MediaItemTree.getRootItem(),
|
||||
params
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onGetItem(
|
||||
session: MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
mediaId: String
|
||||
): ListenableFuture<LibraryResult<MediaItem>> {
|
||||
val item =
|
||||
MediaItemTree.getItem(mediaId)
|
||||
?: return Futures.immediateFuture(
|
||||
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
|
||||
)
|
||||
return Futures.immediateFuture(LibraryResult.ofItem(item, /* params= */ null))
|
||||
}
|
||||
|
||||
override fun onGetChildren(
|
||||
session: MediaLibrarySession,
|
||||
browser: MediaSession.ControllerInfo,
|
||||
parentId: String,
|
||||
page: Int,
|
||||
pageSize: Int,
|
||||
params: LibraryParams?
|
||||
): ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> {
|
||||
val children =
|
||||
MediaItemTree.getChildren(parentId)
|
||||
?: return Futures.immediateFuture(
|
||||
LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)
|
||||
)
|
||||
|
||||
return Futures.immediateFuture(LibraryResult.ofItemList(children, params))
|
||||
}
|
||||
|
||||
private fun setMediaItemFromSearchQuery(query: String) {
|
||||
// Only accept query with pattern "play [Title]" or "[Title]"
|
||||
// Where [Title]: must be exactly matched
|
||||
// If no media with exact name found, play a random media instead
|
||||
val mediaTitle =
|
||||
if (query.startsWith("play ", ignoreCase = true)) {
|
||||
query.drop(5)
|
||||
} else {
|
||||
query
|
||||
}
|
||||
|
||||
val item = MediaItemTree.getItemFromTitle(mediaTitle) ?: MediaItemTree.getRandomItem()
|
||||
player.setMediaItem(item)
|
||||
}
|
||||
|
||||
override fun onSetMediaUri(
|
||||
session: MediaSession,
|
||||
controller: MediaSession.ControllerInfo,
|
||||
uri: Uri,
|
||||
extras: Bundle
|
||||
): Int {
|
||||
|
||||
if (uri.toString().startsWith(SEARCH_QUERY_PREFIX) ||
|
||||
uri.toString().startsWith(SEARCH_QUERY_PREFIX_COMPAT)
|
||||
) {
|
||||
val searchQuery =
|
||||
uri.getQueryParameter("query")
|
||||
?: return SessionResult.RESULT_ERROR_NOT_SUPPORTED
|
||||
setMediaItemFromSearchQuery(searchQuery)
|
||||
|
||||
return SessionResult.RESULT_SUCCESS
|
||||
} else {
|
||||
return SessionResult.RESULT_ERROR_NOT_SUPPORTED
|
||||
}
|
||||
}
|
||||
}
|
||||
private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback
|
||||
|
||||
/*
|
||||
* For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer,
|
||||
|
@ -148,11 +54,9 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
|||
mediaItem: MediaItem
|
||||
): MediaItem {
|
||||
// Again, set the Uri, so that it will get a LocalConfiguration
|
||||
val item = mediaItem.buildUpon()
|
||||
return mediaItem.buildUpon()
|
||||
.setUri(mediaItem.mediaMetadata.mediaUri)
|
||||
.build()
|
||||
|
||||
return item
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -202,9 +106,10 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
|
|||
// Enable audio offload
|
||||
player.experimentalSetOffloadSchedulingEnabled(true)
|
||||
|
||||
MediaItemTree.initialize(assets)
|
||||
// Create browser interface
|
||||
librarySessionCallback = AutoMediaBrowserCallback(player)
|
||||
|
||||
// THIS Will need to use the AutoCalls
|
||||
// This will need to use the AutoCalls
|
||||
mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
|
||||
.setMediaItemFiller(CustomMediaItemFiller())
|
||||
.setSessionActivity(getPendingIntentForContent())
|
||||
|
|
|
@ -64,7 +64,7 @@ class Downloader(
|
|||
|
||||
private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
|
||||
|
||||
var downloadChecker = object : Runnable {
|
||||
private var downloadChecker = object : Runnable {
|
||||
override fun run() {
|
||||
try {
|
||||
Timber.w("Checking Downloads")
|
||||
|
@ -399,11 +399,11 @@ class Downloader(
|
|||
val fileLength = Storage.getFromPath(downloadFile.partialFile)?.length ?: 0
|
||||
|
||||
needsDownloading = (
|
||||
downloadFile.desiredBitRate == 0 ||
|
||||
duration == null ||
|
||||
duration == 0 ||
|
||||
fileLength == 0L
|
||||
)
|
||||
downloadFile.desiredBitRate == 0 ||
|
||||
duration == null ||
|
||||
duration == 0 ||
|
||||
fileLength == 0L
|
||||
)
|
||||
|
||||
if (needsDownloading) {
|
||||
// Attempt partial HTTP GET, appending to the file if it exists.
|
||||
|
|
|
@ -8,6 +8,7 @@ package org.moire.ultrasonic.service
|
|||
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.Gravity
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -145,7 +146,7 @@ class JukeboxMediaPlayer(private val downloader: Downloader) {
|
|||
private fun disableJukeboxOnError(x: Throwable, resourceId: Int) {
|
||||
Timber.w(x.toString())
|
||||
val context = applicationContext()
|
||||
Handler().post { toast(context, resourceId, false) }
|
||||
Handler(Looper.getMainLooper()).post { toast(context, resourceId, false) }
|
||||
mediaPlayerControllerLazy.value.isJukeboxEnabled = false
|
||||
}
|
||||
|
||||
|
|
|
@ -66,12 +66,11 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||
|
||||
if (!created) return
|
||||
|
||||
// TODO
|
||||
// playbackStateSerializer.serializeNow(
|
||||
// downloader.getPlaylist(),
|
||||
// downloader.currentPlayingIndex,
|
||||
// mediaPlayerController.playerPosition
|
||||
// )
|
||||
playbackStateSerializer.serializeNow(
|
||||
mediaPlayerController.playList,
|
||||
mediaPlayerController.currentMediaItemIndex,
|
||||
mediaPlayerController.playerPosition
|
||||
)
|
||||
|
||||
mediaPlayerController.clear(false)
|
||||
mediaButtonEventSubscription?.dispose()
|
||||
|
@ -110,10 +109,10 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||
|
||||
val autoStart =
|
||||
keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE ||
|
||||
keyCode == KeyEvent.KEYCODE_MEDIA_PLAY ||
|
||||
keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
|
||||
keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS ||
|
||||
keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
|
||||
keyCode == KeyEvent.KEYCODE_MEDIA_PLAY ||
|
||||
keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
|
||||
keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS ||
|
||||
keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
|
||||
|
||||
// We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start
|
||||
onCreate(autoStart) {
|
||||
|
@ -150,10 +149,10 @@ class MediaPlayerLifecycleSupport : KoinComponent {
|
|||
return
|
||||
|
||||
val autoStart = action == Constants.CMD_PLAY ||
|
||||
action == Constants.CMD_RESUME_OR_PLAY ||
|
||||
action == Constants.CMD_TOGGLEPAUSE ||
|
||||
action == Constants.CMD_PREVIOUS ||
|
||||
action == Constants.CMD_NEXT
|
||||
action == Constants.CMD_RESUME_OR_PLAY ||
|
||||
action == Constants.CMD_TOGGLEPAUSE ||
|
||||
action == Constants.CMD_PREVIOUS ||
|
||||
action == Constants.CMD_NEXT
|
||||
|
||||
// We can receive intents when everything is stopped, so we need to start
|
||||
onCreate(autoStart) {
|
||||
|
|
|
@ -53,7 +53,7 @@ class PlaybackStateSerializer : KoinComponent {
|
|||
}
|
||||
}
|
||||
|
||||
private fun serializeNow(
|
||||
fun serializeNow(
|
||||
songs: Iterable<DownloadFile>,
|
||||
currentPlayingIndex: Int,
|
||||
currentPlayingPosition: Int
|
||||
|
|
|
@ -9,10 +9,8 @@ package org.moire.ultrasonic.util
|
|||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
|
@ -28,18 +26,20 @@ import android.net.Uri
|
|||
import android.net.wifi.WifiManager
|
||||
import android.net.wifi.WifiManager.WifiLock
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Parcelable
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.text.TextUtils
|
||||
import android.util.TypedValue
|
||||
import android.view.Gravity
|
||||
import android.view.KeyEvent
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.AnyRes
|
||||
import androidx.media.utils.MediaConstants
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||
import org.moire.ultrasonic.domain.Bookmark
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import timber.log.Timber
|
||||
import java.io.Closeable
|
||||
import java.io.UnsupportedEncodingException
|
||||
import java.security.MessageDigest
|
||||
|
@ -49,15 +49,6 @@ import java.util.concurrent.TimeUnit
|
|||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
import org.moire.ultrasonic.R
|
||||
import org.moire.ultrasonic.app.UApp.Companion.applicationContext
|
||||
import org.moire.ultrasonic.domain.Bookmark
|
||||
import org.moire.ultrasonic.domain.MusicDirectory
|
||||
import org.moire.ultrasonic.domain.PlayerState
|
||||
import org.moire.ultrasonic.domain.SearchResult
|
||||
import org.moire.ultrasonic.domain.Track
|
||||
import org.moire.ultrasonic.service.DownloadFile
|
||||
import timber.log.Timber
|
||||
|
||||
private const val LINE_LENGTH = 60
|
||||
private const val DEGRADE_PRECISION_AFTER = 10
|
||||
|
@ -77,11 +68,6 @@ object Util {
|
|||
private var MEGA_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null
|
||||
private var KILO_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null
|
||||
private var BYTE_LOCALIZED_FORMAT: DecimalFormat? = null
|
||||
private const val EVENT_META_CHANGED = "org.moire.ultrasonic.EVENT_META_CHANGED"
|
||||
private const val EVENT_PLAYSTATE_CHANGED = "org.moire.ultrasonic.EVENT_PLAYSTATE_CHANGED"
|
||||
private const val CM_AVRCP_PLAYSTATE_CHANGED = "com.android.music.playstatechanged"
|
||||
private const val CM_AVRCP_PLAYBACK_COMPLETE = "com.android.music.playbackcomplete"
|
||||
private const val CM_AVRCP_METADATA_CHANGED = "com.android.music.metachanged"
|
||||
|
||||
// Used by hexEncode()
|
||||
private val HEX_DIGITS =
|
||||
|
@ -448,150 +434,6 @@ object Util {
|
|||
return musicDirectory
|
||||
}
|
||||
|
||||
/**
|
||||
* Broadcasts the given song info as the new song being played.
|
||||
*/
|
||||
fun broadcastNewTrackInfo(context: Context, song: Track?) {
|
||||
val intent = Intent(EVENT_META_CHANGED)
|
||||
if (song != null) {
|
||||
intent.putExtra("title", song.title)
|
||||
intent.putExtra("artist", song.artist)
|
||||
intent.putExtra("album", song.album)
|
||||
val albumArtFile = FileUtil.getAlbumArtFile(song)
|
||||
intent.putExtra("coverart", albumArtFile)
|
||||
} else {
|
||||
intent.putExtra("title", "")
|
||||
intent.putExtra("artist", "")
|
||||
intent.putExtra("album", "")
|
||||
intent.putExtra("coverart", "")
|
||||
}
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
fun broadcastA2dpMetaDataChange(
|
||||
context: Context,
|
||||
playerPosition: Int,
|
||||
currentPlaying: DownloadFile?,
|
||||
listSize: Int,
|
||||
id: Int
|
||||
) {
|
||||
if (!Settings.shouldSendBluetoothNotifications) return
|
||||
|
||||
var song: Track? = null
|
||||
val avrcpIntent = Intent(CM_AVRCP_METADATA_CHANGED)
|
||||
if (currentPlaying != null) song = currentPlaying.track
|
||||
|
||||
fillIntent(avrcpIntent, song, playerPosition, id, listSize)
|
||||
|
||||
context.sendBroadcast(avrcpIntent)
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
fun broadcastA2dpPlayStatusChange(
|
||||
context: Context,
|
||||
state: PlayerState?,
|
||||
newSong: Track?,
|
||||
listSize: Int,
|
||||
id: Int,
|
||||
playerPosition: Int
|
||||
) {
|
||||
if (!Settings.shouldSendBluetoothNotifications) return
|
||||
|
||||
if (newSong != null) {
|
||||
|
||||
val avrcpIntent = Intent(
|
||||
if (state == PlayerState.COMPLETED) CM_AVRCP_PLAYBACK_COMPLETE
|
||||
else CM_AVRCP_PLAYSTATE_CHANGED
|
||||
)
|
||||
|
||||
fillIntent(avrcpIntent, newSong, playerPosition, id, listSize)
|
||||
|
||||
if (state != PlayerState.COMPLETED) {
|
||||
when (state) {
|
||||
PlayerState.STARTED -> avrcpIntent.putExtra("playing", true)
|
||||
PlayerState.STOPPED,
|
||||
PlayerState.PAUSED -> avrcpIntent.putExtra("playing", false)
|
||||
else -> return // No need to broadcast.
|
||||
}
|
||||
}
|
||||
|
||||
context.sendBroadcast(avrcpIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun fillIntent(
|
||||
intent: Intent,
|
||||
song: Track?,
|
||||
playerPosition: Int,
|
||||
id: Int,
|
||||
listSize: Int
|
||||
) {
|
||||
if (song == null) {
|
||||
intent.putExtra("track", "")
|
||||
intent.putExtra("track_name", "")
|
||||
intent.putExtra("artist", "")
|
||||
intent.putExtra("artist_name", "")
|
||||
intent.putExtra("album", "")
|
||||
intent.putExtra("album_name", "")
|
||||
intent.putExtra("album_artist", "")
|
||||
intent.putExtra("album_artist_name", "")
|
||||
|
||||
if (Settings.shouldSendBluetoothAlbumArt) {
|
||||
intent.putExtra("coverart", null as Parcelable?)
|
||||
intent.putExtra("cover", null as Parcelable?)
|
||||
}
|
||||
|
||||
intent.putExtra("ListSize", 0.toLong())
|
||||
intent.putExtra("id", 0.toLong())
|
||||
intent.putExtra("duration", 0.toLong())
|
||||
intent.putExtra("position", 0.toLong())
|
||||
} else {
|
||||
val title = song.title
|
||||
val artist = song.artist
|
||||
val album = song.album
|
||||
val duration = song.duration
|
||||
|
||||
intent.putExtra("track", title)
|
||||
intent.putExtra("track_name", title)
|
||||
intent.putExtra("artist", artist)
|
||||
intent.putExtra("artist_name", artist)
|
||||
intent.putExtra("album", album)
|
||||
intent.putExtra("album_name", album)
|
||||
intent.putExtra("album_artist", artist)
|
||||
intent.putExtra("album_artist_name", artist)
|
||||
|
||||
if (Settings.shouldSendBluetoothAlbumArt) {
|
||||
val albumArtFile = FileUtil.getAlbumArtFile(song)
|
||||
intent.putExtra("coverart", albumArtFile)
|
||||
intent.putExtra("cover", albumArtFile)
|
||||
}
|
||||
|
||||
intent.putExtra("position", playerPosition.toLong())
|
||||
intent.putExtra("id", id.toLong())
|
||||
intent.putExtra("ListSize", listSize.toLong())
|
||||
|
||||
if (duration != null) {
|
||||
intent.putExtra("duration", duration.toLong())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Broadcasts the given player state as the one being set.
|
||||
*/
|
||||
fun broadcastPlaybackStatusChange(context: Context, state: PlayerState?) {
|
||||
val intent = Intent(EVENT_PLAYSTATE_CHANGED)
|
||||
when (state) {
|
||||
PlayerState.STARTED -> intent.putExtra("state", "play")
|
||||
PlayerState.STOPPED -> intent.putExtra("state", "stop")
|
||||
PlayerState.PAUSED -> intent.putExtra("state", "pause")
|
||||
PlayerState.COMPLETED -> intent.putExtra("state", "complete")
|
||||
else -> return // No need to broadcast.
|
||||
}
|
||||
context.sendBroadcast(intent)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Suppress("MagicNumber")
|
||||
fun getNotificationImageSize(context: Context): Int {
|
||||
|
@ -667,7 +509,7 @@ object Util {
|
|||
val hours = TimeUnit.MILLISECONDS.toHours(millis)
|
||||
val minutes = TimeUnit.MILLISECONDS.toMinutes(millis) - TimeUnit.HOURS.toMinutes(hours)
|
||||
val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) -
|
||||
TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes)
|
||||
TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes)
|
||||
|
||||
return when {
|
||||
hours >= DEGRADE_PRECISION_AFTER -> {
|
||||
|
@ -761,9 +603,9 @@ object Util {
|
|||
fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri {
|
||||
return Uri.parse(
|
||||
ContentResolver.SCHEME_ANDROID_RESOURCE +
|
||||
"://" + context.resources.getResourcePackageName(drawableId) +
|
||||
'/' + context.resources.getResourceTypeName(drawableId) +
|
||||
'/' + context.resources.getResourceEntryName(drawableId)
|
||||
"://" + context.resources.getResourcePackageName(drawableId) +
|
||||
'/' + context.resources.getResourceTypeName(drawableId) +
|
||||
'/' + context.resources.getResourceEntryName(drawableId)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -776,39 +618,6 @@ object Util {
|
|||
var fileFormat: String?,
|
||||
)
|
||||
|
||||
fun getMediaDescriptionForEntry(
|
||||
song: Track,
|
||||
mediaId: String? = null,
|
||||
groupNameId: Int? = null
|
||||
): MediaDescriptionCompat {
|
||||
|
||||
val descriptionBuilder = MediaDescriptionCompat.Builder()
|
||||
val desc = readableEntryDescription(song)
|
||||
val title: String
|
||||
|
||||
if (groupNameId != null)
|
||||
descriptionBuilder.setExtras(
|
||||
Bundle().apply {
|
||||
putString(
|
||||
MediaConstants.DESCRIPTION_EXTRAS_KEY_CONTENT_STYLE_GROUP_TITLE,
|
||||
appContext().getString(groupNameId)
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if (desc.trackNumber.isNotEmpty()) {
|
||||
title = "${desc.trackNumber} - ${desc.title}"
|
||||
} else {
|
||||
title = desc.title
|
||||
}
|
||||
|
||||
descriptionBuilder.setTitle(title)
|
||||
descriptionBuilder.setSubtitle(desc.artist)
|
||||
descriptionBuilder.setMediaId(mediaId)
|
||||
|
||||
return descriptionBuilder.build()
|
||||
}
|
||||
|
||||
@Suppress("ComplexMethod", "LongMethod")
|
||||
fun readableEntryDescription(song: Track): ReadableEntryDescription {
|
||||
val artist = StringBuilder(LINE_LENGTH)
|
||||
|
@ -834,8 +643,8 @@ object Util {
|
|||
|
||||
if (artistName != null) {
|
||||
if (Settings.shouldDisplayBitrateWithArtist && (
|
||||
!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank()
|
||||
)
|
||||
!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank()
|
||||
)
|
||||
) {
|
||||
artist.append(artistName).append(" (").append(
|
||||
String.format(
|
||||
|
@ -880,18 +689,6 @@ object Util {
|
|||
)
|
||||
}
|
||||
|
||||
fun getPendingIntentForMediaAction(
|
||||
context: Context,
|
||||
keycode: Int,
|
||||
requestCode: Int
|
||||
): PendingIntent {
|
||||
val intent = Intent(Constants.CMD_PROCESS_KEYCODE)
|
||||
val flags = PendingIntent.FLAG_UPDATE_CURRENT
|
||||
intent.setPackage(context.packageName)
|
||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, KeyEvent(KeyEvent.ACTION_DOWN, keycode))
|
||||
return PendingIntent.getBroadcast(context, requestCode, intent, flags)
|
||||
}
|
||||
|
||||
fun getConnectivityManager(): ConnectivityManager {
|
||||
val context = appContext()
|
||||
return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
|
|
Loading…
Reference in New Issue