ShareFragment.kt 9.56 KB
Newer Older
1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
3
4
5
 * 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
6

7
8
9
10
11
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_SEND
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.content.pm.ResolveInfo
12
13
14
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkRequest
15
import android.os.Bundle
16
import android.os.Parcelable
17
18
19
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
20
import androidx.annotation.WorkerThread
21
import androidx.appcompat.app.AppCompatDialogFragment
22
import androidx.core.content.getSystemService
23
import androidx.lifecycle.lifecycleScope
24
import androidx.navigation.fragment.findNavController
25
import androidx.navigation.fragment.navArgs
26
import kotlinx.android.parcel.Parcelize
27
import kotlinx.android.synthetic.main.fragment_share.view.*
28
import kotlinx.coroutines.Deferred
29
import kotlinx.coroutines.Dispatchers.IO
30
31
32
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.DeviceCapability
33
import mozilla.components.feature.sendtab.SendTabUseCases
34
import mozilla.components.service.fxa.manager.FxaAccountManager
35
import org.mozilla.fenix.R
36
import org.mozilla.fenix.components.FenixSnackbarPresenter
37
import org.mozilla.fenix.ext.components
38
import org.mozilla.fenix.ext.getRootView
39
import org.mozilla.fenix.ext.requireComponents
40
import org.mozilla.fenix.share.listadapters.AndroidShareOption
41
import org.mozilla.fenix.share.listadapters.SyncShareOption
42

43
@Suppress("TooManyFunctions")
44
class ShareFragment : AppCompatDialogFragment() {
45
46
47
48
    private lateinit var shareInteractor: ShareInteractor
    private lateinit var shareCloseView: ShareCloseView
    private lateinit var shareToAccountDevicesView: ShareToAccountDevicesView
    private lateinit var shareToAppsView: ShareToAppsView
49
    private lateinit var appsListDeferred: Deferred<List<AndroidShareOption>>
50
    private lateinit var devicesListDeferred: Deferred<List<SyncShareOption>>
51
    private var connectivityManager: ConnectivityManager? = null
52

53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
    private val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onLost(network: Network?) = reloadDevices()
        override fun onAvailable(network: Network?) = reloadDevices()

        private fun reloadDevices() {
            context?.let { context ->
                val fxaAccountManager = context.components.backgroundServices.accountManager
                lifecycleScope.launch {
                    fxaAccountManager.authenticatedAccount()
                        ?.deviceConstellation()
                        ?.refreshDevicesAsync()
                        ?.await()

                    val devicesShareOptions = buildDeviceList(fxaAccountManager)
                    shareToAccountDevicesView.setShareTargets(devicesShareOptions)
                }
            }
        }
    }

73
74
75
    override fun onAttach(context: Context) {
        super.onAttach(context)

76
        connectivityManager = context.getSystemService()
77
78
79
        val networkRequest = NetworkRequest.Builder().build()
        connectivityManager?.registerNetworkCallback(networkRequest, networkCallback)

80
        // Start preparing the data as soon as we have a valid Context
81
        appsListDeferred = lifecycleScope.async(IO) {
82
83
84
85
86
87
88
89
            val shareIntent = Intent(ACTION_SEND).apply {
                type = "text/plain"
                flags = FLAG_ACTIVITY_NEW_TASK
            }
            val shareAppsActivities = getIntentActivities(shareIntent, context)
            buildAppsList(shareAppsActivities, context)
        }

90
        devicesListDeferred = lifecycleScope.async(IO) {
91
92
93
94
            val fxaAccountManager = context.components.backgroundServices.accountManager
            buildDeviceList(fxaAccountManager)
        }
    }
95

96
97
98
99
100
    override fun onDetach() {
        connectivityManager?.unregisterNetworkCallback(networkCallback)
        super.onDetach()
    }

101
102
103
104
105
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setStyle(STYLE_NO_TITLE, R.style.ShareDialogStyle)
    }

106
107
108
109
110
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
111
        val view = inflater.inflate(R.layout.fragment_share, container, false)
112
        val args by navArgs<ShareFragmentArgs>()
113
        check(!(args.url == null && args.tabs.isNullOrEmpty())) { "URL and tabs cannot both be null." }
114

Tiger Oakes's avatar
Tiger Oakes committed
115
        val tabs = args.tabs?.toList() ?: listOf(ShareTab(args.url!!, args.title.orEmpty()))
116
        val accountManager = requireComponents.backgroundServices.accountManager
117

