proton-mail-android/app/src/main/java/ch/protonmail/android/mailbox/presentation/ui/MailboxActivity.kt

1621 lines
67 KiB
Kotlin

/*
* Copyright (c) 2022 Proton AG
*
* This file is part of Proton Mail.
*
* Proton Mail 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.
*
* Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.mailbox.presentation.ui
import android.Manifest
import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.res.Resources
import android.graphics.Canvas
import android.net.Uri
import android.os.AsyncTask
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.ActionMode
import android.view.Gravity
import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.annotation.IdRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.os.postDelayed
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.core.view.isVisible
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.Observer
import androidx.lifecycle.flowWithLifecycle
import androidx.lifecycle.lifecycleScope
import androidx.loader.app.LoaderManager
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout.OnRefreshListener
import ch.protonmail.android.R
import ch.protonmail.android.activities.StartCompose
import ch.protonmail.android.activities.StartOnboarding
import ch.protonmail.android.activities.StartSearch
import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository
import ch.protonmail.android.adapters.messages.MailboxItemViewHolder.MessageViewHolder
import ch.protonmail.android.adapters.messages.MailboxRecyclerViewAdapter
import ch.protonmail.android.adapters.swipe.ArchiveSwipeHandler
import ch.protonmail.android.adapters.swipe.MarkReadSwipeHandler
import ch.protonmail.android.adapters.swipe.SpamSwipeHandler
import ch.protonmail.android.adapters.swipe.StarSwipeHandler
import ch.protonmail.android.adapters.swipe.SwipeAction
import ch.protonmail.android.adapters.swipe.TrashSwipeHandler
import ch.protonmail.android.api.models.SimpleMessage
import ch.protonmail.android.api.segments.event.AlarmReceiver
import ch.protonmail.android.core.Constants
import ch.protonmail.android.core.Constants.DrawerOptionType
import ch.protonmail.android.core.Constants.MessageLocationType
import ch.protonmail.android.core.Constants.MessageLocationType.Companion.fromInt
import ch.protonmail.android.core.Constants.Prefs.PREF_DONT_SHOW_PLAY_SERVICES
import ch.protonmail.android.core.Constants.Prefs.PREF_NEW_USER_ONBOARDING_SHOWN
import ch.protonmail.android.core.Constants.Prefs.PREF_USED_SPACE
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.details.presentation.ui.MessageDetailsActivity
import ch.protonmail.android.di.DefaultSharedPreferences
import ch.protonmail.android.events.FetchLabelsEvent
import ch.protonmail.android.events.MailboxLoadedEvent
import ch.protonmail.android.events.MailboxNoMessagesEvent
import ch.protonmail.android.events.SettingsChangedEvent
import ch.protonmail.android.events.Status
import ch.protonmail.android.feature.account.AccountStateManager
import ch.protonmail.android.labels.domain.model.Label
import ch.protonmail.android.labels.domain.model.LabelId
import ch.protonmail.android.labels.domain.model.LabelType
import ch.protonmail.android.labels.presentation.ui.LabelsActionSheet
import ch.protonmail.android.mailbox.presentation.model.EmptyMailboxUiModel
import ch.protonmail.android.mailbox.presentation.model.MailboxItemUiModel
import ch.protonmail.android.mailbox.presentation.model.MailboxListState
import ch.protonmail.android.mailbox.presentation.model.MailboxState
import ch.protonmail.android.mailbox.presentation.model.UnreadChipState
import ch.protonmail.android.mailbox.presentation.util.ConversationModeEnabled
import ch.protonmail.android.mailbox.presentation.viewmodel.FLOW_START_ACTIVITY
import ch.protonmail.android.mailbox.presentation.viewmodel.FLOW_TRY_COMPOSE
import ch.protonmail.android.mailbox.presentation.viewmodel.FLOW_USED_SPACE_CHANGED
import ch.protonmail.android.mailbox.presentation.viewmodel.MailboxViewModel
import ch.protonmail.android.mailbox.presentation.viewmodel.MailboxViewModel.MaxLabelsReached
import ch.protonmail.android.navigation.presentation.EXTRA_FIRST_LOGIN
import ch.protonmail.android.navigation.presentation.NavigationActivity
import ch.protonmail.android.notifications.data.remote.fcm.MultiUserFcmTokenManager
import ch.protonmail.android.notifications.data.remote.fcm.RegisterDeviceWorker
import ch.protonmail.android.notifications.data.remote.fcm.model.FirebaseToken
import ch.protonmail.android.notifications.presentation.utils.EXTRA_MAILBOX_LOCATION
import ch.protonmail.android.pendingaction.data.PendingActionDao
import ch.protonmail.android.pendingaction.data.PendingActionDatabase
import ch.protonmail.android.prefs.SecureSharedPreferences
import ch.protonmail.android.settings.domain.usecase.GetMailSettings
import ch.protonmail.android.ui.actionsheet.MessageActionSheet
import ch.protonmail.android.ui.actionsheet.model.ActionSheetTarget
import ch.protonmail.android.utils.Event
import ch.protonmail.android.utils.MessageUtils
import ch.protonmail.android.utils.NetworkSnackBarUtil
import ch.protonmail.android.utils.extensions.app
import ch.protonmail.android.utils.extensions.getColorIdFromAttr
import ch.protonmail.android.utils.extensions.showToast
import ch.protonmail.android.utils.ui.dialogs.DialogUtils.Companion.showDeleteConfirmationDialog
import ch.protonmail.android.utils.ui.dialogs.DialogUtils.Companion.showInfoDialog
import ch.protonmail.android.utils.ui.dialogs.DialogUtils.Companion.showTwoButtonInfoDialog
import ch.protonmail.android.utils.ui.dialogs.DialogUtils.Companion.showUndoSnackbar
import ch.protonmail.android.utils.ui.selection.SelectionModeEnum
import ch.protonmail.android.views.messageDetails.BottomActionsView
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
import com.google.android.material.snackbar.Snackbar
import com.google.firebase.iid.FirebaseInstanceId
import com.squareup.otto.Subscribe
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.android.synthetic.main.activity_mailbox.*
import kotlinx.android.synthetic.main.layout_mailbox_status_view.*
import kotlinx.android.synthetic.main.navigation_drawer.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import me.proton.core.domain.entity.UserId
import me.proton.core.mailsettings.domain.entity.MailSettings
import me.proton.core.util.android.sharedpreferences.get
import me.proton.core.util.android.sharedpreferences.observe
import me.proton.core.util.android.sharedpreferences.set
import me.proton.core.util.kotlin.EMPTY_STRING
import me.proton.core.util.kotlin.exhaustive
import timber.log.Timber
import java.lang.ref.WeakReference
import java.util.UUID
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlin.time.DurationUnit
import kotlin.time.toDuration
private const val TAG_MAILBOX_ACTIVITY = "MailboxActivity"
private const val PLAY_SERVICES_RESOLUTION_REQUEST = 9000
private const val STATE_MAILBOX_LOCATION = "mailbox_location"
private const val STATE_MAILBOX_LABEL_LOCATION = "mailbox_label_location"
private const val STATE_MAILBOX_LABEL_LOCATION_NAME = "mailbox_label_location_name"
const val LOADER_ID_LABELS_OFFLINE = 32
@AndroidEntryPoint
internal class MailboxActivity :
NavigationActivity(),
ActionMode.Callback,
OnRefreshListener {
private lateinit var pendingActionDao: PendingActionDao
@Inject
lateinit var messageDetailsRepositoryFactory: MessageDetailsRepository.AssistedFactory
@Inject
lateinit var networkSnackBarUtil: NetworkSnackBarUtil
@Inject
lateinit var registerDeviceWorkerEnqueuer: RegisterDeviceWorker.Enqueuer
@Inject
lateinit var multiUserFcmTokenManager: MultiUserFcmTokenManager
@Inject
lateinit var isConversationModeEnabled: ConversationModeEnabled
@Inject
@DefaultSharedPreferences
lateinit var defaultSharedPreferences: SharedPreferences
private lateinit var mailboxAdapter: MailboxRecyclerViewAdapter
private var swipeController: SwipeController = SwipeController()
private val isLoadingMore = AtomicBoolean(false)
private var scrollStateChanged = false
private var actionMode: ActionMode? = null
// For the time being we don't show it at all; will be re-added in MAILAND-2320
private var swipeCustomizeSnack: Snackbar? = null
private var mailboxLabelId: String? = null
set(value) {
field = value
// we need to set it on view model for filtering by label
setNewLabel(value ?: EMPTY_STRING)
}
private var mailboxLabelName: String? = null
private var lastFetchedMailboxItemsIds = emptyList<String>()
private var refreshMailboxJobRunning = false
private lateinit var syncUUID: String
private var customizeSwipeSnackShown = false
private val mailboxViewModel: MailboxViewModel by viewModels()
private var storageLimitApproachingAlertDialog: AlertDialog? = null
private val handler = Handler(Looper.getMainLooper())
private var isOnline = true
private val startMessageDetailsLauncher = registerForActivityResult(MessageDetailsActivity.Launcher()) {}
private val startComposeLauncher = registerForActivityResult(StartCompose()) { messageId ->
messageId?.let {
val snack = Snackbar.make(
findViewById(R.id.drawer_layout),
R.string.snackbar_message_draft_saved,
Snackbar.LENGTH_LONG
)
snack.setAction(R.string.move_to_trash) {
mailboxViewModel.moveToFolder(
listOf(messageId),
userManager.requireCurrentUserId(),
MessageLocationType.DRAFT,
MessageLocationType.TRASH.asLabelIdString()
)
Snackbar.make(
findViewById(R.id.drawer_layout),
R.string.snackbar_message_draft_moved_to_trash,
Snackbar.LENGTH_LONG
).show()
}
snack.show()
}
}
private val startSearchLauncher = registerForActivityResult(StartSearch()) {}
private val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (!isGranted) {
showInfoDialog(
this, getString(R.string.need_permissions_title),
getString(R.string.need_notification_permissions_to_receive_notifications)
) { unit: Unit -> unit }
}
}
private val startOnboardingActivityLauncher = registerForActivityResult(StartOnboarding()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
requestPermissionLauncher.launch(
Manifest.permission.POST_NOTIFICATIONS
)
}
}
override val currentLabelId get() = mailboxLabelId
override fun getLayoutId(): Int = R.layout.activity_mailbox
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen().setKeepOnScreenCondition {
accountStateManager.state.value == AccountStateManager.State.Processing
}
super.onCreate(savedInstanceState)
// TODO if we decide to use special flag for switching (and not login), change this
if (intent.getBooleanExtra(EXTRA_FIRST_LOGIN, false)) {
multiUserFcmTokenManager.setTokenUnsentForAllSavedUsersBlocking() // force FCM to re-register
}
val extras = intent.extras
// Set the padding to match the Status Bar height
if (savedInstanceState != null) {
val locationInt = savedInstanceState.getInt(STATE_MAILBOX_LOCATION)
mailboxLabelId = savedInstanceState.getString(STATE_MAILBOX_LABEL_LOCATION)
mailboxLabelName = savedInstanceState.getString(STATE_MAILBOX_LABEL_LOCATION_NAME)
setMailboxLocation(fromInt(locationInt))
}
if (extras != null && extras.containsKey(EXTRA_MAILBOX_LOCATION)) {
switchToMailboxLocation(extras.getInt(EXTRA_MAILBOX_LOCATION))
}
startObserving()
startObservingPendingActions()
startObservingUsedSpace()
mailboxViewModel.toastMessageMaxLabelsReached.observe(this) { event: Event<MaxLabelsReached?> ->
val maxLabelsReached = event.getContentIfNotHandled()
if (maxLabelsReached != null) {
val message = getString(
R.string.max_labels_exceeded,
maxLabelsReached.subject,
maxLabelsReached.maxAllowedLabels
)
showToast(message, Toast.LENGTH_SHORT)
}
}
mailboxViewModel.hasConnectivity.observe(this, ::onConnectivityEvent)
var actionModeAux: ActionMode? = null
mailboxAdapter = MailboxRecyclerViewAdapter(this) { selectionModeEvent ->
when (selectionModeEvent) {
SelectionModeEnum.STARTED -> {
actionModeAux = startActionMode(this@MailboxActivity)
mailboxActionsView.layoutParams.height = ConstraintLayout.LayoutParams.WRAP_CONTENT
mailboxActionsView.visibility = View.VISIBLE
}
SelectionModeEnum.ENDED -> {
val actionModeEnd = actionModeAux
if (actionModeEnd != null) {
actionModeEnd.finish()
actionModeAux = null
}
mailboxActionsView.visibility = View.GONE
}
}
}
mailboxViewModel.pendingSendsLiveData.observe(this, mailboxAdapter::setPendingForSendingList)
mailboxViewModel.pendingUploadsLiveData.observe(this, mailboxAdapter::setPendingUploadsList)
mailboxViewModel.hasSuccessfullyDeletedMessages.observe(this) { isSuccess ->
Timber.v("Delete message status is success $isSuccess")
if (!isSuccess) {
showToast(R.string.message_deleted_error)
}
}
checkUserAndFetchNews()
setTitle()
mailboxRecyclerView.adapter = mailboxAdapter
mailboxRecyclerView.layoutManager = LinearLayoutManager(this)
// Set the list divider
val itemDecoration = DividerItemDecoration(mailboxRecyclerView.context, DividerItemDecoration.VERTICAL)
itemDecoration.setDrawable(getDrawable(R.drawable.list_divider)!!)
mailboxRecyclerView.addItemDecoration(itemDecoration)
buildSwipeProcessor()
initializeSwipeRefreshLayout(mailboxSwipeRefreshLayout)
if (userManager.isFirstMailboxLoad) {
userManager.firstMailboxLoadDone()
}
mailboxAdapter.setItemClick { mailboxUiItem: MailboxItemUiModel ->
OnMessageClickTask(
WeakReference(this@MailboxActivity),
messageDetailsRepositoryFactory,
mailboxUiItem.itemId,
mailboxUiItem.subject,
currentMailboxLocation,
userManager.requireCurrentUserId()
).execute()
}
mailboxAdapter.setOnItemSelectionChangedListener {
val checkedItems = mailboxAdapter.checkedMailboxItems.size
actionMode?.title = "$checkedItems ${getString(R.string.selected)}"
mailboxActionsView.setAction(
BottomActionsView.ActionPosition.ACTION_FIRST,
true,
if (MessageUtils.areAllUnRead(
selectedMessages
)
) R.drawable.ic_proton_envelope_open_text else R.drawable.ic_proton_envelope_dot,
if (MessageUtils.areAllUnRead(
selectedMessages
)
) getString(R.string.mark_as_read) else getString(R.string.mark_as_unread)
)
}
checkRegistration()
mailboxRecyclerView.addOnScrollListener(listScrollListener)
fetchOrganizationData()
with(mailboxViewModel) {
mailboxState
.onEach(::renderState)
.launchIn(lifecycleScope)
mailboxLocation
.onEach(mailboxAdapter::setNewLocation)
.launchIn(lifecycleScope)
drawerLabels
.onEach { sideDrawer.setFoldersAndLabelsSection(it) }
.launchIn(lifecycleScope)
unreadCounters
.flowWithLifecycle(lifecycle, Lifecycle.State.STARTED)
.onEach(sideDrawer::setUnreadCounters)
.launchIn(lifecycleScope)
exitSelectionModeSharedFlow
.onEach { if (it) actionMode?.finish() }
.launchIn(lifecycleScope)
}
setUpMailboxActionsView()
lifecycleScope.launch {
mailboxViewModel.getMailSettingsState().onEach {
it.fold(
ifLeft = {},
ifRight = { result ->
when (result) {
is GetMailSettings.Result.Error -> {
showToast(result.message.toString(), Toast.LENGTH_LONG)
}
is GetMailSettings.Result.Success -> {
swipeController.setCurrentMailSetting(result.mailSettings)
ItemTouchHelper(swipeController).attachToRecyclerView(mailboxRecyclerView)
mailboxViewModel.refreshMessages()
}
}.exhaustive
}
)
}.launchIn(lifecycleScope)
}
}
private fun startObserving() {
val owner = this
mailboxViewModel.run {
usedSpaceActionEvent(FLOW_START_ACTIVITY)
manageLimitReachedWarning.observe(owner, setupUpLimitReachedObserver)
manageLimitApproachingWarning.observe(owner, setupUpLimitApproachingObserver)
manageLimitBelowCritical.observe(owner, setupUpLimitBelowCriticalObserver)
manageLimitReachedWarningOnTryCompose.observe(owner, setupUpLimitReachedTryComposeObserver)
}
}
private fun startObservingPendingActions() {
val owner = this
mailboxViewModel.run {
pendingSendsLiveData.removeObservers(owner)
pendingUploadsLiveData.removeObservers(owner)
pendingSendsLiveData.observe(owner) { mailboxAdapter.setPendingForSendingList(it) }
pendingUploadsLiveData.observe(owner) { mailboxAdapter.setPendingUploadsList(it) }
}
}
private fun startObservingUsedSpace() {
mailboxViewModel.primaryUserId
.flatMapLatest { primaryUserId ->
val preferences = SecureSharedPreferences.getPrefsForUser(this, primaryUserId)
preferences.observe<Long>(PREF_USED_SPACE)
}
.onEach { mailboxViewModel.usedSpaceActionEvent(FLOW_USED_SPACE_CHANGED) }
.launchIn(lifecycleScope)
}
private val setupUpLimitReachedObserver = Observer { limitReached: Event<Boolean> ->
if (limitReached.getContentIfNotHandled() == true) {
if (storageLimitApproachingAlertDialog != null) {
storageLimitApproachingAlertDialog!!.dismiss()
storageLimitApproachingAlertDialog = null
}
if (userManager.canShowStorageLimitReached()) {
showTwoButtonInfoDialog(
titleStringId = R.string.storage_limit_warning_title,
messageStringId = R.string.storage_limit_reached_text,
positiveStringId = R.string.ok,
negativeStringId = R.string.learn_more,
onNegativeButtonClicked = {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.limit_reached_learn_more))
)
startActivity(browserIntent)
userManager.setShowStorageLimitReached(false)
}
) {
userManager.setShowStorageLimitReached(false)
}
}
userManager.setShowStorageLimitWarning(true)
storageLimitAlert.apply {
visibility = View.VISIBLE
setIcon(getDrawable(R.drawable.ic_proton_inbox)!!)
setText(getString(R.string.storage_limit_alert))
}
}
}
private fun showStorageLimitApproachingAlertDialog() {
storageLimitApproachingAlertDialog = showTwoButtonInfoDialog(
titleStringId = R.string.storage_limit_warning_title,
messageStringId = R.string.storage_limit_approaching_text,
negativeStringId = R.string.dont_remind_again,
onNegativeButtonClicked = {
userManager.setShowStorageLimitWarning(false)
storageLimitApproachingAlertDialog = null
}
) { storageLimitApproachingAlertDialog = null }
}
private val setupUpLimitApproachingObserver = { limitApproaching: Event<Boolean> ->
if (limitApproaching.getContentIfNotHandled() == true) {
if (userManager.canShowStorageLimitWarning()) {
if (storageLimitApproachingAlertDialog == null || !storageLimitApproachingAlertDialog!!.isShowing) {
// This is the first time the dialog is going to be showed or
// the dialog is not showing and had previously been dismissed by clicking the positive
// or negative button or the dialog is not showing and had previously been dismissed on touch
// outside or by clicking the back button
showStorageLimitApproachingAlertDialog()
}
}
userManager.setShowStorageLimitReached(true)
storageLimitAlert.visibility = View.GONE
}
}
private val setupUpLimitBelowCriticalObserver = { limitReached: Event<Boolean> ->
if (limitReached.getContentIfNotHandled() == true) {
userManager.setShowStorageLimitWarning(true)
userManager.setShowStorageLimitReached(true)
storageLimitAlert.visibility = View.GONE
}
}
private val setupUpLimitReachedTryComposeObserver = Observer { limitReached: Event<Boolean> ->
if (limitReached.getContentIfNotHandled() == true) {
showTwoButtonInfoDialog(
titleStringId = R.string.storage_limit_warning_title,
messageStringId = R.string.storage_limit_reached_text,
negativeStringId = R.string.learn_more,
onNegativeButtonClicked = {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.limit_reached_learn_more))
)
startActivity(browserIntent)
}
)
} else {
startComposeLauncher.launch(StartCompose.Input())
}
}
private val selectedMessages: List<SimpleMessage>
get() = mailboxAdapter.checkedMailboxItems.map { SimpleMessage(it) }
private var firstLogin: Boolean? = null
override fun onPrimaryUserId(userId: UserId) {
super.onPrimaryUserId(userId)
mJobManager.start()
pendingActionDao = PendingActionDatabase.getInstance(this, userId).getDao()
checkRegistration()
switchToMailboxLocation(DrawerOptionType.INBOX.drawerOptionTypeValue)
// Set the elevation to 0 since after account switch the list is scrolled to the top
setElevationOnToolbarAndStatusView(false)
}
private fun renderState(state: MailboxState) {
Timber.v("New mailbox state: ${state.javaClass.canonicalName}")
renderUnreadChipState(state.unreadChip)
renderListState(state.list)
}
private fun renderUnreadChipState(state: UnreadChipState) {
when (state) {
UnreadChipState.Loading -> {}
is UnreadChipState.Data -> {
unreadMessagesStatusChip.bind(
model = state.model,
onEnableFilter = mailboxViewModel::enableUnreadFilter,
onDisableFilter = mailboxViewModel::disableUnreadFilter
)
}
}
}
private fun renderListState(state: MailboxListState) {
setLoadingMore(false)
when (state) {
is MailboxListState.Loading -> setRefreshing(true)
is MailboxListState.DataRefresh -> {
lastFetchedMailboxItemsIds = state.lastFetchedItemsIds
setRefreshing(false)
include_mailbox_no_messages.isVisible =
state.lastFetchedItemsIds.isEmpty() && mailboxAdapter.itemCount == 0
}
is MailboxListState.Data -> {
Timber.v("Data state items count: ${state.items.size}")
include_mailbox_error.isVisible = false
include_mailbox_no_messages.isVisible = state.isFreshData && state.items.isEmpty()
mailboxRecyclerView.isVisible != state.items.isEmpty()
mailboxAdapter.submitList(state.items) {
if (state.shouldResetPosition) mailboxRecyclerView.scrollToPosition(0)
}
}
is MailboxListState.Error -> {
setRefreshing(false)
Timber.e(state.throwable, "Mailbox error ${state.error}")
include_mailbox_no_messages.isVisible = false
if (mailboxAdapter.itemCount > 0) {
include_mailbox_error.isVisible = false
if (state.isOffline.not()) showToast(R.string.inbox_could_not_retrieve_messages)
} else {
include_mailbox_error.isVisible = true
}
}
}
}
private val listScrollListener: RecyclerView.OnScrollListener = object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(view: RecyclerView, scrollState: Int) {
scrollStateChanged =
scrollState == RecyclerView.SCROLL_STATE_DRAGGING || scrollState == RecyclerView.SCROLL_STATE_SETTLING
}
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (!scrollStateChanged) {
return
}
val layoutManager = recyclerView.layoutManager as LinearLayoutManager
val firstCompletelyVisibleItemPosition = layoutManager.findFirstCompletelyVisibleItemPosition()
// Load more when showing last fetched messages or at the end of the list
if (dy > 0 && isLoadingMore.get().not()) {
val lastCompletelyVisibleItemPosition = layoutManager.findLastCompletelyVisibleItemPosition()
val isAtBottomOfTheList = lastCompletelyVisibleItemPosition == mailboxAdapter.itemCount - 1
fun inAnyLastFetchedMailboxItemVisible() =
mailboxAdapter.isAnyMailboxItemWithinPositions(
mailboxItemsIds = lastFetchedMailboxItemsIds,
startPosition = firstCompletelyVisibleItemPosition,
endPosition = lastCompletelyVisibleItemPosition
)
if (isAtBottomOfTheList || inAnyLastFetchedMailboxItemVisible()) {
setLoadingMore(true)
mailboxViewModel.loadMore()
lastFetchedMailboxItemsIds = emptyList()
}
}
// Increase the elevation if the list is scrolled down and decrease if it is scrolled to the top
if (firstCompletelyVisibleItemPosition == 0) {
setElevationOnToolbarAndStatusView(false)
} else {
setElevationOnToolbarAndStatusView(true)
}
}
}
private fun loadMailboxItems() {
mailboxViewModel.loadMore()
}
private fun setElevationOnToolbarAndStatusView(shouldIncreaseElevation: Boolean) {
val elevation = if (shouldIncreaseElevation) {
resources.getDimensionPixelSize(R.dimen.action_bar_elevation)
} else {
resources.getDimensionPixelSize(R.dimen.action_bar_no_elevation)
}.toFloat()
supportActionBar?.elevation = elevation
mailboxStatusLayout.elevation = elevation
}
private fun registerFcmReceiver() {
val filter = IntentFilter(getString(R.string.action_notification))
filter.priority = 2
LocalBroadcastManager.getInstance(this).registerReceiver(fcmBroadcastReceiver, filter)
}
private fun onConnectivityCheckRetry() {
mConnectivitySnackLayout?.let {
networkSnackBarUtil.getCheckingConnectionSnackBar(it, mailboxActionsView.id).show()
}
syncUUID = UUID.randomUUID().toString()
lifecycleScope.launch {
delay(3.toDuration(DurationUnit.SECONDS))
mailboxViewModel.loadMore()
}
mailboxViewModel.checkConnectivityDelayed()
}
override fun onDohFailed() {
super.onDohFailed()
lifecycleScope.launch(Dispatchers.Main) {
setAsOffline(Constants.ConnectionState.CANT_REACH_SERVER)
}
}
private fun checkRegistration() {
// Check device for Play Services APK.
lifecycleScope.launchWhenCreated {
if (checkPlayServices()) {
val tokenSent = multiUserFcmTokenManager.isTokenSentForAllLoggedUsers()
if (!tokenSent) {
runCatching {
FirebaseInstanceId.getInstance().instanceId.addOnCompleteListener { task ->
if (task.isSuccessful && task.result != null) {
multiUserFcmTokenManager.saveTokenBlocking(FirebaseToken(task.result!!.token))
registerDeviceWorkerEnqueuer()
} else {
Timber.d(task.exception, "Could not retrieve FirebaseInstanceId")
}
}
}.onFailure {
showToast(R.string.invalid_firebase_api_key_message)
Timber.d(it, "Invalid Firebase API key. Push notifications will not work.")
}
}
}
}
}
private fun checkUserAndFetchNews(): Boolean {
syncUUID = UUID.randomUUID().toString()
if (firstLogin == null) {
firstLogin = intent.getBooleanExtra(EXTRA_FIRST_LOGIN, false)
}
return if (!firstLogin!!) {
val alarmReceiver = AlarmReceiver()
alarmReceiver.setAlarm(this, true)
false
} else {
firstLogin = false
refreshMailboxJobRunning = true
app.updateDone()
// TODO: remove?
loadMailboxItems()
true
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
checkRegistration()
checkUserAndFetchNews()
switchToMailboxLocation(DrawerOptionType.INBOX.drawerOptionTypeValue)
}
override fun onResume() {
super.onResume()
if (userManager.currentUserId != null &&
!defaultSharedPreferences.getBoolean(PREF_NEW_USER_ONBOARDING_SHOWN, false)
) {
startOnboardingActivityLauncher.launch(Unit)
}
registerFcmReceiver()
checkDelinquency()
mailboxViewModel.checkConnectivity()
val mailboxLocation = mailboxViewModel.mailboxLocation.value
if (mailboxLocation == MessageLocationType.INBOX) {
userManager.currentUserId?.let { mailboxViewModel.clearNotifications(it) }
}
mailboxViewModel.startRateAppFlowIfNeeded()
}
override fun onPause() {
runCatching {
LocalBroadcastManager.getInstance(this).unregisterReceiver(fcmBroadcastReceiver)
}
networkSnackBarUtil.hideAllSnackBars()
super.onPause()
}
public override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(
STATE_MAILBOX_LOCATION,
mailboxViewModel.mailboxLocation.value.messageLocationTypeValue
)
outState.putString(STATE_MAILBOX_LABEL_LOCATION, mailboxLabelId)
outState.putString(STATE_MAILBOX_LABEL_LOCATION_NAME, mailboxLabelName)
super.onSaveInstanceState(outState)
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_mailbox_options, menu)
val mailboxLocation = mailboxViewModel.mailboxLocation.value
menu.findItem(R.id.empty).isVisible =
mailboxLocation in listOf(MessageLocationType.DRAFT, MessageLocationType.SPAM, MessageLocationType.TRASH)
return true
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
menu.clear()
menuInflater.inflate(R.menu.menu_mailbox_options, menu)
val mailboxLocation = mailboxViewModel.mailboxLocation.value
menu.findItem(R.id.empty).isVisible =
mailboxLocation in listOf(
MessageLocationType.DRAFT,
MessageLocationType.ALL_DRAFT,
MessageLocationType.SPAM,
MessageLocationType.TRASH,
MessageLocationType.LABEL,
MessageLocationType.LABEL_FOLDER
)
return super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean =
onOptionsItemSelected(menuItem.itemId) || super.onOptionsItemSelected(menuItem)
private fun onOptionsItemSelected(@IdRes itemId: Int): Boolean {
when (itemId) {
R.id.compose -> mailboxViewModel.usedSpaceActionEvent(FLOW_TRY_COMPOSE)
R.id.search -> startSearchLauncher.launch(Unit)
R.id.empty -> showTwoButtonInfoDialog(
titleStringId = R.string.empty_folder,
messageStringId = R.string.are_you_sure_empty,
negativeStringId = R.string.no
) {
mailboxViewModel.emptyFolderAction(
userManager.requireCurrentUserId(),
LabelId(currentLabelId ?: currentMailboxLocation.messageLocationTypeValue.toString())
)
setLoadingMore(false)
}
else -> return false
}
return true
}
override fun onBackPressed() {
val drawerClosed = closeDrawer()
if (!drawerClosed && mailboxViewModel.mailboxLocation.value != MessageLocationType.INBOX) {
switchToMailboxLocation(DrawerOptionType.INBOX.drawerOptionTypeValue)
} else if (!drawerClosed) {
moveTaskToBack(true)
}
}
private fun initializeSwipeRefreshLayout(swipeRefreshLayoutAux: SwipeRefreshLayout) {
swipeRefreshLayoutAux.setColorSchemeResources(getColorIdFromAttr(R.attr.brand_norm))
swipeRefreshLayoutAux.setOnRefreshListener(this)
}
private fun setRefreshing(shouldRefresh: Boolean) {
Timber.v("setRefreshing shouldRefresh: $shouldRefresh")
updatedStatusTextView.setText(
when {
isOnline.not() -> R.string.you_are_offline
shouldRefresh -> R.string.mailbox_updating
else -> R.string.mailbox_updated_recently
}
)
mailboxSwipeRefreshLayout.isRefreshing = shouldRefresh
}
private fun setLoadingMore(loadingMore: Boolean): Boolean =
isLoadingMore.getAndSet(loadingMore)
override fun onInbox(type: DrawerOptionType) {
mailboxViewModel.clearNotifications(userManager.requireCurrentUserId())
switchToMailboxLocation(type.drawerOptionTypeValue)
}
override fun onOtherMailBox(type: DrawerOptionType) {
switchToMailboxLocation(type.drawerOptionTypeValue)
}
public override fun onLabelMailBox(
type: DrawerOptionType,
labelId: String,
labelName: String,
isFolder: Boolean
) {
switchToMailboxCustomLocation(type.drawerOptionTypeValue, labelId, labelName, isFolder)
}
override val currentMailboxLocation: MessageLocationType
get() = mailboxViewModel.mailboxLocation.value
private fun setTitle() {
val titleRes: Int = when (mailboxViewModel.mailboxLocation.value) {
MessageLocationType.INBOX -> R.string.inbox_option
MessageLocationType.STARRED -> R.string.starred_option
MessageLocationType.DRAFT, MessageLocationType.ALL_DRAFT -> R.string.drafts_option
MessageLocationType.SENT, MessageLocationType.ALL_SENT -> R.string.sent_option
MessageLocationType.ARCHIVE -> R.string.archive_option
MessageLocationType.TRASH -> R.string.trash_option
MessageLocationType.SPAM -> R.string.spam_option
MessageLocationType.ALL_MAIL -> R.string.allmail_option
MessageLocationType.ALL_SCHEDULED -> R.string.drawer_scheduled
else -> R.string.app_name
}
supportActionBar?.setTitle(titleRes)
}
private fun setAsOffline(connectivity: Constants.ConnectionState) {
isOnline = false
showNoConnSnackAndScheduleRetry(connectivity)
updatedStatusTextView.setText(R.string.you_are_offline)
}
private fun setAsOnline() {
isOnline = true
hideNoConnSnack()
}
private fun showNoConnSnackAndScheduleRetry(connectivity: Constants.ConnectionState) {
Timber.v("show NoConnection Snackbar ${mConnectivitySnackLayout != null}")
mConnectivitySnackLayout?.let { snackBarLayout ->
lifecycleScope.launchWhenCreated {
networkSnackBarUtil.getNoConnectionSnackBar(
parentView = snackBarLayout,
user = userManager.currentLegacyUser,
netConfiguratorCallback = this@MailboxActivity,
onRetryClick = ::onConnectivityCheckRetry,
isOffline = connectivity == Constants.ConnectionState.NO_INTERNET,
anchorViewId = mailboxActionsView.id
).show()
}
}
}
private fun hideNoConnSnack() {
Timber.v("hideNoConnSnack")
networkSnackBarUtil.hideCheckingConnectionSnackBar()
networkSnackBarUtil.hideNoConnectionSnackBar()
}
@Subscribe
fun onSettingsChangedEvent(event: SettingsChangedEvent) {
if (!event.success) {
showToast(R.string.saving_failed_no_conn, Toast.LENGTH_LONG, Gravity.CENTER)
}
}
@Subscribe
fun onMailboxLoaded(event: MailboxLoadedEvent?) {
Timber.v("Mailbox loaded status ${event?.status}")
if (event == null || event.uuid != null && event.uuid != syncUUID) {
return
}
refreshMailboxJobRunning = false
setLoadingMore(false)
setRefreshing(false)
if (!isDohOngoing) {
showToast(event.status)
}
mNetworkResults.setMailboxLoaded(MailboxLoadedEvent(Status.SUCCESS, null))
}
private fun onConnectivityEvent(connectivity: Constants.ConnectionState) {
Timber.v("onConnectivityEvent hasConnection: ${connectivity.name}")
if (!isDohOngoing) {
Timber.d("DoH NOT ongoing showing UI")
if (connectivity != Constants.ConnectionState.CONNECTED) {
setAsOffline(connectivity)
} else {
setAsOnline()
}
} else {
Timber.d("DoH ongoing, not showing UI")
}
}
@Subscribe
fun onMailboxNoMessages(event: MailboxNoMessagesEvent?) {
// show toast only if user initiated load more
if (isLoadingMore.get()) {
showToast(R.string.no_more_messages, Toast.LENGTH_SHORT)
mailboxAdapter.notifyDataSetChanged()
}
setLoadingMore(false)
}
private fun showToast(status: Status) {
when (status) {
Status.UNAUTHORIZED -> setAsOffline(Constants.ConnectionState.CANT_REACH_SERVER)
Status.NO_NETWORK -> setAsOffline(Constants.ConnectionState.NO_INTERNET)
Status.SUCCESS -> setAsOnline()
else -> return
}
}
@Subscribe
fun onLabelsLoadedEvent(event: FetchLabelsEvent) {
if (event.status == Status.SUCCESS) {
mailboxAdapter.notifyDataSetChanged()
}
}
// region Action mode
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
actionMode = mode
mailboxSwipeRefreshLayout.isEnabled = false
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu) = true
override fun onActionModeStarted(mode: ActionMode?) {
super.onActionModeStarted(mode)
// We need to set a solid color for status bar during Action mode, or it will be black
window.statusBarColor = getColor(R.color.background_norm)
}
override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem) = true
override fun onDestroyActionMode(mode: ActionMode) {
actionMode = null
mailboxActionsView.visibility = View.GONE
mailboxSwipeRefreshLayout.isEnabled = true
mailboxAdapter.endSelectionMode()
}
// endregion
private fun setUpMailboxActionsView() {
val actionsUiModel = BottomActionsView.UiModel(
R.drawable.ic_proton_envelope_dot,
if (currentMailboxLocation in arrayOf(
MessageLocationType.DRAFT,
MessageLocationType.ALL_DRAFT,
MessageLocationType.SENT,
MessageLocationType.ALL_SENT,
MessageLocationType.TRASH,
MessageLocationType.SPAM
)
) R.drawable.ic_proton_trash_cross else R.drawable.ic_proton_trash,
R.drawable.ic_proton_folder_arrow_in,
R.drawable.ic_proton_tag
)
mailboxActionsView.bind(actionsUiModel)
mailboxActionsView.setAction(
BottomActionsView.ActionPosition.ACTION_SECOND, true,
if (currentMailboxLocation in arrayOf(
MessageLocationType.DRAFT,
MessageLocationType.ALL_DRAFT,
MessageLocationType.SENT,
MessageLocationType.ALL_SENT,
MessageLocationType.TRASH,
MessageLocationType.SPAM
)
) R.drawable.ic_proton_trash_cross else R.drawable.ic_proton_trash,
if (currentMailboxLocation in arrayOf(
MessageLocationType.DRAFT,
MessageLocationType.ALL_DRAFT,
MessageLocationType.SENT,
MessageLocationType.ALL_SENT,
MessageLocationType.TRASH,
MessageLocationType.SPAM
)
) getString(R.string.delete) else getString(
R.string.trash
)
)
mailboxActionsView.setAction(
BottomActionsView.ActionPosition.ACTION_THIRD, true,
R.drawable.ic_proton_folder_arrow_in,
getString(R.string.move_to)
)
mailboxActionsView.setOnFirstActionClickListener {
val messageIds = getSelectedMessageIds()
if (MessageUtils.areAllUnRead(selectedMessages)) {
mailboxViewModel.markRead(
messageIds,
UserId(userManager.requireCurrentUserId().id),
currentMailboxLocation,
mailboxLabelId ?: currentMailboxLocation.messageLocationTypeValue.toString()
)
} else {
mailboxViewModel.markUnRead(
messageIds,
UserId(userManager.requireCurrentUserId().id),
currentMailboxLocation,
mailboxLabelId ?: currentMailboxLocation.messageLocationTypeValue.toString()
)
}
}
mailboxActionsView.setOnSecondActionClickListener {
val messageIds = getSelectedMessageIds()
if (currentMailboxLocation in arrayOf(
MessageLocationType.DRAFT,
MessageLocationType.ALL_DRAFT,
MessageLocationType.SENT,
MessageLocationType.ALL_SENT,
MessageLocationType.TRASH,
MessageLocationType.SPAM
)
) {
showDeleteConfirmationDialog(
this,
getString(R.string.delete_messages),
getString(R.string.confirm_destructive_action)
) {
mailboxViewModel.deleteAction(
messageIds,
UserId(userManager.requireCurrentUserId().id),
currentMailboxLocation
)
actionMode?.finish()
}
} else {
if (isScheduledMessageSelected()) {
showTwoButtonInfoDialog(
titleStringId = R.string.scheduled_message_moved_to_trash_title,
messageStringId = R.string.scheduled_message_moved_to_trash_desc,
negativeStringId = R.string.cancel,
onPositiveButtonClicked = {
moveMessagesToTrashFolder(messageIds)
}
)
} else {
moveMessagesToTrashFolder(messageIds)
}
}
}
mailboxActionsView.setOnThirdActionClickListener {
showFoldersManager(getSelectedMessageIds())
}
mailboxActionsView.setOnFourthActionClickListener {
showLabelsManager(getSelectedMessageIds())
}
mailboxActionsView.setOnMoreActionClickListener {
lifecycleScope.launch {
showActionSheet(getSelectedMessageIds(), isConversationModeEnabled(currentMailboxLocation))
}
}
}
private fun getSelectedMessageIds(): List<String> = selectedMessages.map { it.messageId }
private fun isScheduledMessageSelected(): Boolean = selectedMessages.any { it.isScheduled }
private fun showActionSheet(
messagesIds: List<String>,
isConversationsModeOn: Boolean
) {
val messagesStringRes = if (isConversationsModeOn) {
R.plurals.x_conversations_count
} else {
R.plurals.x_messages_count
}
MessageActionSheet.newInstance(
actionSheetTarget = ActionSheetTarget.MAILBOX_ITEMS_IN_MAILBOX_SCREEN,
messagesIds = messagesIds,
currentFolderLocationId = currentMailboxLocation.messageLocationTypeValue,
mailboxLabelId = mailboxLabelId ?: currentMailboxLocation.messageLocationTypeValue.toString(),
title = resources.getQuantityString(
messagesStringRes,
messagesIds.size,
messagesIds.size
),
isScheduled = isScheduledMessageSelected()
)
.show(supportFragmentManager, MessageActionSheet::class.qualifiedName)
}
private fun showFoldersManager(messageIds: List<String>) {
LabelsActionSheet.newInstance(
messageIds = messageIds,
currentFolderLocation = currentMailboxLocation.messageLocationTypeValue,
currentLocationId = mailboxLabelId ?: currentMailboxLocation.messageLocationTypeValue.toString(),
labelType = LabelType.FOLDER,
actionSheetTarget = ActionSheetTarget.MAILBOX_ITEMS_IN_MAILBOX_SCREEN
)
.show(supportFragmentManager, LabelsActionSheet::class.qualifiedName)
}
private fun showLabelsManager(messageIds: List<String>) {
LabelsActionSheet.newInstance(
messageIds = messageIds,
currentFolderLocation = currentMailboxLocation.messageLocationTypeValue,
currentLocationId = mailboxLabelId ?: currentMailboxLocation.messageLocationTypeValue.toString(),
actionSheetTarget = ActionSheetTarget.MAILBOX_ITEMS_IN_MAILBOX_SCREEN
)
.show(supportFragmentManager, LabelsActionSheet::class.qualifiedName)
}
private fun moveMessagesToTrashFolder(messageIds: List<String>) {
undoSnack = showUndoSnackbar(
this@MailboxActivity,
findViewById(R.id.drawer_layout),
resources.getQuantityString(R.plurals.action_move_to_trash, messageIds.size),
{ },
false
).apply { anchorView = mailboxActionsView }
undoSnack!!.show()
mailboxViewModel.moveToFolder(
messageIds,
UserId(userManager.requireCurrentUserId().id),
currentMailboxLocation,
MessageLocationType.TRASH.messageLocationTypeValue.toString()
)
if (currentMailboxLocation != MessageLocationType.ALL_MAIL) actionMode?.finish()
}
/* SwipeRefreshLayout.OnRefreshListener */
override fun onRefresh() {
mailboxViewModel.checkConnectivity()
syncUUID = UUID.randomUUID().toString()
mailboxViewModel.refreshMessages()
}
private fun switchToMailboxLocation(newLocation: Int) {
val newMessageLocationType = fromInt(newLocation)
setElevationOnToolbarAndStatusView(false)
LoaderManager.getInstance(this).destroyLoader(LOADER_ID_LABELS_OFFLINE)
if (actionMode != null) {
actionMode!!.finish()
}
mailboxLabelId = null
invalidateOptionsMenu()
syncUUID = UUID.randomUUID().toString()
setMailboxLocation(newMessageLocationType)
setTitle()
closeDrawer(animate = false)
mailboxRecyclerView.clearFocus()
mailboxRecyclerView.scrollToPosition(0)
setUpMailboxActionsView()
include_mailbox_no_messages.apply {
isVisible = false
bind(EmptyMailboxUiModel.fromLocation(newMessageLocationType))
}
}
private fun switchToMailboxCustomLocation(
newLocation: Int,
labelId: String,
labelName: String?,
isFolder: Boolean
) {
val newMessageLocationType = fromInt(newLocation)
SetUpNewMessageLocationTask(
WeakReference(this),
messageDetailsRepositoryFactory,
labelId,
isFolder,
newLocation,
labelName,
userManager.requireCurrentUserId()
).execute()
include_mailbox_no_messages.apply {
isVisible = false
bind(EmptyMailboxUiModel.fromLocation(newMessageLocationType))
}
}
private var undoSnack: Snackbar? = null
private fun buildSwipeProcessor() {
mSwipeProcessor.apply {
addHandler(SwipeAction.TRASH, TrashSwipeHandler())
addHandler(SwipeAction.SPAM, SpamSwipeHandler())
addHandler(SwipeAction.UPDATE_STAR, StarSwipeHandler())
addHandler(SwipeAction.ARCHIVE, ArchiveSwipeHandler())
addHandler(SwipeAction.MARK_READ, MarkReadSwipeHandler())
}
}
private fun checkPlayServices(): Boolean {
val googleAPI = GoogleApiAvailability.getInstance()
val result = googleAPI.isGooglePlayServicesAvailable(this)
if (result != ConnectionResult.SUCCESS) {
if (googleAPI.isUserResolvableError(result)) {
val setOfConnectionResults =
setOf(
ConnectionResult.SERVICE_MISSING,
ConnectionResult.SERVICE_INVALID,
ConnectionResult.SERVICE_DISABLED,
ConnectionResult.SERVICE_VERSION_UPDATE_REQUIRED
)
if (setOfConnectionResults.any { it == result }) {
val dontShowPlayServices = defaultSharedPreferences[PREF_DONT_SHOW_PLAY_SERVICES] ?: false
if (!dontShowPlayServices) {
showTwoButtonInfoDialog(
titleStringId = R.string.push_notifications_alert_title,
messageStringId = R.string.push_notifications_alert_subtitle,
negativeStringId = R.string.dont_remind_again,
onNegativeButtonClicked = { defaultSharedPreferences[PREF_DONT_SHOW_PLAY_SERVICES] = true }
)
}
} else {
googleAPI.getErrorDialog(
this,
result,
PLAY_SERVICES_RESOLUTION_REQUEST
) {
showToast("cancel", Toast.LENGTH_SHORT)
}.show()
}
} else {
Timber.d("%s: This device is not GCM supported.", TAG_MAILBOX_ACTIVITY)
}
return false
}
return true
}
private fun setMailboxLocation(locationToSet: MessageLocationType) {
mailboxViewModel.disableUnreadFilter()
mailboxViewModel.setNewMailboxLocation(locationToSet)
}
private fun setNewLabel(labelId: String) {
mailboxViewModel.disableUnreadFilter()
mailboxViewModel.setNewMailboxLabel(labelId)
}
private val fcmBroadcastReceiver: BroadcastReceiver = FcmBroadcastReceiver()
private class OnMessageClickTask internal constructor(
private val mailboxActivity: WeakReference<MailboxActivity>,
messageDetailsRepositoryFactory: MessageDetailsRepository.AssistedFactory,
private val messageId: String,
private val messageSubject: String,
private val currentMailboxLocationType: MessageLocationType,
userId: UserId
) : AsyncTask<Unit, Unit, Message>() {
private val messageDetailsRepository = messageDetailsRepositoryFactory.create(userId)
override fun doInBackground(vararg params: Unit): Message? =
messageDetailsRepository.findMessageByIdBlocking(messageId)
public override fun onPostExecute(savedMessage: Message?) {
val mailboxActivity = mailboxActivity.get()
val messageLocation = savedMessage?.locationFromLabel()
if (messageLocation == MessageLocationType.DRAFT || messageLocation == MessageLocationType.ALL_DRAFT) {
TryToOpenMessageTask(
this.mailboxActivity,
mailboxActivity?.pendingActionDao,
savedMessage.messageId,
savedMessage.isInline,
savedMessage.addressID
).execute()
} else {
mailboxActivity?.startMessageDetailsLauncher?.launch(
MessageDetailsActivity.Input(
messageId = messageId,
locationType = currentMailboxLocationType,
labelId = mailboxActivity.mailboxLabelId?.let(::LabelId),
messageSubject = messageSubject
)
)
}
}
}
private class TryToOpenMessageTask internal constructor(
private val mailboxActivity: WeakReference<MailboxActivity>,
private val pendingActionDao: PendingActionDao?,
private val messageId: String?,
private val isInline: Boolean,
private val addressId: String?
) : AsyncTask<Unit, Unit, Boolean>() {
override fun doInBackground(vararg params: Unit): Boolean {
// return true if message is not in sending process and can be opened
val pendingForSending = pendingActionDao?.findPendingSendByMessageIdBlocking(messageId!!)
return pendingForSending == null ||
pendingForSending.sent != null &&
!pendingForSending.sent!!
}
override fun onPostExecute(openMessage: Boolean) {
val mailboxActivity = mailboxActivity.get()
if (!openMessage) {
mailboxActivity?.showToast(R.string.cannot_open_message_while_being_sent, Toast.LENGTH_SHORT)
return
}
mailboxActivity?.startComposeLauncher?.launch(
StartCompose.Input(
messageId = messageId,
isInline = isInline,
addressId = addressId
)
)
}
}
private class SetUpNewMessageLocationTask internal constructor(
private val mailboxActivity: WeakReference<MailboxActivity>,
private val messageDetailsRepositoryFactory: MessageDetailsRepository.AssistedFactory,
private val labelId: String,
private val isFolder: Boolean,
private val newLocation: Int,
private val labelName: String?,
private val userId: UserId
) : AsyncTask<Unit, Unit, Label?>() {
override fun doInBackground(vararg params: Unit): Label? {
return runBlocking {
val messageDetailsRepository = messageDetailsRepositoryFactory.create(userId)
val labels = messageDetailsRepository.findLabelsWithIds(listOf(labelId))
if (labels.isEmpty()) null else labels[0]
}
}
override fun onPostExecute(label: Label?) {
val mailboxActivity = mailboxActivity.get() ?: return
mailboxActivity.setElevationOnToolbarAndStatusView(false)
if (mailboxActivity.actionMode != null) {
mailboxActivity.actionMode!!.finish()
}
mailboxActivity.invalidateOptionsMenu()
val locationToSet: MessageLocationType = if (isFolder) {
MessageLocationType.LABEL_FOLDER
} else {
fromInt(newLocation)
}
mailboxActivity.mailboxLabelId = labelId
mailboxActivity.mailboxLabelName = labelName
mailboxActivity.setMailboxLocation(locationToSet)
if (label != null) {
val actionBar = mailboxActivity.supportActionBar
if (actionBar != null) {
actionBar.title = label.name
}
}
mailboxActivity.closeDrawer()
mailboxActivity.mailboxRecyclerView.scrollToPosition(0)
}
}
private inner class FcmBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.extras != null
) {
syncUUID = UUID.randomUUID().toString()
checkUserAndFetchNews()
mailboxAdapter.notifyDataSetChanged()
}
}
}
private inner class SwipeController : ItemTouchHelper.Callback() {
private var mailSettings: MailSettings? = null
@Deprecated("Subscribe for changes instead of reloading on current User/MailSettings changed.")
fun setCurrentMailSetting(mailSettings: MailSettings) {
this.mailSettings = mailSettings
}
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
if (viewHolder is MessageViewHolder) {
val mailboxLocation = currentMailboxLocation
return if (mailboxLocation == MessageLocationType.DRAFT ||
mailboxLocation == MessageLocationType.ALL_DRAFT
) {
makeMovementFlags(0, 0)
} else {
makeMovementFlags(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT)
}
}
return makeMovementFlags(0, 0)
}
override fun onMove(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
target: RecyclerView.ViewHolder
): Boolean {
throw UnsupportedOperationException("Not implemented")
}
fun normalise(swipeAction: SwipeAction, mailboxLocation: MessageLocationType?): SwipeAction {
return if (mailboxLocation == MessageLocationType.DRAFT ||
mailboxLocation == MessageLocationType.ALL_DRAFT && swipeAction != SwipeAction.UPDATE_STAR
) {
SwipeAction.TRASH
} else swipeAction
}
override fun isItemViewSwipeEnabled(): Boolean {
return if (actionMode != null) {
false
} else {
super.isItemViewSwipeEnabled()
}
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
val mailboxItem = mailboxAdapter.getMailboxItem(position)
val messageSwiped = SimpleMessage(mailboxItem)
val mailboxLocation = currentMailboxLocation
val settings = mailSettings ?: return
val swipeActionOrdinal: Int = when (direction) {
ItemTouchHelper.RIGHT -> settings.swipeRight?.value ?: SwipeAction.TRASH.ordinal
ItemTouchHelper.LEFT -> settings.swipeLeft?.value ?: SwipeAction.ARCHIVE.ordinal
else -> throw IllegalArgumentException("Unrecognised direction: $direction")
}
val swipeAction = normalise(SwipeAction.values()[swipeActionOrdinal], currentMailboxLocation)
val currentLocationId = mailboxLabelId ?: mailboxLocation.messageLocationTypeValue.toString()
if (isConversationModeEnabled(mailboxLocation)) {
mailboxViewModel.handleConversationSwipe(
swipeAction,
mailboxItem,
mailboxLocation,
currentLocationId
)
} else {
if (messageSwiped.isScheduled && swipeAction == SwipeAction.TRASH) {
showTwoButtonInfoDialog(
titleStringId = R.string.scheduled_message_moved_to_trash_title,
messageStringId = R.string.scheduled_message_moved_to_trash_desc,
negativeStringId = R.string.cancel,
onPositiveButtonClicked = {
mSwipeProcessor.handleSwipe(swipeAction, messageSwiped, mJobManager, currentLocationId)
}
)
} else
mSwipeProcessor.handleSwipe(swipeAction, messageSwiped, mJobManager, currentLocationId)
}
if (undoSnack != null && undoSnack!!.isShownOrQueued) {
undoSnack!!.dismiss()
}
undoSnack = showUndoSnackbar(
this@MailboxActivity,
findViewById(R.id.drawer_layout),
getString(swipeAction.actionDescription),
{
mSwipeProcessor.handleUndo(
swipeAction, messageSwiped, mJobManager, mailboxLocation, currentLocationId
)
mailboxAdapter.notifyDataSetChanged()
},
false
).apply { anchorView = mailboxActionsView }
val isDraftLocation =
currentMailboxLocation in listOf(MessageLocationType.DRAFT, MessageLocationType.ALL_DRAFT)
if (!(swipeAction == SwipeAction.TRASH && (isDraftLocation || messageSwiped.isScheduled))) {
undoSnack!!.show()
}
if (swipeCustomizeSnack != null && !customizeSwipeSnackShown) {
handler.postDelayed(2750) {
swipeCustomizeSnack!!.show()
customizeSwipeSnackShown = true
}
}
mailboxAdapter.notifyDataSetChanged()
}
override fun onChildDraw(
canvas: Canvas,
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder,
deltaX: Float,
deltaY: Float,
actionState: Int,
isCurrentlyActive: Boolean
) {
if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
val itemView = viewHolder.itemView
val height = itemView.bottom - itemView.top
val width = itemView.right - itemView.left
val layoutId: Int = when {
currentMailboxLocation in listOf(MessageLocationType.DRAFT, MessageLocationType.ALL_DRAFT) -> {
SwipeAction.TRASH.getActionBackgroundResource(deltaX < 0)
}
deltaX < 0 -> {
mailSettings?.swipeLeft?.let {
SwipeAction.values()[it.value].getActionBackgroundResource(false)
} ?: Resources.ID_NULL
}
else -> {
mailSettings?.swipeRight?.let {
SwipeAction.values()[it.value].getActionBackgroundResource(true)
} ?: Resources.ID_NULL
}
}
val view = layoutInflater.inflate(layoutId, null)
val widthSpec = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY)
val heightSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY)
view.measure(widthSpec, heightSpec)
view.layout(0, 0, width, height)
canvas.save()
canvas.translate(itemView.left.toFloat(), itemView.top.toFloat())
view.draw(canvas)
canvas.restore()
}
super.onChildDraw(canvas, recyclerView, viewHolder, deltaX, deltaY, actionState, isCurrentlyActive)
}
override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) {
super.onSelectedChanged(viewHolder, actionState)
val isSwiping = actionState == ItemTouchHelper.ACTION_STATE_SWIPE
mailboxSwipeRefreshLayout.isEnabled = isSwiping.not()
}
}
}