ShareController.kt 6.06 KB
Newer Older
1
2
3
4
5
6
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

package org.mozilla.fenix.share

7
import android.content.Context
8
9
10
11
import android.content.Intent
import android.content.Intent.ACTION_SEND
import android.content.Intent.EXTRA_TEXT
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
12
import androidx.annotation.VisibleForTesting
13
import androidx.navigation.NavController
14
import com.google.android.material.snackbar.Snackbar
15
16
17
18
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
19
import mozilla.components.concept.sync.Device
20
21
import mozilla.components.concept.sync.TabData
import mozilla.components.feature.sendtab.SendTabUseCases
22
import org.mozilla.fenix.R
23
import org.mozilla.fenix.components.FenixSnackbar
24
import org.mozilla.fenix.components.FenixSnackbarPresenter
25
import org.mozilla.fenix.components.metrics.Event
26
import org.mozilla.fenix.ext.getRootView
27
import org.mozilla.fenix.ext.metrics
28
import org.mozilla.fenix.ext.nav
29
import org.mozilla.fenix.share.listadapters.AndroidShareOption
30
31
32
33
34
35
36

/**
 * [ShareFragment] controller.
 *
 * Delegated by View Interactors, handles container business logic and operates changes on it.
 */
interface ShareController {
37
    fun handleReauth()
38
    fun handleShareClosed()
39
    fun handleShareToApp(app: AndroidShareOption.App)
40
41
42
43
44
45
46
47
48
    fun handleAddNewDevice()
    fun handleShareToDevice(device: Device)
    fun handleShareToAllDevices(devices: List<Device>)
    fun handleSignIn()
}

/**
 * Default behavior of [ShareController]. Other implementations are possible.
 *
49
 * @param context [Context] used for various Android interactions.
50
51
 * @param sharedTabs the list of [ShareTab]s that can be shared.
 * @param sendTabUseCases instance of [SendTabUseCases] which allows sending tabs to account devices.
52
 * @param snackbarPresenter - instance of [FenixSnackbarPresenter] for displaying styled snackbars
53
54
55
 * @param navController - [NavController] used for navigation.
 * @param dismiss - callback signalling sharing can be closed.
 */
56
@Suppress("TooManyFunctions")
57
class DefaultShareController(
58
    private val context: Context,
59
60
    private val sharedTabs: List<ShareTab>,
    private val sendTabUseCases: SendTabUseCases,
61
    private val snackbarPresenter: FenixSnackbarPresenter,
62
63
64
    private val navController: NavController,
    private val dismiss: () -> Unit
) : ShareController {
65
66
67
68
69
70
    override fun handleReauth() {
        val directions = ShareFragmentDirections.actionShareFragmentToAccountProblemFragment()
        navController.nav(R.id.shareFragment, directions)
        dismiss()
    }

71
72
73
74
    override fun handleShareClosed() {
        dismiss()
    }

75
    override fun handleShareToApp(app: AndroidShareOption.App) {
76
        val intent = Intent(ACTION_SEND).apply {
77
            putExtra(EXTRA_TEXT, getShareText())
78
79
80
81
            type = "text/plain"
            flags = FLAG_ACTIVITY_NEW_TASK
            setClassName(app.packageName, app.activityName)
        }
82
83

        try {
84
            context.startActivity(intent)
85
86
87
88
89
90
91
        } catch (e: SecurityException) {
            context.getRootView()?.let {
                FenixSnackbar.make(it, Snackbar.LENGTH_LONG)
                    .setText(context.getString(R.string.share_error_snackbar))
                    .show()
            }
        }
92
93
94
95
        dismiss()
    }

    override fun handleAddNewDevice() {
96
97
        val directions = ShareFragmentDirections.actionShareFragmentToAddNewDeviceFragment()
        navController.navigate(directions)
98
99
100
    }

    override fun handleShareToDevice(device: Device) {
101
        context.metrics.track(Event.SendTab)
102
        shareToDevicesWithRetry { sendTabUseCases.sendToDeviceAsync(device.id, sharedTabs.toTabData()) }
103
104
105
    }

    override fun handleShareToAllDevices(devices: List<Device>) {
106
        shareToDevicesWithRetry { sendTabUseCases.sendToAllAsync(sharedTabs.toTabData()) }
107
108
109
    }

    override fun handleSignIn() {
110
        context.metrics.track(Event.SignInToSendTab)
111
112
113
114
115
        val directions = ShareFragmentDirections.actionShareFragmentToTurnOnSyncFragment()
        navController.nav(R.id.shareFragment, directions)
        dismiss()
    }

116
117
118
    private fun shareToDevicesWithRetry(shareOperation: () -> Deferred<Boolean>) {
        // Use GlobalScope to allow the continuation of this method even if the share fragment is closed.
        GlobalScope.launch(Dispatchers.Main) {
119
120
121
122
            if (shareOperation.invoke().await()) {
                showSuccess()
            } else {
                showFailureWithRetryOption { shareToDevicesWithRetry(shareOperation) }
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
            }
            dismiss()
        }
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun showSuccess() {
        snackbarPresenter.present(
            getSuccessMessage(),
            Snackbar.LENGTH_SHORT
        )
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun showFailureWithRetryOption(operation: () -> Unit) {
        snackbarPresenter.present(
            text = context.getString(R.string.sync_sent_tab_error_snackbar),
            length = Snackbar.LENGTH_LONG,
            action = operation,
            actionName = context.getString(R.string.sync_sent_tab_error_snackbar_action),
            isError = true
        )
    }

    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun getSuccessMessage(): String = with(context) {
        when (sharedTabs.size) {
            1 -> getString(R.string.sync_sent_tab_snackbar)
            else -> getString(R.string.sync_sent_tabs_snackbar)
        }
    }

155
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
156
    fun getShareText() = sharedTabs.joinToString("\n") { tab -> tab.url }
157

158
    // Navigation between app fragments uses ShareTab as arguments. SendTabUseCases uses TabData.
159
160
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun ShareTab.toTabData() = TabData(title, url)
161

162
163
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun List<ShareTab>.toTabData() = map { it.toTabData() }
164
}