TF-32 Add presentation layer of download attachment for android platform

This commit is contained in:
dab246
2021-09-16 09:08:17 +07:00
committed by Dat H. Pham
parent 30dfb5aec6
commit 20f98ce541
18 changed files with 209 additions and 71 deletions
+13 -1
View File
@@ -3,6 +3,7 @@
package="com.linagora.android.tmail">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
<!-- webviewflutter support minSdk version to at least 19 -->
<!-- flutter_inappwebview support minSdk version to at least 17 -->
@@ -17,7 +18,7 @@
</queries>
<application
android:label="TMail"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
@@ -48,6 +49,17 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<provider
android:name="vn.hunghd.flutterdownloader.DownloadedFileProvider"
android:authorities="${applicationId}.flutter_downloader.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
</provider>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
@@ -0,0 +1,9 @@
<resources>
<string name="app_name">Team Mail</string>
<string name="flutter_downloader_notification_started">Started</string>
<string name="flutter_downloader_notification_in_progress">In Progress</string>
<string name="flutter_downloader_notification_canceled">Canceled</string>
<string name="flutter_downloader_notification_failed">Failed</string>
<string name="flutter_downloader_notification_complete">Completed</string>
<string name="flutter_downloader_notification_paused">Paused</string>
</resources>
@@ -1,12 +1,17 @@
import 'package:core/core.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tmail_ui_user/features/email/data/datasource/email_datasource.dart';
import 'package:tmail_ui_user/features/email/data/datasource_impl/email_datasource_impl.dart';
import 'package:tmail_ui_user/features/email/data/network/email_api.dart';
import 'package:tmail_ui_user/features/email/data/repository/email_repository_impl.dart';
import 'package:tmail_ui_user/features/email/domain/repository/email_repository.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart';
import 'package:tmail_ui_user/features/email/presentation/email_controller.dart';
import 'package:tmail_ui_user/features/login/data/repository/credential_repository_impl.dart';
import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.dart';
class EmailBindings extends Bindings {
@override
@@ -20,5 +25,14 @@ class EmailBindings extends Bindings {
Get.put(EmailController(
Get.find<GetEmailContentInteractor>(),
Get.find<MarkAsEmailReadInteractor>()));
Get.lazyPut(() => CredentialRepositoryImpl(Get.find<SharedPreferences>()));
Get.lazyPut<CredentialRepository>(() => Get.find<CredentialRepositoryImpl>());
Get.lazyPut(() => DownloadAttachmentsInteractor(
Get.find<EmailRepository>(),
Get.find<CredentialRepository>()));
Get.put(EmailController(
Get.find<GetEmailContentInteractor>(),
Get.find<DownloadAttachmentsInteractor>(),
Get.find<DeviceManager>()));
}
}
@@ -1,16 +1,21 @@
import 'dart:io';
import 'package:core/core.dart';
import 'package:dartz/dartz.dart';
import 'package:get/get.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/mail/email/email.dart';
import 'package:model/model.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:tmail_ui_user/features/base/base_controller.dart';
import 'package:tmail_ui_user/features/email/domain/state/get_email_content_state.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/download_attachments_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/state/mark_as_email_read_state.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/get_email_content_interactor.dart';
import 'package:tmail_ui_user/features/email/domain/usecases/mark_as_email_read_interactor.dart';
import 'package:tmail_ui_user/features/email/presentation/model/composer_arguments.dart';
import 'package:tmail_ui_user/features/mailbox_dashboard/presentation/mailbox_dashboard_controller.dart';
import 'package:tmail_ui_user/main/localizations/app_localizations.dart';
import 'package:tmail_ui_user/main/routes/app_routes.dart';
import 'package:tmail_ui_user/main/routes/route_navigation.dart';
@@ -21,11 +26,18 @@ class EmailController extends BaseController {
final GetEmailContentInteractor _getEmailContentInteractor;
final MarkAsEmailReadInteractor _markAsEmailReadInteractor;
final DownloadAttachmentsInteractor _downloadAttachmentsInteractor;
final DeviceManager _deviceManager;
final emailAddressExpandMode = ExpandMode.COLLAPSE.obs;
EmailContent? emailContent;
EmailController(this._getEmailContentInteractor, this._markAsEmailReadInteractor);
EmailController(
this._getEmailContentInteractor,
this._downloadAttachmentsInteractor,
this._deviceManager
);
@override
void onReady() {
@@ -102,6 +114,46 @@ class EmailController extends BaseController {
backToThreadView();
}
void downloadAttachments(BuildContext context, List<Attachment> attachments) async {
final needRequestPermission = await _deviceManager.isNeedRequestStoragePermissionOnAndroid();
if (Platform.isAndroid && needRequestPermission) {
final status = await Permission.storage.status;
switch (status) {
case PermissionStatus.granted:
_downloadAttachmentsAction(context, attachments);
break;
case PermissionStatus.permanentlyDenied:
_appToast.showToast(AppLocalizations.of(context).you_need_to_grant_files_permission_to_download_attachments);
break;
default: {
final requested = await Permission.storage.request();
switch (requested) {
case PermissionStatus.granted:
_downloadAttachmentsAction(context, attachments);
break;
default:
_appToast.showToast(AppLocalizations.of(context).you_need_to_grant_files_permission_to_download_attachments);
break;
}
}
}
} else {
_downloadAttachmentsAction(context, attachments);
}
}
void _downloadAttachmentsAction(BuildContext context, List<Attachment> attachments) async {
final accountId = mailboxDashBoardController.accountId.value;
if (accountId != null && mailboxDashBoardController.sessionCurrent != null) {
final baseDownloadUrl = mailboxDashBoardController.sessionCurrent!.getDownloadUrl();
await _downloadAttachmentsInteractor.execute(attachments, accountId, baseDownloadUrl)
.then((result) => result.fold(
(failure) => AppLocalizations.of(context).attachment_download_failed,
(success) => AppLocalizations.of(context).attachment_download_successfully));
}
}
bool canComposeEmail() => mailboxDashBoardController.sessionCurrent != null
&& mailboxDashBoardController.userProfile.value != null
&& mailboxDashBoardController.mapMailboxId.containsKey(PresentationMailbox.roleOutbox)
@@ -141,9 +141,8 @@ class EmailView extends GetWidget {
.onOpenExpandAddressReceiverActionClick(() => emailController.toggleDisplayEmailAddressAction(expandMode: ExpandMode.EXPAND))
.build()),
_buildLoadingView(),
SizedBox(height: 16),
_buildListAttachments(context),
_buildListMessageContent(),
_buildListAttachments(context),
],
)
);
@@ -161,14 +160,18 @@ class EmailView extends GetWidget {
key: Key('list_attachment'),
primary: false,
shrinkWrap: true,
padding: EdgeInsets.only(bottom: 16),
padding: EdgeInsets.only(top: 16),
itemCount: attachments.length,
gridDelegate: SliverGridDelegateFixedHeight(
height: 60,
crossAxisCount: responsiveUtils.isMobile(context) ? 2 : 4,
crossAxisSpacing: 16.0,
mainAxisSpacing: 8.0),
itemBuilder: (context, index) => AttachmentFileTileBuilder(imagePaths, attachments[index]).build())
itemBuilder: (context, index) =>
(AttachmentFileTileBuilder(imagePaths, attachments[index])
..onDownloadAttachmentFileActionClick((attachment) =>
emailController.downloadAttachments(context, [attachment])))
.build())
: SizedBox.shrink();
} else {
return SizedBox.shrink();
@@ -191,6 +194,7 @@ class EmailView extends GetWidget {
? ListView.builder(
primary: false,
shrinkWrap: true,
padding: EdgeInsets.only(top: 16),
key: Key('list_message_content'),
itemCount: messageContents.length,
itemBuilder: (context, index) =>
@@ -1,6 +1,6 @@
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart';
import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.dart';
import 'package:model/model.dart';
extension EmailBodyPartExtension on EmailBodyPart {
AttachmentFile toAttachmentFile() => AttachmentFile(partId, blobId, size, name, type, cid);
Attachment toAttachment() => Attachment(partId, blobId, size, name, type, cid);
}
@@ -1,7 +1,7 @@
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart';
import 'package:model/email/email_content.dart';
import 'package:tmail_ui_user/features/email/presentation/constants/email_constants.dart';
import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.dart';
import 'package:model/model.dart';
import 'package:tmail_ui_user/features/email/presentation/model/message_content.dart';
import 'package:tmail_ui_user/features/email/presentation/model/text_format.dart';
import 'package:tmail_ui_user/features/email/presentation/extensions/email_body_part_extension.dart';
@@ -27,21 +27,21 @@ extension EmailContentExtension on EmailContent {
return listMessageContent;
}
List<AttachmentFile> getListAttachment() {
List<Attachment> getListAttachment() {
if (attachments != null) {
return attachments!
.where((element) => (element.disposition != null && element.disposition != 'inline'))
.map((item) => item.toAttachmentFile())
.map((item) => item.toAttachment())
.toList();
}
return [];
}
List<AttachmentFile> getListAttachmentInline() {
List<Attachment> getListAttachmentInline() {
if (attachments != null) {
return attachments!
.where((element) => element.disposition == 'inline')
.map((item) => item.toAttachmentFile())
.map((item) => item.toAttachment())
.toList();
}
return [];
@@ -1,28 +0,0 @@
import 'package:equatable/equatable.dart';
import 'package:http_parser/http_parser.dart';
import 'package:jmap_dart_client/jmap/core/id.dart';
import 'package:jmap_dart_client/jmap/core/unsigned_int.dart';
import 'package:jmap_dart_client/jmap/mail/email/email_body_part.dart';
class AttachmentFile with EquatableMixin {
final PartId? partId;
final Id? blobId;
final UnsignedInt? size;
final String? name;
final MediaType? type;
final String? cid;
AttachmentFile(
this.partId,
this.blobId,
this.size,
this.name,
this.type,
this.cid
);
@override
List<Object?> get props => [partId, blobId, size, name, type, cid];
}
@@ -1,8 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.dart';
import 'package:tmail_ui_user/features/email/presentation/model/text_format.dart';
import 'package:model/model.dart';
@@ -15,16 +13,11 @@ class MessageContent with EquatableMixin {
bool hasImageInlineWithCid() => content.contains('cid:');
String getContentHasInlineAttachment(Session session, AccountId accountId, List<AttachmentFile> attachmentFiles) {
String getContentHasInlineAttachment(String baseDownloadUrl, AccountId accountId, List<Attachment> attachments) {
var contentValid = content;
attachmentFiles.forEach((attachment) {
attachments.forEach((attachment) {
if(attachment.cid != null) {
final urlDownloadImage = session.getDownloadUrl(
'${accountId.id.value}',
'${attachment.blobId?.value}',
'${attachment.name}',
'${attachment.type?.mimeType}');
final urlDownloadImage = attachment.getDownloadUrl(baseDownloadUrl, accountId);
contentValid = content.replaceAll('cid:${attachment.cid}', '$urlDownloadImage');
}
});
@@ -4,23 +4,21 @@ import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.dart';
import 'package:model/model.dart';
typedef OnOpenAttachmentFileActionClick = void Function();
typedef OnDownloadAttachmentFileActionClick = void Function(Attachment attachment);
class AttachmentFileTileBuilder {
final ImagePaths _imagePaths;
final AttachmentFile _attachmentFile;
final Attachment _attachment;
OnOpenAttachmentFileActionClick? _onOpenAttachmentFileActionClick;
OnDownloadAttachmentFileActionClick? _onDownloadAttachmentFileActionClick;
AttachmentFileTileBuilder(this._imagePaths, this._attachmentFile);
AttachmentFileTileBuilder(this._imagePaths, this._attachment);
AttachmentFileTileBuilder onOpenAttachmentFileActionClick(
OnOpenAttachmentFileActionClick onOpenAttachmentFileActionClick) {
_onOpenAttachmentFileActionClick = onOpenAttachmentFileActionClick;
return this;
void onDownloadAttachmentFileActionClick(OnDownloadAttachmentFileActionClick onDownloadAttachmentFileActionClick) {
_onDownloadAttachmentFileActionClick = onDownloadAttachmentFileActionClick;
}
Widget build() {
@@ -38,26 +36,29 @@ class AttachmentFileTileBuilder {
child: MediaQuery(
data: MediaQueryData(padding: EdgeInsets.zero),
child: ListTile(
contentPadding: EdgeInsets.zero,
focusColor: AppColor.primaryColor,
hoverColor: AppColor.primaryColor,
onTap: () {
if (_onOpenAttachmentFileActionClick != null) {
_onOpenAttachmentFileActionClick!();
if (_onDownloadAttachmentFileActionClick != null) {
_onDownloadAttachmentFileActionClick!(_attachment);
}},
leading: Transform(
transform: Matrix4.translationValues(0.0, 2.0, 0.0),
transform: Matrix4.translationValues(14.0, 2.0, 0.0),
child: SvgPicture.asset(_imagePaths.icAttachmentFile, width: 24, height: 24, fit: BoxFit.fill)),
title: Transform(
transform: Matrix4.translationValues(-18.0, -8.0, 0.0),
transform: Matrix4.translationValues(-8.0, -8.0, 0.0),
child: Text(
_attachmentFile.name ?? '',
_attachment.name ?? '',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 12, color: AppColor.attachmentFileNameColor, fontWeight: FontWeight.w500),
)),
subtitle: _attachmentFile.size != null && _attachmentFile.size?.value != 0
subtitle: _attachment.size != null && _attachment.size?.value != 0
? Transform(
transform: Matrix4.translationValues(-18.0, -8.0, 0.0),
transform: Matrix4.translationValues(-8.0, -8.0, 0.0),
child: Text(
filesize(_attachmentFile.size?.value),
filesize(_attachment.size?.value),
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(fontSize: 12, color: AppColor.attachmentFileSizeColor)))
@@ -5,14 +5,14 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:jmap_dart_client/jmap/account_id.dart';
import 'package:jmap_dart_client/jmap/core/session/session.dart';
import 'package:tmail_ui_user/features/email/presentation/model/attachment_file.dart';
import 'package:model/model.dart';
import 'package:tmail_ui_user/features/email/presentation/model/message_content.dart';
import 'package:tmail_ui_user/features/email/presentation/model/text_format.dart';
class MessageContentTileBuilder {
final MessageContent messageContent;
final List<AttachmentFile> attachmentInlines;
final List<Attachment> attachmentInlines;
final Session? session;
final AccountId? accountId;
final HtmlMessagePurifier htmlMessagePurifier;
@@ -51,7 +51,7 @@ class MessageContentTileBuilder {
String getHtmlMessageText() {
final message = (attachmentInlines.isNotEmpty && session != null && accountId != null && messageContent.hasImageInlineWithCid())
? '${messageContent.getContentHasInlineAttachment(session!, accountId!, attachmentInlines)}'
? '${messageContent.getContentHasInlineAttachment(session!.getDownloadUrl(), accountId!, attachmentInlines)}'
: '${messageContent.content}';
final trustAsHtml = htmlMessagePurifier.purifyHtmlMessage(
+12
View File
@@ -269,5 +269,17 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"attachment_download_failed": "Attachment download failed",
"@attachment_download_failed": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"attachment_download_successfully": "Attachment downloaded successfully",
"@attachment_download_successfully": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
+12
View File
@@ -269,5 +269,17 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"attachment_download_failed": "Attachment download failed",
"@attachment_download_failed": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"attachment_download_successfully": "Attachment downloaded successfully",
"@attachment_download_successfully": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
+13 -1
View File
@@ -1,5 +1,5 @@
{
"@@last_modified": "2021-09-15T13:25:41.117609",
"@@last_modified": "2021-09-15T23:44:29.035668",
"initializing_data": "Initializing data...",
"@initializing_data": {
"type": "text",
@@ -269,5 +269,17 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"attachment_download_failed": "Attachment download failed",
"@attachment_download_failed": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"attachment_download_successfully": "Attachment downloaded successfully",
"@attachment_download_successfully": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
+12
View File
@@ -269,5 +269,17 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"attachment_download_failed": "Attachment download failed",
"@attachment_download_failed": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"attachment_download_successfully": "Attachment downloaded successfully",
"@attachment_download_successfully": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
+12
View File
@@ -269,5 +269,17 @@
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"attachment_download_failed": "Attachment download failed",
"@attachment_download_failed": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
},
"attachment_download_successfully": "Attachment downloaded successfully",
"@attachment_download_successfully": {
"type": "text",
"placeholders_order": [],
"placeholders": {}
}
}
@@ -1,5 +1,6 @@
import 'package:core/core.dart';
import 'package:core/presentation/utils/app_toast.dart';
import 'package:device_info/device_info.dart';
import 'package:get/get.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -13,6 +14,7 @@ class CoreBindings extends Bindings {
_bindingKeyboardManager();
_bindingValidator();
_bindingToast();
_bindingDeviceManager();
}
void _bindingAppImagePaths() {
@@ -38,4 +40,9 @@ class CoreBindings extends Bindings {
void _bindingToast() {
Get.put(AppToast());
}
void _bindingDeviceManager() {
Get.put(DeviceInfoPlugin());
Get.put(DeviceManager(Get.find<DeviceInfoPlugin>()));
}
}
@@ -300,5 +300,19 @@ class AppLocalizations {
name: 'an_error_occurred',
);
}
String get attachment_download_failed {
return Intl.message(
'Attachment download failed',
name: 'attachment_download_failed',
);
}
String get attachment_download_successfully {
return Intl.message(
'Attachment downloaded successfully',
name: 'attachment_download_successfully',
);
}
}