mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
Handle download in dart to support customized request
This commit is contained in:
parent
15523c8bda
commit
50a80fe2a3
9 changed files with 141 additions and 353 deletions
|
@ -1,109 +0,0 @@
|
||||||
package com.nkming.nc_photos
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Environment
|
|
||||||
import io.flutter.plugin.common.MethodCall
|
|
||||||
import io.flutter.plugin.common.MethodChannel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Download a file
|
|
||||||
*
|
|
||||||
* Methods:
|
|
||||||
* Download a file at @a url. If @a shouldNotify is false, no progress
|
|
||||||
* notifications would be shown
|
|
||||||
* fun downloadUrl(url: String, headers: Map<String, String>?,
|
|
||||||
* mimeType: String?, filename: String, shouldNotify: Boolean?): String
|
|
||||||
*/
|
|
||||||
class DownloadChannelHandler(activity: Activity) :
|
|
||||||
MethodChannel.MethodCallHandler {
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
val CHANNEL = "com.nkming.nc_photos/download"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
|
||||||
when (call.method) {
|
|
||||||
"downloadUrl" -> {
|
|
||||||
try {
|
|
||||||
downloadUrl(
|
|
||||||
call.argument("url")!!,
|
|
||||||
call.argument("headers"),
|
|
||||||
call.argument("mimeType"),
|
|
||||||
call.argument("filename")!!,
|
|
||||||
call.argument("shouldNotify"),
|
|
||||||
result
|
|
||||||
)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
result.error("systemException", e.toString(), null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"cancel" -> {
|
|
||||||
try {
|
|
||||||
cancel(
|
|
||||||
call.argument("id")!!, result
|
|
||||||
)
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
result.error("systemException", e.toString(), null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
result.notImplemented()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun downloadUrl(
|
|
||||||
url: String,
|
|
||||||
headers: Map<String, String>?,
|
|
||||||
mimeType: String?,
|
|
||||||
filename: String,
|
|
||||||
shouldNotify: Boolean?,
|
|
||||||
result: MethodChannel.Result
|
|
||||||
) {
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
|
||||||
if (!PermissionHandler.ensureWriteExternalStorage(_activity)) {
|
|
||||||
result.error("permissionError", "Permission not granted", null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val uri = Uri.parse(url)
|
|
||||||
val req = DownloadManager.Request(uri).apply {
|
|
||||||
setDestinationInExternalPublicDir(
|
|
||||||
Environment.DIRECTORY_DOWNLOADS, filename
|
|
||||||
)
|
|
||||||
for (h in headers ?: mapOf()) {
|
|
||||||
addRequestHeader(h.key, h.value)
|
|
||||||
}
|
|
||||||
if (mimeType != null) {
|
|
||||||
setMimeType(mimeType)
|
|
||||||
}
|
|
||||||
setVisibleInDownloadsUi(true)
|
|
||||||
setNotificationVisibility(
|
|
||||||
if (shouldNotify == false) DownloadManager.Request.VISIBILITY_HIDDEN
|
|
||||||
else DownloadManager.Request.VISIBILITY_VISIBLE
|
|
||||||
)
|
|
||||||
allowScanningByMediaScanner()
|
|
||||||
}
|
|
||||||
|
|
||||||
val id = _downloadManager.enqueue(req)
|
|
||||||
result.success(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun cancel(
|
|
||||||
id: Long, result: MethodChannel.Result
|
|
||||||
) {
|
|
||||||
val count = _downloadManager.remove(id)
|
|
||||||
result.success(count > 0)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _activity = activity
|
|
||||||
private val _context get() = _activity
|
|
||||||
private val _downloadManager by lazy {
|
|
||||||
_context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,99 +1,10 @@
|
||||||
package com.nkming.nc_photos
|
package com.nkming.nc_photos
|
||||||
|
|
||||||
import android.app.DownloadManager
|
|
||||||
import android.content.BroadcastReceiver
|
import android.content.BroadcastReceiver
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import android.net.Uri
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import io.flutter.Log
|
|
||||||
import io.flutter.plugin.common.EventChannel
|
import io.flutter.plugin.common.EventChannel
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send DownloadManager ACTION_DOWNLOAD_COMPLETE events to flutter
|
|
||||||
*/
|
|
||||||
class DownloadEventCompleteChannelHandler(context: Context) :
|
|
||||||
BroadcastReceiver(), EventChannel.StreamHandler {
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
val CHANNEL =
|
|
||||||
"com.nkming.nc_photos/download_event/action_download_complete"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onReceive(context: Context?, intent: Intent?) {
|
|
||||||
if (intent?.action != DownloadManager.ACTION_DOWNLOAD_COMPLETE || !intent.hasExtra(
|
|
||||||
DownloadManager.EXTRA_DOWNLOAD_ID
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
val downloadId =
|
|
||||||
intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0)
|
|
||||||
// check the status of the job and retrieve the local URI
|
|
||||||
val c = _downloadManager.query(
|
|
||||||
DownloadManager.Query().setFilterById(downloadId)
|
|
||||||
)
|
|
||||||
if (c.moveToFirst()) {
|
|
||||||
val status =
|
|
||||||
c.getInt(c.getColumnIndex(DownloadManager.COLUMN_STATUS))
|
|
||||||
if (status == DownloadManager.STATUS_SUCCESSFUL) {
|
|
||||||
val uri =
|
|
||||||
c.getString(c.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI))
|
|
||||||
val contentUri = FileProvider.getUriForFile(
|
|
||||||
_context,
|
|
||||||
"${BuildConfig.APPLICATION_ID}.fileprovider",
|
|
||||||
File(Uri.parse(uri).path!!)
|
|
||||||
)
|
|
||||||
_eventSink?.success(
|
|
||||||
mapOf(
|
|
||||||
"downloadId" to downloadId,
|
|
||||||
"uri" to contentUri.toString()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else if (status == DownloadManager.STATUS_FAILED) {
|
|
||||||
val reason =
|
|
||||||
c.getInt(c.getColumnIndex(DownloadManager.COLUMN_REASON))
|
|
||||||
_eventSink?.error(
|
|
||||||
"downloadError",
|
|
||||||
"Download #$downloadId was not successful, status: $status, reason: $reason",
|
|
||||||
mapOf(
|
|
||||||
"downloadId" to downloadId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Log.i(
|
|
||||||
"DownloadEventCompleteChannelHandler.onReceive",
|
|
||||||
"ID #$downloadId not found, user canceled the job?"
|
|
||||||
)
|
|
||||||
_eventSink?.error(
|
|
||||||
"userCanceled", "Download #$downloadId was canceled", mapOf(
|
|
||||||
"downloadId" to downloadId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
|
|
||||||
_context.registerReceiver(
|
|
||||||
this, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)
|
|
||||||
)
|
|
||||||
_eventSink = events
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancel(arguments: Any?) {
|
|
||||||
_context.unregisterReceiver(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val _context = context
|
|
||||||
private var _eventSink: EventChannel.EventSink? = null
|
|
||||||
private val _downloadManager by lazy {
|
|
||||||
_context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class DownloadEventCancelChannelHandler(context: Context) : BroadcastReceiver(),
|
class DownloadEventCancelChannelHandler(context: Context) : BroadcastReceiver(),
|
||||||
EventChannel.StreamHandler {
|
EventChannel.StreamHandler {
|
||||||
|
|
|
@ -9,9 +9,6 @@ import io.flutter.plugin.common.MethodChannel
|
||||||
class MainActivity : FlutterActivity() {
|
class MainActivity : FlutterActivity() {
|
||||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger,
|
|
||||||
DownloadChannelHandler.CHANNEL).setMethodCallHandler(
|
|
||||||
DownloadChannelHandler(this))
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger,
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger,
|
||||||
MediaStoreChannelHandler.CHANNEL).setMethodCallHandler(
|
MediaStoreChannelHandler.CHANNEL).setMethodCallHandler(
|
||||||
MediaStoreChannelHandler(this))
|
MediaStoreChannelHandler(this))
|
||||||
|
@ -25,9 +22,6 @@ class MainActivity : FlutterActivity() {
|
||||||
ShareChannelHandler.CHANNEL).setMethodCallHandler(
|
ShareChannelHandler.CHANNEL).setMethodCallHandler(
|
||||||
ShareChannelHandler(this))
|
ShareChannelHandler(this))
|
||||||
|
|
||||||
EventChannel(flutterEngine.dartExecutor.binaryMessenger,
|
|
||||||
DownloadEventCompleteChannelHandler.CHANNEL).setStreamHandler(
|
|
||||||
DownloadEventCompleteChannelHandler(this))
|
|
||||||
EventChannel(flutterEngine.dartExecutor.binaryMessenger,
|
EventChannel(flutterEngine.dartExecutor.binaryMessenger,
|
||||||
DownloadEventCancelChannelHandler.CHANNEL).setStreamHandler(
|
DownloadEventCancelChannelHandler.CHANNEL).setStreamHandler(
|
||||||
DownloadEventCancelChannelHandler(this))
|
DownloadEventCancelChannelHandler(this))
|
||||||
|
|
|
@ -11,9 +11,7 @@ import androidx.annotation.RequiresApi
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import io.flutter.plugin.common.MethodCall
|
import io.flutter.plugin.common.MethodCall
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
import java.io.BufferedOutputStream
|
import java.io.*
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Save downloaded item on device
|
* Save downloaded item on device
|
||||||
|
@ -31,8 +29,8 @@ class MediaStoreChannelHandler(activity: Activity) :
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||||
if (call.method == "saveFileToDownload") {
|
when (call.method) {
|
||||||
try {
|
"saveFileToDownload" -> try {
|
||||||
saveFileToDownload(
|
saveFileToDownload(
|
||||||
call.argument("fileName")!!,
|
call.argument("fileName")!!,
|
||||||
call.argument("content")!!,
|
call.argument("content")!!,
|
||||||
|
@ -41,24 +39,47 @@ class MediaStoreChannelHandler(activity: Activity) :
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
result.error("systemException", e.message, null)
|
result.error("systemException", e.message, null)
|
||||||
}
|
}
|
||||||
} else {
|
"copyFileToDownload" -> try {
|
||||||
result.notImplemented()
|
copyFileToDownload(
|
||||||
|
call.argument("toFileName")!!,
|
||||||
|
call.argument("fromFilePath")!!,
|
||||||
|
result
|
||||||
|
)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
result.error("systemException", e.message, null)
|
||||||
|
}
|
||||||
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveFileToDownload(
|
private fun saveFileToDownload(
|
||||||
fileName: String, content: ByteArray, result: MethodChannel.Result
|
fileName: String, content: ByteArray, result: MethodChannel.Result
|
||||||
|
) {
|
||||||
|
val stream = ByteArrayInputStream(content)
|
||||||
|
writeFileToDownload(fileName, stream, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyFileToDownload(
|
||||||
|
toFileName: String, fromFilePath: String, result: MethodChannel.Result
|
||||||
|
) {
|
||||||
|
val file = File(fromFilePath)
|
||||||
|
val stream = file.inputStream()
|
||||||
|
writeFileToDownload(toFileName, stream, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun writeFileToDownload(
|
||||||
|
fileName: String, data: InputStream, result: MethodChannel.Result
|
||||||
) {
|
) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
saveFileToDownload29(fileName, content, result)
|
writeFileToDownload29(fileName, data, result)
|
||||||
} else {
|
} else {
|
||||||
saveFileToDownload0(fileName, content, result)
|
writeFileToDownload0(fileName, data, result)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(Build.VERSION_CODES.Q)
|
@RequiresApi(Build.VERSION_CODES.Q)
|
||||||
private fun saveFileToDownload29(
|
private fun writeFileToDownload29(
|
||||||
fileName: String, content: ByteArray, result: MethodChannel.Result
|
fileName: String, data: InputStream, result: MethodChannel.Result
|
||||||
) {
|
) {
|
||||||
// Add a media item that other apps shouldn't see until the item is
|
// Add a media item that other apps shouldn't see until the item is
|
||||||
// fully written to the media store.
|
// fully written to the media store.
|
||||||
|
@ -77,14 +98,14 @@ class MediaStoreChannelHandler(activity: Activity) :
|
||||||
resolver.openFileDescriptor(contentUri!!, "w", null).use { pfd ->
|
resolver.openFileDescriptor(contentUri!!, "w", null).use { pfd ->
|
||||||
// Write data into the pending audio file.
|
// Write data into the pending audio file.
|
||||||
BufferedOutputStream(FileOutputStream(pfd!!.fileDescriptor)).use { stream ->
|
BufferedOutputStream(FileOutputStream(pfd!!.fileDescriptor)).use { stream ->
|
||||||
stream.write(content)
|
data.copyTo(stream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
result.success(contentUri.toString())
|
result.success(contentUri.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun saveFileToDownload0(
|
private fun writeFileToDownload0(
|
||||||
fileName: String, content: ByteArray, result: MethodChannel.Result
|
fileName: String, data: InputStream, result: MethodChannel.Result
|
||||||
) {
|
) {
|
||||||
if (!PermissionHandler.ensureWriteExternalStorage(_activity)) {
|
if (!PermissionHandler.ensureWriteExternalStorage(_activity)) {
|
||||||
result.error("permissionError", "Permission not granted", null)
|
result.error("permissionError", "Permission not granted", null)
|
||||||
|
@ -103,7 +124,7 @@ class MediaStoreChannelHandler(activity: Activity) :
|
||||||
++count
|
++count
|
||||||
}
|
}
|
||||||
BufferedOutputStream(FileOutputStream(file)).use { stream ->
|
BufferedOutputStream(FileOutputStream(file)).use { stream ->
|
||||||
stream.write(content)
|
data.copyTo(stream)
|
||||||
}
|
}
|
||||||
|
|
||||||
val fileUri = Uri.fromFile(file)
|
val fileUri = Uri.fromFile(file)
|
||||||
|
|
|
@ -2,66 +2,13 @@ import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class Download {
|
|
||||||
static Future<int> downloadUrl({
|
|
||||||
required String url,
|
|
||||||
Map<String, String>? headers,
|
|
||||||
String? mimeType,
|
|
||||||
required String filename,
|
|
||||||
bool? shouldNotify,
|
|
||||||
}) async {
|
|
||||||
return (await _channel.invokeMethod<int>("downloadUrl", <String, dynamic>{
|
|
||||||
"url": url,
|
|
||||||
"headers": headers,
|
|
||||||
"mimeType": mimeType,
|
|
||||||
"filename": filename,
|
|
||||||
"shouldNotify": shouldNotify,
|
|
||||||
}))!;
|
|
||||||
}
|
|
||||||
|
|
||||||
static Future<bool> cancel({
|
|
||||||
required int id,
|
|
||||||
}) async {
|
|
||||||
return (await _channel.invokeMethod<bool>("cancel", <String, dynamic>{
|
|
||||||
"id": id,
|
|
||||||
}))!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The download job has failed
|
|
||||||
static const exceptionCodeDownloadError = "downloadError";
|
|
||||||
|
|
||||||
static const _channel = MethodChannel("com.nkming.nc_photos/download");
|
|
||||||
}
|
|
||||||
|
|
||||||
class DownloadEvent {
|
class DownloadEvent {
|
||||||
static StreamSubscription<DownloadCompleteEvent> listenDownloadComplete() =>
|
|
||||||
_completeStream.listen(null);
|
|
||||||
|
|
||||||
static StreamSubscription<DownloadCancelEvent> listenDownloadCancel() =>
|
static StreamSubscription<DownloadCancelEvent> listenDownloadCancel() =>
|
||||||
_cancelStream.listen(null);
|
_cancelStream.listen(null);
|
||||||
|
|
||||||
/// User canceled the download job
|
/// User canceled the download job
|
||||||
static const exceptionCodeUserCanceled = "userCanceled";
|
static const exceptionCodeUserCanceled = "userCanceled";
|
||||||
|
|
||||||
static const _downloadCompleteChannel = EventChannel(
|
|
||||||
"com.nkming.nc_photos/download_event/action_download_complete");
|
|
||||||
|
|
||||||
static late final _completeStream = _downloadCompleteChannel
|
|
||||||
.receiveBroadcastStream()
|
|
||||||
.map((data) => DownloadCompleteEvent(
|
|
||||||
data["downloadId"],
|
|
||||||
data["uri"],
|
|
||||||
))
|
|
||||||
.handleError(
|
|
||||||
(e, stackTrace) {
|
|
||||||
throw AndroidDownloadError(e.details["downloadId"], e, stackTrace);
|
|
||||||
},
|
|
||||||
test: (e) =>
|
|
||||||
e is PlatformException &&
|
|
||||||
e.details is Map &&
|
|
||||||
e.details["downloadId"] is int,
|
|
||||||
);
|
|
||||||
|
|
||||||
static const _downloadCancelChannel = EventChannel(
|
static const _downloadCancelChannel = EventChannel(
|
||||||
"com.nkming.nc_photos/download_event/action_download_cancel");
|
"com.nkming.nc_photos/download_event/action_download_cancel");
|
||||||
|
|
||||||
|
@ -72,21 +19,6 @@ class DownloadEvent {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
class DownloadCompleteEvent {
|
|
||||||
const DownloadCompleteEvent(this.downloadId, this.uri);
|
|
||||||
|
|
||||||
final int downloadId;
|
|
||||||
final String uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
class AndroidDownloadError implements Exception {
|
|
||||||
const AndroidDownloadError(this.downloadId, this.error, this.stackTrace);
|
|
||||||
|
|
||||||
final int downloadId;
|
|
||||||
final dynamic error;
|
|
||||||
final StackTrace stackTrace;
|
|
||||||
}
|
|
||||||
|
|
||||||
class DownloadCancelEvent {
|
class DownloadCancelEvent {
|
||||||
const DownloadCancelEvent(this.notificationId);
|
const DownloadCancelEvent(this.notificationId);
|
||||||
|
|
||||||
|
|
|
@ -14,5 +14,14 @@ class MediaStore {
|
||||||
}))!;
|
}))!;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static Future<String> copyFileToDownload(
|
||||||
|
String toFileName, String fromFilePath) async {
|
||||||
|
return (await _channel
|
||||||
|
.invokeMethod<String>("copyFileToDownload", <String, dynamic>{
|
||||||
|
"toFileName": toFileName,
|
||||||
|
"fromFilePath": fromFilePath,
|
||||||
|
}))!;
|
||||||
|
}
|
||||||
|
|
||||||
static const _channel = MethodChannel("com.nkming.nc_photos/media_store");
|
static const _channel = MethodChannel("com.nkming.nc_photos/media_store");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/exception.dart';
|
import 'package:nc_photos/exception.dart';
|
||||||
import 'package:nc_photos/mobile/android/download.dart' as android;
|
|
||||||
import 'package:nc_photos/mobile/android/media_store.dart';
|
import 'package:nc_photos/mobile/android/media_store.dart';
|
||||||
import 'package:nc_photos/platform/download.dart' as itf;
|
import 'package:nc_photos/platform/download.dart' as itf;
|
||||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:uuid/uuid.dart';
|
||||||
|
|
||||||
class DownloadBuilder extends itf.DownloadBuilder {
|
class DownloadBuilder extends itf.DownloadBuilder {
|
||||||
@override
|
@override
|
||||||
|
@ -45,77 +47,99 @@ class _AndroidDownload extends itf.Download {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
call() async {
|
call() async {
|
||||||
final String path;
|
if (_isInitialDownload) {
|
||||||
if (parentDir?.isNotEmpty == true) {
|
await _cleanUp();
|
||||||
path = "$parentDir/$filename";
|
_isInitialDownload = false;
|
||||||
} else {
|
|
||||||
path = filename;
|
|
||||||
}
|
}
|
||||||
|
final file = await _createTempFile();
|
||||||
try {
|
try {
|
||||||
_log.info("[call] Start downloading '$url'");
|
// download file to a temp dir
|
||||||
_downloadId = await android.Download.downloadUrl(
|
final fileWrite = file.openWrite();
|
||||||
url: url,
|
|
||||||
headers: headers,
|
|
||||||
mimeType: mimeType,
|
|
||||||
filename: path,
|
|
||||||
shouldNotify: shouldNotify,
|
|
||||||
);
|
|
||||||
_log.info("[call] #$_downloadId -> '$url'");
|
|
||||||
late final String uri;
|
|
||||||
final completer = Completer();
|
|
||||||
onDownloadComplete(android.DownloadCompleteEvent ev) {
|
|
||||||
if (ev.downloadId == _downloadId) {
|
|
||||||
_log.info("[call] Finished downloading '$url' to '${ev.uri}'");
|
|
||||||
uri = ev.uri;
|
|
||||||
completer.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
StreamSubscription<android.DownloadCompleteEvent>? subscription;
|
|
||||||
try {
|
try {
|
||||||
subscription = android.DownloadEvent.listenDownloadComplete()
|
final uri = Uri.parse(url);
|
||||||
..onData(onDownloadComplete)
|
final req = http.Request("GET", uri)..headers.addAll(headers ?? {});
|
||||||
..onError((e, stackTrace) {
|
final response = await http.Client().send(req);
|
||||||
if (e is android.AndroidDownloadError) {
|
bool isEnd = false;
|
||||||
if (e.downloadId != _downloadId) {
|
Object? error;
|
||||||
// not us, ignore
|
final subscription = response.stream.listen(
|
||||||
return;
|
fileWrite.add,
|
||||||
}
|
onDone: () {
|
||||||
completer.completeError(e.error, e.stackTrace);
|
isEnd = true;
|
||||||
} else {
|
},
|
||||||
completer.completeError(e, stackTrace);
|
onError: (e, stackTrace) {
|
||||||
}
|
_log.severe("Failed while request", e, stackTrace);
|
||||||
});
|
isEnd = true;
|
||||||
await completer.future;
|
error = e;
|
||||||
|
},
|
||||||
|
cancelOnError: true,
|
||||||
|
);
|
||||||
|
// wait until download finished
|
||||||
|
while (!isEnd) {
|
||||||
|
if (shouldInterrupt) {
|
||||||
|
await subscription.cancel();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
await Future.delayed(const Duration(seconds: 1));
|
||||||
|
}
|
||||||
|
if (error != null) {
|
||||||
|
throw error!;
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
subscription?.cancel();
|
fileWrite.close();
|
||||||
}
|
}
|
||||||
return uri;
|
if (shouldInterrupt) {
|
||||||
} on PlatformException catch (e) {
|
throw JobCanceledException();
|
||||||
switch (e.code) {
|
|
||||||
case MediaStore.exceptionCodePermissionError:
|
|
||||||
throw PermissionException();
|
|
||||||
|
|
||||||
case android.Download.exceptionCodeDownloadError:
|
|
||||||
throw DownloadException(e.message);
|
|
||||||
|
|
||||||
case android.DownloadEvent.exceptionCodeUserCanceled:
|
|
||||||
throw JobCanceledException(e.message);
|
|
||||||
|
|
||||||
default:
|
|
||||||
rethrow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// copy the file to the actual dir
|
||||||
|
final String path;
|
||||||
|
if (parentDir?.isNotEmpty == true) {
|
||||||
|
path = "$parentDir/$filename";
|
||||||
|
} else {
|
||||||
|
path = filename;
|
||||||
|
}
|
||||||
|
return await MediaStore.copyFileToDownload(path, file.path);
|
||||||
|
} finally {
|
||||||
|
file.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
cancel() async {
|
cancel() {
|
||||||
if (_downloadId != null) {
|
shouldInterrupt = true;
|
||||||
_log.info("[cancel] Cancel #$_downloadId");
|
return true;
|
||||||
return await android.Download.cancel(id: _downloadId!);
|
}
|
||||||
|
|
||||||
|
Future<Directory> _openDownloadDir() async {
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final downloadDir = Directory("${tempDir.path}/downloads");
|
||||||
|
if (!await downloadDir.exists()) {
|
||||||
|
return downloadDir.create();
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return downloadDir;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<File> _createTempFile() async {
|
||||||
|
final downloadDir = await _openDownloadDir();
|
||||||
|
while (true) {
|
||||||
|
final fileName = const Uuid().v4();
|
||||||
|
final file = File("${downloadDir.path}/$fileName");
|
||||||
|
if (await file.exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clean up remaining cache files from previous runs
|
||||||
|
///
|
||||||
|
/// Normally the files will be deleted automatically
|
||||||
|
Future<void> _cleanUp() async {
|
||||||
|
final downloadDir = await _openDownloadDir();
|
||||||
|
await for (final f in downloadDir.list(followLinks: false)) {
|
||||||
|
_log.warning("[_cleanUp] Deleting file: ${f.path}");
|
||||||
|
await f.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -125,7 +149,10 @@ class _AndroidDownload extends itf.Download {
|
||||||
final String filename;
|
final String filename;
|
||||||
final String? parentDir;
|
final String? parentDir;
|
||||||
final bool? shouldNotify;
|
final bool? shouldNotify;
|
||||||
int? _downloadId;
|
|
||||||
|
bool shouldInterrupt = false;
|
||||||
|
|
||||||
|
static bool _isInitialDownload = true;
|
||||||
|
|
||||||
static final _log = Logger("mobile.download._AndroidDownload");
|
static final _log = Logger("mobile.download._AndroidDownload");
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,8 +8,11 @@ abstract class Download {
|
||||||
|
|
||||||
/// Cancel a download
|
/// Cancel a download
|
||||||
///
|
///
|
||||||
/// Not all platforms support canceling an ongoing download
|
/// Not all platforms support canceling an ongoing download. Return true if
|
||||||
Future<bool> cancel();
|
/// the current platform supports it, however there's no guarantee if and when
|
||||||
|
/// the download task would be canceled. After a download is canceled
|
||||||
|
/// successfully, [JobCanceledException] will be thrown in [call]
|
||||||
|
bool cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
abstract class DownloadBuilder {
|
abstract class DownloadBuilder {
|
||||||
|
|
|
@ -43,7 +43,7 @@ class _WebDownload extends itf.Download {
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
cancel() => Future.value(false);
|
cancel() => false;
|
||||||
|
|
||||||
final String url;
|
final String url;
|
||||||
final Map<String, String>? headers;
|
final Map<String, String>? headers;
|
||||||
|
|
Loading…
Reference in a new issue