Skip to content

Commit 9d04f4c

Browse files
feat: Save thread on kDrive (#2088)
2 parents 7f08a30 + e1c66c8 commit 9d04f4c

30 files changed

+613
-289
lines changed

.idea/navEditor.xml

+64-122
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ android {
4747
buildConfigField 'String', 'GITHUB_REPO_URL', '"https://github.com/Infomaniak/android-kMail"'
4848

4949
resValue 'string', 'ATTACHMENTS_AUTHORITY', 'com.infomaniak.mail.attachments'
50+
resValue 'string', 'EML_AUTHORITY', 'com.infomaniak.mail.eml'
51+
resValue 'string', 'FILES_AUTHORITY', 'com.infomaniak.mail.attachments;com.infomaniak.mail.eml'
5052

5153
resourceConfigurations += ["en", "de", "es", "fr", "it"]
5254
}

app/src/main/AndroidManifest.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -204,7 +204,7 @@
204204

205205
<provider
206206
android:name="androidx.core.content.FileProvider"
207-
android:authorities="@string/ATTACHMENTS_AUTHORITY"
207+
android:authorities="@string/FILES_AUTHORITY"
208208
android:exported="false"
209209
android:grantUriPermissions="true">
210210
<meta-data

app/src/main/java/com/infomaniak/mail/MatomoMail.kt

+1
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ object MatomoMail : MatomoCore {
5656
const val ACTION_SPAM_NAME = "spam"
5757
const val ACTION_PRINT_NAME = "print"
5858
const val ACTION_SHARE_LINK_NAME = "shareLink"
59+
const val ACTION_SAVE_TO_KDRIVE_NAME = "saveInkDrive"
5960
const val ACTION_POSTPONE_NAME = "postpone"
6061
const val ADD_MAILBOX_NAME = "addMailbox"
6162
const val DISCOVER_LATER = "discoverLater"

app/src/main/java/com/infomaniak/mail/data/api/ApiRepository.kt

+11-6
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ import com.infomaniak.mail.data.models.signature.SignaturesResult
5858
import com.infomaniak.mail.data.models.thread.ThreadResult
5959
import com.infomaniak.mail.ui.newMessage.AiViewModel.Shortcut
6060
import com.infomaniak.mail.utils.Utils
61+
import com.infomaniak.mail.utils.Utils.EML_CONTENT_TYPE
6162
import io.realm.kotlin.ext.copyFromRealm
6263
import kotlinx.serialization.json.Json
6364
import okhttp3.MultipartBody
@@ -451,13 +452,17 @@ object ApiRepository : ApiRepositoryCore() {
451452
return callApi(url = ApiRoutes.shareLink(mailboxUuid, folderId, mailId), method = POST)
452453
}
453454

455+
fun getDownloadedMessage(mailboxUuid: String, folderId: String, shortUid: Int): Response {
456+
val request = Request.Builder().url(ApiRoutes.downloadMessage(mailboxUuid, folderId, shortUid))
457+
.headers(HttpUtils.getHeaders(EML_CONTENT_TYPE))
458+
.get()
459+
.build()
460+
461+
return HttpClient.okHttpClient.newCall(request).execute()
462+
}
463+
454464
fun getMyKSuiteData(okHttpClient: OkHttpClient): ApiResponse<MyKSuiteData> {
455-
return ApiController.callApi(
456-
url = MyKSuiteApiRoutes.myKSuiteData(),
457-
method = ApiController.ApiMethod.GET,
458-
okHttpClient = okHttpClient,
459-
useKotlinxSerialization = true,
460-
)
465+
return callApi(url = MyKSuiteApiRoutes.myKSuiteData(), method = GET, okHttpClient = okHttpClient)
461466
}
462467

463468
/**

app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt

+5-18
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ package com.infomaniak.mail.ui
2020
import android.app.Application
2121
import androidx.lifecycle.*
2222
import com.infomaniak.lib.core.models.ApiResponse
23-
import com.infomaniak.lib.core.networking.HttpUtils
2423
import com.infomaniak.lib.core.networking.NetworkAvailability
2524
import com.infomaniak.lib.core.utils.ApiErrorCode.Companion.translateError
2625
import com.infomaniak.lib.core.utils.DownloadManagerUtils
@@ -30,7 +29,6 @@ import com.infomaniak.mail.MatomoMail.trackMultiSelectionEvent
3029
import com.infomaniak.mail.R
3130
import com.infomaniak.mail.data.LocalSettings
3231
import com.infomaniak.mail.data.api.ApiRepository
33-
import com.infomaniak.mail.data.api.ApiRoutes
3432
import com.infomaniak.mail.data.cache.RealmDatabase
3533
import com.infomaniak.mail.data.cache.mailboxContent.FolderController
3634
import com.infomaniak.mail.data.cache.mailboxContent.MessageController
@@ -60,6 +58,7 @@ import com.infomaniak.mail.utils.ContactUtils.getPhoneContacts
6058
import com.infomaniak.mail.utils.ContactUtils.mergeApiContactsIntoPhoneContacts
6159
import com.infomaniak.mail.utils.NotificationUtils.Companion.cancelNotification
6260
import com.infomaniak.mail.utils.SharedUtils.Companion.updateSignatures
61+
import com.infomaniak.mail.utils.Utils.EML_CONTENT_TYPE
6362
import com.infomaniak.mail.utils.Utils.isPermanentDeleteFolder
6463
import com.infomaniak.mail.utils.Utils.runCatchingRealm
6564
import com.infomaniak.mail.utils.extensions.*
@@ -75,7 +74,6 @@ import io.sentry.Sentry
7574
import io.sentry.SentryLevel
7675
import kotlinx.coroutines.*
7776
import kotlinx.coroutines.flow.*
78-
import okhttp3.Request
7977
import java.util.Date
8078
import java.util.UUID
8179
import javax.inject.Inject
@@ -1019,29 +1017,19 @@ class MainViewModel @Inject constructor(
10191017
fun reportDisplayProblem(messageUid: String) = viewModelScope.launch(ioCoroutineContext) {
10201018

10211019
val message = messageController.getMessage(messageUid) ?: return@launch
1022-
10231020
val mailbox = currentMailbox.value ?: return@launch
10241021

1025-
val userApiToken = AccountUtils.getUserById(mailbox.userId)?.apiToken?.accessToken ?: return@launch
1026-
val headers = HttpUtils.getHeaders(contentType = null).newBuilder()
1027-
.set("Authorization", "Bearer $userApiToken")
1028-
.build()
1029-
val request = Request.Builder().url(ApiRoutes.downloadMessage(mailbox.uuid, message.folderId, message.shortUid))
1030-
.headers(headers)
1031-
.get()
1032-
.build()
1033-
1034-
val response = AccountUtils.getHttpClient(mailbox.userId).newCall(request).execute()
1022+
val apiResponse = ApiRepository.getDownloadedMessage(mailbox.uuid, message.folderId, message.shortUid)
10351023

1036-
if (!response.isSuccessful || response.body == null) {
1024+
if (apiResponse.body == null || !apiResponse.isSuccessful) {
10371025
reportDisplayProblemTrigger.postValue(Unit)
10381026
snackbarManager.postValue(appContext.getString(RCore.string.anErrorHasOccurred))
10391027

10401028
return@launch
10411029
}
10421030

10431031
val filename = UUID.randomUUID().toString()
1044-
val emlAttachment = Attachment(response.body?.bytes(), filename, EML_CONTENT_TYPE)
1032+
val emlAttachment = Attachment(apiResponse.body?.bytes(), filename, EML_CONTENT_TYPE)
10451033
Sentry.captureMessage("Message display problem reported", SentryLevel.ERROR) { scope ->
10461034
scope.addAttachment(emlAttachment)
10471035
}
@@ -1191,7 +1179,7 @@ class MainViewModel @Inject constructor(
11911179
}
11921180

11931181
fun hasOtherExpeditors(threadUid: String) = liveData(ioCoroutineContext) {
1194-
val hasOtherExpeditors = threadController.getThread(threadUid)?.messages?.flatMap { it.from }?.any { !it.isMe() } ?: false
1182+
val hasOtherExpeditors = threadController.getThread(threadUid)?.messages?.flatMap { it.from }?.any { !it.isMe() } == true
11951183
emit(hasOtherExpeditors)
11961184
}
11971185

@@ -1312,6 +1300,5 @@ class MainViewModel @Inject constructor(
13121300
private val DEFAULT_SELECTED_FOLDER = FolderRole.INBOX
13131301
private const val REFRESH_DELAY = 2_000L // We add this delay because `etop` isn't always big enough.
13141302
private const val MAX_REFRESH_DELAY = 6_000L
1315-
private const val EML_CONTENT_TYPE = "message/rfc822"
13161303
}
13171304
}

app/src/main/java/com/infomaniak/mail/ui/main/folder/TwoPaneFragment.kt

+8
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ package com.infomaniak.mail.ui.main.folder
2020
import android.content.res.Configuration
2121
import android.os.Bundle
2222
import android.view.View
23+
import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
2324
import androidx.annotation.ColorRes
2425
import androidx.core.content.res.ResourcesCompat
2526
import androidx.core.view.isGone
@@ -41,6 +42,8 @@ import com.infomaniak.mail.ui.MainActivity
4142
import com.infomaniak.mail.ui.MainViewModel
4243
import com.infomaniak.mail.ui.main.search.SearchFragment
4344
import com.infomaniak.mail.ui.main.thread.ThreadFragment
45+
import com.infomaniak.mail.ui.main.thread.actions.DownloadMessagesProgressDialog
46+
import com.infomaniak.mail.utils.LocalStorageUtils.clearEmlCacheDir
4447
import com.infomaniak.mail.utils.extensions.*
4548
import javax.inject.Inject
4649

@@ -118,8 +121,13 @@ abstract class TwoPaneFragment : Fragment() {
118121
}
119122
}
120123

124+
private val resultActivityResultLauncher = registerForActivityResult(StartActivityForResult()) { _ ->
125+
clearEmlCacheDir(requireContext())
126+
}
127+
121128
private fun observeThreadNavigation() = with(twoPaneViewModel) {
122129
getBackNavigationResult(AttachmentExtensions.DOWNLOAD_ATTACHMENT_RESULT, ::startActivity)
130+
getBackNavigationResult(DownloadMessagesProgressDialog.DOWNLOAD_MESSAGES_RESULT, resultActivityResultLauncher::launch)
123131

124132
newMessageArgs.observe(viewLifecycleOwner) {
125133
safeNavigateToNewMessageActivity(args = it.toBundle())

app/src/main/java/com/infomaniak/mail/ui/main/thread/actions/DownloadAttachmentProgressDialog.kt

+11-46
Original file line numberDiff line numberDiff line change
@@ -17,59 +17,33 @@
1717
*/
1818
package com.infomaniak.mail.ui.main.thread.actions
1919

20-
import android.app.Dialog
2120
import android.os.Bundle
22-
import android.view.KeyEvent
21+
import android.view.LayoutInflater
22+
import android.view.View
23+
import android.view.ViewGroup
2324
import androidx.appcompat.content.res.AppCompatResources
24-
import androidx.fragment.app.DialogFragment
25-
import androidx.fragment.app.activityViewModels
25+
import androidx.core.view.isVisible
2626
import androidx.fragment.app.viewModels
27-
import androidx.lifecycle.lifecycleScope
2827
import androidx.navigation.fragment.findNavController
2928
import androidx.navigation.fragment.navArgs
30-
import com.google.android.material.dialog.MaterialAlertDialogBuilder
31-
import com.infomaniak.lib.core.R
32-
import com.infomaniak.lib.core.utils.SnackbarUtils.showSnackbar
3329
import com.infomaniak.lib.core.utils.setBackNavigationResult
34-
import com.infomaniak.mail.databinding.DialogDownloadProgressBinding
35-
import com.infomaniak.mail.ui.MainViewModel
3630
import com.infomaniak.mail.utils.extensions.AttachmentExtensions
3731
import com.infomaniak.mail.utils.extensions.AttachmentExtensions.getIntentOrGoToPlayStore
3832
import dagger.hilt.android.AndroidEntryPoint
39-
import kotlinx.coroutines.flow.first
40-
import kotlinx.coroutines.launch
4133

42-
@AndroidEntryPoint
43-
class DownloadAttachmentProgressDialog : DialogFragment() {
44-
45-
private val binding by lazy { DialogDownloadProgressBinding.inflate(layoutInflater) }
34+
class DownloadAttachmentProgressDialog : DownloadProgressDialog() {
4635
private val navigationArgs: DownloadAttachmentProgressDialogArgs by navArgs()
47-
private val mainViewModel: MainViewModel by activityViewModels()
4836
private val downloadAttachmentViewModel: DownloadAttachmentViewModel by viewModels()
4937

50-
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
51-
isCancelable = false
52-
val iconDrawable = AppCompatResources.getDrawable(requireContext(), navigationArgs.attachmentType.icon)
53-
binding.icon.setImageDrawable(iconDrawable)
54-
55-
return MaterialAlertDialogBuilder(requireContext())
56-
.setTitle(navigationArgs.attachmentName)
57-
.setView(binding.root)
58-
.setOnKeyListener { _, keyCode, event ->
59-
if (keyCode == KeyEvent.KEYCODE_BACK && event.action == KeyEvent.ACTION_UP) {
60-
findNavController().popBackStack()
61-
true
62-
} else false
63-
}
64-
.create()
65-
}
38+
override val dialogTitle: String? by lazy { navigationArgs.attachmentName }
6639

67-
override fun onStart() {
68-
super.onStart()
69-
downloadAttachment()
40+
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
41+
binding.icon.isVisible = true
42+
binding.icon.setImageDrawable(AppCompatResources.getDrawable(requireContext(), navigationArgs.attachmentType.icon))
43+
return super.onCreateView(inflater, container, savedInstanceState)
7044
}
7145

72-
private fun downloadAttachment() {
46+
override fun download() {
7347
downloadAttachmentViewModel.downloadAttachment().observe(this) { cachedAttachment ->
7448
if (cachedAttachment == null) {
7549
popBackStackWithError()
@@ -80,13 +54,4 @@ class DownloadAttachmentProgressDialog : DialogFragment() {
8054
}
8155
}
8256
}
83-
84-
private fun popBackStackWithError() {
85-
lifecycleScope.launch {
86-
mainViewModel.isNetworkAvailable.first { it != null }?.let { isNetworkAvailable ->
87-
showSnackbar(title = if (isNetworkAvailable) R.string.anErrorHasOccurred else R.string.noConnection)
88-
findNavController().popBackStack()
89-
}
90-
}
91-
}
9257
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Infomaniak Mail - Android
3+
* Copyright (C) 2024 Infomaniak Network SA
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
package com.infomaniak.mail.ui.main.thread.actions
19+
20+
import android.content.ComponentName
21+
import android.content.Context
22+
import android.content.Intent
23+
import android.net.Uri
24+
import android.os.Bundle
25+
import androidx.fragment.app.viewModels
26+
import androidx.navigation.fragment.findNavController
27+
import com.infomaniak.lib.core.utils.goToPlayStore
28+
import com.infomaniak.lib.core.utils.setBackNavigationResult
29+
import com.infomaniak.mail.utils.LocalStorageUtils.clearEmlCacheDir
30+
import com.infomaniak.mail.utils.SaveOnKDriveUtils.DRIVE_PACKAGE
31+
import com.infomaniak.mail.utils.SaveOnKDriveUtils.SAVE_EXTERNAL_ACTIVITY_CLASS
32+
import com.infomaniak.mail.utils.SaveOnKDriveUtils.canSaveOnKDrive
33+
34+
class DownloadMessagesProgressDialog : DownloadProgressDialog() {
35+
private val downloadThreadsViewModel: DownloadMessagesViewModel by viewModels()
36+
37+
override val dialogTitle: String? by lazy { downloadThreadsViewModel.getDialogName() }
38+
39+
override fun onCreate(savedInstanceState: Bundle?) {
40+
observeDownload()
41+
super.onCreate(savedInstanceState)
42+
}
43+
44+
override fun download() {
45+
downloadThreadsViewModel.downloadMessages(mainViewModel.currentMailbox.value)
46+
}
47+
48+
private fun observeDownload() {
49+
downloadThreadsViewModel.downloadMessagesLiveData.observe(this) { messageUris ->
50+
messageUris?.openKDriveOrPlayStore(requireContext())?.let { openKDriveIntent ->
51+
setBackNavigationResult(DOWNLOAD_MESSAGES_RESULT, openKDriveIntent)
52+
} ?: run {
53+
clearEmlCacheDir(requireContext())
54+
if (messageUris == null) popBackStackWithError() else findNavController().popBackStack()
55+
}
56+
}
57+
}
58+
59+
private fun List<Uri>.openKDriveOrPlayStore(context: Context): Intent? {
60+
return if (canSaveOnKDrive(context)) {
61+
saveToDriveIntent()
62+
} else {
63+
context.goToPlayStore(DRIVE_PACKAGE)
64+
null
65+
}
66+
}
67+
68+
private fun List<Uri>.saveToDriveIntent(): Intent {
69+
return Intent().apply {
70+
component = ComponentName(DRIVE_PACKAGE, SAVE_EXTERNAL_ACTIVITY_CLASS)
71+
action = Intent.ACTION_SEND_MULTIPLE
72+
putParcelableArrayListExtra(Intent.EXTRA_STREAM, ArrayList(this@saveToDriveIntent))
73+
}
74+
}
75+
76+
companion object {
77+
const val DOWNLOAD_MESSAGES_RESULT = "download_messages_result"
78+
}
79+
}

0 commit comments

Comments
 (0)