Here's a list of changes/features: https://github.com/RetroMusicPlayer/RetroMusicPlayer/releases/tag/v5.0

Internal Changes:
1) Migrated to ViewBinding
2) Migrated to Glide V4
3) Migrated to kotlin version of Material Dialogs
This commit is contained in:
Prathamesh More 2021-09-09 00:00:20 +05:30
parent fc42767031
commit bce6dbfa27
421 changed files with 13285 additions and 5757 deletions

View file

@ -0,0 +1,83 @@
package code.name.monkey.retromusic.cast
import androidx.core.net.toUri
import code.name.monkey.retromusic.cast.RetroWebServer.Companion.MIME_TYPE_AUDIO
import code.name.monkey.retromusic.cast.RetroWebServer.Companion.PART_COVER_ART
import code.name.monkey.retromusic.cast.RetroWebServer.Companion.PART_SONG
import code.name.monkey.retromusic.model.Song
import code.name.monkey.retromusic.util.RetroUtil
import com.google.android.gms.cast.*
import com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED
import com.google.android.gms.cast.MediaMetadata.*
import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.common.images.WebImage
import java.net.MalformedURLException
import java.net.URL
object CastHelper {
private const val CAST_MUSIC_METADATA_ID = "metadata_id"
private const val CAST_MUSIC_METADATA_ALBUM_ID = "metadata_album_id"
private const val CAST_URL_PROTOCOL = "http"
fun castSong(castSession: CastSession, song: Song) {
try {
val remoteMediaClient = castSession.remoteMediaClient
val mediaLoadOptions = MediaLoadOptions.Builder().apply {
setPlayPosition(0)
setAutoplay(true)
}.build()
remoteMediaClient?.load(song.toMediaInfo()!!, mediaLoadOptions)
} catch (e: Exception) {
e.printStackTrace()
}
}
fun castQueue(castSession: CastSession, songs: List<Song>, position: Int, progress: Long) {
try {
val remoteMediaClient = castSession.remoteMediaClient
remoteMediaClient?.queueLoad(
songs.toMediaInfoList(),
position,
MediaStatus.REPEAT_MODE_REPEAT_OFF,
progress,
null
)
} catch (e: Exception) {
e.printStackTrace()
}
}
private fun List<Song>.toMediaInfoList(): Array<MediaQueueItem> {
return map { MediaQueueItem.Builder(it.toMediaInfo()!!).build() }.toTypedArray()
}
private fun Song.toMediaInfo(): MediaInfo? {
val song = this
val baseUrl: URL
try {
baseUrl = URL(CAST_URL_PROTOCOL, RetroUtil.getIpAddress(true), SERVER_PORT, "")
} catch (e: MalformedURLException) {
return null
}
val songUrl = "$baseUrl/$PART_SONG?id=${song.id}"
val albumArtUrl = "$baseUrl/$PART_COVER_ART?id=${song.albumId}"
val musicMetadata = MediaMetadata(MEDIA_TYPE_MUSIC_TRACK).apply {
putInt(CAST_MUSIC_METADATA_ID, song.id.toInt())
putInt(CAST_MUSIC_METADATA_ALBUM_ID, song.albumId.toInt())
putString(KEY_TITLE, song.title)
putString(KEY_ARTIST, song.artistName)
putString(KEY_ALBUM_TITLE, song.albumName)
putInt(KEY_TRACK_NUMBER, song.trackNumber)
addImage(WebImage(albumArtUrl.toUri()))
}
return MediaInfo.Builder(songUrl).apply {
setStreamType(STREAM_TYPE_BUFFERED)
setContentType(MIME_TYPE_AUDIO)
setMetadata(musicMetadata)
setStreamDuration(song.duration)
}.build()
}
}

View file

@ -0,0 +1,41 @@
package code.name.monkey.retromusic.cast
import android.content.Context
import com.google.android.gms.cast.CastMediaControlIntent
import com.google.android.gms.cast.framework.CastOptions
import com.google.android.gms.cast.framework.OptionsProvider
import com.google.android.gms.cast.framework.SessionProvider
import com.google.android.gms.cast.framework.media.CastMediaOptions
import com.google.android.gms.cast.framework.media.MediaIntentReceiver
import com.google.android.gms.cast.framework.media.NotificationOptions
import java.util.*
class CastOptionsProvider : OptionsProvider {
override fun getCastOptions(context: Context): CastOptions {
val buttonActions: MutableList<String> = ArrayList()
buttonActions.add(MediaIntentReceiver.ACTION_SKIP_PREV)
buttonActions.add(MediaIntentReceiver.ACTION_TOGGLE_PLAYBACK)
buttonActions.add(MediaIntentReceiver.ACTION_SKIP_NEXT)
buttonActions.add(MediaIntentReceiver.ACTION_STOP_CASTING)
val compatButtonActionsIndices = intArrayOf(1, 3)
val notificationOptions = NotificationOptions.Builder()
.setActions(buttonActions, compatButtonActionsIndices)
.setTargetActivityClassName(ExpandedControlsActivity::class.java.name)
.build()
val mediaOptions = CastMediaOptions.Builder()
.setNotificationOptions(notificationOptions)
.setExpandedControllerActivityClassName(ExpandedControlsActivity::class.java.name)
.build()
return CastOptions.Builder()
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
.setCastMediaOptions(mediaOptions)
.build()
}
override fun getAdditionalSessionProviders(context: Context?): List<SessionProvider>? {
return null
}
}

View file

