fix: avoid native worker binder payload limit

This commit is contained in:
zarzet
2026-05-08 01:06:48 +07:00
parent 4bc28704ff
commit fb5d8826a2
7 changed files with 138 additions and 12 deletions
@@ -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" -> {
+4 -3
View File
@@ -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 {
+2 -1
View File
@@ -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")
}
+2 -1
View File
@@ -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 {
+9
View File
@@ -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) {
+53 -4
View File
@@ -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 {