Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Save thread on kDrive #2088

Open
wants to merge 33 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c3b4d8d
refactor: Share code of 'reportDisplayProblem'
NicolasBourdin88 Oct 29, 2024
8f80954
feat: Add save to kDrive option
NicolasBourdin88 Oct 29, 2024
fd1a074
feat: Save eml to kDrive
NicolasBourdin88 Oct 29, 2024
9373170
refactor: Rename & Clean code
NicolasBourdin88 Oct 30, 2024
dd945ec
feat: Replace illegal file characters with a blank space
NicolasBourdin88 Oct 31, 2024
b5d5379
feat: Allow to download a thread instead of only one message
NicolasBourdin88 Oct 31, 2024
f74d756
feat: Add option in 'MultiSelectBottomSheet'
NicolasBourdin88 Oct 31, 2024
65921c9
feat: Allow to add different threads or messages with same name
NicolasBourdin88 Nov 1, 2024
a6c6d76
feat: Save in cache instead of files
NicolasBourdin88 Nov 1, 2024
bbc357c
refactor: Put all 'clickListener' from message bottom sheet together
NicolasBourdin88 Nov 18, 2024
edefe06
refactor: Use a list instead of a set
NicolasBourdin88 Nov 18, 2024
d751cf3
feat: Display progression while loading messages
NicolasBourdin88 Nov 21, 2024
80e33b2
refactor: Send messages uids instead of thread uuids
NicolasBourdin88 Nov 21, 2024
21b6933
refactor: Rename 'messageUuids' to 'messageUids'
NicolasBourdin88 Nov 21, 2024
257f106
feat: Import downloading String
NicolasBourdin88 Nov 21, 2024
f856b61
feat: Add dialog name
NicolasBourdin88 Nov 22, 2024
bd951f1
feat: Centralize shared code in abstract class
NicolasBourdin88 Nov 22, 2024
583955d
refactor: Rename for better clarity
NicolasBourdin88 Nov 22, 2024
82cfb1a
refactor: Apply suggestion from code review
NicolasBourdin88 Nov 22, 2024
3fea8cb
refactor: Put EML_CONTENT_TYPE for all
NicolasBourdin88 Dec 16, 2024
0649e40
refactor: Apply suggestion from code review
NicolasBourdin88 Dec 16, 2024
63f19ce
refactor: Remove 'EXPOSED_EML_PATH' from 'build.gradle'
NicolasBourdin88 Dec 16, 2024
b34c2dd
refactor: Optimize and remove 'while' loop
NicolasBourdin88 Jan 21, 2025
18e5f4f
refactor: Replace ArrayList by List for maintainability
NicolasBourdin88 Jan 21, 2025
62ffef9
feat: Delete all files in cache dir after share them to kDrive
NicolasBourdin88 Jan 21, 2025
34d7b07
refactor: Allow to manage error
NicolasBourdin88 Jan 21, 2025
0e28ea6
feat: Limit file name length
NicolasBourdin88 Jan 21, 2025
19480d5
refactor: No need to check for existing files because they are delete…
NicolasBourdin88 Jan 21, 2025
5665c66
feat: Optimize message downloads with parallel execution
NicolasBourdin88 Jan 22, 2025
9075364
refactor: Send both 'threadUid' and 'messagesUid' to put all logic to…
NicolasBourdin88 Jan 27, 2025
a1f0d34
refactor: Better use of liveData
NicolasBourdin88 Jan 27, 2025
9e81557
refactor: Optimize code
NicolasBourdin88 Jan 28, 2025
471d507
feat: Manage error when one message failed
NicolasBourdin88 Jan 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions .idea/navEditor.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ android {
buildConfigField 'String', 'GITHUB_REPO_URL', '"https://github.com/Infomaniak/android-kMail"'

resValue 'string', 'ATTACHMENTS_AUTHORITY', 'com.infomaniak.mail.attachments'
resValue 'string', 'EML_AUTHORITY', 'com.infomaniak.mail.eml'
resValue 'string', 'FILES_AUTHORITY', 'com.infomaniak.mail.attachments;com.infomaniak.mail.eml'

resourceConfigurations += ["en", "de", "es", "fr", "it"]
}
Expand Down
2 changes: 1 addition & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -208,7 +208,7 @@

