mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-02 06:46:22 +01:00
Migrate to DownloadManage on Android
Fix OOME when downloading large files
This commit is contained in:
parent
ff87cd3099
commit
734807733c
16 changed files with 457 additions and 28 deletions
|
@ -6,6 +6,7 @@
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="28"
|
android:maxSdkVersion="28"
|
||||||
tools:ignore="ScopedStorage" />
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"/>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
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) {
|
||||||
|
if (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)
|
||||||
|
}
|
||||||
|
} 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 val _activity = activity
|
||||||
|
private val _context get() = _activity
|
||||||
|
private val _downloadManager by lazy {
|
||||||
|
_context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,92 @@
|
||||||
|
package com.nkming.nc_photos
|
||||||
|
|
||||||
|
import android.app.DownloadManager
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import io.flutter.Log
|
||||||
|
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",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.i(
|
||||||
|
"DownloadEventCompleteChannelHandler.onReceive",
|
||||||
|
"ID #$downloadId not found, user canceled the job?"
|
||||||
|
)
|
||||||
|
_eventSink?.error(
|
||||||
|
"userCanceled", "Download #$downloadId was canceled", null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,11 +3,15 @@ package com.nkming.nc_photos
|
||||||
import androidx.annotation.NonNull
|
import androidx.annotation.NonNull
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
import io.flutter.embedding.engine.FlutterEngine
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.EventChannel
|
||||||
import io.flutter.plugin.common.MethodChannel
|
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))
|
||||||
|
@ -20,5 +24,9 @@ class MainActivity : FlutterActivity() {
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger,
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger,
|
||||||
ShareChannelHandler.CHANNEL).setMethodCallHandler(
|
ShareChannelHandler.CHANNEL).setMethodCallHandler(
|
||||||
ShareChannelHandler(this))
|
ShareChannelHandler(this))
|
||||||
|
|
||||||
|
EventChannel(flutterEngine.dartExecutor.binaryMessenger,
|
||||||
|
DownloadEventCompleteChannelHandler.CHANNEL).setStreamHandler(
|
||||||
|
DownloadEventCompleteChannelHandler(this))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,8 +19,6 @@ import java.io.BufferedOutputStream
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
private const val PERMISSION_REQUEST_CODE = 11011
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Save downloaded item on device
|
* Save downloaded item on device
|
||||||
*
|
*
|
||||||
|
@ -85,12 +83,7 @@ class MediaStoreChannelHandler(activity: Activity)
|
||||||
|
|
||||||
private fun saveFileToDownload0(fileName: String, content: ByteArray,
|
private fun saveFileToDownload0(fileName: String, content: ByteArray,
|
||||||
result: MethodChannel.Result) {
|
result: MethodChannel.Result) {
|
||||||
if (ContextCompat.checkSelfPermission(_activity,
|
if (!PermissionHandler.ensureWriteExternalStorage(_activity)) {
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
|
||||||
!= PackageManager.PERMISSION_GRANTED) {
|
|
||||||
ActivityCompat.requestPermissions(_activity,
|
|
||||||
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
|
||||||
PERMISSION_REQUEST_CODE)
|
|
||||||
result.error("permissionError", "Permission not granted", null)
|
result.error("permissionError", "Permission not granted", null)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
package com.nkming.nc_photos
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.pm.PackageManager
|
||||||
|
import androidx.core.app.ActivityCompat
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
|
||||||
|
private const val PERMISSION_REQUEST_CODE = 11011
|
||||||
|
|
||||||
|
class PermissionHandler {
|
||||||
|
companion object {
|
||||||
|
fun ensureWriteExternalStorage(activity: Activity): Boolean {
|
||||||
|
return if (ContextCompat.checkSelfPermission(
|
||||||
|
activity, Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||||
|
) != PackageManager.PERMISSION_GRANTED
|
||||||
|
) {
|
||||||
|
ActivityCompat.requestPermissions(
|
||||||
|
activity,
|
||||||
|
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||||
|
PERMISSION_REQUEST_CODE
|
||||||
|
)
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -65,3 +65,27 @@ class InvalidBaseUrlException implements Exception {
|
||||||
|
|
||||||
final dynamic message;
|
final dynamic message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A download job has failed
|
||||||
|
class DownloadException implements Exception {
|
||||||
|
DownloadException([this.message]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "DownloadException: $message";
|
||||||
|
}
|
||||||
|
|
||||||
|
final dynamic message;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A running job has been canceled
|
||||||
|
class JobCanceledException implements Exception {
|
||||||
|
JobCanceledException([this.message]);
|
||||||
|
|
||||||
|
@override
|
||||||
|
toString() {
|
||||||
|
return "JobCanceledException: $message";
|
||||||
|
}
|
||||||
|
|
||||||
|
final dynamic message;
|
||||||
|
}
|
||||||
|
|
51
lib/mobile/android/download.dart
Normal file
51
lib/mobile/android/download.dart
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
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,
|
||||||
|
}))!;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The download job has failed
|
||||||
|
static const exceptionCodeDownloadError = "downloadError";
|
||||||
|
|
||||||
|
static const _channel = MethodChannel("com.nkming.nc_photos/download");
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadEvent {
|
||||||
|
static StreamSubscription<DownloadCompleteEvent> listenDownloadComplete() =>
|
||||||
|
_stream.listen(null);
|
||||||
|
|
||||||
|
/// User canceled the download job
|
||||||
|
static const exceptionCodeUserCanceled = "userCanceled";
|
||||||
|
|
||||||
|
static const _downloadCompleteChannel = EventChannel(
|
||||||
|
"com.nkming.nc_photos/download_event/action_download_complete");
|
||||||
|
|
||||||
|
static late final _stream = _downloadCompleteChannel
|
||||||
|
.receiveBroadcastStream()
|
||||||
|
.map((data) => DownloadCompleteEvent(
|
||||||
|
data["downloadId"],
|
||||||
|
data["uri"],
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
class DownloadCompleteEvent {
|
||||||
|
const DownloadCompleteEvent(this.downloadId, this.uri);
|
||||||
|
|
||||||
|
final int downloadId;
|
||||||
|
final String uri;
|
||||||
|
}
|
85
lib/mobile/file_downloader.dart
Normal file
85
lib/mobile/file_downloader.dart
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:logging/logging.dart';
|
||||||
|
import 'package:nc_photos/exception.dart';
|
||||||
|
import 'package:nc_photos/mobile/android/download.dart';
|
||||||
|
import 'package:nc_photos/mobile/android/media_store.dart';
|
||||||
|
import 'package:nc_photos/platform/file_downloader.dart' as itf;
|
||||||
|
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||||
|
|
||||||
|
class FileDownloader extends itf.FileDownloader {
|
||||||
|
@override
|
||||||
|
downloadUrl({
|
||||||
|
required String url,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
String? mimeType,
|
||||||
|
required String filename,
|
||||||
|
bool? shouldNotify,
|
||||||
|
}) {
|
||||||
|
if (platform_k.isAndroid) {
|
||||||
|
return _downloadUrlAndroid(
|
||||||
|
url: url,
|
||||||
|
headers: headers,
|
||||||
|
mimeType: mimeType,
|
||||||
|
filename: filename,
|
||||||
|
shouldNotify: shouldNotify,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
throw UnimplementedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _downloadUrlAndroid({
|
||||||
|
required String url,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
String? mimeType,
|
||||||
|
required String filename,
|
||||||
|
bool? shouldNotify,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
_log.info("[_downloadUrlAndroid] Start downloading '$url'");
|
||||||
|
final id = await Download.downloadUrl(
|
||||||
|
url: url,
|
||||||
|
headers: headers,
|
||||||
|
mimeType: mimeType,
|
||||||
|
filename: filename,
|
||||||
|
shouldNotify: shouldNotify,
|
||||||
|
);
|
||||||
|
late final String uri;
|
||||||
|
final completer = Completer();
|
||||||
|
onDownloadComplete(DownloadCompleteEvent ev) {
|
||||||
|
if (ev.downloadId == id) {
|
||||||
|
_log.info(
|
||||||
|
"[_downloadUrlAndroid] Finished downloading '$url' to '${ev.uri}'");
|
||||||
|
uri = ev.uri;
|
||||||
|
completer.complete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadEvent.listenDownloadComplete()
|
||||||
|
..onData(onDownloadComplete)
|
||||||
|
..onError((e, stackTrace) {
|
||||||
|
completer.completeError(e, stackTrace);
|
||||||
|
});
|
||||||
|
await completer.future;
|
||||||
|
return uri;
|
||||||
|
} on PlatformException catch (e) {
|
||||||
|
switch (e.code) {
|
||||||
|
case MediaStore.exceptionCodePermissionError:
|
||||||
|
throw PermissionException();
|
||||||
|
|
||||||
|
case Download.exceptionCodeDownloadError:
|
||||||
|
throw DownloadException(e.message);
|
||||||
|
|
||||||
|
case DownloadEvent.exceptionCodeUserCanceled:
|
||||||
|
throw JobCanceledException(e.message);
|
||||||
|
|
||||||
|
default:
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static final _log = Logger("mobile.file_downloader.FileDownloader");
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
export 'db_util.dart';
|
export 'db_util.dart';
|
||||||
|
export 'file_downloader.dart';
|
||||||
export 'file_saver.dart';
|
export 'file_saver.dart';
|
||||||
export 'map_widget.dart';
|
export 'map_widget.dart';
|
||||||
export 'universal_storage.dart';
|
export 'universal_storage.dart';
|
||||||
|
|
18
lib/platform/file_downloader.dart
Normal file
18
lib/platform/file_downloader.dart
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
abstract class FileDownloader {
|
||||||
|
/// Download a file
|
||||||
|
///
|
||||||
|
/// The return data depends on the platform
|
||||||
|
/// - web: null
|
||||||
|
/// - android: Uri to the downloaded file
|
||||||
|
///
|
||||||
|
/// [shouldNotify] is a hint that suggest whether to notify user about the
|
||||||
|
/// progress. The actual decision is made by the underlying platform code and
|
||||||
|
/// is not guaranteed to respect this flag
|
||||||
|
Future<dynamic> downloadUrl({
|
||||||
|
required String url,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
String? mimeType,
|
||||||
|
required String filename,
|
||||||
|
bool? shouldNotify,
|
||||||
|
});
|
||||||
|
}
|
|
@ -4,7 +4,6 @@ import 'package:logging/logging.dart';
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
import 'package:nc_photos/app_localizations.dart';
|
import 'package:nc_photos/app_localizations.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/entity/file/data_source.dart';
|
|
||||||
import 'package:nc_photos/exception.dart';
|
import 'package:nc_photos/exception.dart';
|
||||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||||
import 'package:nc_photos/k.dart' as k;
|
import 'package:nc_photos/k.dart' as k;
|
||||||
|
@ -25,11 +24,16 @@ class ShareHandler {
|
||||||
builder: (context) =>
|
builder: (context) =>
|
||||||
ProcessingDialog(text: L10n.global().shareDownloadingDialogContent),
|
ProcessingDialog(text: L10n.global().shareDownloadingDialogContent),
|
||||||
);
|
);
|
||||||
final fileRepo = FileRepo(FileCachedDataSource());
|
|
||||||
final results = <Tuple2<File, dynamic>>[];
|
final results = <Tuple2<File, dynamic>>[];
|
||||||
for (final f in files) {
|
for (final f in files) {
|
||||||
try {
|
try {
|
||||||
results.add(Tuple2(f, await DownloadFile(fileRepo)(account, f)));
|
results.add(Tuple2(
|
||||||
|
f,
|
||||||
|
await DownloadFile()(
|
||||||
|
account,
|
||||||
|
f,
|
||||||
|
shouldNotify: false,
|
||||||
|
)));
|
||||||
} on PermissionException catch (_) {
|
} on PermissionException catch (_) {
|
||||||
_log.warning("[shareFiles] Permission not granted");
|
_log.warning("[shareFiles] Permission not granted");
|
||||||
SnackBarManager().showSnackBar(SnackBar(
|
SnackBarManager().showSnackBar(SnackBar(
|
||||||
|
|
|
@ -1,23 +1,31 @@
|
||||||
import 'package:nc_photos/account.dart';
|
import 'package:nc_photos/account.dart';
|
||||||
|
import 'package:nc_photos/api/api.dart';
|
||||||
import 'package:nc_photos/entity/file.dart';
|
import 'package:nc_photos/entity/file.dart';
|
||||||
import 'package:nc_photos/mobile/platform.dart'
|
import 'package:nc_photos/mobile/platform.dart'
|
||||||
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform;
|
||||||
import 'package:nc_photos/use_case/get_file_binary.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
class DownloadFile {
|
class DownloadFile {
|
||||||
DownloadFile(this._fileRepo);
|
DownloadFile();
|
||||||
|
|
||||||
/// Download [file]
|
/// Download [file]
|
||||||
///
|
///
|
||||||
/// The return data depends on the platform
|
/// See [FileDownloader.downloadUrl]
|
||||||
/// - web: null
|
Future<dynamic> call(
|
||||||
/// - android: Uri to the downloaded file
|
Account account,
|
||||||
Future<dynamic> call(Account account, File file) async {
|
File file, {
|
||||||
final content = await GetFileBinary(_fileRepo)(account, file);
|
bool? shouldNotify,
|
||||||
final saver = platform.FileSaver();
|
}) {
|
||||||
return saver.saveFile(path.basename(file.path), content);
|
final downloader = platform.FileDownloader();
|
||||||
|
final url = "${account.url}/${file.path}";
|
||||||
|
return downloader.downloadUrl(
|
||||||
|
url: url,
|
||||||
|
headers: {
|
||||||
|
"authorization": Api.getAuthorizationHeaderValue(account),
|
||||||
|
},
|
||||||
|
mimeType: file.contentType,
|
||||||
|
filename: path.basename(file.path),
|
||||||
|
shouldNotify: shouldNotify,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final FileRepo _fileRepo;
|
|
||||||
}
|
}
|
||||||
|
|
26
lib/web/file_downloader.dart
Normal file
26
lib/web/file_downloader.dart
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
import 'package:nc_photos/exception.dart';
|
||||||
|
import 'package:nc_photos/platform/file_downloader.dart' as itf;
|
||||||
|
import 'package:nc_photos/web/file_saver.dart';
|
||||||
|
|
||||||
|
class FileDownloader extends itf.FileDownloader {
|
||||||
|
@override
|
||||||
|
downloadUrl({
|
||||||
|
required String url,
|
||||||
|
Map<String, String>? headers,
|
||||||
|
String? mimeType,
|
||||||
|
required String filename,
|
||||||
|
bool? shouldNotify,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse(url);
|
||||||
|
final req = http.Request("GET", uri)..headers.addAll(headers ?? {});
|
||||||
|
final response =
|
||||||
|
await http.Response.fromStream(await http.Client().send(req));
|
||||||
|
if (response.statusCode ~/ 100 != 2) {
|
||||||
|
throw DownloadException(
|
||||||
|
"Failed downloading $filename (HTTP ${response.statusCode})");
|
||||||
|
}
|
||||||
|
final saver = FileSaver();
|
||||||
|
await saver.saveFile(filename, response.bodyBytes);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +1,5 @@
|
||||||
export 'db_util.dart';
|
export 'db_util.dart';
|
||||||
|
export 'file_downloader.dart';
|
||||||
export 'file_saver.dart';
|
export 'file_saver.dart';
|
||||||
export 'map_widget.dart';
|
export 'map_widget.dart';
|
||||||
export 'universal_storage.dart';
|
export 'universal_storage.dart';
|
||||||
|
|
|
@ -452,9 +452,9 @@ class _ViewerState extends State<Viewer>
|
||||||
});
|
});
|
||||||
dynamic result;
|
dynamic result;
|
||||||
try {
|
try {
|
||||||
final fileRepo = FileRepo(FileCachedDataSource());
|
result = await DownloadFile()(widget.account, file);
|
||||||
result = await DownloadFile(fileRepo)(widget.account, file);
|
|
||||||
controller?.close();
|
controller?.close();
|
||||||
|
_onDownloadSuccessful(file, result);
|
||||||
} on PermissionException catch (_) {
|
} on PermissionException catch (_) {
|
||||||
_log.warning("[_onDownloadPressed] Permission not granted");
|
_log.warning("[_onDownloadPressed] Permission not granted");
|
||||||
controller?.close();
|
controller?.close();
|
||||||
|
@ -462,7 +462,8 @@ class _ViewerState extends State<Viewer>
|
||||||
content: Text(L10n.global().downloadFailureNoPermissionNotification),
|
content: Text(L10n.global().downloadFailureNoPermissionNotification),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
return;
|
} on JobCanceledException catch (_) {
|
||||||
|
_log.info("[_onDownloadPressed] Canceled");
|
||||||
} catch (e, stacktrace) {
|
} catch (e, stacktrace) {
|
||||||
_log.shout(
|
_log.shout(
|
||||||
"[_onDownloadPressed] Failed while downloadFile", e, stacktrace);
|
"[_onDownloadPressed] Failed while downloadFile", e, stacktrace);
|
||||||
|
@ -472,10 +473,7 @@ class _ViewerState extends State<Viewer>
|
||||||
"${exception_util.toUserString(e)}"),
|
"${exception_util.toUserString(e)}"),
|
||||||
duration: k.snackBarDurationNormal,
|
duration: k.snackBarDurationNormal,
|
||||||
));
|
));
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_onDownloadSuccessful(file, result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onDownloadSuccessful(File file, dynamic result) {
|
void _onDownloadSuccessful(File file, dynamic result) {
|
||||||
|
|
Loading…
Reference in a new issue