118
119
        shareInteractor = ShareInteractor(
            DefaultShareController(
120
                context = requireContext(),
121
                sharedTabs = tabs,
122
                snackbarPresenter = FenixSnackbarPresenter(activity!!.getRootView()!!),
123
                navController = findNavController(),
124
                sendTabUseCases = SendTabUseCases(accountManager),
125
126
127
                dismiss = ::dismiss
            )
        )
128

129
        view.shareWrapper.setOnClickListener { shareInteractor.onShareClosed() }
130
131
        shareToAccountDevicesView =
            ShareToAccountDevicesView(view.devicesShareLayout, shareInteractor)
132
133
134
135
136
137
138
139
140
141
142
143
144

        if (args.url != null && args.tabs == null) {
            // If sharing one tab from the browser fragment, show it.
            // If URL is set and tabs is null, we assume the browser is visible, since navigation
            // does not tell us the back stack state.
            view.closeSharingScrim.alpha = SHOW_PAGE_ALPHA
            view.shareWrapper.setOnClickListener { shareInteractor.onShareClosed() }
        } else {
            // Otherwise, show a list of tabs to share.
            view.closeSharingScrim.alpha = 1.0f
            shareCloseView = ShareCloseView(view.closeSharingContent, shareInteractor)
            shareCloseView.setTabs(tabs)
        }
145
        shareToAppsView = ShareToAppsView(view.appsShareLayout, shareInteractor)
146

147
        return view
148
    }
149
150
151
152

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

153
154
155
156
157
        // Start with some invisible views so the share menu height doesn't jump later
        shareToAppsView.setShareTargets(
            listOf(AndroidShareOption.Invisible, AndroidShareOption.Invisible)
        )

158
159
        lifecycleScope.launch {
            val devicesShareOptions = devicesListDeferred.await()
160
            shareToAccountDevicesView.setShareTargets(devicesShareOptions)
161
            val appsToShareTo = appsListDeferred.await()
162
            shareToAppsView.setShareTargets(appsToShareTo)
163
164
165
        }
    }

166
    @WorkerThread
167
168
169
170
    private fun getIntentActivities(shareIntent: Intent, context: Context): List<ResolveInfo>? {
        return context.packageManager.queryIntentActivities(shareIntent, 0)
    }

171
172
173
174
175
    /**
     * Returns a list of apps that can be shared to.
     * @param intentActivities List of activities from [getIntentActivities].
     */
    @WorkerThread
176
177
178
    private fun buildAppsList(
        intentActivities: List<ResolveInfo>?,
        context: Context
179
180
181
182
183
184
185
186
187
188
189
190
    ): List<AndroidShareOption> {
        return intentActivities
            .orEmpty()
            .filter { it.activityInfo.packageName != context.packageName }
            .map { resolveInfo ->
                AndroidShareOption.App(
                    resolveInfo.loadLabel(context.packageManager).toString(),
                    resolveInfo.loadIcon(context.packageManager),
                    resolveInfo.activityInfo.packageName,
                    resolveInfo.activityInfo.name
                )
            }
191
192
    }

193
194
195
196
197
    /**
     * Builds list of options to display in the top row of the share sheet.
     * This will primarily include devices that tabs can be sent to, but also options
     * for reconnecting the account or sending to all devices.
     */
198
    private fun buildDeviceList(accountManager: FxaAccountManager): List<SyncShareOption> {
199
        val activeNetwork = connectivityManager?.activeNetworkInfo
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
        val account = accountManager.authenticatedAccount()

        return when {
            // No network
            activeNetwork?.isConnected != true -> listOf(SyncShareOption.Offline)
            // No account signed in
            account == null -> listOf(SyncShareOption.SignIn)
            // Account needs to be re-authenticated
            accountManager.accountNeedsReauth() -> listOf(SyncShareOption.Reconnect)
            // Signed in
            else -> {
                val shareableDevices = account.deviceConstellation().state()
                    ?.otherDevices
                    .orEmpty()
                    .filter { it.capabilities.contains(DeviceCapability.SEND_TAB) }

                val list = mutableListOf<SyncShareOption>()
                if (shareableDevices.isEmpty()) {
                    // Show add device button if there are no devices
                    list.add(SyncShareOption.AddNewDevice)
                }
221

222
                shareableDevices.mapTo(list) { SyncShareOption.SingleDevice(it) }
223

224
225
226
                if (shareableDevices.size > 1) {
                    // Show send all button if there are multiple devices
                    list.add(SyncShareOption.SendAll(shareableDevices))
227
                }
228
                list
229
230
231
            }
        }
    }
232
233
234
235

    companion object {
        const val SHOW_PAGE_ALPHA = 0.6f
    }
236
}
237
238

@Parcelize
239
data class ShareTab(val url: String, val title: String) : Parcelable