Migrate AutoMediaBrowser

This commit is contained in:
tzugen 2022-04-04 21:18:07 +02:00
parent 2f7f47783a
commit dd65a12b53
No known key found for this signature in database
GPG Key ID: 61E9C34BC10EC930
13 changed files with 465 additions and 990 deletions

View File

@ -21,6 +21,7 @@ multidex = "2.0.1"
room = "2.4.0" room = "2.4.0"
kotlin = "1.6.10" kotlin = "1.6.10"
kotlinxCoroutines = "1.6.0-native-mt" kotlinxCoroutines = "1.6.0-native-mt"
kotlinxGuava = "1.6.0"
viewModelKtx = "2.3.0" viewModelKtx = "2.3.0"
retrofit = "2.9.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" } kotlinStdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } kotlinReflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" }
kotlinxCoroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutines" } 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" } retrofit = { module = "com.squareup.retrofit2:retrofit", version.ref = "retrofit" }
gsonConverter = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" } gsonConverter = { module = "com.squareup.retrofit2:converter-gson", version.ref = "retrofit" }
jacksonConverter = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" } jacksonConverter = { module = "com.squareup.retrofit2:converter-jackson", version.ref = "retrofit" }

View File

@ -112,6 +112,7 @@ dependencies {
implementation libs.kotlinStdlib implementation libs.kotlinStdlib
implementation libs.kotlinxCoroutines implementation libs.kotlinxCoroutines
implementation libs.kotlinxGuava
implementation libs.koinAndroid implementation libs.koinAndroid
implementation libs.okhttpLogging implementation libs.okhttpLogging
implementation libs.fastScroll implementation libs.fastScroll

View File

@ -30,7 +30,7 @@ class CachedDataSource(
) : BaseDataSource(false) { ) : BaseDataSource(false) {
class Factory( class Factory(
var upstreamDataSourceFactory: DataSource.Factory private var upstreamDataSourceFactory: DataSource.Factory
) : DataSource.Factory { ) : DataSource.Factory {
private var eventListener: EventListener? = null private var eventListener: EventListener? = null
@ -112,16 +112,16 @@ class CachedDataSource(
} }
override fun read(buffer: ByteArray, offset: Int, length: Int): Int { override fun read(buffer: ByteArray, offset: Int, length: Int): Int {
if (cachePath != null) { return if (cachePath != null) {
try { try {
return readInternal(buffer, offset, length) readInternal(buffer, offset, length)
} catch (e: IOException) { } catch (e: IOException) {
throw HttpDataSource.HttpDataSourceException.createForIOException( throw HttpDataSource.HttpDataSourceException.createForIOException(
e, Util.castNonNull(dataSpec), HttpDataSource.HttpDataSourceException.TYPE_READ e, Util.castNonNull(dataSpec), HttpDataSource.HttpDataSourceException.TYPE_READ
) )
} }
} else { } else {
return upstreamDataSource.read(buffer, offset, length) upstreamDataSource.read(buffer, offset, length)
} }
} }

View File

@ -64,20 +64,6 @@ class LegacyPlaylistManager : KoinComponent {
currentPlaying = mediaItemCache[item?.mediaMetadata?.mediaUri.toString()] 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 @Synchronized
fun clearPlaylist() { fun clearPlaylist() {
_playlist.clear() _playlist.clear()

View File

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

View File

@ -50,7 +50,6 @@ internal class MediaNotificationProvider(context: Context) :
context, context,
NOTIFICATION_CHANNEL_ID NOTIFICATION_CHANNEL_ID
) )
// TODO(b/193193926): Filter actions depending on the player's available commands.
// Skip to previous action. // Skip to previous action.
builder.addAction( builder.addAction(
actionFactory.createMediaAction( actionFactory.createMediaAction(

View File

@ -18,8 +18,6 @@ package org.moire.ultrasonic.playback
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.media3.common.AudioAttributes import androidx.media3.common.AudioAttributes
import androidx.media3.common.C import androidx.media3.common.C
import androidx.media3.common.C.CONTENT_TYPE_MUSIC 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.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession 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.KoinComponent
import org.koin.core.component.inject import org.koin.core.component.inject
import org.moire.ultrasonic.activity.NavigationActivity import org.moire.ultrasonic.activity.NavigationActivity
@ -48,94 +41,7 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var dataSourceFactory: DataSource.Factory private lateinit var dataSourceFactory: DataSource.Factory
private val librarySessionCallback = CustomMediaLibrarySessionCallback() private lateinit var librarySessionCallback: MediaLibrarySession.MediaLibrarySessionCallback
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
}
}
}
/* /*
* For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer, * For some reason the LocalConfiguration of MediaItem are stripped somewhere in ExoPlayer,
@ -148,11 +54,9 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
mediaItem: MediaItem mediaItem: MediaItem
): MediaItem { ): MediaItem {
// Again, set the Uri, so that it will get a LocalConfiguration // Again, set the Uri, so that it will get a LocalConfiguration
val item = mediaItem.buildUpon() return mediaItem.buildUpon()
.setUri(mediaItem.mediaMetadata.mediaUri) .setUri(mediaItem.mediaMetadata.mediaUri)
.build() .build()
return item
} }
} }
@ -202,9 +106,10 @@ class PlaybackService : MediaLibraryService(), KoinComponent {
// Enable audio offload // Enable audio offload
player.experimentalSetOffloadSchedulingEnabled(true) 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) mediaLibrarySession = MediaLibrarySession.Builder(this, player, librarySessionCallback)
.setMediaItemFiller(CustomMediaItemFiller()) .setMediaItemFiller(CustomMediaItemFiller())
.setSessionActivity(getPendingIntentForContent()) .setSessionActivity(getPendingIntentForContent())

View File

@ -64,7 +64,7 @@ class Downloader(
private val rxBusSubscription: CompositeDisposable = CompositeDisposable() private val rxBusSubscription: CompositeDisposable = CompositeDisposable()
var downloadChecker = object : Runnable { private var downloadChecker = object : Runnable {
override fun run() { override fun run() {
try { try {
Timber.w("Checking Downloads") Timber.w("Checking Downloads")
@ -399,11 +399,11 @@ class Downloader(
val fileLength = Storage.getFromPath(downloadFile.partialFile)?.length ?: 0 val fileLength = Storage.getFromPath(downloadFile.partialFile)?.length ?: 0
needsDownloading = ( needsDownloading = (
downloadFile.desiredBitRate == 0 || downloadFile.desiredBitRate == 0 ||
duration == null || duration == null ||
duration == 0 || duration == 0 ||
fileLength == 0L fileLength == 0L
) )
if (needsDownloading) { if (needsDownloading) {
// Attempt partial HTTP GET, appending to the file if it exists. // Attempt partial HTTP GET, appending to the file if it exists.

View File

@ -8,6 +8,7 @@ package org.moire.ultrasonic.service
import android.content.Context import android.content.Context
import android.os.Handler import android.os.Handler
import android.os.Looper
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -145,7 +146,7 @@ class JukeboxMediaPlayer(private val downloader: Downloader) {
private fun disableJukeboxOnError(x: Throwable, resourceId: Int) { private fun disableJukeboxOnError(x: Throwable, resourceId: Int) {
Timber.w(x.toString()) Timber.w(x.toString())
val context = applicationContext() val context = applicationContext()
Handler().post { toast(context, resourceId, false) } Handler(Looper.getMainLooper()).post { toast(context, resourceId, false) }
mediaPlayerControllerLazy.value.isJukeboxEnabled = false mediaPlayerControllerLazy.value.isJukeboxEnabled = false
} }

View File

@ -66,12 +66,11 @@ class MediaPlayerLifecycleSupport : KoinComponent {
if (!created) return if (!created) return
// TODO playbackStateSerializer.serializeNow(
// playbackStateSerializer.serializeNow( mediaPlayerController.playList,
// downloader.getPlaylist(), mediaPlayerController.currentMediaItemIndex,
// downloader.currentPlayingIndex, mediaPlayerController.playerPosition
// mediaPlayerController.playerPosition )
// )
mediaPlayerController.clear(false) mediaPlayerController.clear(false)
mediaButtonEventSubscription?.dispose() mediaButtonEventSubscription?.dispose()
@ -110,10 +109,10 @@ class MediaPlayerLifecycleSupport : KoinComponent {
val autoStart = val autoStart =
keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE ||
keyCode == KeyEvent.KEYCODE_MEDIA_PLAY || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY ||
keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS || keyCode == KeyEvent.KEYCODE_MEDIA_PREVIOUS ||
keyCode == KeyEvent.KEYCODE_MEDIA_NEXT keyCode == KeyEvent.KEYCODE_MEDIA_NEXT
// We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start // We can receive intents (e.g. MediaButton) when everything is stopped, so we need to start
onCreate(autoStart) { onCreate(autoStart) {
@ -150,10 +149,10 @@ class MediaPlayerLifecycleSupport : KoinComponent {
return return
val autoStart = action == Constants.CMD_PLAY || val autoStart = action == Constants.CMD_PLAY ||
action == Constants.CMD_RESUME_OR_PLAY || action == Constants.CMD_RESUME_OR_PLAY ||
action == Constants.CMD_TOGGLEPAUSE || action == Constants.CMD_TOGGLEPAUSE ||
action == Constants.CMD_PREVIOUS || action == Constants.CMD_PREVIOUS ||
action == Constants.CMD_NEXT action == Constants.CMD_NEXT
// We can receive intents when everything is stopped, so we need to start // We can receive intents when everything is stopped, so we need to start
onCreate(autoStart) { onCreate(autoStart) {

View File

@ -53,7 +53,7 @@ class PlaybackStateSerializer : KoinComponent {
} }
} }
private fun serializeNow( fun serializeNow(
songs: Iterable<DownloadFile>, songs: Iterable<DownloadFile>,
currentPlayingIndex: Int, currentPlayingIndex: Int,
currentPlayingPosition: Int currentPlayingPosition: Int

View File

@ -9,10 +9,8 @@ package org.moire.ultrasonic.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.PendingIntent
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
@ -28,18 +26,20 @@ import android.net.Uri
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.net.wifi.WifiManager.WifiLock import android.net.wifi.WifiManager.WifiLock
import android.os.Build import android.os.Build
import android.os.Bundle
import android.os.Environment import android.os.Environment
import android.os.Parcelable
import android.support.v4.media.MediaDescriptionCompat
import android.text.TextUtils import android.text.TextUtils
import android.util.TypedValue import android.util.TypedValue
import android.view.Gravity import android.view.Gravity
import android.view.KeyEvent
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.Toast import android.widget.Toast
import androidx.annotation.AnyRes 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.Closeable
import java.io.UnsupportedEncodingException import java.io.UnsupportedEncodingException
import java.security.MessageDigest import java.security.MessageDigest
@ -49,15 +49,6 @@ import java.util.concurrent.TimeUnit
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.roundToInt 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 LINE_LENGTH = 60
private const val DEGRADE_PRECISION_AFTER = 10 private const val DEGRADE_PRECISION_AFTER = 10
@ -77,11 +68,6 @@ object Util {
private var MEGA_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null private var MEGA_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null
private var KILO_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null private var KILO_BYTE_LOCALIZED_FORMAT: DecimalFormat? = null
private var 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() // Used by hexEncode()
private val HEX_DIGITS = private val HEX_DIGITS =
@ -448,150 +434,6 @@ object Util {
return musicDirectory 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 @JvmStatic
@Suppress("MagicNumber") @Suppress("MagicNumber")
fun getNotificationImageSize(context: Context): Int { fun getNotificationImageSize(context: Context): Int {
@ -667,7 +509,7 @@ object Util {
val hours = TimeUnit.MILLISECONDS.toHours(millis) val hours = TimeUnit.MILLISECONDS.toHours(millis)
val minutes = TimeUnit.MILLISECONDS.toMinutes(millis) - TimeUnit.HOURS.toMinutes(hours) val minutes = TimeUnit.MILLISECONDS.toMinutes(millis) - TimeUnit.HOURS.toMinutes(hours)
val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) - val seconds = TimeUnit.MILLISECONDS.toSeconds(millis) -
TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes) TimeUnit.MINUTES.toSeconds(hours * MINUTES_IN_HOUR + minutes)
return when { return when {
hours >= DEGRADE_PRECISION_AFTER -> { hours >= DEGRADE_PRECISION_AFTER -> {
@ -761,9 +603,9 @@ object Util {
fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri { fun getUriToDrawable(context: Context, @AnyRes drawableId: Int): Uri {
return Uri.parse( return Uri.parse(
ContentResolver.SCHEME_ANDROID_RESOURCE + ContentResolver.SCHEME_ANDROID_RESOURCE +
"://" + context.resources.getResourcePackageName(drawableId) + "://" + context.resources.getResourcePackageName(drawableId) +
'/' + context.resources.getResourceTypeName(drawableId) + '/' + context.resources.getResourceTypeName(drawableId) +
'/' + context.resources.getResourceEntryName(drawableId) '/' + context.resources.getResourceEntryName(drawableId)
) )
} }
@ -776,39 +618,6 @@ object Util {
var fileFormat: String?, 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") @Suppress("ComplexMethod", "LongMethod")
fun readableEntryDescription(song: Track): ReadableEntryDescription { fun readableEntryDescription(song: Track): ReadableEntryDescription {
val artist = StringBuilder(LINE_LENGTH) val artist = StringBuilder(LINE_LENGTH)
@ -834,8 +643,8 @@ object Util {
if (artistName != null) { if (artistName != null) {
if (Settings.shouldDisplayBitrateWithArtist && ( if (Settings.shouldDisplayBitrateWithArtist && (
!bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank() !bitRate.isNullOrBlank() || !fileFormat.isNullOrBlank()
) )
) { ) {
artist.append(artistName).append(" (").append( artist.append(artistName).append(" (").append(
String.format( 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 { fun getConnectivityManager(): ConnectivityManager {
val context = appContext() val context = appContext()
return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager return context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager