mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-14 21:08:04 +02:00
fix: avoid native worker binder payload limit
This commit is contained in:
@@ -58,6 +58,8 @@ class DownloadService : Service() {
|
||||
const val EXTRA_STATUS = "status"
|
||||
const val EXTRA_REQUESTS_JSON = "requests_json"
|
||||
const val EXTRA_SETTINGS_JSON = "settings_json"
|
||||
const val EXTRA_REQUESTS_PATH = "requests_path"
|
||||
const val EXTRA_SETTINGS_PATH = "settings_path"
|
||||
private const val NATIVE_WORKER_STATE_FILE = "native_download_worker_state.json"
|
||||
private const val NATIVE_WORKER_PROGRESS_FILE = "native_download_worker_progress.json"
|
||||
private const val NATIVE_REPLAYGAIN_JOURNAL_FILE = "native_replaygain_journal.json"
|
||||
@@ -117,6 +119,19 @@ class DownloadService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
fun startNativeQueueFromFiles(context: Context, requestsPath: String, settingsPath: String = "") {
|
||||
val intent = Intent(context, DownloadService::class.java).apply {
|
||||
action = ACTION_START_NATIVE_QUEUE
|
||||
putExtra(EXTRA_REQUESTS_PATH, requestsPath)
|
||||
putExtra(EXTRA_SETTINGS_PATH, settingsPath)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
context.startForegroundService(intent)
|
||||
} else {
|
||||
context.startService(intent)
|
||||
}
|
||||
}
|
||||
|
||||
fun pauseNativeQueue(context: Context) {
|
||||
val intent = Intent(context, DownloadService::class.java).apply {
|
||||
action = ACTION_PAUSE_NATIVE_QUEUE
|
||||
@@ -282,8 +297,18 @@ class DownloadService : Service() {
|
||||
stopForegroundService()
|
||||
}
|
||||
ACTION_START_NATIVE_QUEUE -> {
|
||||
val requestsJson = intent.getStringExtra(EXTRA_REQUESTS_JSON) ?: "[]"
|
||||
val settingsJson = intent.getStringExtra(EXTRA_SETTINGS_JSON) ?: "{}"
|
||||
val requestsJson = readNativeQueuePayload(
|
||||
intent,
|
||||
EXTRA_REQUESTS_JSON,
|
||||
EXTRA_REQUESTS_PATH,
|
||||
"[]"
|
||||
)
|
||||
val settingsJson = readNativeQueuePayload(
|
||||
intent,
|
||||
EXTRA_SETTINGS_JSON,
|
||||
EXTRA_SETTINGS_PATH,
|
||||
"{}"
|
||||
)
|
||||
startNativeWorker(requestsJson, settingsJson)
|
||||
}
|
||||
ACTION_PAUSE_NATIVE_QUEUE -> {
|
||||
@@ -366,6 +391,36 @@ class DownloadService : Service() {
|
||||
}
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
private fun readNativeQueuePayload(
|
||||
intent: Intent,
|
||||
jsonExtra: String,
|
||||
pathExtra: String,
|
||||
defaultValue: String,
|
||||
): String {
|
||||
val path = intent.getStringExtra(pathExtra).orEmpty()
|
||||
if (path.isNotBlank()) {
|
||||
return try {
|
||||
val file = File(path)
|
||||
val payload = file.readText()
|
||||
if (!file.delete()) {
|
||||
android.util.Log.w(
|
||||
"DownloadService",
|
||||
"Failed to delete native worker payload file: $path"
|
||||
)
|
||||
}
|
||||
payload.ifBlank { defaultValue }
|
||||
} catch (e: Exception) {
|
||||
android.util.Log.w(
|
||||
"DownloadService",
|
||||
"Failed to read native worker payload file: ${e.message}"
|
||||
)
|
||||
defaultValue
|
||||
}
|
||||
}
|
||||
|
||||
return intent.getStringExtra(jsonExtra) ?: defaultValue
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? = null
|
||||
|
||||
|
||||
@@ -2797,7 +2797,17 @@ class MainActivity: FlutterFragmentActivity() {
|
||||
"startNativeDownloadWorker" -> {
|
||||
val requestsJson = call.argument<String>("requests_json") ?: "[]"
|
||||
val settingsJson = call.argument<String>("settings_json") ?: "{}"
|
||||
DownloadService.startNativeQueue(this@MainActivity, requestsJson, settingsJson)
|
||||
val requestsPath = call.argument<String>("requests_path") ?: ""
|
||||
val settingsPath = call.argument<String>("settings_path") ?: ""
|
||||
if (requestsPath.isNotBlank()) {
|
||||
DownloadService.startNativeQueueFromFiles(
|
||||
this@MainActivity,
|
||||
requestsPath,
|
||||
settingsPath
|
||||
)
|
||||
} else {
|
||||
DownloadService.startNativeQueue(this@MainActivity, requestsJson, settingsJson)
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
"pauseNativeDownloadWorker" -> {
|
||||
|
||||
@@ -25,9 +25,10 @@ const (
|
||||
)
|
||||
|
||||
type ExtensionPermissions struct {
|
||||
Network []string `json:"network"`
|
||||
Storage bool `json:"storage"`
|
||||
File bool `json:"file"`
|
||||
Network []string `json:"network"`
|
||||
Storage bool `json:"storage"`
|
||||
File bool `json:"file"`
|
||||
AllowHTTP bool `json:"allowHttp,omitempty"`
|
||||
}
|
||||
|
||||
type ExtensionSetting struct {
|
||||
|
||||
@@ -258,7 +258,8 @@ func newExtensionHTTPClient(ext *loadedExtension, jar http.CookieJar, timeout ti
|
||||
Jar: jar,
|
||||
}
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
if req.URL.Scheme != "https" {
|
||||
if req.URL.Scheme != "https" &&
|
||||
!(req.URL.Scheme == "http" && ext.Manifest.Permissions.AllowHTTP) {
|
||||
GoLog("[Extension:%s] Redirect blocked: non-https scheme '%s'\n", ext.ID, req.URL.Scheme)
|
||||
return fmt.Errorf("redirect blocked: only https is allowed")
|
||||
}
|
||||
|
||||
@@ -44,7 +44,8 @@ func (r *extensionRuntime) validateDomain(urlStr string) error {
|
||||
if parsed.Scheme == "" {
|
||||
return fmt.Errorf("invalid URL: scheme is required")
|
||||
}
|
||||
if parsed.Scheme != "https" {
|
||||
if parsed.Scheme != "https" &&
|
||||
!(parsed.Scheme == "http" && r.manifest.Permissions.AllowHTTP) {
|
||||
return fmt.Errorf("network access denied: only https is allowed")
|
||||
}
|
||||
if parsed.User != nil {
|
||||
|
||||
@@ -144,6 +144,15 @@ func TestExtensionRuntime_NetworkSandbox(t *testing.T) {
|
||||
if err := runtime.validateDomain("https://notallowed.com/path"); err == nil {
|
||||
t.Error("Expected notallowed.com to be denied")
|
||||
}
|
||||
|
||||
if err := runtime.validateDomain("http://api.allowed.com/path"); err == nil {
|
||||
t.Error("Expected http URL to be denied without allowHttp")
|
||||
}
|
||||
|
||||
ext.Manifest.Permissions.AllowHTTP = true
|
||||
if err := runtime.validateDomain("http://api.allowed.com/path"); err != nil {
|
||||
t.Errorf("Expected http URL to be allowed with allowHttp, got error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtensionRuntime_FileSandbox(t *testing.T) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotiflac_android/services/download_request_payload.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
@@ -841,10 +842,58 @@ class PlatformBridge {
|
||||
required List<Map<String, dynamic>> requests,
|
||||
Map<String, dynamic> settings = const {},
|
||||
}) async {
|
||||
await _channel.invokeMethod('startNativeDownloadWorker', {
|
||||
'requests_json': jsonEncode(requests),
|
||||
'settings_json': jsonEncode(settings),
|
||||
});
|
||||
final requestsJson = jsonEncode(requests);
|
||||
final settingsJson = jsonEncode(settings);
|
||||
final payloadDir = await _nativeWorkerPayloadDir();
|
||||
await _cleanupNativeWorkerPayloads(payloadDir);
|
||||
final stamp = DateTime.now().microsecondsSinceEpoch;
|
||||
final requestPath = '${payloadDir.path}/requests_$stamp.json';
|
||||
final settingsPath = '${payloadDir.path}/settings_$stamp.json';
|
||||
await File(requestPath).writeAsString(requestsJson, flush: true);
|
||||
await File(settingsPath).writeAsString(settingsJson, flush: true);
|
||||
try {
|
||||
await _channel.invokeMethod('startNativeDownloadWorker', {
|
||||
'requests_path': requestPath,
|
||||
'settings_path': settingsPath,
|
||||
});
|
||||
} catch (_) {
|
||||
unawaited(_deleteFileIfExists(requestPath));
|
||||
unawaited(_deleteFileIfExists(settingsPath));
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
static Future<void> _deleteFileIfExists(String path) async {
|
||||
try {
|
||||
final file = File(path);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<Directory> _nativeWorkerPayloadDir() async {
|
||||
final tempDir = await getTemporaryDirectory();
|
||||
final payloadDir = Directory('${tempDir.path}/native_worker_payloads');
|
||||
if (!await payloadDir.exists()) {
|
||||
await payloadDir.create(recursive: true);
|
||||
}
|
||||
return payloadDir;
|
||||
}
|
||||
|
||||
static Future<void> _cleanupNativeWorkerPayloads(Directory payloadDir) async {
|
||||
final cutoff = DateTime.now().subtract(const Duration(days: 1));
|
||||
try {
|
||||
await for (final entity in payloadDir.list(followLinks: false)) {
|
||||
if (entity is! File || !entity.path.endsWith('.json')) continue;
|
||||
final stat = await entity.stat();
|
||||
if (stat.modified.isBefore(cutoff)) {
|
||||
try {
|
||||
await entity.delete();
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
static Future<void> pauseNativeDownloadWorker() async {
|
||||
|
||||
Reference in New Issue
Block a user