mirror of
https://github.com/cmclark00/RetroMusicPlayer.git
synced 2025-05-19 08:35:20 +01:00
Added F-Droid FOSS flavor
This commit is contained in:
parent
bc39d3a462
commit
2a5e6d7756
42 changed files with 243 additions and 360 deletions
9
app/src/normal/AndroidManifest.xml
Normal file
9
app/src/normal/AndroidManifest.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="code.name.monkey.retromusic">
|
||||
|
||||
<application tools:ignore="MissingApplicationIcon">
|
||||
<activity android:name=".activities.PurchaseActivity"/>
|
||||
</application>
|
||||
</manifest>
|
|
@ -0,0 +1,105 @@
|
|||
package code.name.monkey.retromusic.activities
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import code.name.monkey.appthemehelper.util.MaterialUtil
|
||||
import code.name.monkey.retromusic.App
|
||||
import code.name.monkey.retromusic.BuildConfig
|
||||
import code.name.monkey.retromusic.Constants
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.activities.base.AbsThemeActivity
|
||||
import code.name.monkey.retromusic.databinding.ActivityProVersionBinding
|
||||
import code.name.monkey.retromusic.extensions.accentColor
|
||||
import code.name.monkey.retromusic.extensions.setLightStatusBar
|
||||
import code.name.monkey.retromusic.extensions.setStatusBarColor
|
||||
import code.name.monkey.retromusic.extensions.showToast
|
||||
import com.anjlab.android.iab.v3.BillingProcessor
|
||||
import com.anjlab.android.iab.v3.PurchaseInfo
|
||||
|
||||
class PurchaseActivity : AbsThemeActivity(), BillingProcessor.IBillingHandler {
|
||||
|
||||
private lateinit var binding: ActivityProVersionBinding
|
||||
private lateinit var billingProcessor: BillingProcessor
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityProVersionBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setStatusBarColor(Color.TRANSPARENT)
|
||||
setLightStatusBar(false)
|
||||
binding.toolbar.navigationIcon?.setTint(Color.WHITE)
|
||||
binding.toolbar.setNavigationOnClickListener { onBackPressed() }
|
||||
|
||||
binding.restoreButton.isEnabled = false
|
||||
binding.purchaseButton.isEnabled = false
|
||||
|
||||
billingProcessor = BillingProcessor(this, BuildConfig.GOOGLE_PLAY_LICENSING_KEY, this)
|
||||
|
||||
MaterialUtil.setTint(binding.purchaseButton, true)
|
||||
|
||||
binding.restoreButton.setOnClickListener {
|
||||
restorePurchase()
|
||||
}
|
||||
binding.purchaseButton.setOnClickListener {
|
||||
billingProcessor.purchase(this@PurchaseActivity, Constants.PRO_VERSION_PRODUCT_ID)
|
||||
}
|
||||
binding.bannerContainer.backgroundTintList =
|
||||
ColorStateList.valueOf(accentColor())
|
||||
}
|
||||
|
||||
private fun restorePurchase() {
|
||||
showToast(R.string.restoring_purchase)
|
||||
billingProcessor.loadOwnedPurchasesFromGoogleAsync(object :
|
||||
BillingProcessor.IPurchasesResponseListener {
|
||||
override fun onPurchasesSuccess() {
|
||||
onPurchaseHistoryRestored()
|
||||
}
|
||||
|
||||
override fun onPurchasesError() {
|
||||
showToast(R.string.could_not_restore_purchase)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
override fun onProductPurchased(productId: String, details: PurchaseInfo?) {
|
||||
showToast(R.string.thank_you)
|
||||
setResult(RESULT_OK)
|
||||
}
|
||||
|
||||
override fun onPurchaseHistoryRestored() {
|
||||
if (App.isProVersion()) {
|
||||
showToast(R.string.restored_previous_purchase_please_restart)
|
||||
setResult(RESULT_OK)
|
||||
} else {
|
||||
showToast(R.string.no_purchase_found)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBillingError(errorCode: Int, error: Throwable?) {
|
||||
Log.e(TAG, "Billing error: code = $errorCode", error)
|
||||
}
|
||||
|
||||
override fun onBillingInitialized() {
|
||||
binding.restoreButton.isEnabled = true
|
||||
binding.purchaseButton.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
android.R.id.home -> finish()
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
billingProcessor.release()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG: String = "PurchaseActivity"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package code.name.monkey.retromusic.activities.base
|
||||
|
||||
import code.name.monkey.retromusic.cast.RetroSessionManagerListener
|
||||
import code.name.monkey.retromusic.cast.RetroWebServer
|
||||
import code.name.monkey.retromusic.helper.MusicPlayerRemote
|
||||
import code.name.monkey.retromusic.service.CastPlayer
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.cast.framework.CastSession
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
|
||||
abstract class AbsCastActivity : AbsSlidingMusicPanelActivity() {
|
||||
|
||||
private var mCastSession: CastSession? = null
|
||||
private val sessionManager by lazy {
|
||||
CastContext.getSharedInstance(this).sessionManager
|
||||
}
|
||||
|
||||
private val webServer: RetroWebServer by inject()
|
||||
|
||||
private val playServicesAvailable: Boolean by lazy {
|
||||
try {
|
||||
GoogleApiAvailability
|
||||
.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private val sessionManagerListener by lazy {
|
||||
object : RetroSessionManagerListener {
|
||||
override fun onSessionStarting(castSession: CastSession) {
|
||||
webServer.start()
|
||||
}
|
||||
|
||||
override fun onSessionStarted(castSession: CastSession, p1: String) {
|
||||
invalidateOptionsMenu()
|
||||
mCastSession = castSession
|
||||
MusicPlayerRemote.switchToRemotePlayback(CastPlayer(castSession))
|
||||
}
|
||||
|
||||
override fun onSessionEnded(castSession: CastSession, p1: Int) {
|
||||
invalidateOptionsMenu()
|
||||
if (mCastSession == castSession) {
|
||||
mCastSession = null
|
||||
}
|
||||
MusicPlayerRemote.switchToLocalPlayback()
|
||||
webServer.stop()
|
||||
}
|
||||
|
||||
override fun onSessionResumed(castSession: CastSession, p1: Boolean) {
|
||||
invalidateOptionsMenu()
|
||||
mCastSession = castSession
|
||||
webServer.start()
|
||||
MusicPlayerRemote.switchToRemotePlayback(CastPlayer(castSession))
|
||||
}
|
||||
|
||||
override fun onSessionSuspended(castSession: CastSession, p1: Int) {
|
||||
invalidateOptionsMenu()
|
||||
if (mCastSession == castSession) {
|
||||
mCastSession = null
|
||||
}
|
||||
MusicPlayerRemote.switchToLocalPlayback()
|
||||
webServer.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
if (playServicesAvailable) {
|
||||
sessionManager.addSessionManagerListener(
|
||||
sessionManagerListener,
|
||||
CastSession::class.java
|
||||
)
|
||||
if (mCastSession == null) {
|
||||
mCastSession = sessionManager.currentCastSession
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (playServicesAvailable) {
|
||||
sessionManager.removeSessionManagerListener(
|
||||
sessionManagerListener,
|
||||
CastSession::class.java
|
||||
)
|
||||
mCastSession = null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
package code.name.monkey.retromusic.billing
|
||||
|
||||
import android.content.Context
|
||||
import code.name.monkey.retromusic.BuildConfig
|
||||
import code.name.monkey.retromusic.Constants
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.extensions.showToast
|
||||
import com.anjlab.android.iab.v3.BillingProcessor
|
||||
import com.anjlab.android.iab.v3.PurchaseInfo
|
||||
|
||||
class BillingManager(context: Context) {
|
||||
private val billingProcessor: BillingProcessor
|
||||
|
||||
init {
|
||||
// automatically restores purchases
|
||||
billingProcessor = BillingProcessor(
|
||||
context, BuildConfig.GOOGLE_PLAY_LICENSING_KEY,
|
||||
object : BillingProcessor.IBillingHandler {
|
||||
override fun onProductPurchased(productId: String, details: PurchaseInfo?) {}
|
||||
|
||||
override fun onPurchaseHistoryRestored() {
|
||||
context.showToast(R.string.restored_previous_purchase_please_restart)
|
||||
}
|
||||
|
||||
override fun onBillingError(errorCode: Int, error: Throwable?) {}
|
||||
|
||||
override fun onBillingInitialized() {}
|
||||
})
|
||||
}
|
||||
|
||||
fun release() {
|
||||
billingProcessor.release()
|
||||
}
|
||||
|
||||
val isProVersion: Boolean
|
||||
get() = billingProcessor.isPurchased(Constants.PRO_VERSION_PRODUCT_ID)
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
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.MediaInfo
|
||||
import com.google.android.gms.cast.MediaInfo.STREAM_TYPE_BUFFERED
|
||||
import com.google.android.gms.cast.MediaMetadata
|
||||
import com.google.android.gms.cast.MediaMetadata.*
|
||||
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 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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
@file:Suppress("unused")
|
||||
|
||||
package code.name.monkey.retromusic.cast
|
||||
|
||||
import android.content.Context
|
||||
import code.name.monkey.retromusic.activities.MainActivity
|
||||
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
|
||||
|
||||
|
||||
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(MainActivity::class.java.name)
|
||||
.build()
|
||||
|
||||
val mediaOptions = CastMediaOptions.Builder()
|
||||
.setNotificationOptions(notificationOptions)
|
||||
.setExpandedControllerActivityClassName(MainActivity::class.java.name)
|
||||
.build()
|
||||
|
||||
return CastOptions.Builder()
|
||||
.setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
|
||||
.setCastMediaOptions(mediaOptions)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getAdditionalSessionProviders(context: Context): MutableList<SessionProvider>? {
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package code.name.monkey.retromusic.cast
|
||||
|
||||
import com.google.android.gms.cast.framework.CastSession
|
||||
import com.google.android.gms.cast.framework.SessionManagerListener
|
||||
|
||||
interface RetroSessionManagerListener : 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(castSession: CastSession) {}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
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"
|
||||
}
|
||||
|
||||
override fun serve(session: IHTTPSession?): Response {
|
||||
if (session?.uri?.contains(PART_COVER_ART) == true) {
|
||||
val albumId = session.parameters?.get(PARAM_ID)?.get(0) ?: 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 (session?.uri?.contains(PART_SONG) == true) {
|
||||
val songId = session.parameters?.get(PARAM_ID)?.get(0) ?: return errorResponse()
|
||||
val songUri = MusicUtil.getSongFileUri(songId.toLong())
|
||||
val songPath = MusicUtil.getSongFilePath(context, songUri)
|
||||
val song = File(songPath)
|
||||
return serveFile(session.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,
|
||||
file.inputStream(), 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package code.name.monkey.retromusic.extensions
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.view.Menu
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import code.name.monkey.retromusic.R
|
||||
import code.name.monkey.retromusic.activities.PurchaseActivity
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||
import com.google.android.play.core.splitcompat.SplitCompat
|
||||
import com.google.android.play.core.splitinstall.SplitInstallManagerFactory
|
||||
import com.google.android.play.core.splitinstall.SplitInstallRequest
|
||||
import java.util.*
|
||||
|
||||
fun Context.setUpMediaRouteButton(menu: Menu) {
|
||||
CastButtonFactory.setUpMediaRouteButton(this, menu, R.id.action_cast)
|
||||
}
|
||||
|
||||
fun FragmentActivity.installLanguageAndRecreate(code: String) {
|
||||
val manager = SplitInstallManagerFactory.create(this)
|
||||
if (code != "auto") {
|
||||
// Try to download language resources
|
||||
val request =
|
||||
SplitInstallRequest.newBuilder().addLanguage(Locale.forLanguageTag(code))
|
||||
.build()
|
||||
manager.startInstall(request)
|
||||
// Recreate the activity on download complete
|
||||
.addOnCompleteListener {
|
||||
recreate()
|
||||
}
|
||||
} else {
|
||||
recreate()
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.goToProVersion() {
|
||||
startActivity(Intent(this, PurchaseActivity::class.java))
|
||||
}
|
||||
|
||||
fun Context.installSplitCompat() {
|
||||
SplitCompat.install(this)
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
package code.name.monkey.retromusic.service
|
||||
|
||||
import code.name.monkey.retromusic.cast.CastHelper.toMediaInfo
|
||||
import code.name.monkey.retromusic.model.Song
|
||||
import code.name.monkey.retromusic.service.playback.Playback
|
||||
import code.name.monkey.retromusic.util.PreferenceUtil.playbackSpeed
|
||||
import com.google.android.gms.cast.MediaLoadOptions
|
||||
import com.google.android.gms.cast.MediaSeekOptions
|
||||
import com.google.android.gms.cast.MediaStatus
|
||||
import com.google.android.gms.cast.framework.CastSession
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient
|
||||
|
||||
class CastPlayer(castSession: CastSession) : Playback, RemoteMediaClient.Callback() {
|
||||
|
||||
override val isInitialized: Boolean = true
|
||||
|
||||
private val remoteMediaClient: RemoteMediaClient? = castSession.remoteMediaClient
|
||||
|
||||
init {
|
||||
remoteMediaClient?.registerCallback(this)
|
||||
remoteMediaClient?.setPlaybackRate(playbackSpeed.toDouble().coerceIn(0.5, 2.0))
|
||||
}
|
||||
|
||||
private var isActuallyPlaying = false
|
||||
|
||||
override val isPlaying: Boolean
|
||||
get() {
|
||||
return remoteMediaClient?.isPlaying == true || isActuallyPlaying
|
||||
}
|
||||
|
||||
override val audioSessionId: Int = 0
|
||||
|
||||
override var callbacks: Playback.PlaybackCallbacks? = null
|
||||
|
||||
override fun setDataSource(
|
||||
song: Song,
|
||||
force: Boolean,
|
||||
completion: (success: Boolean) -> Unit,
|
||||
) {
|
||||
try {
|
||||
val mediaLoadOptions =
|
||||
MediaLoadOptions.Builder().setPlayPosition(0).setAutoplay(true).build()
|
||||
remoteMediaClient?.load(song.toMediaInfo()!!, mediaLoadOptions)
|
||||
completion(true)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
completion(false)
|
||||
}
|
||||
}
|
||||
|
||||
override fun setNextDataSource(path: String?) {}
|
||||
|
||||
override fun start(): Boolean {
|
||||
isActuallyPlaying = true
|
||||
remoteMediaClient?.play()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun stop() {
|
||||
isActuallyPlaying = false
|
||||
remoteMediaClient?.stop()
|
||||
}
|
||||
|
||||
override fun release() {
|
||||
stop()
|
||||
}
|
||||
|
||||
override fun pause(): Boolean {
|
||||
isActuallyPlaying = false
|
||||
remoteMediaClient?.pause()
|
||||
return true
|
||||
}
|
||||
|
||||
override fun duration(): Int {
|
||||
return remoteMediaClient?.streamDuration?.toInt() ?: 0
|
||||
}
|
||||
|
||||
override fun position(): Int {
|
||||
return remoteMediaClient?.approximateStreamPosition?.toInt() ?: 0
|
||||
}
|
||||
|
||||
override fun seek(whereto: Int): Int {
|
||||
remoteMediaClient?.seek(MediaSeekOptions.Builder().setPosition(whereto.toLong()).build())
|
||||
return whereto
|
||||
}
|
||||
|
||||
override fun setVolume(vol: Float) = true
|
||||
|
||||
override fun setAudioSessionId(sessionId: Int) = true
|
||||
|
||||
override fun setCrossFadeDuration(duration: Int) {}
|
||||
|
||||
override fun setPlaybackSpeedPitch(speed: Float, pitch: Float) {
|
||||
remoteMediaClient?.setPlaybackRate(speed.toDouble().coerceIn(0.5, 2.0))
|
||||
}
|
||||
|
||||
override fun onStatusUpdated() {
|
||||
when (remoteMediaClient?.playerState) {
|
||||
MediaStatus.PLAYER_STATE_IDLE -> {
|
||||
val idleReason = remoteMediaClient.idleReason
|
||||
if (idleReason == MediaStatus.IDLE_REASON_FINISHED) {
|
||||
callbacks?.onTrackEnded()
|
||||
}
|
||||
}
|
||||
MediaStatus.PLAYER_STATE_PLAYING, MediaStatus.PLAYER_STATE_PAUSED -> {
|
||||
callbacks?.onPlayStateChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright (c) 2019 Hemanth Savarala.
|
||||
*
|
||||
* Licensed under the GNU General Public License v3
|
||||
*
|
||||
* This is free software: you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation either version 3 of the License, or (at your option) any later version.
|
||||
*
|
||||
* This software is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
|
||||
* See the GNU General Public License for more details.
|
||||
*/
|
||||
|
||||
package code.name.monkey.retromusic.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.edit
|
||||
import com.google.android.play.core.review.ReviewManagerFactory
|
||||
|
||||
object AppRater {
|
||||
private const val DO_NOT_SHOW_AGAIN = "do_not_show_again"// Package Name
|
||||
private const val APP_RATING = "app_rating"// Package Name
|
||||
private const val LAUNCH_COUNT = "launch_count"// Package Name
|
||||
private const val DATE_FIRST_LAUNCH = "date_first_launch"// Package Name
|
||||
|
||||
private const val DAYS_UNTIL_PROMPT = 3//Min number of days
|
||||
private const val LAUNCHES_UNTIL_PROMPT = 5//Min number of launches
|
||||
|
||||
fun appLaunched(context: Activity) {
|
||||
val prefs = context.getSharedPreferences(APP_RATING, 0)
|
||||
if (prefs.getBoolean(DO_NOT_SHOW_AGAIN, false)) {
|
||||
return
|
||||
}
|
||||
|
||||
prefs.edit {
|
||||
|
||||
// Increment launch counter
|
||||
val launchCount = prefs.getLong(LAUNCH_COUNT, 0) + 1
|
||||
putLong(LAUNCH_COUNT, launchCount)
|
||||
|
||||
// Get date of first launch
|
||||
var dateFirstLaunch = prefs.getLong(DATE_FIRST_LAUNCH, 0)
|
||||
if (dateFirstLaunch == 0L) {
|
||||
dateFirstLaunch = System.currentTimeMillis()
|
||||
putLong(DATE_FIRST_LAUNCH, dateFirstLaunch)
|
||||
}
|
||||
|
||||
// Wait at least n days before opening
|
||||
if (launchCount >= LAUNCHES_UNTIL_PROMPT) {
|
||||
if (System.currentTimeMillis() >= dateFirstLaunch + DAYS_UNTIL_PROMPT * 24 * 60 * 60 * 1000) {
|
||||
//showRateDialog(context, editor)
|
||||
showPlayStoreReviewDialog(context, this)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPlayStoreReviewDialog(context: Activity, editor: SharedPreferences.Editor) {
|
||||
val manager = ReviewManagerFactory.create(context)
|
||||
val flow = manager.requestReviewFlow()
|
||||
flow.addOnCompleteListener { request ->
|
||||
if (request.isSuccessful) {
|
||||
val reviewInfo = request.result
|
||||
val flowManager = manager.launchReviewFlow(context, reviewInfo)
|
||||
flowManager.addOnCompleteListener {
|
||||
if (it.isSuccessful) {
|
||||
editor.putBoolean(DO_NOT_SHOW_AGAIN, true)
|
||||
editor.commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue