mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 12:52:13 +02:00
refactor(discord): remove rich presence and bundled SDK
Remove the presence setting, platform bridge, SDK archive, and SDK preparation from local and CI builds. Reject leftover Discord native libraries in release APK audits.
This commit is contained in:
@@ -15,19 +15,6 @@ if (keystorePropertiesFile.exists()) {
|
||||
}
|
||||
|
||||
val rustBackendDir = rootProject.file("../rust_backend")
|
||||
val cachedDiscordSdkDir = rootProject.file("../.dart_tool/discord-sdk/1.10.19337")
|
||||
val discordSdkDir = providers.environmentVariable("SPOTIFLAC_DISCORD_SDK_DIR").orNull
|
||||
?: cachedDiscordSdkDir.takeIf { it.isDirectory }?.absolutePath
|
||||
val discordSdkAar = discordSdkDir?.let { file("$it/lib/release/discord_partner_sdk.aar") }
|
||||
if (discordSdkAar != null) {
|
||||
require(discordSdkAar.isFile) { "SPOTIFLAC_DISCORD_SDK_DIR must contain the official Social SDK" }
|
||||
require(file("$discordSdkDir/License-Notices.txt").isFile) { "Discord SDK license notices are required" }
|
||||
}
|
||||
val discordNotices = if (discordSdkDir != null) tasks.register<Copy>("copyDiscordNotices") {
|
||||
from(file("$discordSdkDir/License-Notices.txt"))
|
||||
into(layout.buildDirectory.dir("generated/discordAssets"))
|
||||
rename { "discord-sdk-notices.txt" }
|
||||
} else null
|
||||
val rustAndroidAbis = providers.environmentVariable("SPOTIFLAC_RUST_ANDROID_ABIS")
|
||||
.orElse("arm64-v8a,armeabi-v7a")
|
||||
.get()
|
||||
@@ -47,16 +34,12 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
buildConfig = true
|
||||
prefab = discordSdkAar != null
|
||||
}
|
||||
|
||||
sourceSets.getByName("main") {
|
||||
java.srcDir("src/rust/kotlin")
|
||||
java.srcDir(rustBackendDir.resolve("target/bindings/kotlin"))
|
||||
jniLibs.srcDir(rustBackendDir.resolve("target/android/jniLibs"))
|
||||
if (discordNotices != null) {
|
||||
assets.srcDir(layout.buildDirectory.dir("generated/discordAssets").get().asFile)
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -87,7 +70,6 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.zarz.spotiflac"
|
||||
buildConfigField("boolean", "HAS_DISCORD_SDK", (discordSdkAar != null).toString())
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
minSdk = flutter.minSdkVersion
|
||||
targetSdk = 37
|
||||
@@ -101,15 +83,6 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
if (discordSdkAar != null) {
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path = file("src/main/cpp/CMakeLists.txt")
|
||||
version = "3.22.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
getByName("debug") {
|
||||
applicationIdSuffix = ".debug"
|
||||
@@ -175,24 +148,12 @@ val buildRustBackend = tasks.register<Exec>("buildRustBackend") {
|
||||
outputs.dir(rustBackendDir.resolve("target/android/jniLibs"))
|
||||
}
|
||||
tasks.named("preBuild").configure { dependsOn(buildRustBackend) }
|
||||
if (discordNotices != null) tasks.named("preBuild").configure { dependsOn(discordNotices) }
|
||||
tasks.matching { it.name == "preReleaseBuild" }.configureEach {
|
||||
doFirst {
|
||||
check(discordSdkAar != null) {
|
||||
"Release APKs require Discord SDK. Run python3 scripts/prepare_discord_sdk.py; see DISCORD.md."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
flutter {
|
||||
source = "../.."
|
||||
}
|
||||
|
||||
dependencies {
|
||||
if (discordSdkAar != null) {
|
||||
implementation(files(discordSdkAar))
|
||||
implementation("androidx.browser:browser:1.10.0")
|
||||
}
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.5")
|
||||
implementation("net.java.dev.jna:jna:5.19.1@aar")
|
||||
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
package com.zarz.spotiflac.discord
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.zarz.spotiflac.BuildConfig
|
||||
import com.zarz.spotiflac.MainActivity
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Assume.assumeTrue
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
/** Exercises the actual JNI library/SDK in the emulator, without account credentials. */
|
||||
@RunWith(AndroidJUnit4::class)
|
||||
class DiscordPresenceTest {
|
||||
@Test
|
||||
fun testSdkLifecycleAndLateCallbacks() {
|
||||
val instrumentation = InstrumentationRegistry.getInstrumentation()
|
||||
assumeTrue("Build with SPOTIFLAC_DISCORD_SDK_DIR", BuildConfig.HAS_DISCORD_SDK)
|
||||
assumeTrue("This test must not publish to a signed-in account",
|
||||
instrumentation.targetContext.packageManager.getLaunchIntentForPackage("com.discord") == null)
|
||||
val activity = instrumentation.startActivitySync(
|
||||
Intent(instrumentation.targetContext, MainActivity::class.java)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
)
|
||||
try {
|
||||
instrumentation.runOnMainSync {
|
||||
assertEquals("discord_missing", DiscordPresenceBridge.configure(activity, true))
|
||||
assertEquals("disabled", DiscordPresenceBridge.configure(activity, false))
|
||||
System.loadLibrary("spotiflac_discord")
|
||||
Class.forName("com.discord.socialsdk.DiscordSocialSdkInit")
|
||||
.getMethod("setEngineActivity", android.app.Activity::class.java)
|
||||
.invoke(null, activity)
|
||||
DiscordNative.start(1549854098801692862L)
|
||||
DiscordNative.update("Track 🎵", "Lead & Guest", "Album", "", 1000, 2000)
|
||||
DiscordNative.clear()
|
||||
DiscordNative.stop()
|
||||
}
|
||||
repeat(20) {
|
||||
Thread.sleep(50)
|
||||
instrumentation.runOnMainSync { assertEquals("disabled", DiscordNative.poll()) }
|
||||
}
|
||||
instrumentation.runOnMainSync {
|
||||
DiscordNative.start(1549854098801692862L)
|
||||
assertEquals("ready", DiscordNative.poll())
|
||||
DiscordNative.stop()
|
||||
assertEquals("disabled", DiscordNative.poll())
|
||||
}
|
||||
} finally {
|
||||
instrumentation.runOnMainSync { activity.finish() }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,11 @@
|
||||
package="com.zarz.spotiflac">
|
||||
|
||||
<!-- Permissions -->
|
||||
<!-- Presence never records audio or uses the SDK voice service. -->
|
||||
<!-- Playback never records audio. -->
|
||||
<uses-permission android:name="android.permission.RECORD_AUDIO" tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" tools:node="remove" />
|
||||
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" tools:node="remove" />
|
||||
<queries><package android:name="com.discord" /></queries>
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
@@ -36,9 +35,6 @@
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:localeConfig="@xml/locale_config">
|
||||
<service android:name="com.discord.socialsdk.ForegroundService" tools:node="remove" />
|
||||
<activity android:name="com.discord.socialsdk.AuthenticationActivity" tools:node="remove" />
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
cmake_minimum_required(VERSION 3.22.1)
|
||||
project(spotiflac_discord LANGUAGES CXX)
|
||||
|
||||
find_package(discord_partner_sdk REQUIRED CONFIG)
|
||||
add_library(spotiflac_discord SHARED discord_presence.cpp)
|
||||
target_compile_features(spotiflac_discord PRIVATE cxx_std_20)
|
||||
target_link_libraries(spotiflac_discord discord_partner_sdk::discord_partner_sdk)
|
||||
target_link_options(spotiflac_discord PRIVATE "-Wl,-z,max-page-size=16384")
|
||||
@@ -1,105 +0,0 @@
|
||||
// Rich Presence initiative: @itsmegaaa, SpotiFLAC-Mobile #575 / #576.
|
||||
// Uses the official Social SDK transport instead of a custom user Gateway.
|
||||
#define DISCORDPP_IMPLEMENTATION
|
||||
#include <discordpp.h>
|
||||
#include <jni.h>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
// All entry points and SDK callbacks run on Android's main thread.
|
||||
std::unique_ptr<discordpp::Client> client;
|
||||
uint64_t generation = 0;
|
||||
std::string status = "disabled";
|
||||
|
||||
std::string utf8(JNIEnv* env, jstring value) {
|
||||
if (!value) return {};
|
||||
// JNI's modified UTF-8 is not suitable for emoji in track titles.
|
||||
const auto length = env->GetStringLength(value);
|
||||
const auto* chars = env->GetStringChars(value, nullptr);
|
||||
std::string out;
|
||||
for (jsize i = 0; i < length; ++i) {
|
||||
uint32_t c = chars[i];
|
||||
if (c >= 0xd800 && c <= 0xdbff && i + 1 < length &&
|
||||
chars[i + 1] >= 0xdc00 && chars[i + 1] <= 0xdfff) {
|
||||
c = 0x10000 + ((c - 0xd800) << 10) + (chars[++i] - 0xdc00);
|
||||
} else if (c >= 0xd800 && c <= 0xdfff) {
|
||||
c = 0xfffd;
|
||||
}
|
||||
if (c < 0x80) out += static_cast<char>(c);
|
||||
else if (c < 0x800) {
|
||||
out += static_cast<char>(0xc0 | (c >> 6));
|
||||
out += static_cast<char>(0x80 | (c & 0x3f));
|
||||
} else if (c < 0x10000) {
|
||||
out += static_cast<char>(0xe0 | (c >> 12));
|
||||
out += static_cast<char>(0x80 | ((c >> 6) & 0x3f));
|
||||
out += static_cast<char>(0x80 | (c & 0x3f));
|
||||
} else {
|
||||
out += static_cast<char>(0xf0 | (c >> 18));
|
||||
out += static_cast<char>(0x80 | ((c >> 12) & 0x3f));
|
||||
out += static_cast<char>(0x80 | ((c >> 6) & 0x3f));
|
||||
out += static_cast<char>(0x80 | (c & 0x3f));
|
||||
}
|
||||
}
|
||||
env->ReleaseStringChars(value, chars);
|
||||
return out;
|
||||
}
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_com_zarz_spotiflac_discord_DiscordNative_start(JNIEnv*, jobject, jlong appId) {
|
||||
if (client) return;
|
||||
++generation;
|
||||
client = std::make_unique<discordpp::Client>();
|
||||
client->SetApplicationId(static_cast<uint64_t>(appId));
|
||||
status = "ready";
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_com_zarz_spotiflac_discord_DiscordNative_update(
|
||||
JNIEnv* env, jobject, jstring title, jstring artist, jstring album,
|
||||
jstring cover, jlong start, jlong end) {
|
||||
if (!client) return;
|
||||
discordpp::Activity activity;
|
||||
activity.SetType(discordpp::ActivityTypes::Listening);
|
||||
activity.SetDetails(utf8(env, title));
|
||||
activity.SetState(utf8(env, artist));
|
||||
discordpp::ActivityTimestamps timestamps;
|
||||
timestamps.SetStart(static_cast<uint64_t>(start));
|
||||
if (end > start) timestamps.SetEnd(static_cast<uint64_t>(end));
|
||||
activity.SetTimestamps(timestamps);
|
||||
const auto image = utf8(env, cover);
|
||||
if (!image.empty()) {
|
||||
discordpp::ActivityAssets assets;
|
||||
assets.SetLargeImage(image);
|
||||
assets.SetLargeText(utf8(env, album));
|
||||
activity.SetAssets(assets);
|
||||
}
|
||||
const auto current = ++generation;
|
||||
status = "updating";
|
||||
client->UpdateRichPresence(activity, [current](discordpp::ClientResult result) {
|
||||
if (current != generation) return;
|
||||
status = result.Successful() ? "sharing" : "unavailable";
|
||||
});
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_com_zarz_spotiflac_discord_DiscordNative_clear(JNIEnv*, jobject) {
|
||||
++generation;
|
||||
if (client) client->ClearRichPresence();
|
||||
status = client ? "ready" : "disabled";
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT void JNICALL
|
||||
Java_com_zarz_spotiflac_discord_DiscordNative_stop(JNIEnv*, jobject) {
|
||||
++generation;
|
||||
if (client) client->ClearRichPresence();
|
||||
client.reset();
|
||||
status = "disabled";
|
||||
}
|
||||
|
||||
extern "C" JNIEXPORT jstring JNICALL
|
||||
Java_com_zarz_spotiflac_discord_DiscordNative_poll(JNIEnv* env, jobject) {
|
||||
discordpp::RunCallbacks();
|
||||
return env->NewStringUTF(status.c_str());
|
||||
}
|
||||
@@ -959,7 +959,6 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
PlayerWidgetBridge.attach(this, flutterEngine)
|
||||
com.zarz.spotiflac.discord.DiscordPresenceBridge.attach(this, flutterEngine.dartExecutor.binaryMessenger)
|
||||
// Select and initialize the runtime before Flutter can dispatch a call.
|
||||
val selectedBackend = coreBackend
|
||||
sweepStaleCacheFiles()
|
||||
|
||||
@@ -1,113 +0,0 @@
|
||||
package com.zarz.spotiflac.discord
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import com.zarz.spotiflac.BuildConfig
|
||||
import io.flutter.plugin.common.BinaryMessenger
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
internal object DiscordNative {
|
||||
external fun start(appId: Long)
|
||||
external fun update(title: String, artist: String, album: String, cover: String, start: Long, end: Long)
|
||||
external fun clear()
|
||||
external fun stop()
|
||||
external fun poll(): String
|
||||
}
|
||||
|
||||
/** Official client RPC only. No user tokens, custom Gateway, or background login. */
|
||||
internal object DiscordPresenceBridge {
|
||||
private const val APPLICATION_ID = 1549854098801692862L
|
||||
private val handler = Handler(Looper.getMainLooper())
|
||||
private var loaded = false
|
||||
private var enabled = false
|
||||
private var status = "disabled"
|
||||
private var channel: MethodChannel? = null
|
||||
private val pump = object : Runnable {
|
||||
override fun run() {
|
||||
if (!enabled) return
|
||||
val next = DiscordNative.poll()
|
||||
if (next != status) {
|
||||
status = next
|
||||
channel?.invokeMethod("status", status)
|
||||
}
|
||||
if (status != "unavailable") handler.postDelayed(this, 250)
|
||||
}
|
||||
}
|
||||
|
||||
fun attach(activity: Activity, messenger: BinaryMessenger) {
|
||||
channel?.setMethodCallHandler(null)
|
||||
channel = MethodChannel(messenger, "com.zarz.spotiflac/discord").also { channel ->
|
||||
channel.setMethodCallHandler { call, result ->
|
||||
try {
|
||||
when (call.method) {
|
||||
"configure" -> result.success(configure(activity, call.argument<Boolean>("enabled") == true))
|
||||
"update" -> {
|
||||
if (enabled) {
|
||||
DiscordNative.start(APPLICATION_ID)
|
||||
val cover = call.argument<String>("cover").orEmpty()
|
||||
DiscordNative.update(
|
||||
call.argument<String>("title").orEmpty().take(128),
|
||||
call.argument<String>("artist").orEmpty().take(128),
|
||||
call.argument<String>("album").orEmpty().take(128),
|
||||
cover.takeIf { it.startsWith("https://") && it.length <= 300 }.orEmpty(),
|
||||
call.argument<Number>("start")?.toLong()?.coerceAtLeast(0) ?: 0,
|
||||
call.argument<Number>("end")?.toLong()?.coerceAtLeast(0) ?: 0,
|
||||
)
|
||||
handler.removeCallbacks(pump)
|
||||
handler.post(pump)
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
"clear" -> {
|
||||
if (enabled) {
|
||||
handler.removeCallbacks(pump)
|
||||
DiscordNative.stop()
|
||||
status = "ready"
|
||||
channel.invokeMethod("status", status)
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
"status" -> result.success(status)
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
result.error("discord_unavailable", "Discord presence is unavailable", null)
|
||||
} catch (error: LinkageError) {
|
||||
result.error("discord_unavailable", "Discord SDK could not be loaded", null)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (loaded) setActivity(activity)
|
||||
}
|
||||
|
||||
private fun setActivity(activity: Activity) {
|
||||
Class.forName("com.discord.socialsdk.DiscordSocialSdkInit")
|
||||
.getMethod("setEngineActivity", Activity::class.java).invoke(null, activity)
|
||||
}
|
||||
|
||||
internal fun configure(activity: Activity, requested: Boolean): String {
|
||||
check(Looper.myLooper() == Looper.getMainLooper())
|
||||
handler.removeCallbacks(pump)
|
||||
enabled = false
|
||||
if (loaded) DiscordNative.stop()
|
||||
if (!requested) {
|
||||
status = "disabled"
|
||||
return status
|
||||
}
|
||||
if (!BuildConfig.HAS_DISCORD_SDK) return "sdk_unavailable".also { status = it }
|
||||
if (!isDiscordInstalled(activity)) return "discord_missing".also { status = it }
|
||||
if (!loaded) {
|
||||
System.loadLibrary("spotiflac_discord")
|
||||
loaded = true
|
||||
}
|
||||
setActivity(activity)
|
||||
enabled = true
|
||||
status = "ready"
|
||||
return status
|
||||
}
|
||||
|
||||
private fun isDiscordInstalled(context: Context): Boolean =
|
||||
context.packageManager.getLaunchIntentForPackage("com.discord") != null
|
||||
}
|
||||
Reference in New Issue
Block a user