@ -0,0 +1,19 @@
package code.name.monkey.retromusic.cast
import android.view.Menu
import code.name.monkey.retromusic.R
import com.google.android.gms.cast.framework.CastButtonFactory
import com.google.android.gms.cast.framework.media.widget.ExpandedControllerActivity
class ExpandedControlsActivity : ExpandedControllerActivity() {
override fun onCreateOptionsMenu(menu: Menu?): Boolean {
super.onCreateOptionsMenu(menu)
menuInflater.inflate(R.menu.menu_cast, menu)
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.action_cast)
return true
}
}

View file

@ -0,0 +1,22 @@
package code.name.monkey.retromusic.cast
import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.SessionManagerListener
interface RetroSessionManager : SessionManagerListener<CastSession> {
override fun onSessionResuming(p0: CastSession, p1: String) {
}
override fun onSessionStartFailed(p0: CastSession, p1: Int) {
}
override fun onSessionResumeFailed(p0: CastSession, p1: Int) {
}
override fun onSessionEnding(p0: CastSession) {
}
}

View file

@ -0,0 +1,140 @@
package code.name.monkey.retromusic.cast
import android.content.Context
import code.name.monkey.retromusic.util.MusicUtil
import fi.iki.elonen.NanoHTTPD
import fi.iki.elonen.NanoHTTPD.Response.Status
import java.io.*
const val SERVER_PORT = 9090
class RetroWebServer(val context: Context) : NanoHTTPD(SERVER_PORT) {
companion object {
private const val MIME_TYPE_IMAGE = "image/jpg"
const val MIME_TYPE_AUDIO = "audio/mp3"
const val PART_COVER_ART = "coverart"
const val PART_SONG = "song"
const val PARAM_ID = "id"
var mRetroWebServer: RetroWebServer? = null
fun getInstance(context: Context): RetroWebServer {
if (mRetroWebServer == null) {
mRetroWebServer = RetroWebServer(context)
}
return mRetroWebServer!!
}
}
override fun serve(
uri: String?,
method: Method?,
headers: MutableMap<String, String>?,
parms: MutableMap<String, String>?,
files: MutableMap<String, String>?
): Response {
if (uri?.contains(PART_COVER_ART) == true) {
val albumId = parms?.get(PARAM_ID) ?: return errorResponse()
val albumArtUri = MusicUtil.getMediaStoreAlbumCoverUri(albumId.toLong())
val fis: InputStream?
try {
fis = context.contentResolver.openInputStream(albumArtUri)
} catch (e: FileNotFoundException) {
return errorResponse()
}
return newChunkedResponse(Status.OK, MIME_TYPE_IMAGE, fis)
} else if (uri?.contains(PART_SONG) == true) {
val songId = parms?.get(PARAM_ID) ?: return errorResponse()
val songUri = MusicUtil.getSongFileUri(songId.toLong())
val songPath = MusicUtil.getSongFilePath(context, songUri)
val song = File(songPath)
return serveFile(headers!!, song, MIME_TYPE_AUDIO)
}
return newFixedLengthResponse(Status.NOT_FOUND, MIME_PLAINTEXT, "Not Found")
}
private fun serveFile(
header: MutableMap<String, String>, file: File,
mime: String
): Response {
var res: Response
try {
// Support (simple) skipping:
var startFrom: Long = 0
var endAt: Long = -1
// The value of header range will be bytes=0-1024 something like this
// We get the value of from Bytes i.e. startFrom and toBytes i.e. endAt below
var range = header["range"]
if (range != null) {
if (range.startsWith("bytes=")) {
range = range.substring("bytes=".length)
val minus = range.indexOf('-')
try {
if (minus > 0) {
startFrom = range
.substring(0, minus).toLong()
endAt = range.substring(minus + 1).toLong()
}
} catch (ignored: NumberFormatException) {
}
}
}
// Chunked Response is used when serving audio file
// Change return code and add Content-Range header when skipping is
// requested
val fileLen = file.length()
if (range != null && startFrom >= 0) {
if (startFrom >= fileLen) {
res = newFixedLengthResponse(
Status.RANGE_NOT_SATISFIABLE,
MIME_PLAINTEXT, ""
)
res.addHeader("Content-Range", "bytes 0-0/$fileLen")
} else {
if (endAt < 0) {
endAt = fileLen - 1
}
var newLen = endAt - startFrom + 1
if (newLen < 0) {
newLen = 0
}
val dataLen = newLen
val fis: FileInputStream = object : FileInputStream(file) {
@Throws(IOException::class)
override fun available(): Int {
return dataLen.toInt()
}
}
fis.skip(startFrom)
res = newChunkedResponse(
Status.PARTIAL_CONTENT, mime,
fis
)
res.addHeader("Content-Length", "" + dataLen)
res.addHeader(
"Content-Range", "bytes " + startFrom + "-"
+ endAt + "/" + fileLen
)
}
} else {
res = newFixedLengthResponse(
Status.OK, mime,
FileInputStream(file), file.length()
)
res.addHeader("Accept-Ranges", "bytes")
res.addHeader("Content-Length", "" + fileLen)
}
} catch (ioe: IOException) {
res = newFixedLengthResponse(
Status.FORBIDDEN,
MIME_PLAINTEXT, "FORBIDDEN: Reading file failed."
)
}
return res
}
private fun errorResponse(message: String = "Error Occurred"): Response {
return newFixedLengthResponse(Status.INTERNAL_ERROR, MIME_PLAINTEXT, message)
}
}