mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-01-22 16:56:19 +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"
|
||||
android:maxSdkVersion="28"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION"/>
|
||||
|
||||
<application
|
||||
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 io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.EventChannel
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger,
|
||||
DownloadChannelHandler.CHANNEL).setMethodCallHandler(
|
||||
DownloadChannelHandler(this))
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger,
|
||||
MediaStoreChannelHandler.CHANNEL).setMethodCallHandler(
|
||||
MediaStoreChannelHandler(this))
|
||||
|
@ -20,5 +24,9 @@ class MainActivity : FlutterActivity() {
|
|||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger,
|
||||
ShareChannelHandler.CHANNEL).setMethodCallHandler(
|
||||
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.FileOutputStream
|
||||
|
||||
private const val PERMISSION_REQUEST_CODE = 11011
|
||||
|
||||
/*
|
||||
* Save downloaded item on device
|
||||
*
|
||||
|
@ -85,12 +83,7 @@ class MediaStoreChannelHandler(activity: Activity)
|
|||
|
||||
private fun saveFileToDownload0(fileName: String, content: ByteArray,
|
||||
result: MethodChannel.Result) {
|
||||
if (ContextCompat.checkSelfPermission(_activity,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(_activity,
|
||||
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||
PERMISSION_REQUEST_CODE)
|
||||
if (!PermissionHandler.ensureWriteExternalStorage(_activity)) {
|
||||
result.error("permissionError", "Permission not granted", null)
|
||||
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;
|
||||
}
|
||||
|
||||
/// 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 'file_downloader.dart';
|
||||
export 'file_saver.dart';
|
||||
export 'map_widget.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/app_localizations.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_util.dart' as exception_util;
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
|
@ -25,11 +24,16 @@ class ShareHandler {
|
|||
builder: (context) =>
|
||||
ProcessingDialog(text: L10n.global().shareDownloadingDialogContent),
|
||||
);
|
||||
final fileRepo = FileRepo(FileCachedDataSource());
|
||||
final results = <Tuple2<File, dynamic>>[];
|
||||
for (final f in files) {
|
||||
try {
|
||||
results.add(Tuple2(f, await DownloadFile(fileRepo)(account, f)));
|
||||
results.add(Tuple2(
|
||||
f,
|
||||
await DownloadFile()(
|
||||
account,
|
||||
f,
|
||||
shouldNotify: false,
|
||||
)));
|
||||
} on PermissionException catch (_) {
|
||||
_log.warning("[shareFiles] Permission not granted");
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
|
|
|
@ -1,23 +1,31 @@
|
|||
import 'package:nc_photos/account.dart';
|
||||
import 'package:nc_photos/api/api.dart';
|
||||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/mobile/platform.dart'
|
||||
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;
|
||||
|
||||
class DownloadFile {
|
||||
DownloadFile(this._fileRepo);
|
||||
DownloadFile();
|
||||
|
||||
/// Download [file]
|
||||
///
|
||||
/// The return data depends on the platform
|
||||
/// - web: null
|
||||
/// - android: Uri to the downloaded file
|
||||
Future<dynamic> call(Account account, File file) async {
|
||||
final content = await GetFileBinary(_fileRepo)(account, file);
|
||||
final saver = platform.FileSaver();
|
||||
return saver.saveFile(path.basename(file.path), content);
|
||||
/// See [FileDownloader.downloadUrl]
|
||||
Future<dynamic> call(
|
||||
Account account,
|
||||
File file, {
|
||||
bool? shouldNotify,
|
||||
}) {
|
||||
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 'file_downloader.dart';
|
||||
export 'file_saver.dart';
|
||||
export 'map_widget.dart';
|
||||
export 'universal_storage.dart';
|
||||
|
|
|
@ -452,9 +452,9 @@ class _ViewerState extends State<Viewer>
|
|||
});
|
||||
dynamic result;
|
||||
try {
|
||||
final fileRepo = FileRepo(FileCachedDataSource());
|
||||
result = await DownloadFile(fileRepo)(widget.account, file);
|
||||
result = await DownloadFile()(widget.account, file);
|
||||
controller?.close();
|
||||
_onDownloadSuccessful(file, result);
|
||||
} on PermissionException catch (_) {
|
||||
_log.warning("[_onDownloadPressed] Permission not granted");
|
||||
controller?.close();
|
||||
|
@ -462,7 +462,8 @@ class _ViewerState extends State<Viewer>
|
|||
content: Text(L10n.global().downloadFailureNoPermissionNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
return;
|
||||
} on JobCanceledException catch (_) {
|
||||
_log.info("[_onDownloadPressed] Canceled");
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
"[_onDownloadPressed] Failed while downloadFile", e, stacktrace);
|
||||
|
@ -472,10 +473,7 @@ class _ViewerState extends State<Viewer>
|
|||
"${exception_util.toUserString(e)}"),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
_onDownloadSuccessful(file, result);
|
||||
}
|
||||
|
||||
void _onDownloadSuccessful(File file, dynamic result) {
|
||||
|
|
Loading…
Reference in a new issue