Skip to content

Commit

Permalink
feat: Save thread on kDrive (#2088)
Browse files Browse the repository at this point in the history
  • Loading branch information
NicolasBourdin88 authored Feb 27, 2025
2 parents 7f08a30 + e1c66c8 commit 9d04f4c
Show file tree
Hide file tree
Showing 30 changed files with 613 additions and 289 deletions.
186 changes: 64 additions & 122 deletions .idea/navEditor.xml

Large diffs are not rendered by default.

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 @@ -204,7 +204,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_TO_KDRIVE_NAME = "saveInkDrive"
const val ACTION_POSTPONE_NAME = "postpone"
const val ADD_MAILBOX_NAME = "addMailbox"
const val DISCOVER_LATER = "discoverLater"
Expand Down
17 changes: 11 additions & 6 deletions app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ import com.infomaniak.mail.data.models.signature.SignaturesResult
import com.infomaniak.mail.data.models.thread.ThreadResult
import com.infomaniak.mail.ui.newMessage.AiViewModel.Shortcut
import com.infomaniak.mail.utils.Utils
import com.infomaniak.mail.utils.Utils.EML_CONTENT_TYPE
import io.realm.kotlin.ext.copyFromRealm
import kotlinx.serialization.json.Json
import okhttp3.MultipartBody
Expand Down Expand Up @@ -451,13 +452,17 @@ object ApiRepository : ApiRepositoryCore() {
return callApi(url = ApiRoutes.shareLink(mailboxUuid, folderId, mailId), method = POST)
}

fun getDownloadedMessage(mailboxUuid: String, folderId: String, shortUid: Int): Response {
val request = Request.Builder().url(ApiRoutes.downloadMessage(mailboxUuid, folderId, shortUid))
.headers(HttpUtils.getHeaders(EML_CONTENT_TYPE))
.get()
.build()

return HttpClient.okHttpClient.newCall(request).execute()
}

fun getMyKSuiteData(okHttpClient: OkHttpClient): ApiResponse<MyKSuiteData> {
return ApiController.callApi(
url = MyKSuiteApiRoutes.myKSuiteData(),
method = ApiController.ApiMethod.GET,
okHttpClient = okHttpClient,
useKotlinxSerialization = true,
)
return callApi(url = MyKSuiteApiRoutes.myKSuiteData(), method = GET, okHttpClient = okHttpClient)
}

/**
Expand Down
23 changes: 5 additions & 18 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 @@ -30,7 +29,6 @@ import com.infomaniak.mail.MatomoMail.trackMultiSelectionEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings
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 @@ -60,6 +58,7 @@ import com.infomaniak.mail.utils.ContactUtils.getPhoneContacts
import com.infomaniak.mail.utils.ContactUtils.mergeApiContactsIntoPhoneContacts
import com.infomaniak.mail.utils.NotificationUtils.Companion.cancelNotification
import com.infomaniak.mail.utils.SharedUtils.Companion.updateSignatures
import com.infomaniak.mail.utils.Utils.EML_CONTENT_TYPE
import com.infomaniak.mail.utils.Utils.isPermanentDeleteFolder
import com.infomaniak.mail.utils.Utils.runCatchingRealm
import com.infomaniak.mail.utils.extensions.*
Expand All @@ -75,7 +74,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 @@ -1019,29 +1017,19 @@ 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 apiResponse = ApiRepository.getDownloadedMessage(mailbox.uuid, message.folderId, message.shortUid)

if (!response.isSuccessful || response.body == null) {
if (apiResponse.body == null || !apiResponse.isSuccessful) {
reportDisplayProblemTrigger.postValue(Unit)
snackbarManager.postValue(appContext.getString(RCore.string.anErrorHasOccurred))

return@launch
}

val filename = UUID.randomUUID().toString()
val emlAttachment = Attachment(response.body?.bytes(), filename, EML_CONTENT_TYPE)
val emlAttachment = Attachment(apiResponse.body?.bytes(), filename, EML_CONTENT_TYPE)
Sentry.captureMessage("Message display problem reported", SentryLevel.ERROR) { scope ->
scope.addAttachment(emlAttachment)
}
Expand Down Expand Up @@ -1191,7 +1179,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 Expand Up @@ -1312,6 +1300,5 @@ class MainViewModel @Inject constructor(
private val DEFAULT_SELECTED_FOLDER = FolderRole.INBOX
private const val REFRESH_DELAY = 2_000L // We add this delay because `etop` isn't always big enough.
private const val MAX_REFRESH_DELAY = 6_000L
private const val EML_CONTENT_TYPE = "message/rfc822"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ package com.infomaniak.mail.ui.main.folder
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,6 +42,8 @@ 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.clearEmlCacheDir
import com.infomaniak.mail.utils.extensions.*
import javax.inject.Inject

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

private val resultActivityResultLauncher = registerForActivityResult(StartActivityForResult()) { _ ->
clearEmlCacheDir(requireContext())
}

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,79 @@
/*
* 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.utils.LocalStorageUtils.clearEmlCacheDir
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 { downloadThreadsViewModel.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) { messageUris ->
messageUris?.openKDriveOrPlayStore(requireContext())?.let { openKDriveIntent ->
setBackNavigationResult(DOWNLOAD_MESSAGES_RESULT, openKDriveIntent)
} ?: run {
clearEmlCacheDir(requireContext())
if (messageUris == null) popBackStackWithError() else findNavController().popBackStack()
}
}
}

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

0 comments on commit 9d04f4c

Please sign in to comment.