mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-25 21:40:57 +02:00
chore: rebuild dev history without streaming-era commits
This commit is contained in:
@@ -12,6 +12,7 @@
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
@@ -21,6 +22,7 @@
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:usesCleartextTraffic="false"
|
||||
android:networkSecurityConfig="@xml/network_security_config"
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:localeConfig="@xml/locale_config">
|
||||
|
||||
@@ -92,6 +94,24 @@
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
|
||||
<!-- Audio playback service for media notification / background audio -->
|
||||
<service
|
||||
android:name="com.ryanheise.audioservice.AudioService"
|
||||
android:exported="true"
|
||||
android:foregroundServiceType="mediaPlayback">
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name="com.ryanheise.audioservice.MediaButtonReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- flutter_local_notifications receivers -->
|
||||
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationReceiver" />
|
||||
<receiver android:exported="false" android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver">
|
||||
|
||||
@@ -4,20 +4,25 @@ import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.ryanheise.audioservice.AudioServiceFragmentActivity
|
||||
import io.flutter.embedding.android.FlutterActivityLaunchConfigs.BackgroundMode
|
||||
import io.flutter.embedding.android.FlutterFragment
|
||||
import io.flutter.embedding.android.FlutterFragmentActivity
|
||||
import io.flutter.embedding.android.RenderMode
|
||||
import io.flutter.embedding.android.TransparencyMode
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.embedding.engine.FlutterShellArgs
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import gobackend.Gobackend
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.json.JSONArray
|
||||
@@ -27,13 +32,24 @@ import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.util.Locale
|
||||
|
||||
class MainActivity: FlutterFragmentActivity() {
|
||||
class MainActivity: AudioServiceFragmentActivity() {
|
||||
private val CHANNEL = "com.zarz.spotiflac/backend"
|
||||
private val DOWNLOAD_PROGRESS_STREAM_CHANNEL =
|
||||
"com.zarz.spotiflac/download_progress_stream"
|
||||
private val LIBRARY_SCAN_PROGRESS_STREAM_CHANNEL =
|
||||
"com.zarz.spotiflac/library_scan_progress_stream"
|
||||
private val STREAM_POLLING_INTERVAL_MS = 800L
|
||||
private val scope = CoroutineScope(SupervisorJob() + Dispatchers.Main)
|
||||
private var pendingSafTreeResult: MethodChannel.Result? = null
|
||||
private val safScanLock = Any()
|
||||
private val safDirLock = Any()
|
||||
private var safScanProgress = SafScanProgress()
|
||||
private var downloadProgressStreamJob: Job? = null
|
||||
private var downloadProgressEventSink: EventChannel.EventSink? = null
|
||||
private var lastDownloadProgressPayload: String? = null
|
||||
private var libraryScanProgressStreamJob: Job? = null
|
||||
private var libraryScanProgressEventSink: EventChannel.EventSink? = null
|
||||
private var lastLibraryScanProgressPayload: String? = null
|
||||
@Volatile private var safScanCancel = false
|
||||
@Volatile private var safScanActive = false
|
||||
private val safTreeLauncher = registerForActivityResult(
|
||||
@@ -380,6 +396,78 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
return obj.toString()
|
||||
}
|
||||
|
||||
private fun readLibraryScanProgressJsonForStream(): String {
|
||||
return if (safScanActive) {
|
||||
safProgressToJson()
|
||||
} else {
|
||||
Gobackend.getLibraryScanProgressJSON()
|
||||
}
|
||||
}
|
||||
|
||||
private fun startDownloadProgressStream(sink: EventChannel.EventSink) {
|
||||
stopDownloadProgressStream()
|
||||
downloadProgressEventSink = sink
|
||||
lastDownloadProgressPayload = null
|
||||
downloadProgressStreamJob = scope.launch {
|
||||
while (isActive && downloadProgressEventSink === sink) {
|
||||
try {
|
||||
val payload = withContext(Dispatchers.IO) {
|
||||
Gobackend.getAllDownloadProgress()
|
||||
}
|
||||
if (payload != lastDownloadProgressPayload) {
|
||||
lastDownloadProgressPayload = payload
|
||||
sink.success(payload)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.w(
|
||||
"SpotiFLAC",
|
||||
"Download progress stream poll failed: ${e.message}",
|
||||
)
|
||||
}
|
||||
delay(STREAM_POLLING_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopDownloadProgressStream() {
|
||||
downloadProgressStreamJob?.cancel()
|
||||
downloadProgressStreamJob = null
|
||||
downloadProgressEventSink = null
|
||||
lastDownloadProgressPayload = null
|
||||
}
|
||||
|
||||
private fun startLibraryScanProgressStream(sink: EventChannel.EventSink) {
|
||||
stopLibraryScanProgressStream()
|
||||
libraryScanProgressEventSink = sink
|
||||
lastLibraryScanProgressPayload = null
|
||||
libraryScanProgressStreamJob = scope.launch {
|
||||
while (isActive && libraryScanProgressEventSink === sink) {
|
||||
try {
|
||||
val payload = withContext(Dispatchers.IO) {
|
||||
readLibraryScanProgressJsonForStream()
|
||||
}
|
||||
if (payload != lastLibraryScanProgressPayload) {
|
||||
lastLibraryScanProgressPayload = payload
|
||||
sink.success(payload)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.w(
|
||||
"SpotiFLAC",
|
||||
"Library scan progress stream poll failed: ${e.message}",
|
||||
)
|
||||
}
|
||||
delay(STREAM_POLLING_INTERVAL_MS)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopLibraryScanProgressStream() {
|
||||
libraryScanProgressStreamJob?.cancel()
|
||||
libraryScanProgressStreamJob = null
|
||||
libraryScanProgressEventSink = null
|
||||
lastLibraryScanProgressPayload = null
|
||||
}
|
||||
|
||||
private fun resolveSafFile(treeUriStr: String, relativeDir: String, fileName: String): String {
|
||||
val obj = JSONObject()
|
||||
if (treeUriStr.isBlank() || fileName.isBlank()) {
|
||||
@@ -1252,16 +1340,79 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
return respObj.toString()
|
||||
}
|
||||
|
||||
// Disable Flutter's built-in deep linking so that incoming ACTION_VIEW URLs
|
||||
// (Spotify, Deezer, Tidal, YouTube Music) are NOT forwarded to GoRouter.
|
||||
// We handle these URLs ourselves via receive_sharing_intent + ShareIntentService.
|
||||
override fun shouldHandleDeeplinking(): Boolean = false
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
// Update the intent so receive_sharing_intent can access the new data
|
||||
setIntent(intent)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
try {
|
||||
Gobackend.cleanupExtensions()
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.w("SpotiFLAC", "Failed to cleanup extensions on destroy: ${e.message}")
|
||||
}
|
||||
stopDownloadProgressStream()
|
||||
stopLibraryScanProgressStream()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
// Always-enabled back callback to ensure back presses reach Flutter.
|
||||
// Nested tab navigators can incorrectly set frameworkHandlesBack(false),
|
||||
// which disables Flutter's own OnBackPressedCallback and causes the
|
||||
// system default (finish activity) to run. This callback guarantees
|
||||
// popRoute is always forwarded to Flutter, where PopScope handles it.
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
flutterEngine.navigationChannel.popRoute()
|
||||
}
|
||||
})
|
||||
|
||||
val messenger = flutterEngine.dartExecutor.binaryMessenger
|
||||
|
||||
EventChannel(
|
||||
messenger,
|
||||
DOWNLOAD_PROGRESS_STREAM_CHANNEL,
|
||||
).setStreamHandler(
|
||||
object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
if (events != null) {
|
||||
startDownloadProgressStream(events)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
stopDownloadProgressStream()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
EventChannel(
|
||||
messenger,
|
||||
LIBRARY_SCAN_PROGRESS_STREAM_CHANNEL,
|
||||
).setStreamHandler(
|
||||
object : EventChannel.StreamHandler {
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
|
||||
if (events != null) {
|
||||
startLibraryScanProgressStream(events)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
stopLibraryScanProgressStream()
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
MethodChannel(messenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||
scope.launch {
|
||||
try {
|
||||
when (call.method) {
|
||||
@@ -1296,6 +1447,14 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"getSpotifyRelatedArtists" -> {
|
||||
val artistId = call.argument<String>("artist_id") ?: ""
|
||||
val limit = call.argument<Int>("limit") ?: 12
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.getSpotifyRelatedArtists(artistId, limit.toLong())
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"checkAvailability" -> {
|
||||
val spotifyId = call.argument<String>("spotify_id") ?: ""
|
||||
val isrc = call.argument<String>("isrc") ?: ""
|
||||
@@ -1973,6 +2132,14 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"getDeezerRelatedArtists" -> {
|
||||
val artistId = call.argument<String>("artist_id") ?: ""
|
||||
val limit = call.argument<Int>("limit") ?: 12
|
||||
val response = withContext(Dispatchers.IO) {
|
||||
Gobackend.getDeezerRelatedArtists(artistId, limit.toLong())
|
||||
}
|
||||
result.success(response)
|
||||
}
|
||||
"getDeezerMetadata" -> {
|
||||
val resourceType = call.argument<String>("resource_type") ?: ""
|
||||
val resourceId = call.argument<String>("resource_id") ?: ""
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFFFF"
|
||||
android:pathData="M16.5,3C14.76,3 13.09,3.81 12,5.09C10.91,3.81 9.24,3 7.5,3C4.42,3 2,5.42 2,8.5C2,12.28 5.4,15.36 10.55,20.03L12,21.35L13.45,20.03C18.6,15.36 22,12.28 22,8.5C22,5.42 19.58,3 16.5,3ZM12.1,18.55L12,18.65L11.9,18.55C7.14,14.24 4,11.39 4,8.5C4,6.5 5.5,5 7.5,5C9.04,5 10.54,5.99 11.07,7.36H12.94C13.46,5.99 14.96,5 16.5,5C18.5,5 20,6.5 20,8.5C20,11.39 16.86,14.24 12.1,18.55Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,3 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:keep="@drawable/ic_stat_favorite,@drawable/ic_stat_favorite_border" />
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<network-security-config>
|
||||
<base-config cleartextTrafficPermitted="false" />
|
||||
|
||||
<!-- Allow local loopback cleartext for FFmpeg live decrypt tunnel only. -->
|
||||
<domain-config cleartextTrafficPermitted="true">
|
||||
<domain includeSubdomains="true">localhost</domain>
|
||||
<domain includeSubdomains="true">127.0.0.1</domain>
|
||||
</domain-config>
|
||||
</network-security-config>
|
||||
Reference in New Issue
Block a user