<provider
android:name="androidx.core.content.FileProvider"
android:authorities="@string/ATTACHMENTS_AUTHORITY"
android:authorities="@string/FILES_AUTHORITY"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
Expand Down
1 change: 1 addition & 0 deletions app/src/main/java/com/infomaniak/mail/MatomoMail.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ object MatomoMail : MatomoCore {
const val ACTION_SPAM_NAME = "spam"
const val ACTION_PRINT_NAME = "print"
const val ACTION_SHARE_LINK_NAME = "shareLink"
const val ACTION_SAVE_KDRIVE_NAME = "saveInkDrive"
const val ACTION_POSTPONE_NAME = "postpone"
const val ADD_MAILBOX_NAME = "addMailbox"
const val DISCOVER_LATER = "discoverLater"
Expand Down
11 changes: 11 additions & 0 deletions app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,17 @@ object ApiRepository : ApiRepositoryCore() {
return callApi(url = ApiRoutes.shareLink(mailboxUuid, folderId, mailId), method = POST)
}

fun getDownloadedMessage(mailboxUuid: String, folderId: String, shortUid: Int): Response {
val emlContentType = "message/rfc822"

val request = Request.Builder().url(ApiRoutes.downloadMessage(mailboxUuid, folderId, shortUid))
.headers(HttpUtils.getHeaders(emlContentType))
.get()
.build()

return HttpClient.okHttpClient.newCall(request).execute()
NicolasBourdin88 marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Create batches of the given values to perform the given request
* @param values Data to batch
Expand Down
17 changes: 2 additions & 15 deletions app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ package com.infomaniak.mail.ui
import android.app.Application
import androidx.lifecycle.*
import com.infomaniak.lib.core.models.ApiResponse
import com.infomaniak.lib.core.networking.HttpUtils
import com.infomaniak.lib.core.networking.NetworkAvailability
import com.infomaniak.lib.core.utils.ApiErrorCode.Companion.translateError
import com.infomaniak.lib.core.utils.DownloadManagerUtils
Expand All @@ -29,7 +28,6 @@ import com.infomaniak.lib.core.utils.SingleLiveEvent
import com.infomaniak.mail.MatomoMail.trackMultiSelectionEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.api.ApiRepository
import com.infomaniak.mail.data.api.ApiRoutes
import com.infomaniak.mail.data.cache.RealmDatabase
import com.infomaniak.mail.data.cache.mailboxContent.FolderController
import com.infomaniak.mail.data.cache.mailboxContent.MessageController
Expand Down Expand Up @@ -75,7 +73,6 @@ import io.sentry.Sentry
import io.sentry.SentryLevel
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
import okhttp3.Request
import java.util.Date
import java.util.UUID
import javax.inject.Inject
Expand Down Expand Up @@ -929,19 +926,9 @@ class MainViewModel @Inject constructor(
fun reportDisplayProblem(messageUid: String) = viewModelScope.launch(ioCoroutineContext) {

val message = messageController.getMessage(messageUid) ?: return@launch

val mailbox = currentMailbox.value ?: return@launch

val userApiToken = AccountUtils.getUserById(mailbox.userId)?.apiToken?.accessToken ?: return@launch
val headers = HttpUtils.getHeaders(contentType = null).newBuilder()
.set("Authorization", "Bearer $userApiToken")
.build()
val request = Request.Builder().url(ApiRoutes.downloadMessage(mailbox.uuid, message.folderId, message.shortUid))
.headers(headers)
.get()
.build()

val response = AccountUtils.getHttpClient(mailbox.userId).newCall(request).execute()
val response = ApiRepository.getDownloadedMessage(mailbox.uuid, message.folderId, message.shortUid)

if (!response.isSuccessful || response.body == null) {
reportDisplayProblemTrigger.postValue(Unit)
Expand Down Expand Up @@ -1101,7 +1088,7 @@ class MainViewModel @Inject constructor(
}

fun hasOtherExpeditors(threadUid: String) = liveData(ioCoroutineContext) {
val hasOtherExpeditors = threadController.getThread(threadUid)?.messages?.flatMap { it.from }?.any { !it.isMe() } ?: false
val hasOtherExpeditors = threadController.getThread(threadUid)?.messages?.flatMap { it.from }?.any { !it.isMe() } == true
emit(hasOtherExpeditors)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,11 @@
*/
package com.infomaniak.mail.ui.main.folder

import android.app.Activity
import android.content.res.Configuration
import android.os.Bundle
import android.view.View
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.annotation.ColorRes
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isGone
Expand All @@ -41,7 +43,10 @@ import com.infomaniak.mail.ui.MainActivity
import com.infomaniak.mail.ui.MainViewModel
import com.infomaniak.mail.ui.main.search.SearchFragment
import com.infomaniak.mail.ui.main.thread.ThreadFragment
import com.infomaniak.mail.ui.main.thread.actions.DownloadMessagesProgressDialog
import com.infomaniak.mail.utils.LocalStorageUtils.getEmlCacheDir
import com.infomaniak.mail.utils.extensions.*
import java.io.File
import javax.inject.Inject

abstract class TwoPaneFragment : Fragment() {
Expand Down Expand Up @@ -118,8 +123,16 @@ abstract class TwoPaneFragment : Fragment() {
}
}

private val resultActivityResultLauncher = registerForActivityResult(StartActivityForResult()) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val fileDir: File = getEmlCacheDir(requireContext())
fileDir.deleteRecursively()
}
}

private fun observeThreadNavigation() = with(twoPaneViewModel) {
getBackNavigationResult(AttachmentExtensions.DOWNLOAD_ATTACHMENT_RESULT, ::startActivity)
getBackNavigationResult(DownloadMessagesProgressDialog.DOWNLOAD_MESSAGES_RESULT, resultActivityResultLauncher::launch)

newMessageArgs.observe(viewLifecycleOwner) {
safeNavigateToNewMessageActivity(args = it.toBundle())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,59 +17,33 @@
*/
package com.infomaniak.mail.ui.main.thread.actions

import android.app.Dialog
import android.os.Bundle
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.content.res.AppCompatResources
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.activityViewModels
import androidx.core.view.isVisible
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.infomaniak.lib.core.R
import com.infomaniak.lib.core.utils.SnackbarUtils.showSnackbar
import com.infomaniak.lib.core.utils.setBackNavigationResult
import com.infomaniak.mail.databinding.DialogDownloadProgressBinding
import com.infomaniak.mail.ui.MainViewModel
import com.infomaniak.mail.utils.extensions.AttachmentExtensions
import com.infomaniak.mail.utils.extensions.AttachmentExtensions.getIntentOrGoToPlayStore
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch

@AndroidEntryPoint
class DownloadAttachmentProgressDialog : DialogFragment() {

private val binding by lazy { DialogDownloadProgressBinding.inflate(layoutInflater) }
class DownloadAttachmentProgressDialog : DownloadProgressDialog() {
private val navigationArgs: DownloadAttachmentProgressDialogArgs by navArgs()
private val mainViewModel: MainViewModel by activityViewModels()
private val downloadAttachmentViewModel: DownloadAttachmentViewModel by viewModels()

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
isCancelable = false
val iconDrawable = AppCompatResources.getDrawable(requireContext(), navigationArgs.attachmentType.icon)
binding.icon.setImageDrawable(iconDrawable)

return MaterialAlertDialogBuilder(requireContext())
.setTitle(navigationArgs.attachmentName)
.setView(binding.root)
.setOnKeyListener { _, keyCode, event ->
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
findNavController().popBackStack()
true
} else false
}
.create()
}
override val dialogTitle: String? by lazy { navigationArgs.attachmentName }

override fun onStart() {
super.onStart()
downloadAttachment()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding.icon.isVisible = true
binding.icon.setImageDrawable(AppCompatResources.getDrawable(requireContext(), navigationArgs.attachmentType.icon))
return super.onCreateView(inflater, container, savedInstanceState)
}

private fun downloadAttachment() {
override fun download() {
downloadAttachmentViewModel.downloadAttachment().observe(this) { cachedAttachment ->
if (cachedAttachment == null) {
popBackStackWithError()
Expand All @@ -80,13 +54,4 @@ class DownloadAttachmentProgressDialog : DialogFragment() {
}
}
}

private fun popBackStackWithError() {
lifecycleScope.launch {
mainViewModel.isNetworkAvailable.first { it != null }?.let { isNetworkAvailable ->
showSnackbar(title = if (isNetworkAvailable) R.string.anErrorHasOccurred else R.string.noConnection)
findNavController().popBackStack()
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Infomaniak Mail - Android
* Copyright (C) 2024 Infomaniak Network SA
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.mail.ui.main.thread.actions

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import com.infomaniak.lib.core.utils.goToPlayStore
import com.infomaniak.lib.core.utils.setBackNavigationResult
import com.infomaniak.mail.R
import com.infomaniak.mail.utils.SaveOnKDriveUtils.DRIVE_PACKAGE
import com.infomaniak.mail.utils.SaveOnKDriveUtils.SAVE_EXTERNAL_ACTIVITY_CLASS
import com.infomaniak.mail.utils.SaveOnKDriveUtils.canSaveOnKDrive

class DownloadMessagesProgressDialog : DownloadProgressDialog() {
private val downloadThreadsViewModel: DownloadMessagesViewModel by viewModels()
override val dialogTitle: String? by lazy { getDialogName() }

override fun onCreate(savedInstanceState: Bundle?) {
observeDownload()
super.onCreate(savedInstanceState)
}

override fun download() {
downloadThreadsViewModel.downloadMessages(mainViewModel.currentMailbox.value)
}

private fun observeDownload() {
downloadThreadsViewModel.downloadMessagesLiveData.observe(this) { threadUris ->
if (threadUris == null) {
popBackStackWithError()
} else {
threadUris.openKDriveOrPlayStore(requireContext())?.let { openKDriveIntent ->
setBackNavigationResult(DOWNLOAD_MESSAGES_RESULT, openKDriveIntent)
} ?: run { findNavController().popBackStack() }
}
}
}

private fun getDialogName(): String {
val numberOfMessagesToDownload = downloadThreadsViewModel.numberOfMessagesToDownloads()

return if (numberOfMessagesToDownload == 1) {
downloadThreadsViewModel.getFirstMessageSubject() ?: requireContext().getString(R.string.noSubjectTitle)
} else {
requireContext().resources.getQuantityString(
R.plurals.downloadingEmailsTitle,
numberOfMessagesToDownload,
numberOfMessagesToDownload,
)
}
}

private fun List<Uri>.openKDriveOrPlayStore(context: Context): Intent? {
return if (canSaveOnKDrive(context)) {
saveToDriveIntent()
} else {
context.goToPlayStore(DRIVE_PACKAGE)
null
}
}

private fun List<Uri>.saveToDriveIntent(): Intent {
return Intent().apply {
component = ComponentName(DRIVE_PACKAGE, SAVE_EXTERNAL_ACTIVITY_CLASS)
action = Intent.ACTION_SEND_MULTIPLE
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(this@saveToDriveIntent))
}
}

companion object {
const val DOWNLOAD_MESSAGES_RESULT = "download_messages_result"
}
}
Loading
Loading