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

1823 lines
76 KiB
Kotlin

/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.mailbox.presentation
import android.app.Activity
import android.app.AlertDialog
import android.content.BroadcastReceiver
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.Color
import android.net.Uri
import android.os.AsyncTask
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.view.WindowManager
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.viewModels
import androidx.core.os.postDelayed
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.switchMap
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 androidx.work.WorkInfo
import ch.protonmail.android.R
import ch.protonmail.android.activities.EXTRA_FIRST_LOGIN
import ch.protonmail.android.activities.EXTRA_SETTINGS_ITEM_TYPE
import ch.protonmail.android.activities.EditSettingsItemActivity
import ch.protonmail.android.activities.EngagementActivity
import ch.protonmail.android.activities.NavigationActivity
import ch.protonmail.android.activities.SearchActivity
import ch.protonmail.android.activities.SettingsActivity
import ch.protonmail.android.activities.SettingsItem
import ch.protonmail.android.activities.composeMessage.ComposeMessageActivity
import ch.protonmail.android.activities.dialogs.ManageLabelsDialogFragment.ILabelCreationListener
import ch.protonmail.android.activities.dialogs.ManageLabelsDialogFragment.ILabelsChangeListener
import ch.protonmail.android.activities.dialogs.MoveToFolderDialogFragment
import ch.protonmail.android.activities.dialogs.MoveToFolderDialogFragment.IMoveMessagesListener
import ch.protonmail.android.activities.labelsManager.EXTRA_CREATE_ONLY
import ch.protonmail.android.activities.labelsManager.EXTRA_MANAGE_FOLDERS
import ch.protonmail.android.activities.labelsManager.EXTRA_POPUP_STYLE
import ch.protonmail.android.activities.labelsManager.LabelsManagerActivity
import ch.protonmail.android.activities.mailbox.RefreshEmptyViewTask
import ch.protonmail.android.activities.mailbox.RefreshTotalCountersTask
import ch.protonmail.android.activities.mailbox.ShowLabelsManagerDialogTask
import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository
import ch.protonmail.android.activities.settings.EXTRA_CURRENT_MAILBOX_LABEL_ID
import ch.protonmail.android.activities.settings.EXTRA_CURRENT_MAILBOX_LOCATION
import ch.protonmail.android.activities.settings.SettingsEnum
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.MailSettings
import ch.protonmail.android.api.models.MessageCount
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_SWIPE_GESTURES_DIALOG_SHOWN
import ch.protonmail.android.core.Constants.Prefs.PREF_USED_SPACE
import ch.protonmail.android.core.Constants.SWIPE_GESTURES_CHANGED_VERSION
import ch.protonmail.android.core.ProtonMailApplication
import ch.protonmail.android.data.local.CounterDao
import ch.protonmail.android.data.local.CounterDatabase
import ch.protonmail.android.data.local.PendingActionDao
import ch.protonmail.android.data.local.PendingActionDatabase
import ch.protonmail.android.data.local.model.Label
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.data.local.model.TotalLabelCounter
import ch.protonmail.android.data.local.model.TotalLocationCounter
import ch.protonmail.android.details.presentation.MessageDetailsActivity
import ch.protonmail.android.events.FetchLabelsEvent
import ch.protonmail.android.events.FetchUpdatesEvent
import ch.protonmail.android.events.MailboxLoadedEvent
import ch.protonmail.android.events.MailboxNoMessagesEvent
import ch.protonmail.android.events.MessageCountsEvent
import ch.protonmail.android.events.SettingsChangedEvent
import ch.protonmail.android.events.Status
import ch.protonmail.android.fcm.MultiUserFcmTokenManager
import ch.protonmail.android.fcm.RegisterDeviceWorker
import ch.protonmail.android.fcm.model.FirebaseToken
import ch.protonmail.android.feature.account.AccountStateManager
import ch.protonmail.android.jobs.EmptyFolderJob
import ch.protonmail.android.jobs.FetchLabelsJob
import ch.protonmail.android.jobs.PostArchiveJob
import ch.protonmail.android.jobs.PostInboxJob
import ch.protonmail.android.jobs.PostReadJob
import ch.protonmail.android.jobs.PostSpamJob
import ch.protonmail.android.jobs.PostStarJob
import ch.protonmail.android.jobs.PostTrashJobV2
import ch.protonmail.android.jobs.PostUnreadJob
import ch.protonmail.android.jobs.PostUnstarJob
import ch.protonmail.android.labels.presentation.ui.ManageLabelsActionSheet
import ch.protonmail.android.mailbox.presentation.MailboxViewModel.MaxLabelsReached
import ch.protonmail.android.mailbox.presentation.model.MailboxUiItem
import ch.protonmail.android.prefs.SecureSharedPreferences
import ch.protonmail.android.servers.notification.EXTRA_MAILBOX_LOCATION
import ch.protonmail.android.settings.pin.EXTRA_TOTAL_COUNT_EVENT
import ch.protonmail.android.ui.dialog.MessageActionSheet
import ch.protonmail.android.utils.AppUtil
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.showToast
import ch.protonmail.android.utils.ui.dialogs.DialogUtils.Companion.showDeleteConfirmationDialog
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 ch.protonmail.android.worker.KEY_POST_LABEL_WORKER_RESULT_ERROR
import ch.protonmail.android.worker.PostLabelWorker
import ch.protonmail.libs.core.utils.contains
import com.birbit.android.jobqueue.Job
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.activity_mailbox.screenShotPreventerView
import kotlinx.android.synthetic.main.activity_message_details.*
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
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.android.workmanager.activity.getWorkManager
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.seconds
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 = 0
const val LOADER_ID_LABELS_OFFLINE = 32
private const val REQUEST_CODE_TRASH_MESSAGE_DETAILS = 1
private const val REQUEST_CODE_COMPOSE_MESSAGE = 19
@AndroidEntryPoint
class MailboxActivity :
NavigationActivity(),
ActionMode.Callback,
OnRefreshListener,
ILabelCreationListener,
ILabelsChangeListener,
IMoveMessagesListener,
DialogInterface.OnDismissListener {
private lateinit var counterDao: CounterDao
private lateinit var pendingActionDao: PendingActionDao
@Inject
lateinit var messageDetailsRepositoryFactory: MessageDetailsRepository.AssistedFactory
lateinit var messageDetailsRepository: MessageDetailsRepository
@Inject
lateinit var networkSnackBarUtil: NetworkSnackBarUtil
@Inject
lateinit var registerDeviceWorkerEnqueuer: RegisterDeviceWorker.Enqueuer
@Inject
lateinit var multiUserFcmTokenManager: MultiUserFcmTokenManager
private lateinit var mailboxAdapter: MailboxRecyclerViewAdapter
private var swipeController: SwipeController = SwipeController()
private val mailboxLocationMain = MutableLiveData<MessageLocationType>()
private val isLoadingMore = AtomicBoolean(false)
private var scrollStateChanged = false
private var actionMode: ActionMode? = null
private var swipeCustomizeSnack: Snackbar? = null
private var mailboxLabelId: String? = null
private var mailboxLabelName: String? = null
private var refreshMailboxJobRunning = false
private lateinit var syncUUID: String
private var customizeSwipeSnackShown = false
private var catchLabelEvents = false
private val mailboxViewModel: MailboxViewModel by viewModels()
private var storageLimitApproachingAlertDialog: AlertDialog? = null
private val handler = Handler(Looper.getMainLooper())
override val currentLabelId get() = mailboxLabelId
val currentLocation get() = mailboxLocationMain
override fun getLayoutId(): Int = R.layout.activity_mailbox
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val userId = userManager.currentUserId ?: return
mailboxViewModel.userId = userId
messageDetailsRepository = messageDetailsRepositoryFactory.create(userId)
counterDao = CounterDatabase.getInstance(this, userId).getDao()
pendingActionDao = PendingActionDatabase.getInstance(this, userId).getDao()
// force reload of MessageDetailsRepository's internal dependencies in case we just switched user
// TODO if we decide to use special flag for switching (and not login), change this
if (intent.getBooleanExtra(EXTRA_FIRST_LOGIN, false)) {
messageDetailsRepository.reloadDependenciesForUser(userId)
multiUserFcmTokenManager.setTokenUnsentForAllSavedUsersBlocking() // force FCM to re-register
}
val extras = intent.extras
if (!userManager.isEngagementShown) {
startActivity(AppUtil.decorInAppIntent(Intent(this, EngagementActivity::class.java)))
}
mailboxLocationMain.value = MessageLocationType.INBOX
// 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)
mailboxLocationMain.value = fromInt(locationInt)
}
if (extras != null && extras.containsKey(EXTRA_MAILBOX_LOCATION)) {
switchToMailboxLocation(extras.getInt(EXTRA_MAILBOX_LOCATION))
}
startObserving()
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)
startObservingUsedSpace()
var actionModeAux: ActionMode? = null
mailboxAdapter = MailboxRecyclerViewAdapter(this) { selectionModeEvent ->
when (selectionModeEvent) {
SelectionModeEnum.STARTED -> {
actionModeAux = startActionMode(this@MailboxActivity)
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)
messageDetailsRepository.getAllLabelsLiveData().observe(this, mailboxAdapter::setLabels)
mailboxViewModel.hasSuccessfullyDeletedMessages.observe(this) { isSuccess ->
Timber.v("Delete message status is success $isSuccess")
if (!isSuccess) {
showToast(R.string.message_deleted_error)
}
}
checkUserAndFetchNews()
setUpDrawer()
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)
initializeSwipeRefreshLayout(noMessagesSwipeRefreshLayout)
if (userManager.isFirstMailboxLoad) {
swipeCustomizeSnack = Snackbar.make(
findViewById(R.id.drawer_layout),
getString(R.string.customize_swipe_actions),
Snackbar.LENGTH_INDEFINITE
).apply {
view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
?.setTextColor(getColor(R.color.text_inverted))
setAction(getString(R.string.settings)) {
val settingsIntent = AppUtil.decorInAppIntent(
Intent(
this@MailboxActivity,
SettingsActivity::class.java
)
)
settingsIntent.putExtra(
EXTRA_CURRENT_MAILBOX_LOCATION,
if (mailboxLocationMain.value != null) mailboxLocationMain.value!!.messageLocationTypeValue
else MessageLocationType.INBOX.messageLocationTypeValue
)
settingsIntent.putExtra(EXTRA_CURRENT_MAILBOX_LABEL_ID, mailboxLabelId)
startActivity(settingsIntent)
}
setActionTextColor(getColor(R.color.text_inverted))
setBackgroundTint(getColor(R.color.interaction_strong))
}
userManager.firstMailboxLoadDone()
}
mailboxAdapter.setItemClick { mailboxUiItem: MailboxUiItem ->
OnMessageClickTask(
WeakReference(this@MailboxActivity),
messageDetailsRepository,
mailboxUiItem.itemId
).execute()
}
mailboxAdapter.setOnItemSelectionChangedListener {
val checkedItems = mailboxAdapter.checkedMailboxItems.size
actionMode?.title = "$checkedItems ${getString(R.string.selected)}"
mailboxActionsView.setAction(
BottomActionsView.ActionPosition.ACTION_SECOND,
currentLocation.value != MessageLocationType.DRAFT,
if (MessageUtils.areAllUnRead(
selectedMessages
)
) R.drawable.ic_envelope_open_text else R.drawable.ic_envelope_dot
)
}
checkRegistration()
closeDrawer()
mailboxRecyclerView.addOnScrollListener(listScrollListener)
fetchOrganizationData()
observeMailboxItemsByLocation(syncId = syncUUID, includeLabels = true)
mailboxLocationMain.observe(this, mailboxAdapter::setNewLocation)
ItemTouchHelper(swipeController).attachToRecyclerView(mailboxRecyclerView)
setUpMailboxActionsView()
}
override fun secureContent(): Boolean = true
override fun enableScreenshotProtector() {
screenShotPreventerView.visibility = View.VISIBLE
}
override fun disableScreenshotProtector() {
screenShotPreventerView.visibility = View.GONE
}
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 startObservingUsedSpace() {
val preferences = SecureSharedPreferences.getPrefsForUser(this, userManager.requireCurrentUserId())
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,
rightStringId = R.string.okay,
leftStringId = R.string.learn_more,
onPositiveButtonClicked = {
userManager.setShowStorageLimitReached(false)
},
onNegativeButtonClicked = {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.limit_reached_learn_more))
)
startActivity(browserIntent)
userManager.setShowStorageLimitReached(false)
}
)
}
userManager.setShowStorageLimitWarning(true)
storageLimitAlert.apply {
visibility = View.VISIBLE
setIcon(getDrawable(R.drawable.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,
leftStringId = R.string.dont_remind_again,
onPositiveButtonClicked = { storageLimitApproachingAlertDialog = null },
onNegativeButtonClicked = {
userManager.setShowStorageLimitWarning(false)
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,
leftStringId = R.string.learn_more,
onNegativeButtonClicked = {
val browserIntent = Intent(
Intent.ACTION_VIEW,
Uri.parse(getString(R.string.limit_reached_learn_more))
)
startActivity(browserIntent)
}
)
} else {
val intent = AppUtil.decorInAppIntent(
Intent(
this@MailboxActivity,
ComposeMessageActivity::class.java
)
)
startActivityForResult(intent, REQUEST_CODE_COMPOSE_MESSAGE)
}
}
private val selectedMessages: List<SimpleMessage>
get() = mailboxAdapter.checkedMailboxItems.map { SimpleMessage(it) }
private var firstLogin: Boolean? = null
private fun startObservingPendingActions() {
val owner = this
mailboxViewModel.run {
pendingSendsLiveData.removeObservers(owner)
pendingUploadsLiveData.removeObservers(owner)
reloadDependenciesForUser()
pendingSendsLiveData.observe(owner) { mailboxAdapter.setPendingForSendingList(it) }
pendingUploadsLiveData.observe(owner) { mailboxAdapter.setPendingUploadsList(it) }
}
}
override fun onAccountSwitched(switch: AccountStateManager.AccountSwitch) {
super.onAccountSwitched(switch)
val currentUserId = userManager.currentUserId ?: return
mailboxViewModel.userId = currentUserId
mJobManager.start()
counterDao = CounterDatabase.getInstance(this, currentUserId).getDao()
pendingActionDao = PendingActionDatabase.getInstance(this, currentUserId).getDao()
messageDetailsRepository.reloadDependenciesForUser(currentUserId)
swipeController.loadCurrentMailSetting()
startObservingPendingActions()
AppUtil.clearNotifications(this, currentUserId)
lazyManager.reset()
setUpDrawer()
setupAccountsList()
checkRegistration()
// Loading mailbox items for the newly switched account.
// This method also "reloads dependencies" for the instance of `messageDetailsRepo` held by
// MailboxVM. This should be done before triggering an "update" of the Mailbox for the new user
loadMailboxItems(refreshMessages = true)
switchToMailboxLocation(DrawerOptionType.INBOX.drawerOptionTypeValue)
messageDetailsRepository.getAllLabelsLiveData().observe(this, mailboxAdapter::setLabels)
// Account has been switched, so used space changed as well
mailboxViewModel.usedSpaceActionEvent(FLOW_USED_SPACE_CHANGED)
// Observe used space for current account
startObservingUsedSpace()
// manually update the flags for preventing screenshots
if (isPreventingScreenshots || userManager.currentLegacyUser?.isPreventTakingScreenshots == true) {
window.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
}
// Set the elevation to 0 since after account switch the list is scrolled to the top
setElevationOnToolbarAndStatusView(false)
}
/**
* @param refreshMessages whether the existing local messages should be deleted and re-fetched from network
*/
private fun observeMailboxItemsByLocation(
includeLabels: Boolean = true,
refreshMessages: Boolean = false,
syncId: String
) {
mailboxLocationMain.switchMap { location ->
mailboxViewModel.getMailboxItems(
location,
mailboxLabelId,
includeLabels,
syncId,
refreshMessages
)
}
.distinctUntilChanged()
.observe(this) { state ->
setLoadingMore(false)
setRefreshing(false)
if (state.error.isNotEmpty()) {
Toast.makeText(this, getString(R.string.error_loading_conversations), Toast.LENGTH_SHORT).show()
}
mailboxAdapter.clear()
mailboxAdapter.addAll(state.items)
}
}
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 adapter = recyclerView.adapter
val lastVisibleItem = layoutManager!!.findLastVisibleItemPosition()
val lastPosition = adapter!!.itemCount - 1
if (lastVisibleItem == lastPosition && dy > 0 && !setLoadingMore(true)) {
loadMailboxItems(oldestItemTimestamp = mailboxAdapter.getOldestMailboxItemTimestamp())
}
// Increase the elevation if the list is scrolled down and decrease if it is scrolled to the top
val firstVisibleItem = layoutManager.findFirstCompletelyVisibleItemPosition()
if (firstVisibleItem == 0) {
setElevationOnToolbarAndStatusView(false)
} else {
setElevationOnToolbarAndStatusView(true)
}
}
}
private fun loadMailboxItems(
includeLabels: Boolean = false,
refreshMessages: Boolean = false,
oldestItemTimestamp: Long = now()
) {
val mailboxLocation = mailboxLocationMain.value ?: MessageLocationType.INBOX
mailboxViewModel.loadMailboxItems(
mailboxLocation,
mailboxLabelId,
includeLabels,
syncUUID,
refreshMessages,
oldestItemTimestamp
)
}
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).show()
}
syncUUID = UUID.randomUUID().toString()
lifecycleScope.launch {
delay(3.seconds.toLongMilliseconds())
loadMailboxItems(includeLabels = true)
}
mailboxViewModel.checkConnectivityDelayed()
}
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.e(task.exception, "Could not retrieve FirebaseInstanceId")
}
}
}.onFailure {
showToast(R.string.invalid_firebase_api_key_message)
Timber.e(it, getString(R.string.invalid_firebase_api_key_message))
}
}
}
}
}
private fun checkUserAndFetchNews(): Boolean {
syncUUID = UUID.randomUUID().toString()
if (userManager.isBackgroundSyncEnabled) {
setRefreshing(true)
}
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()
loadMailboxItems()
true
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
checkRegistration()
checkUserAndFetchNews()
switchToMailboxLocation(DrawerOptionType.INBOX.drawerOptionTypeValue)
}
private fun shouldShowSwipeGesturesChangedDialog(): Boolean {
val prefs: SharedPreferences = app.defaultSharedPreferences
val previousVersion: Int = prefs.getInt(Constants.Prefs.PREF_PREVIOUS_APP_VERSION, Int.MIN_VALUE)
// The dialog should be shown once on the update when swiping gestures are switched
return previousVersion in 1 until SWIPE_GESTURES_CHANGED_VERSION &&
!prefs.getBoolean(PREF_SWIPE_GESTURES_DIALOG_SHOWN, false)
}
private fun showSwipeGesturesChangedDialog() {
val prefs: SharedPreferences = (applicationContext as ProtonMailApplication).defaultSharedPreferences
showTwoButtonInfoDialog(
titleStringId = R.string.swipe_gestures_changed,
messageStringId = R.string.swipe_gestures_changed_message,
leftStringId = R.string.go_to_settings,
onNegativeButtonClicked = {
val swipeGestureIntent = Intent(
this,
EditSettingsItemActivity::class.java
)
swipeGestureIntent.putExtra(EXTRA_SETTINGS_ITEM_TYPE, SettingsItem.SWIPE)
startActivityForResult(
AppUtil.decorInAppIntent(swipeGestureIntent),
SettingsEnum.SWIPING_GESTURE.ordinal
)
}
)
prefs.edit().putBoolean(PREF_SWIPE_GESTURES_DIALOG_SHOWN, true).apply()
}
override fun onResume() {
super.onResume()
if (mailboxViewModel.userId != userManager.currentUserId) {
onAccountSwitched(AccountStateManager.AccountSwitch())
}
mailboxViewModel.refreshMailboxCount(currentMailboxLocation)
registerFcmReceiver()
checkDelinquency()
noMessagesSwipeRefreshLayout.visibility = View.GONE
mailboxViewModel.checkConnectivity()
swipeController.loadCurrentMailSetting()
val mailboxLocation = mailboxLocationMain.value
if (mailboxLocation == MessageLocationType.INBOX) {
AppUtil.clearNotifications(this, userManager.requireCurrentUserId())
}
setUpDrawer()
closeDrawer(true)
if (shouldShowSwipeGesturesChangedDialog()) {
showSwipeGesturesChangedDialog()
}
}
override fun onPause() {
runCatching {
LocalBroadcastManager.getInstance(this).unregisterReceiver(fcmBroadcastReceiver)
}
networkSnackBarUtil.hideAllSnackBars()
super.onPause()
}
public override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(
STATE_MAILBOX_LOCATION,
if (mailboxLocationMain.value != null) {
mailboxLocationMain.value!!.messageLocationTypeValue
} else {
MessageLocationType.INBOX.messageLocationTypeValue
}
)
outState.putString(STATE_MAILBOX_LABEL_LOCATION, mailboxLabelId)
outState.putString(STATE_MAILBOX_LABEL_LOCATION_NAME, mailboxLabelName)
super.onSaveInstanceState(outState)
}
private fun setUpMenuItems(composeMenuItem: MenuItem, searchMenuItem: MenuItem) {
composeMenuItem.actionView.findViewById<ImageView>(R.id.composeImageButton)
.setOnClickListener {
mailboxViewModel.usedSpaceActionEvent(FLOW_TRY_COMPOSE)
}
searchMenuItem.actionView.findViewById<ImageView>(R.id.searchImageButton)
.setOnClickListener {
val intent = AppUtil.decorInAppIntent(
Intent(
this@MailboxActivity,
SearchActivity::class.java
)
)
startActivity(intent)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.menu_mailbox_options, menu)
setUpMenuItems(menu.findItem(R.id.compose), menu.findItem(R.id.search))
val mailboxLocation = mailboxLocationMain.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)
setUpMenuItems(menu.findItem(R.id.compose), menu.findItem(R.id.search))
val mailboxLocation = mailboxLocationMain.value
menu.findItem(R.id.empty).isVisible =
mailboxLocation in listOf(
MessageLocationType.DRAFT,
MessageLocationType.SPAM,
MessageLocationType.TRASH,
MessageLocationType.LABEL,
MessageLocationType.LABEL_FOLDER
)
return super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(menuItem: MenuItem): Boolean {
return when (menuItem.itemId) {
R.id.empty -> {
if (!isFinishing) {
showTwoButtonInfoDialog(
titleStringId = R.string.empty_folder,
messageStringId = R.string.are_you_sure_empty,
leftStringId = R.string.no
) {
setRefreshing(true)
mJobManager.addJobInBackground(EmptyFolderJob(mailboxLocationMain.value, this.mailboxLabelId))
setLoadingMore(false)
}
}
true
}
else -> super.onOptionsItemSelected(menuItem)
}
}
override fun onBackPressed() {
saveLastInteraction()
val drawerClosed = closeDrawer()
if (!drawerClosed && mailboxLocationMain.value != MessageLocationType.INBOX) {
switchToMailboxLocation(DrawerOptionType.INBOX.drawerOptionTypeValue)
} else if (!drawerClosed) {
moveTaskToBack(true)
}
}
private fun initializeSwipeRefreshLayout(swipeRefreshLayoutAux: SwipeRefreshLayout) {
swipeRefreshLayoutAux.setColorSchemeResources(R.color.cornflower_blue)
swipeRefreshLayoutAux.setOnRefreshListener(this)
}
fun setRefreshing(shouldRefresh: Boolean) {
Timber.v("setRefreshing shouldRefresh:$shouldRefresh")
mailboxSwipeRefreshLayout.isRefreshing = shouldRefresh
noMessagesSwipeRefreshLayout.isRefreshing = shouldRefresh
}
private fun setLoadingMore(loadingMore: Boolean): Boolean {
val previousValue = isLoadingMore.getAndSet(loadingMore)
mailboxRecyclerView.post { mailboxAdapter.includeFooter = isLoadingMore.get() }
return previousValue
}
override fun onInbox(type: DrawerOptionType) {
AppUtil.clearNotifications(applicationContext, 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() = if (mailboxLocationMain.value != null) {
mailboxLocationMain.value!!
} else {
MessageLocationType.INBOX
}
private fun setTitle() {
val titleRes: Int = when (mailboxLocationMain.value) {
MessageLocationType.INBOX -> R.string.inbox_option
MessageLocationType.STARRED -> R.string.starred_option
MessageLocationType.DRAFT -> R.string.drafts_option
MessageLocationType.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
else -> R.string.app_name
}
supportActionBar?.setTitle(titleRes)
}
private fun showNoConnSnackAndScheduleRetry(connectivity: Constants.ConnectionState) {
Timber.v("show NoConnection Snackbar ${mConnectivitySnackLayout != null}")
mConnectivitySnackLayout?.let { snackBarLayout ->
lifecycleScope.launchWhenCreated {
networkSnackBarUtil.getNoConnectionSnackBar(
parentView = snackBarLayout,
user = userManager.requireCurrentLegacyUser(),
netConfiguratorCallback = this@MailboxActivity,
onRetryClick = ::onConnectivityCheckRetry,
isOffline = connectivity == Constants.ConnectionState.NO_INTERNET
).show()
}
}
}
private fun hideNoConnSnack() {
Timber.v("hideNoConnSnack")
networkSnackBarUtil.hideCheckingConnectionSnackBar()
networkSnackBarUtil.hideNoConnectionSnackBar()
}
@Subscribe
fun onSettingsChangedEvent(event: SettingsChangedEvent) {
val user = userManager.requireCurrentUser()
if (event.success) {
refreshDrawerHeader(user)
} else {
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)
if (!isDohOngoing) {
showToast(event.status)
}
val mailboxLocation = mailboxLocationMain.value
val setOfLabels =
setOf(
MessageLocationType.LABEL,
MessageLocationType.LABEL_FOLDER,
MessageLocationType.LABEL_OFFLINE
)
if (event.status == Status.NO_NETWORK && setOfLabels.any { it == mailboxLocation }) {
mailboxLocationMain.value = MessageLocationType.LABEL_OFFLINE
}
mNetworkResults.setMailboxLoaded(MailboxLoadedEvent(Status.SUCCESS, null))
setRefreshing(false)
}
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) {
setRefreshing(false)
showNoConnSnackAndScheduleRetry(connectivity)
} else {
hideNoConnSnack()
}
} 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)
}
@Subscribe
fun onUpdatesLoaded(event: FetchUpdatesEvent?) {
lifecycleScope.launchWhenCreated {
userManager.currentUser?.let { refreshDrawerHeader(it) }
}
}
private fun showToast(status: Status) {
when (status) {
Status.UNAUTHORIZED -> {
showNoConnSnackAndScheduleRetry(Constants.ConnectionState.CANT_REACH_SERVER)
}
Status.NO_NETWORK -> {
showNoConnSnackAndScheduleRetry(Constants.ConnectionState.NO_INTERNET)
}
Status.SUCCESS -> {
hideNoConnSnack()
}
else -> {
return
}
}
}
@Subscribe
fun onLabelsLoadedEvent(event: FetchLabelsEvent) {
if (event.status == Status.SUCCESS) {
mailboxAdapter.notifyDataSetChanged()
}
}
@Subscribe
fun onMessageCountsEvent(event: MessageCountsEvent) {
//region old total count
if (event.status != Status.SUCCESS) {
return
}
val response = event.unreadMessagesResponse ?: return
val messageCountsList = response.counts ?: emptyList()
counterDao = CounterDatabase
.getInstance(applicationContext, userManager.requireCurrentUserId()).getDao()
OnMessageCountsListTask(WeakReference(this), counterDao, messageCountsList).execute()
//endregion
}
fun refreshEmptyView(count: Int) {
if (count == 0) {
mailboxSwipeRefreshLayout.visibility = View.GONE
noMessagesSwipeRefreshLayout.visibility = View.VISIBLE
} else {
mailboxSwipeRefreshLayout.visibility = View.VISIBLE
noMessagesSwipeRefreshLayout.visibility = View.GONE
}
}
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
actionMode = mode
mailboxSwipeRefreshLayout.isEnabled = false
return true
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
return true
}
private fun containsStar(messages: List<SimpleMessage>): Boolean = messages.any { it.isStarred }
private fun containsUnstar(messages: List<SimpleMessage>): Boolean = messages.any { !it.isStarred }
override fun move(folderId: String) {
MessageUtils.moveMessage(this, mJobManager, folderId, mutableListOf(mailboxLabelId), selectedMessages)
if (actionModeRunnable != null) {
actionModeRunnable!!.run()
}
}
override fun showFoldersManager() {
val foldersManagerIntent = Intent(this, LabelsManagerActivity::class.java)
foldersManagerIntent.putExtra(EXTRA_MANAGE_FOLDERS, true)
foldersManagerIntent.putExtra(EXTRA_POPUP_STYLE, true)
foldersManagerIntent.putExtra(EXTRA_CREATE_ONLY, true)
startActivity(AppUtil.decorInAppIntent(foldersManagerIntent))
}
override fun onDismiss(dialog: DialogInterface) {
catchLabelEvents = true
}
internal inner class ActionModeInteractionRunnable(private val actionModeAux: ActionMode?) : Runnable {
override fun run() {
actionModeAux?.finish()
}
}
private var actionModeRunnable: ActionModeInteractionRunnable? = null
override fun onActionItemClicked(mode: ActionMode, menuItem: MenuItem): Boolean {
// TODO: These actions need to be extracted to the view model and then removed from here
val messageIds = selectedMessages.map { message -> message.messageId }
val menuItemId = menuItem.itemId
var job: Job? = null
when (menuItemId) {
R.id.move_to_trash -> {
job = PostTrashJobV2(messageIds, mailboxLabelId)
undoSnack = showUndoSnackbar(
this@MailboxActivity,
findViewById(R.id.drawer_layout),
resources.getQuantityString(R.plurals.action_move_to_trash, messageIds.size),
{ },
false
)
undoSnack!!.show()
}
R.id.delete_message ->
showDeleteConfirmationDialog(
this,
getString(R.string.delete_messages),
getString(R.string.confirm_destructive_action)
) {
mailboxViewModel.deleteMessages(
messageIds,
currentMailboxLocation.messageLocationTypeValue.toString()
)
mode.finish()
}
R.id.mark_read -> job = PostReadJob(messageIds)
R.id.mark_unread -> job = PostUnreadJob(messageIds)
R.id.add_star -> job = PostStarJob(messageIds)
R.id.add_label -> {
actionModeRunnable = ActionModeInteractionRunnable(mode)
ShowLabelsManagerDialogTask(supportFragmentManager, messageDetailsRepository, messageIds).execute()
}
R.id.add_folder -> {
actionModeRunnable = ActionModeInteractionRunnable(mode)
showFoldersManagerDialog(messageIds)
}
R.id.remove_star -> job = PostUnstarJob(messageIds)
R.id.move_to_archive -> {
job = PostArchiveJob(messageIds)
undoSnack = showUndoSnackbar(
this@MailboxActivity,
findViewById(R.id.drawer_layout),
resources.getQuantityString(R.plurals.action_move_to_archive, messageIds.size),
{ },
false
)
undoSnack!!.show()
}
R.id.move_to_inbox -> job = PostInboxJob(messageIds, listOf(mailboxLabelId))
R.id.move_to_spam -> {
job = PostSpamJob(messageIds)
undoSnack = showUndoSnackbar(
this@MailboxActivity, findViewById(R.id.drawer_layout),
resources.getQuantityString(R.plurals.action_move_to_spam, messageIds.size),
{ },
false
)
undoSnack!!.show()
}
}
if (job != null) {
// show progress bar for visual representation of work in background,
// if all the messages inside the folder are impacted by the action
if (mailboxAdapter.itemCount == messageIds.size) {
setRefreshing(true)
}
mJobManager.addJobInBackground(job)
}
if (menuItemId !in listOf(R.id.add_label, R.id.add_folder, R.id.delete_message)) {
mode.finish()
}
return true
}
private fun setUpMailboxActionsView() {
val actionsUiModel = BottomActionsView.UiModel(
if (currentLocation.value in arrayOf(
MessageLocationType.TRASH,
MessageLocationType.DRAFT
)
) R.drawable.ic_trash_empty else R.drawable.ic_trash,
R.drawable.ic_envelope_dot,
R.drawable.ic_folder_move,
R.drawable.ic_label
)
mailboxActionsView.bind(actionsUiModel)
mailboxActionsView.setOnFirstActionClickListener {
val messageIds = selectedMessages.map { message -> message.messageId }
if (currentLocation.value in arrayOf(MessageLocationType.TRASH, MessageLocationType.DRAFT)) {
showDeleteConfirmationDialog(
this,
getString(R.string.delete_messages),
getString(R.string.confirm_destructive_action)
) {
mailboxViewModel.deleteMessages(
messageIds,
currentLocation.value?.messageLocationTypeValue.toString()
)
}
} else {
undoSnack = showUndoSnackbar(
this@MailboxActivity,
findViewById(R.id.drawer_layout),
resources.getQuantityString(R.plurals.action_move_to_trash, messageIds.size),
{ },
false
)
undoSnack!!.show()
// show progress bar for visual representation of work in background,
// if all the messages inside the folder are impacted by the action
if (mailboxAdapter.itemCount == messageIds.size) {
setRefreshing(true)
}
mJobManager.addJobInBackground(PostTrashJobV2(messageIds, mailboxLabelId))
}
actionMode?.finish()
}
mailboxActionsView.setOnSecondActionClickListener {
val messageIds = selectedMessages.map { message -> message.messageId }
if (MessageUtils.areAllUnRead(selectedMessages)) {
mJobManager.addJobInBackground(PostReadJob(messageIds))
} else {
mJobManager.addJobInBackground(PostUnreadJob(messageIds))
}
actionMode?.finish()
}
mailboxActionsView.setOnThirdActionClickListener {
val messageIds = selectedMessages.map { message -> message.messageId }
actionModeRunnable = ActionModeInteractionRunnable(actionMode)
showFoldersManagerDialog(messageIds)
}
mailboxActionsView.setOnFourthActionClickListener {
val messageIds = selectedMessages.map { message -> message.messageId }
actionModeRunnable = ActionModeInteractionRunnable(actionMode)
ShowLabelsManagerDialogTask(supportFragmentManager, messageDetailsRepository, messageIds).execute()
ManageLabelsActionSheet.newInstance(
messageIds,
currentMailboxLocation.messageLocationTypeValue,
)
.show(supportFragmentManager, ManageLabelsActionSheet::class.qualifiedName)
}
mailboxActionsView.setOnMoreActionClickListener {
val messagesIds = selectedMessages.map { message -> message.messageId }
MessageActionSheet.newInstance(
messagesIds,
currentMailboxLocation.messageLocationTypeValue,
resources.getQuantityString(
R.plurals.messages_count,
messagesIds.size,
messagesIds.size
),
originatorLocationId = MessageActionSheet.ARG_ORIGINATOR_SCREEN_MESSAGES_LIST_ID
)
.show(supportFragmentManager, MessageActionSheet::class.qualifiedName)
}
}
override fun onDestroyActionMode(mode: ActionMode) {
actionMode = null
mailboxActionsView.visibility = View.GONE
mailboxSwipeRefreshLayout.isEnabled = true
mailboxAdapter.endSelectionMode()
}
private fun showFoldersManagerDialog(messageIds: List<String>) {
// show progress bar for visual representation of work in background,
// if all the messages inside the folder are impacted by the action
if (mailboxAdapter.itemCount == messageIds.size) {
setRefreshing(true)
}
catchLabelEvents = false
val moveToFolderDialogFragment = MoveToFolderDialogFragment.newInstance(mailboxLocationMain.value)
val transaction = supportFragmentManager.beginTransaction()
transaction.add(moveToFolderDialogFragment, moveToFolderDialogFragment.fragmentKey)
transaction.commitAllowingStateLoss()
}
override fun onLabelCreated(labelName: String, color: String) {
val postLabelResult = PostLabelWorker.Enqueuer(getWorkManager()).enqueue(labelName, color)
postLabelResult.observe(this) {
val state: WorkInfo.State = it.state
if (state == WorkInfo.State.SUCCEEDED) {
showToast(getString(R.string.label_created), Toast.LENGTH_SHORT)
return@observe
}
if (state == WorkInfo.State.FAILED) {
val errorMessage = it.outputData.getString(KEY_POST_LABEL_WORKER_RESULT_ERROR)
?: getString(R.string.label_invalid)
showToast(errorMessage, Toast.LENGTH_SHORT)
}
}
}
override fun onLabelsDeleted(checkedLabelIds: List<String>) {
// NOOP
}
override fun onLabelsChecked(
checkedLabelIds: List<String>,
unchangedLabelss: List<String>,
messageIds: List<String>
) {
var unchangedLabels: List<String>? = unchangedLabelss
if (actionModeRunnable != null) {
actionModeRunnable!!.run()
}
if (unchangedLabels == null) {
unchangedLabels = mutableListOf()
}
mailboxViewModel.processLabels(messageIds, checkedLabelIds, unchangedLabels)
}
override fun onLabelsChecked(
checkedLabelIds: List<String>,
unchangedLabels: List<String>,
messageIds: List<String>,
messagesToArchive: List<String>
) {
mJobManager.addJobInBackground(PostArchiveJob(messagesToArchive))
onLabelsChecked(checkedLabelIds, unchangedLabels, messageIds)
}
/* SwipeRefreshLayout.OnRefreshListener */
override fun onRefresh() {
setRefreshing(true)
syncUUID = UUID.randomUUID().toString()
mailboxViewModel.refreshMailboxCount(currentMailboxLocation)
loadMailboxItems(
includeLabels = true,
refreshMessages = true
)
}
private fun now() = System.currentTimeMillis() / 1000
private fun switchToMailboxLocation(newLocation: Int) {
val newMessageLocationType = fromInt(newLocation)
mailboxSwipeRefreshLayout.visibility = View.VISIBLE
mailboxSwipeRefreshLayout.isRefreshing = true
noMessagesSwipeRefreshLayout.visibility = View.GONE
setElevationOnToolbarAndStatusView(false)
LoaderManager.getInstance(this).destroyLoader(LOADER_ID_LABELS_OFFLINE)
if (actionMode != null) {
actionMode!!.finish()
}
mailboxLabelId = null
invalidateOptionsMenu()
syncUUID = UUID.randomUUID().toString()
mailboxLocationMain.value = newMessageLocationType
setTitle()
closeDrawer()
mailboxRecyclerView.clearFocus()
mailboxRecyclerView.scrollToPosition(0)
setUpMailboxActionsView()
RefreshEmptyViewTask(
WeakReference(this),
counterDao,
messagesDatabase,
newMessageLocationType,
mailboxLabelId
).execute()
}
private fun switchToMailboxCustomLocation(
newLocation: Int,
labelId: String,
labelName: String?,
isFolder: Boolean
) {
SetUpNewMessageLocationTask(
WeakReference(this),
messageDetailsRepository,
labelId,
isFolder,
newLocation,
labelName
).execute()
}
private var undoSnack: Snackbar? = null
private fun buildSwipeProcessor() {
mSwipeProcessor.apply {
addHandler(SwipeAction.TRASH, TrashSwipeHandler())
addHandler(SwipeAction.SPAM, SpamSwipeHandler())
addHandler(SwipeAction.STAR, StarSwipeHandler())
addHandler(SwipeAction.ARCHIVE, ArchiveSwipeHandler())
addHandler(SwipeAction.MARK_READ, MarkReadSwipeHandler())
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_CODE_TRASH_MESSAGE_DETAILS -> {
// move_to_trash.visibility = View.VISIBLE
// handler.postDelayed({ move_to_trash.visibility = View.GONE }, 1000)
}
REQUEST_CODE_VALIDATE_PIN -> {
requireNotNull(data) { "No data for request $requestCode" }
if (EXTRA_TOTAL_COUNT_EVENT in data) {
val totalCountEvent: Any? = data.getSerializableExtra(
EXTRA_TOTAL_COUNT_EVENT
)
if (totalCountEvent is MessageCountsEvent) {
onMessageCountsEvent(totalCountEvent)
}
}
super.onActivityResult(requestCode, resultCode, data)
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
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 prefs = app.defaultSharedPreferences
val dontShowPlayServices = prefs[PREF_DONT_SHOW_PLAY_SERVICES] ?: false
if (!dontShowPlayServices) {
showTwoButtonInfoDialog(
titleStringId = R.string.push_notifications_alert_title,
messageStringId = R.string.push_notifications_alert_subtitle,
leftStringId = R.string.dont_remind_again,
onNegativeButtonClicked = { prefs[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 val fcmBroadcastReceiver: BroadcastReceiver = FcmBroadcastReceiver()
private class OnMessageClickTask internal constructor(
private val mailboxActivity: WeakReference<MailboxActivity>,
private val messageDetailsRepository: MessageDetailsRepository,
private val messageId: String
) : AsyncTask<Unit, Unit, Message>() {
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 {
val intent = AppUtil.decorInAppIntent(
Intent(
mailboxActivity, MessageDetailsActivity::class.java
)
)
if (!mailboxActivity!!.mailboxLabelId.isNullOrEmpty()) {
intent.putExtra(MessageDetailsActivity.EXTRA_TRANSIENT_MESSAGE, false)
}
intent.putExtra(MessageDetailsActivity.EXTRA_MESSAGE_ID, messageId)
mailboxActivity.startActivityForResult(intent, REQUEST_CODE_TRASH_MESSAGE_DETAILS)
}
}
}
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?.findPendingSendByMessageId(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
}
val intent = AppUtil.decorInAppIntent(Intent(mailboxActivity, ComposeMessageActivity::class.java))
intent.putExtra(ComposeMessageActivity.EXTRA_MESSAGE_ID, messageId)
intent.putExtra(ComposeMessageActivity.EXTRA_MESSAGE_RESPONSE_INLINE, isInline)
intent.putExtra(ComposeMessageActivity.EXTRA_MESSAGE_ADDRESS_ID, addressId)
mailboxActivity?.startActivityForResult(intent, REQUEST_CODE_COMPOSE_MESSAGE)
}
}
private class SetUpNewMessageLocationTask internal constructor(
private val mailboxActivity: WeakReference<MailboxActivity>,
private val messageDetailsRepository: MessageDetailsRepository,
private val labelId: String,
private val isFolder: Boolean,
private val newLocation: Int,
private val labelName: String?
) : AsyncTask<Unit, Unit, Label?>() {
override fun doInBackground(vararg params: Unit): Label? {
val labels = messageDetailsRepository.findAllLabelsWithIds(listOf(labelId))
return if (labels.isEmpty()) null else labels[0]
}
override fun onPostExecute(label: Label?) {
val mailboxActivity = mailboxActivity.get() ?: return
mailboxActivity.mailboxSwipeRefreshLayout.visibility = View.VISIBLE
mailboxActivity.mailboxSwipeRefreshLayout.isRefreshing = true
mailboxActivity.noMessagesSwipeRefreshLayout.visibility = View.GONE
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.mailboxLocationMain.value = locationToSet
if (label != null) {
val actionBar = mailboxActivity.supportActionBar
if (actionBar != null) {
actionBar.title = label.name
}
}
mailboxActivity.closeDrawer()
mailboxActivity.mailboxRecyclerView.scrollToPosition(0)
RefreshEmptyViewTask(
this.mailboxActivity,
mailboxActivity.counterDao,
mailboxActivity.messagesDatabase,
locationToSet,
labelId
).execute()
}
}
private inner class FcmBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (intent.extras != null
) {
syncUUID = UUID.randomUUID().toString()
checkUserAndFetchNews()
if ((mailboxRecyclerView.layoutManager as LinearLayoutManager?)!!.findFirstVisibleItemPosition() > 1) {
handler.postDelayed(750) {
val newMessageSnack =
Snackbar.make(
findViewById(R.id.drawer_layout),
getString(R.string.new_message_arrived),
Snackbar.LENGTH_LONG
)
val view = newMessageSnack.view
val tv =
view.findViewById<TextView>(com.google.android.material.R.id.snackbar_text)
tv.setTextColor(Color.WHITE)
newMessageSnack.show()
}
}
mailboxAdapter.notifyDataSetChanged()
}
}
}
private inner class SwipeController : ItemTouchHelper.Callback() {
private var mailSettings: MailSettings? = null
init {
loadCurrentMailSetting()
}
@Deprecated("Subscribe for changes instead of reloading on current User/MailSettings changed.")
fun loadCurrentMailSetting() {
lifecycleScope.launchWhenResumed {
mailSettings = requireNotNull(userManager.getCurrentUserMailSettings())
}
}
override fun getMovementFlags(
recyclerView: RecyclerView,
viewHolder: RecyclerView.ViewHolder
): Int {
if (viewHolder is MessageViewHolder) {
val mailboxLocation = mailboxLocationMain.value
return if (mailboxLocationMain.value != null && 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.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.getItem(position)
val messageSwiped = SimpleMessage(mailboxItem)
val mailboxLocation = mailboxLocationMain.value
val settings = mailSettings ?: return
val swipeActionOrdinal: Int = when (direction) {
ItemTouchHelper.RIGHT -> settings.rightSwipeAction
ItemTouchHelper.LEFT -> settings.leftSwipeAction
else -> throw IllegalArgumentException("Unrecognised direction: $direction")
}
val swipeAction = normalise(SwipeAction.values()[swipeActionOrdinal], mailboxLocationMain.value)
mSwipeProcessor.handleSwipe(swipeAction, messageSwiped, mJobManager, mailboxLabelId)
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, mailboxLabelId)
mailboxAdapter.notifyDataSetChanged()
},
true
)
if (!(swipeAction == SwipeAction.TRASH && mailboxLocationMain.value == MessageLocationType.DRAFT)) {
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 {
mailboxLocationMain.value == MessageLocationType.DRAFT -> {
SwipeAction.TRASH.getActionBackgroundResource(deltaX < 0)
}
deltaX < 0 -> {
mailSettings?.let {
SwipeAction.values()[it.leftSwipeAction].getActionBackgroundResource(false)
} ?: Resources.ID_NULL
}
else -> {
mailSettings?.let {
SwipeAction.values()[it.rightSwipeAction].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)
}
}
private class OnMessageCountsListTask internal constructor(
private val mailboxActivity: WeakReference<MailboxActivity>,
private val counterDao: CounterDao,
private val messageCountsList: List<MessageCount>
) : AsyncTask<Unit, Unit, Int>() {
override fun doInBackground(vararg params: Unit): Int {
val totalInbox = counterDao.findTotalLocationById(MessageLocationType.INBOX.messageLocationTypeValue)
return totalInbox?.count ?: -1
}
override fun onPostExecute(inboxMessagesCount: Int) {
val mailboxActivity = mailboxActivity.get() ?: return
var foundMailbox = false
val locationCounters: MutableList<TotalLocationCounter> = ArrayList()
val labelCounters: MutableList<TotalLabelCounter> = ArrayList()
for (messageCount in messageCountsList) {
val labelId = messageCount.labelId
val total = messageCount.total
if (labelId.length <= 2) {
val location = fromInt(Integer.valueOf(labelId))
if (location == MessageLocationType.INBOX &&
inboxMessagesCount in 0 until total &&
!mailboxActivity.refreshMailboxJobRunning
) {
mailboxActivity.checkUserAndFetchNews()
}
if (mailboxActivity.mailboxLocationMain.value == location) {
mailboxActivity.refreshEmptyView(total)
foundMailbox = true
}
locationCounters.add(TotalLocationCounter(location.messageLocationTypeValue, total))
} else {
// label
if (labelId == mailboxActivity.mailboxLabelId) {
mailboxActivity.refreshEmptyView(total)
foundMailbox = true
}
if (!foundMailbox) {
mailboxActivity.refreshEmptyView(0)
}
labelCounters.add(TotalLabelCounter(labelId, total))
}
}
mailboxActivity.setRefreshing(false)
RefreshTotalCountersTask(counterDao, locationCounters, labelCounters).execute()
}
}
}