Skip to content

Commit

Permalink
Implemented discord rich presence #3555 #1836 #63
Browse files Browse the repository at this point in the history
  • Loading branch information
fast4x committed Sep 8, 2024
1 parent e1db0cf commit 95243b8
Show file tree
Hide file tree
Showing 9 changed files with 287 additions and 2 deletions.
3 changes: 2 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ android {

defaultConfig {
applicationId = "it.fast4x.rimusic"
minSdk = 21
minSdk = 27
targetSdk = 35
versionCode = 55
versionName = "0.6.51"
Expand Down Expand Up @@ -138,6 +138,7 @@ dependencies {
implementation(libs.haze)
implementation(libs.androidyoutubeplayer)
implementation(libs.glance.widgets)
implementation(libs.kizzy.rpc)

implementation(libs.room)
ksp(libs.room.compiler)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Surface
import androidx.compose.material3.rememberModalBottomSheetState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.SnapshotMutationPolicy
Expand All @@ -39,6 +42,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.password
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.rememberNavController
import io.ktor.http.Url
import it.fast4x.compose.persist.persistList
import it.fast4x.piped.models.Instance
Expand All @@ -48,8 +52,11 @@ import it.fast4x.rimusic.R
import it.fast4x.rimusic.enums.CheckUpdateState
import it.fast4x.rimusic.enums.NavigationBarPosition
import it.fast4x.rimusic.enums.PopupType
import it.fast4x.rimusic.enums.ThumbnailRoundness
import it.fast4x.rimusic.enums.ValidationType
import it.fast4x.rimusic.extensions.discord.DiscordLoginAndGetToken
import it.fast4x.rimusic.service.PlayerMediaBrowserService
import it.fast4x.rimusic.ui.components.CustomModalBottomSheet
import it.fast4x.rimusic.ui.components.LocalMenuState
import it.fast4x.rimusic.ui.components.themed.DefaultDialog
import it.fast4x.rimusic.ui.components.themed.HeaderWithIcon
Expand All @@ -63,10 +70,13 @@ import it.fast4x.rimusic.utils.CheckAvailableNewVersion
import it.fast4x.rimusic.utils.TextCopyToClipboard
import it.fast4x.rimusic.utils.checkUpdateStateKey
import it.fast4x.rimusic.utils.defaultFolderKey
import it.fast4x.rimusic.utils.discordPersonalAccessTokenKey
import it.fast4x.rimusic.utils.extraspaceKey
import it.fast4x.rimusic.utils.isAtLeastAndroid10
import it.fast4x.rimusic.utils.isAtLeastAndroid12
import it.fast4x.rimusic.utils.isAtLeastAndroid6
import it.fast4x.rimusic.utils.isAtLeastAndroid81
import it.fast4x.rimusic.utils.isDiscordPresenceEnabledKey
import it.fast4x.rimusic.utils.isIgnoringBatteryOptimizations
import it.fast4x.rimusic.utils.isInvincibilityEnabledKey
import it.fast4x.rimusic.utils.isKeepScreenOnEnabledKey
Expand All @@ -86,17 +96,23 @@ import it.fast4x.rimusic.utils.proxyPortKey
import it.fast4x.rimusic.utils.rememberEncryptedPreference
import it.fast4x.rimusic.utils.rememberPreference
import it.fast4x.rimusic.utils.showFoldersOnDeviceKey
import it.fast4x.rimusic.utils.thumbnailRoundnessKey
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.net.Proxy

@OptIn(ExperimentalMaterial3Api::class)
@SuppressLint("BatteryLife")
@ExperimentalAnimationApi
@Composable
fun OtherSettings() {
val context = LocalContext.current
val (colorPalette) = LocalAppearance.current
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
val thumbnailRoundness by rememberPreference(
thumbnailRoundnessKey,
ThumbnailRoundness.Heavy
)

var isAndroidAutoEnabled by remember {
val component = ComponentName(context, PlayerMediaBrowserService::class.java)
Expand Down Expand Up @@ -418,6 +434,68 @@ fun OtherSettings() {

/****** PIPED ******/

/****** DISCORD ******/
var isDiscordPresenceEnabled by rememberPreference(isDiscordPresenceEnabledKey, false)
var loginDiscord by remember { mutableStateOf(false) }
var discordPersonalAccessToken by rememberEncryptedPreference(key = discordPersonalAccessTokenKey, defaultValue = "")
SettingsGroupSpacer()
SettingsEntryGroupText(title = "Discord")
SwitchSettingEntry(
isEnabled = isAtLeastAndroid81,
title = "Enable Rich Presence",
text = "",
isChecked = isDiscordPresenceEnabled,
onCheckedChange = { isDiscordPresenceEnabled = it }
)

AnimatedVisibility(visible = isDiscordPresenceEnabled) {
Column {
ButtonBarSettingEntry(
isEnabled = true,
title = if (discordPersonalAccessToken.isNotEmpty()) "Disconnect" else "Connect",
text = "Connected to Discord Account",
icon = R.drawable.logo_discord,
iconColor = colorPalette.text,
onClick = {
if (discordPersonalAccessToken.isNotEmpty())
discordPersonalAccessToken = ""
else
loginDiscord = true
}
)

CustomModalBottomSheet(
showSheet = loginDiscord,
onDismissRequest = {
loginDiscord = false
},
containerColor = colorPalette.background0,
contentColor = colorPalette.background0,
modifier = Modifier.fillMaxWidth(),
sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true),
dragHandle = {
Surface(
modifier = Modifier.padding(vertical = 0.dp),
color = colorPalette.background0,
shape = thumbnailShape
) {}
},
shape = thumbnailRoundness.shape()
) {
DiscordLoginAndGetToken(
rememberNavController(),
onGetToken = { token ->
loginDiscord = false
discordPersonalAccessToken = token
SmartMessage(token, type = PopupType.Info, context = context)
}
)
}
}
}

/****** DISCORD ******/

SettingsGroupSpacer()
SettingsEntryGroupText(stringResource(R.string.on_device))
StringListValueSelectorSettingsEntry(
Expand Down
157 changes: 157 additions & 0 deletions app/src/main/kotlin/it/fast4x/rimusic/extensions/discord/Discord.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package it.fast4x.rimusic.extensions.discord

import android.annotation.SuppressLint
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.JavascriptInterface
import android.webkit.WebResourceRequest
import android.webkit.WebStorage
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.activity.compose.BackHandler
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.MediaItem
import androidx.navigation.NavController
import com.my.kizzyrpc.KizzyRPC
import com.my.kizzyrpc.model.Activity
import com.my.kizzyrpc.model.Assets
import com.my.kizzyrpc.model.Timestamps
import it.fast4x.rimusic.LocalPlayerAwareWindowInsets
import it.fast4x.rimusic.R
import it.fast4x.rimusic.ui.components.themed.IconButton
import it.fast4x.rimusic.utils.discordPersonalAccessTokenKey
import it.fast4x.rimusic.utils.rememberEncryptedPreference
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch

@SuppressLint("SetJavaScriptEnabled")
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DiscordLoginAndGetToken(
navController: NavController,
onGetToken: (String) -> Unit
) {
val scope = rememberCoroutineScope()

var webView: WebView? = null

AndroidView(
modifier = Modifier
.windowInsetsPadding(LocalPlayerAwareWindowInsets.current)
.fillMaxSize(),
factory = { context ->
WebView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)

webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(
webView: WebView,
request: WebResourceRequest,
): Boolean {
stopLoading()
if (request.url.toString().endsWith("/app")) {
loadUrl("javascript:Android.onRetrieveToken((webpackChunkdiscord_app.push([[''],{},e=>{m=[];for(let c in e.c)m.push(e.c[c])}]),m).find(m=>m?.exports?.default?.getToken!==void 0).exports.default.getToken());")
}
return false
}
}
settings.apply {
javaScriptEnabled = true
domStorageEnabled = true
setSupportZoom(true)
builtInZoomControls = true
}
val cookieManager = CookieManager.getInstance()
cookieManager.removeAllCookies(null)
cookieManager.flush()

WebStorage.getInstance().deleteAllData()
addJavascriptInterface(object {
@JavascriptInterface
fun onRetrieveToken(token: String) {
scope.launch(Dispatchers.Main) {
onGetToken(token)
}
}
}, "Android")

webView = this
loadUrl("https://discord.com/login")
}
}
)

TopAppBar(
title = { Text("Login to Discord") },
navigationIcon = {
IconButton(
icon = R.drawable.chevron_back,
onClick = navController::navigateUp,
color = Color.White
)
}
)

BackHandler(enabled = webView?.canGoBack() == true) {
webView?.goBack()
}
}

fun sendDiscordPresence(
token: String,
mediaItem: MediaItem
) {
if (token.isEmpty()) return

val rpc = KizzyRPC(token)
rpc.setActivity(
activity = Activity(
applicationId = "1281989764358082570",
name = "RiMusic",
details = mediaItem.mediaMetadata.title.toString(),
state = mediaItem.mediaMetadata.artist.toString(),
type = TypeDiscordActivity.LISTENING.value,
timestamps = Timestamps(
start = System.currentTimeMillis(),
end = System.currentTimeMillis() + 500000
),
assets = Assets(
largeImage = mediaItem.mediaMetadata.artworkUri.toString(),
smallImage = mediaItem.mediaMetadata.artworkUri.toString(),
largeText = mediaItem.mediaMetadata.title.toString(),
smallText = mediaItem.mediaMetadata.artist.toString(),
),
buttons = listOf("Get RiMusic", "Listen to YTMusic"),
metadata = com.my.kizzyrpc.model.Metadata(
listOf(
"https://rimusic.xyz/",
"https://music.youtube.com/watch?v=${mediaItem.mediaId}",
)
)
),
status = "online",
since = System.currentTimeMillis()
)
}

enum class TypeDiscordActivity (val value: Int) {
PLAYING(0),
STREAMING(1),
LISTENING(2),
WATCHING(3),
COMPETING(5)
}
27 changes: 27 additions & 0 deletions app/src/main/kotlin/it/fast4x/rimusic/service/PlayerService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ import it.fast4x.rimusic.enums.ExoPlayerMinTimeForEvent
import it.fast4x.rimusic.enums.PopupType
import it.fast4x.rimusic.extensions.audiovolume.AudioVolumeObserver
import it.fast4x.rimusic.extensions.audiovolume.OnAudioVolumeChangedListener
import it.fast4x.rimusic.extensions.discord.sendDiscordPresence
import it.fast4x.rimusic.models.Event
import it.fast4x.rimusic.models.Format
import it.fast4x.rimusic.models.PersistentQueue
Expand All @@ -133,7 +134,9 @@ import it.fast4x.rimusic.utils.audioQualityFormatKey
import it.fast4x.rimusic.utils.broadCastPendingIntent
import it.fast4x.rimusic.utils.cleanPrefix
import it.fast4x.rimusic.utils.closebackgroundPlayerKey
import it.fast4x.rimusic.utils.discordPersonalAccessTokenKey
import it.fast4x.rimusic.utils.discoverKey
import it.fast4x.rimusic.utils.encryptedPreferences
import it.fast4x.rimusic.utils.exoPlayerCacheLocationKey
import it.fast4x.rimusic.utils.exoPlayerCustomCacheKey
import it.fast4x.rimusic.utils.exoPlayerDiskCacheMaxSizeKey
Expand All @@ -149,6 +152,8 @@ import it.fast4x.rimusic.utils.isAtLeastAndroid12
import it.fast4x.rimusic.utils.isAtLeastAndroid13
import it.fast4x.rimusic.utils.isAtLeastAndroid6
import it.fast4x.rimusic.utils.isAtLeastAndroid8
import it.fast4x.rimusic.utils.isAtLeastAndroid81
import it.fast4x.rimusic.utils.isDiscordPresenceEnabledKey
import it.fast4x.rimusic.utils.isInvincibilityEnabledKey
import it.fast4x.rimusic.utils.isPauseOnVolumeZeroEnabledKey
import it.fast4x.rimusic.utils.isShowingThumbnailInLockscreenKey
Expand All @@ -160,6 +165,7 @@ import it.fast4x.rimusic.utils.persistentQueueKey
import it.fast4x.rimusic.utils.playbackFadeAudioDurationKey
import it.fast4x.rimusic.utils.preferences
import it.fast4x.rimusic.utils.queueLoopEnabledKey
import it.fast4x.rimusic.utils.rememberPreference
import it.fast4x.rimusic.utils.resumePlaybackWhenDeviceConnectedKey
import it.fast4x.rimusic.utils.shouldBePlaying
import it.fast4x.rimusic.utils.showDownloadButtonBackgroundPlayerKey
Expand Down Expand Up @@ -723,8 +729,28 @@ class PlayerService : InvincibleService(),
Timber.e("PlayerService oncreate startForeground ${it.stackTraceToString()}")
}


updateDiscordPresence()

}

private fun updateDiscordPresence() {
if (!isAtLeastAndroid81) return
val discordPersonalAccessToken = encryptedPreferences.getString(discordPersonalAccessTokenKey, "")
val isDiscordPresenceEnabled = preferences.getBoolean(isDiscordPresenceEnabledKey, false)
runCatching {
if (!discordPersonalAccessToken.isNullOrEmpty() && isDiscordPresenceEnabled) {
player.currentMediaItem?.let {
sendDiscordPresence(
discordPersonalAccessToken,
it
)
}
}
}.onFailure {
Timber.e("PlayerService Failed sendDiscordPresence in PlayerService ${it.stackTraceToString()}")
}
}

private fun getVolumeProvider(): VolumeProviderCompat {
val audio = getSystemService(AUDIO_SERVICE) as AudioManager?
Expand Down Expand Up @@ -928,6 +954,7 @@ class PlayerService : InvincibleService(),
}

updateWidgets()
updateDiscordPresence()

}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ const val pipedPasswordKey = "pipedPassword"
const val pipedInstanceNameKey = "pipedInstanceName"
const val pipedApiBaseUrlKey = "pipedApiBaseUrl"
const val pipedApiTokenKey = "pipedApiToken"
const val discordPersonalAccessTokenKey = "DiscordPersonalAccessToken"

inline fun <reified T : Enum<T>> EncryptedSharedPreferences.getEnum(
key: String,
Expand Down
Loading

0 comments on commit 95243b8

Please sign in to comment.