mirror of
https://gitlab.com/nkming2/nc-photos.git
synced 2025-02-24 10:28:50 +01:00
(Android) Show our own download notifs
This commit is contained in:
parent
808c654e75
commit
60a0a387b2
17 changed files with 509 additions and 103 deletions
|
@ -41,6 +41,15 @@ class DownloadChannelHandler(activity: Activity) :
|
|||
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()
|
||||
}
|
||||
|
@ -85,6 +94,13 @@ class DownloadChannelHandler(activity: Activity) :
|
|||
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 {
|
||||
|
|
|
@ -94,3 +94,42 @@ class DownloadEventCompleteChannelHandler(context: Context) :
|
|||
_context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadEventCancelChannelHandler(context: Context) : BroadcastReceiver(),
|
||||
EventChannel.StreamHandler {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val CHANNEL =
|
||||
"com.nkming.nc_photos/download_event/action_download_cancel"
|
||||
}
|
||||
|
||||
override fun onReceive(context: Context?, intent: Intent?) {
|
||||
if (intent?.action != K.ACTION_DOWNLOAD_CANCEL || !intent.hasExtra(
|
||||
K.EXTRA_NOTIFICATION_ID
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
val id = intent.getIntExtra(K.EXTRA_NOTIFICATION_ID, 0)
|
||||
_eventSink?.success(
|
||||
mapOf(
|
||||
"notificationId" to id
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
|
||||
_context.registerReceiver(
|
||||
this, IntentFilter(K.ACTION_DOWNLOAD_CANCEL)
|
||||
)
|
||||
_eventSink = events
|
||||
}
|
||||
|
||||
override fun onCancel(arguments: Any?) {
|
||||
_context.unregisterReceiver(this)
|
||||
}
|
||||
|
||||
private val _context = context
|
||||
private var _eventSink: EventChannel.EventSink? = null
|
||||
}
|
||||
|
|
|
@ -4,5 +4,9 @@ interface K {
|
|||
companion object {
|
||||
const val DOWNLOAD_NOTIFICATION_ID_MIN = 1000
|
||||
const val DOWNLOAD_NOTIFICATION_ID_MAX = 2000
|
||||
|
||||
const val ACTION_DOWNLOAD_CANCEL = "com.nkming.nc_photos.ACTION_DOWNLOAD_CANCEL"
|
||||
|
||||
const val EXTRA_NOTIFICATION_ID = "com.nkming.nc_photos.EXTRA_NOTIFICATION_ID"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,5 +28,8 @@ class MainActivity : FlutterActivity() {
|
|||
EventChannel(flutterEngine.dartExecutor.binaryMessenger,
|
||||
DownloadEventCompleteChannelHandler.CHANNEL).setStreamHandler(
|
||||
DownloadEventCompleteChannelHandler(this))
|
||||
EventChannel(flutterEngine.dartExecutor.binaryMessenger,
|
||||
DownloadEventCancelChannelHandler.CHANNEL).setStreamHandler(
|
||||
DownloadEventCancelChannelHandler(this))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,14 +30,14 @@ class NotificationChannelHandler(activity: Activity) :
|
|||
companion object {
|
||||
const val CHANNEL = "com.nkming.nc_photos/notification"
|
||||
|
||||
private fun getNextNotificationId(): Int {
|
||||
fun getNextNotificationId(): Int {
|
||||
if (++notificationId >= K.DOWNLOAD_NOTIFICATION_ID_MAX) {
|
||||
notificationId = K.DOWNLOAD_NOTIFICATION_ID_MIN
|
||||
}
|
||||
return notificationId
|
||||
}
|
||||
|
||||
private const val DOWNLOAD_CHANNEL_ID = "download"
|
||||
const val DOWNLOAD_CHANNEL_ID = "download"
|
||||
private var notificationId = K.DOWNLOAD_NOTIFICATION_ID_MIN
|
||||
}
|
||||
|
||||
|
@ -47,11 +47,25 @@ class NotificationChannelHandler(activity: Activity) :
|
|||
|
||||
override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
|
||||
when (call.method) {
|
||||
"notifyItemsDownloadSuccessful" -> {
|
||||
"notifyDownloadSuccessful" -> {
|
||||
try {
|
||||
notifyItemsDownloadSuccessful(
|
||||
notifyDownloadSuccessful(
|
||||
call.argument("fileUris")!!,
|
||||
call.argument("mimeTypes")!!,
|
||||
call.argument("notificationId"),
|
||||
result
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
result.error("systemException", e.toString(), null)
|
||||
}
|
||||
}
|
||||
"notifyDownloadProgress" -> {
|
||||
try {
|
||||
notifyDownloadProgress(
|
||||
call.argument("progress")!!,
|
||||
call.argument("max")!!,
|
||||
call.argument("currentItemTitle"),
|
||||
call.argument("notificationId"),
|
||||
result
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
|
@ -67,15 +81,23 @@ class NotificationChannelHandler(activity: Activity) :
|
|||
result.error("systemException", e.toString(), null)
|
||||
}
|
||||
}
|
||||
"dismiss" -> {
|
||||
try {
|
||||
dismiss(call.argument("notificationId")!!, result)
|
||||
} catch (e: Throwable) {
|
||||
result.error("systemException", e.toString(), null)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
result.notImplemented()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyItemsDownloadSuccessful(
|
||||
private fun notifyDownloadSuccessful(
|
||||
fileUris: List<String>,
|
||||
mimeTypes: List<String?>,
|
||||
notificationId: Int?,
|
||||
result: MethodChannel.Result
|
||||
) {
|
||||
assert(fileUris.isNotEmpty())
|
||||
|
@ -88,7 +110,7 @@ class NotificationChannelHandler(activity: Activity) :
|
|||
RingtoneManager.getDefaultUri(
|
||||
RingtoneManager.TYPE_NOTIFICATION
|
||||
)
|
||||
).setOnlyAlertOnce(true).setAutoCancel(true).setLocalOnly(true)
|
||||
).setOnlyAlertOnce(false).setAutoCancel(true).setLocalOnly(true)
|
||||
|
||||
if (uris.size == 1) {
|
||||
builder.setContentTitle(
|
||||
|
@ -159,10 +181,57 @@ class NotificationChannelHandler(activity: Activity) :
|
|||
), sharePendingIntent
|
||||
)
|
||||
|
||||
val id = notificationId ?: getNextNotificationId()
|
||||
with(NotificationManagerCompat.from(_context)) {
|
||||
notify(getNextNotificationId(), builder.build())
|
||||
notify(id, builder.build())
|
||||
}
|
||||
result.success(null)
|
||||
result.success(id)
|
||||
}
|
||||
|
||||
private fun notifyDownloadProgress(
|
||||
progress: Int,
|
||||
max: Int,
|
||||
currentItemTitle: String?,
|
||||
notificationId: Int?,
|
||||
result: MethodChannel.Result
|
||||
) {
|
||||
val id = notificationId ?: getNextNotificationId()
|
||||
val builder = NotificationCompat.Builder(_context, DOWNLOAD_CHANNEL_ID)
|
||||
.setSmallIcon(android.R.drawable.stat_sys_download)
|
||||
.setWhen(System.currentTimeMillis())
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH).setSound(
|
||||
RingtoneManager.getDefaultUri(
|
||||
RingtoneManager.TYPE_NOTIFICATION
|
||||
)
|
||||
).setOnlyAlertOnce(true).setAutoCancel(false).setLocalOnly(true)
|
||||
.setProgress(max, progress, false).setContentText("$progress/$max")
|
||||
if (currentItemTitle == null) {
|
||||
builder.setContentTitle(_context.getString(R.string.download_progress_notification_untitled_text))
|
||||
} else {
|
||||
builder.setContentTitle(
|
||||
_context.getString(
|
||||
R.string.download_progress_notification_text,
|
||||
currentItemTitle
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val cancelIntent = Intent().apply {
|
||||
`package` = BuildConfig.APPLICATION_ID
|
||||
action = K.ACTION_DOWNLOAD_CANCEL
|
||||
putExtra(K.EXTRA_NOTIFICATION_ID, id)
|
||||
}
|
||||
val cancelPendingIntent = PendingIntent.getBroadcast(
|
||||
_context, 0, cancelIntent, PendingIntent.FLAG_UPDATE_CURRENT
|
||||
)
|
||||
builder.addAction(
|
||||
0, _context.getString(android.R.string.cancel), cancelPendingIntent
|
||||
)
|
||||
|
||||
with(NotificationManagerCompat.from(_context)) {
|
||||
notify(id, builder.build())
|
||||
}
|
||||
result.success(id)
|
||||
}
|
||||
|
||||
private fun notifyLogSaveSuccessful(
|
||||
|
@ -205,8 +274,16 @@ class NotificationChannelHandler(activity: Activity) :
|
|||
// can't add the share action here because android will share the URI as
|
||||
// plain text instead of treating it as a text file...
|
||||
|
||||
val id = getNextNotificationId()
|
||||
with(NotificationManagerCompat.from(_context)) {
|
||||
notify(getNextNotificationId(), builder.build())
|
||||
notify(id, builder.build())
|
||||
}
|
||||
result.success(id)
|
||||
}
|
||||
|
||||
private fun dismiss(notificationId: Int, result: MethodChannel.Result) {
|
||||
with(NotificationManagerCompat.from(_context)) {
|
||||
cancel(notificationId)
|
||||
}
|
||||
result.success(null)
|
||||
}
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
<string name="download_successful_notification_action_share">SHARE</string>
|
||||
<string name="download_successful_notification_action_share_chooser">Share with:</string>
|
||||
<string name="download_multiple_successful_notification_title">Downloaded %1$d items successfully</string>
|
||||
<string name="download_progress_notification_text">Downloading %1$s</string>
|
||||
<string name="download_progress_notification_untitled_text">Downloading</string>
|
||||
<string name="log_save_successful_notification_title">Logs saved successfully</string>
|
||||
<string name="log_save_successful_notification_text">Tap to view your saved logs</string>
|
||||
</resources>
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:logging/logging.dart';
|
||||
|
@ -6,7 +8,9 @@ import 'package:nc_photos/app_localizations.dart';
|
|||
import 'package:nc_photos/entity/file.dart';
|
||||
import 'package:nc_photos/exception.dart';
|
||||
import 'package:nc_photos/exception_util.dart' as exception_util;
|
||||
import 'package:nc_photos/iterable_extension.dart';
|
||||
import 'package:nc_photos/k.dart' as k;
|
||||
import 'package:nc_photos/mobile/android/download.dart';
|
||||
import 'package:nc_photos/mobile/notification.dart';
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
import 'package:nc_photos/snack_bar_manager.dart';
|
||||
|
@ -18,6 +22,136 @@ class DownloadHandler {
|
|||
Account account,
|
||||
List<File> files, {
|
||||
String? parentDir,
|
||||
}) {
|
||||
final _DownloadHandlerBase handler;
|
||||
if (platform_k.isAndroid) {
|
||||
handler = _DownlaodHandlerAndroid();
|
||||
} else {
|
||||
handler = _DownloadHandlerWeb();
|
||||
}
|
||||
return handler.downloadFiles(
|
||||
account,
|
||||
files,
|
||||
parentDir: parentDir,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _DownloadHandlerBase {
|
||||
Future<void> downloadFiles(
|
||||
Account account,
|
||||
List<File> files, {
|
||||
String? parentDir,
|
||||
});
|
||||
}
|
||||
|
||||
class _DownlaodHandlerAndroid extends _DownloadHandlerBase {
|
||||
@override
|
||||
downloadFiles(
|
||||
Account account,
|
||||
List<File> files, {
|
||||
String? parentDir,
|
||||
}) async {
|
||||
_log.info("[downloadFiles] Downloading ${files.length} file");
|
||||
final notif = AndroidDownloadProgressNotification(
|
||||
0,
|
||||
files.length,
|
||||
currentItemTitle: files.firstOrNull?.filename,
|
||||
);
|
||||
await notif.notify();
|
||||
|
||||
final successes = <Tuple2<File, dynamic>>[];
|
||||
StreamSubscription<DownloadCancelEvent>? subscription;
|
||||
try {
|
||||
bool isCancel = false;
|
||||
subscription = DownloadEvent.listenDownloadCancel()
|
||||
..onData((data) {
|
||||
if (data.notificationId == notif.notificationId) {
|
||||
isCancel = true;
|
||||
}
|
||||
});
|
||||
|
||||
int count = 0;
|
||||
for (final f in files) {
|
||||
if (isCancel == true) {
|
||||
_log.info("[downloadFiles] User canceled remaining files");
|
||||
break;
|
||||
}
|
||||
notif.update(
|
||||
count++,
|
||||
currentItemTitle: f.filename,
|
||||
);
|
||||
|
||||
StreamSubscription<DownloadCancelEvent>? itemSubscription;
|
||||
try {
|
||||
final download = DownloadFile().build(
|
||||
account,
|
||||
f,
|
||||
parentDir: parentDir,
|
||||
shouldNotify: false,
|
||||
);
|
||||
itemSubscription = DownloadEvent.listenDownloadCancel()
|
||||
..onData((data) {
|
||||
if (data.notificationId == notif.notificationId) {
|
||||
_log.info("[downloadFiles] Cancel requested");
|
||||
download.cancel();
|
||||
}
|
||||
});
|
||||
final result = await download();
|
||||
successes.add(Tuple2(f, result));
|
||||
} on PermissionException catch (_) {
|
||||
_log.warning("[downloadFiles] Permission not granted");
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content:
|
||||
Text(L10n.global().downloadFailureNoPermissionNotification),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
break;
|
||||
} on JobCanceledException catch (_) {
|
||||
_log.info("[downloadFiles] User canceled");
|
||||
break;
|
||||
} catch (e, stackTrace) {
|
||||
_log.shout(
|
||||
"[downloadFiles] Failed while DownloadFile", e, stackTrace);
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text("${L10n.global().downloadFailureNotification}: "
|
||||
"${exception_util.toUserString(e)}"),
|
||||
duration: k.snackBarDurationNormal,
|
||||
));
|
||||
} finally {
|
||||
itemSubscription?.cancel();
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
subscription?.cancel();
|
||||
if (successes.isNotEmpty) {
|
||||
await _onDownloadSuccessful(successes.map((e) => e.item1).toList(),
|
||||
successes.map((e) => e.item2).toList(), notif.notificationId);
|
||||
} else {
|
||||
await notif.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDownloadSuccessful(
|
||||
List<File> files, List<dynamic> results, int? notificationId) async {
|
||||
final notif = AndroidDownloadSuccessfulNotification(
|
||||
results.cast<String>(),
|
||||
files.map((e) => e.contentType).toList(),
|
||||
notificationId: notificationId,
|
||||
);
|
||||
await notif.notify();
|
||||
}
|
||||
|
||||
static final _log = Logger("download_handler._DownloadHandlerAndroid");
|
||||
}
|
||||
|
||||
class _DownloadHandlerWeb extends _DownloadHandlerBase {
|
||||
@override
|
||||
downloadFiles(
|
||||
Account account,
|
||||
List<File> files, {
|
||||
String? parentDir,
|
||||
}) async {
|
||||
_log.info("[downloadFiles] Downloading ${files.length} file");
|
||||
var controller = SnackBarManager().showSnackBar(SnackBar(
|
||||
|
@ -27,15 +161,15 @@ class DownloadHandler {
|
|||
controller?.closed.whenComplete(() {
|
||||
controller = null;
|
||||
});
|
||||
final successes = <Tuple2<File, dynamic>>[];
|
||||
int successCount = 0;
|
||||
for (final f in files) {
|
||||
try {
|
||||
final result = await DownloadFile()(
|
||||
await DownloadFile()(
|
||||
account,
|
||||
f,
|
||||
parentDir: parentDir,
|
||||
);
|
||||
successes.add(Tuple2(f, result));
|
||||
++successCount;
|
||||
} on PermissionException catch (_) {
|
||||
_log.warning("[downloadFiles] Permission not granted");
|
||||
controller?.close();
|
||||
|
@ -57,38 +191,14 @@ class DownloadHandler {
|
|||
));
|
||||
}
|
||||
}
|
||||
if (successes.isNotEmpty) {
|
||||
if (successCount > 0) {
|
||||
controller?.close();
|
||||
await _onDownloadSuccessful(successes.map((e) => e.item1).toList(),
|
||||
successes.map((e) => e.item2).toList());
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global().downloadSuccessNotification),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDownloadSuccessful(
|
||||
List<File> files, List<dynamic> results) async {
|
||||
dynamic notif;
|
||||
if (platform_k.isAndroid) {
|
||||
notif = AndroidItemDownloadSuccessfulNotification(
|
||||
results.cast<String>(), files.map((e) => e.contentType).toList());
|
||||
}
|
||||
if (notif != null) {
|
||||
try {
|
||||
await notif.notify();
|
||||
return;
|
||||
} catch (e, stacktrace) {
|
||||
_log.shout(
|
||||
"[_onDownloadSuccessful] Failed showing platform notification",
|
||||
e,
|
||||
stacktrace);
|
||||
}
|
||||
}
|
||||
|
||||
// fallback
|
||||
SnackBarManager().showSnackBar(SnackBar(
|
||||
content: Text(L10n.global().downloadSuccessNotification),
|
||||
duration: k.snackBarDurationShort,
|
||||
));
|
||||
}
|
||||
|
||||
static final _log = Logger("download_handler.DownloadHandler");
|
||||
static final _log = Logger("download_handler._DownloadHandlerWeb");
|
||||
}
|
||||
|
|
|
@ -19,6 +19,14 @@ class Download {
|
|||
}))!;
|
||||
}
|
||||
|
||||
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";
|
||||
|
||||
|
@ -27,7 +35,10 @@ class Download {
|
|||
|
||||
class DownloadEvent {
|
||||
static StreamSubscription<DownloadCompleteEvent> listenDownloadComplete() =>
|
||||
_stream.listen(null);
|
||||
_completeStream.listen(null);
|
||||
|
||||
static StreamSubscription<DownloadCancelEvent> listenDownloadCancel() =>
|
||||
_cancelStream.listen(null);
|
||||
|
||||
/// User canceled the download job
|
||||
static const exceptionCodeUserCanceled = "userCanceled";
|
||||
|
@ -35,7 +46,7 @@ class DownloadEvent {
|
|||
static const _downloadCompleteChannel = EventChannel(
|
||||
"com.nkming.nc_photos/download_event/action_download_complete");
|
||||
|
||||
static late final _stream = _downloadCompleteChannel
|
||||
static late final _completeStream = _downloadCompleteChannel
|
||||
.receiveBroadcastStream()
|
||||
.map((data) => DownloadCompleteEvent(
|
||||
data["downloadId"],
|
||||
|
@ -50,6 +61,15 @@ class DownloadEvent {
|
|||
e.details is Map &&
|
||||
e.details["downloadId"] is int,
|
||||
);
|
||||
|
||||
static const _downloadCancelChannel = EventChannel(
|
||||
"com.nkming.nc_photos/download_event/action_download_cancel");
|
||||
|
||||
static late final _cancelStream = _downloadCancelChannel
|
||||
.receiveBroadcastStream()
|
||||
.map((data) => DownloadCancelEvent(
|
||||
data["notificationId"],
|
||||
));
|
||||
}
|
||||
|
||||
class DownloadCompleteEvent {
|
||||
|
@ -66,3 +86,9 @@ class AndroidDownloadError implements Exception {
|
|||
final dynamic error;
|
||||
final StackTrace stackTrace;
|
||||
}
|
||||
|
||||
class DownloadCancelEvent {
|
||||
const DownloadCancelEvent(this.notificationId);
|
||||
|
||||
final int notificationId;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,32 @@
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
class Notification {
|
||||
static Future<void> notifyItemsDownloadSuccessful(
|
||||
List<String> fileUris, List<String?> mimeTypes) =>
|
||||
_channel.invokeMethod("notifyItemsDownloadSuccessful", <String, dynamic>{
|
||||
static Future<int?> notifyDownloadSuccessful(List<String> fileUris,
|
||||
List<String?> mimeTypes, int? notificationId) =>
|
||||
_channel.invokeMethod("notifyDownloadSuccessful", <String, dynamic>{
|
||||
"fileUris": fileUris,
|
||||
"mimeTypes": mimeTypes,
|
||||
"notificationId": notificationId,
|
||||
});
|
||||
|
||||
static Future<void> notifyLogSaveSuccessful(String fileUri) =>
|
||||
static Future<int?> notifyDownloadProgress(int progress, int max,
|
||||
String? currentItemTitle, int? notificationId) =>
|
||||
_channel.invokeMethod("notifyDownloadProgress", <String, dynamic>{
|
||||
"progress": progress,
|
||||
"max": max,
|
||||
"currentItemTitle": currentItemTitle,
|
||||
"notificationId": notificationId,
|
||||
});
|
||||
|
||||
static Future<int?> notifyLogSaveSuccessful(String fileUri) =>
|
||||
_channel.invokeMethod("notifyLogSaveSuccessful", <String, dynamic>{
|
||||
"fileUri": fileUri,
|
||||
});
|
||||
|
||||
static Future<void> dismiss(int notificationId) =>
|
||||
_channel.invokeMethod("dismiss", <String, dynamic>{
|
||||
"notificationId": notificationId,
|
||||
});
|
||||
|
||||
static const _channel = MethodChannel("com.nkming.nc_photos/notification");
|
||||
}
|
||||
|
|
|
@ -3,14 +3,14 @@ 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/download.dart' as android;
|
||||
import 'package:nc_photos/mobile/android/media_store.dart';
|
||||
import 'package:nc_photos/platform/file_downloader.dart' as itf;
|
||||
import 'package:nc_photos/platform/download.dart' as itf;
|
||||
import 'package:nc_photos/platform/k.dart' as platform_k;
|
||||
|
||||
class FileDownloader extends itf.FileDownloader {
|
||||
class DownloadBuilder extends itf.DownloadBuilder {
|
||||
@override
|
||||
downloadUrl({
|
||||
build({
|
||||
required String url,
|
||||
Map<String, String>? headers,
|
||||
String? mimeType,
|
||||
|
@ -19,7 +19,7 @@ class FileDownloader extends itf.FileDownloader {
|
|||
bool? shouldNotify,
|
||||
}) {
|
||||
if (platform_k.isAndroid) {
|
||||
return _downloadUrlAndroid(
|
||||
return _AndroidDownload(
|
||||
url: url,
|
||||
headers: headers,
|
||||
mimeType: mimeType,
|
||||
|
@ -31,15 +31,20 @@ class FileDownloader extends itf.FileDownloader {
|
|||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _downloadUrlAndroid({
|
||||
required String url,
|
||||
Map<String, String>? headers,
|
||||
String? mimeType,
|
||||
required String filename,
|
||||
String? parentDir,
|
||||
bool? shouldNotify,
|
||||
}) async {
|
||||
class _AndroidDownload extends itf.Download {
|
||||
_AndroidDownload({
|
||||
required this.url,
|
||||
this.headers,
|
||||
this.mimeType,
|
||||
required this.filename,
|
||||
this.parentDir,
|
||||
this.shouldNotify,
|
||||
});
|
||||
|
||||
@override
|
||||
call() async {
|
||||
final String path;
|
||||
if (parentDir?.isNotEmpty == true) {
|
||||
path = "$parentDir/$filename";
|
||||
|
@ -48,32 +53,32 @@ class FileDownloader extends itf.FileDownloader {
|
|||
}
|
||||
|
||||
try {
|
||||
_log.info("[_downloadUrlAndroid] Start downloading '$url'");
|
||||
final id = await Download.downloadUrl(
|
||||
_log.info("[call] Start downloading '$url'");
|
||||
_downloadId = await android.Download.downloadUrl(
|
||||
url: url,
|
||||
headers: headers,
|
||||
mimeType: mimeType,
|
||||
filename: path,
|
||||
shouldNotify: shouldNotify,
|
||||
);
|
||||
_log.info("[call] #$_downloadId -> '$url'");
|
||||
late final String uri;
|
||||
final completer = Completer();
|
||||
onDownloadComplete(DownloadCompleteEvent ev) {
|
||||
if (ev.downloadId == id) {
|
||||
_log.info(
|
||||
"[_downloadUrlAndroid] Finished downloading '$url' to '${ev.uri}'");
|
||||
onDownloadComplete(android.DownloadCompleteEvent ev) {
|
||||
if (ev.downloadId == _downloadId) {
|
||||
_log.info("[call] Finished downloading '$url' to '${ev.uri}'");
|
||||
uri = ev.uri;
|
||||
completer.complete();
|
||||
}
|
||||
}
|
||||
|
||||
StreamSubscription<DownloadCompleteEvent>? subscription;
|
||||
StreamSubscription<android.DownloadCompleteEvent>? subscription;
|
||||
try {
|
||||
subscription = DownloadEvent.listenDownloadComplete()
|
||||
subscription = android.DownloadEvent.listenDownloadComplete()
|
||||
..onData(onDownloadComplete)
|
||||
..onError((e, stackTrace) {
|
||||
if (e is AndroidDownloadError) {
|
||||
if (e.downloadId != id) {
|
||||
if (e is android.AndroidDownloadError) {
|
||||
if (e.downloadId != _downloadId) {
|
||||
// not us, ignore
|
||||
return;
|
||||
}
|
||||
|
@ -92,10 +97,10 @@ class FileDownloader extends itf.FileDownloader {
|
|||
case MediaStore.exceptionCodePermissionError:
|
||||
throw PermissionException();
|
||||
|
||||
case Download.exceptionCodeDownloadError:
|
||||
case android.Download.exceptionCodeDownloadError:
|
||||
throw DownloadException(e.message);
|
||||
|
||||
case DownloadEvent.exceptionCodeUserCanceled:
|
||||
case android.DownloadEvent.exceptionCodeUserCanceled:
|
||||
throw JobCanceledException(e.message);
|
||||
|
||||
default:
|
||||
|
@ -104,5 +109,23 @@ class FileDownloader extends itf.FileDownloader {
|
|||
}
|
||||
}
|
||||
|
||||
static final _log = Logger("mobile.file_downloader.FileDownloader");
|
||||
@override
|
||||
cancel() async {
|
||||
if (_downloadId != null) {
|
||||
_log.info("[cancel] Cancel #$_downloadId");
|
||||
return await android.Download.cancel(id: _downloadId!);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final String url;
|
||||
final Map<String, String>? headers;
|
||||
final String? mimeType;
|
||||
final String filename;
|
||||
final String? parentDir;
|
||||
final bool? shouldNotify;
|
||||
int? _downloadId;
|
||||
|
||||
static final _log = Logger("mobile.download._AndroidDownload");
|
||||
}
|
|
@ -1,27 +1,72 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:nc_photos/mobile/android/notification.dart';
|
||||
import 'package:nc_photos/platform/notification.dart' as itf;
|
||||
|
||||
class AndroidItemDownloadSuccessfulNotification
|
||||
extends itf.ItemDownloadSuccessfulNotification {
|
||||
AndroidItemDownloadSuccessfulNotification(this.fileUris, this.mimeTypes);
|
||||
class AndroidDownloadSuccessfulNotification extends _AndroidNotification {
|
||||
AndroidDownloadSuccessfulNotification(
|
||||
this.fileUris,
|
||||
this.mimeTypes, {
|
||||
int? notificationId,
|
||||
}) : replaceId = notificationId;
|
||||
|
||||
@override
|
||||
Future<void> notify() {
|
||||
return Notification.notifyItemsDownloadSuccessful(fileUris, mimeTypes);
|
||||
}
|
||||
doNotify() =>
|
||||
Notification.notifyDownloadSuccessful(fileUris, mimeTypes, replaceId);
|
||||
|
||||
final List<String> fileUris;
|
||||
final List<String?> mimeTypes;
|
||||
final int? replaceId;
|
||||
}
|
||||
|
||||
class AndroidLogSaveSuccessfulNotification
|
||||
extends itf.LogSaveSuccessfulNotification {
|
||||
class AndroidDownloadProgressNotification extends _AndroidNotification {
|
||||
AndroidDownloadProgressNotification(
|
||||
this.progress,
|
||||
this.max, {
|
||||
this.currentItemTitle,
|
||||
});
|
||||
|
||||
@override
|
||||
doNotify() => Notification.notifyDownloadProgress(
|
||||
progress, max, currentItemTitle, notificationId);
|
||||
|
||||
Future<void> update(
|
||||
int progress, {
|
||||
String? currentItemTitle,
|
||||
}) async {
|
||||
this.progress = progress;
|
||||
this.currentItemTitle = currentItemTitle;
|
||||
await doNotify();
|
||||
}
|
||||
|
||||
int progress;
|
||||
final int max;
|
||||
String? currentItemTitle;
|
||||
}
|
||||
|
||||
class AndroidLogSaveSuccessfulNotification extends _AndroidNotification {
|
||||
AndroidLogSaveSuccessfulNotification(this.fileUri);
|
||||
|
||||
@override
|
||||
Future<void> notify() {
|
||||
return Notification.notifyLogSaveSuccessful(fileUri);
|
||||
}
|
||||
doNotify() => Notification.notifyLogSaveSuccessful(fileUri);
|
||||
|
||||
final String fileUri;
|
||||
}
|
||||
|
||||
abstract class _AndroidNotification extends itf.Notification {
|
||||
@override
|
||||
notify() async {
|
||||
notificationId = await doNotify();
|
||||
}
|
||||
|
||||
@override
|
||||
dismiss() async {
|
||||
if (notificationId != null) {
|
||||
await Notification.dismiss(notificationId!);
|
||||
}
|
||||
}
|
||||
|
||||
@protected
|
||||
Future<int?> doNotify();
|
||||
|
||||
int? notificationId;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export 'db_util.dart';
|
||||
export 'file_downloader.dart';
|
||||
export 'download.dart';
|
||||
export 'file_saver.dart';
|
||||
export 'map_widget.dart';
|
||||
export 'universal_storage.dart';
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
abstract class FileDownloader {
|
||||
abstract class Download {
|
||||
/// Download a file
|
||||
///
|
||||
/// The return data depends on the platform
|
||||
/// - web: null
|
||||
/// - android: Uri to the downloaded file
|
||||
Future call();
|
||||
|
||||
/// Cancel a download
|
||||
///
|
||||
/// Not all platforms support canceling an ongoing download
|
||||
Future<bool> cancel();
|
||||
}
|
||||
|
||||
abstract class DownloadBuilder {
|
||||
/// Create a platform specific download
|
||||
///
|
||||
/// [parentDir] is a hint that set the parent directory where the files are
|
||||
/// saved. Whether this is supported or not is implementation specific
|
||||
|
@ -11,7 +21,7 @@ abstract class FileDownloader {
|
|||
/// [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({
|
||||
Download build({
|
||||
required String url,
|
||||
Map<String, String>? headers,
|
||||
String? mimeType,
|
|
@ -1,7 +1,5 @@
|
|||
abstract class ItemDownloadSuccessfulNotification {
|
||||
abstract class Notification {
|
||||
Future<void> notify();
|
||||
}
|
||||
|
||||
abstract class LogSaveSuccessfulNotification {
|
||||
Future<void> notify();
|
||||
Future<void> dismiss();
|
||||
}
|
||||
|
|
|
@ -3,20 +3,18 @@ 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/platform/download.dart';
|
||||
|
||||
class DownloadFile {
|
||||
/// Download [file]
|
||||
///
|
||||
/// See [FileDownloader.downloadUrl]
|
||||
Future<dynamic> call(
|
||||
/// Create a new download but don't start it yet
|
||||
Download build(
|
||||
Account account,
|
||||
File file, {
|
||||
String? parentDir,
|
||||
bool? shouldNotify,
|
||||
}) {
|
||||
final downloader = platform.FileDownloader();
|
||||
final url = "${account.url}/${file.path}";
|
||||
return downloader.downloadUrl(
|
||||
return platform.DownloadBuilder().build(
|
||||
url: url,
|
||||
headers: {
|
||||
"authorization": Api.getAuthorizationHeaderValue(account),
|
||||
|
@ -27,4 +25,20 @@ class DownloadFile {
|
|||
shouldNotify: shouldNotify,
|
||||
);
|
||||
}
|
||||
|
||||
/// Download [file]
|
||||
///
|
||||
/// See [DownloadBuilder]
|
||||
Future<dynamic> call(
|
||||
Account account,
|
||||
File file, {
|
||||
String? parentDir,
|
||||
bool? shouldNotify,
|
||||
}) =>
|
||||
build(
|
||||
account,
|
||||
file,
|
||||
parentDir: parentDir,
|
||||
shouldNotify: shouldNotify,
|
||||
)();
|
||||
}
|
||||
|
|
|
@ -1,18 +1,35 @@
|
|||
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/platform/download.dart' as itf;
|
||||
import 'package:nc_photos/web/file_saver.dart';
|
||||
|
||||
class FileDownloader extends itf.FileDownloader {
|
||||
class DownloadBuilder extends itf.DownloadBuilder {
|
||||
@override
|
||||
downloadUrl({
|
||||
build({
|
||||
required String url,
|
||||
Map<String, String>? headers,
|
||||
String? mimeType,
|
||||
required String filename,
|
||||
String? parentDir,
|
||||
bool? shouldNotify,
|
||||
}) async {
|
||||
}) {
|
||||
return _WebDownload(
|
||||
url: url,
|
||||
headers: headers,
|
||||
filename: filename,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _WebDownload extends itf.Download {
|
||||
_WebDownload({
|
||||
required this.url,
|
||||
this.headers,
|
||||
required this.filename,
|
||||
});
|
||||
|
||||
@override
|
||||
call() async {
|
||||
final uri = Uri.parse(url);
|
||||
final req = http.Request("GET", uri)..headers.addAll(headers ?? {});
|
||||
final response =
|
||||
|
@ -24,4 +41,11 @@ class FileDownloader extends itf.FileDownloader {
|
|||
final saver = FileSaver();
|
||||
await saver.saveFile(filename, response.bodyBytes);
|
||||
}
|
||||
|
||||
@override
|
||||
cancel() => Future.value(false);
|
||||
|
||||
final String url;
|
||||
final Map<String, String>? headers;
|
||||
final String filename;
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
export 'db_util.dart';
|
||||
export 'file_downloader.dart';
|
||||
export 'download.dart';
|
||||
export 'file_saver.dart';
|
||||
export 'map_widget.dart';
|
||||
export 'universal_storage.dart';
|
||||
|
|
Loading…
Reference in a new issue