ShareFragment.kt 8.9 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.appcompat.app.AppCompatDialogFragment
21
import androidx.lifecycle.lifecycleScope
22
import androidx.navigation.fragment.findNavController
23
import kotlinx.android.parcel.Parcelize
24
import kotlinx.android.synthetic.main.fragment_share.view.*
25
26
27
28
29
30
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.DeviceCapability
import mozilla.components.concept.sync.DeviceType
31
import mozilla.components.feature.sendtab.SendTabUseCases
32
import mozilla.components.service.fxa.manager.FxaAccountManager
33
import org.mozilla.fenix.R
34
import org.mozilla.fenix.components.FenixSnackbarPresenter
35
import org.mozilla.fenix.ext.components
36
import org.mozilla.fenix.ext.getRootView
37
import org.mozilla.fenix.ext.requireComponents
38
39
import org.mozilla.fenix.share.listadapters.AppShareOption
import org.mozilla.fenix.share.listadapters.SyncShareOption
40

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

    override fun onAttach(context: Context) {
        super.onAttach(context)

54
55
56
57
58
        connectivityManager =
            context.getSystemService(Context.CONNECTIVITY_SERVICE) as? ConnectivityManager
        val networkRequest = NetworkRequest.Builder().build()
        connectivityManager?.registerNetworkCallback(networkRequest, networkCallback)

59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
        // Start preparing the data as soon as we have a valid Context
        appsListDeferred = lifecycleScope.async(Dispatchers.IO) {
            val shareIntent = Intent(ACTION_SEND).apply {
                type = "text/plain"
                flags = FLAG_ACTIVITY_NEW_TASK
            }
            val shareAppsActivities = getIntentActivities(shareIntent, context)
            buildAppsList(shareAppsActivities, context)
        }

        devicesListDeferred = lifecycleScope.async(Dispatchers.IO) {
            val fxaAccountManager = context.components.backgroundServices.accountManager
            buildDeviceList(fxaAccountManager)
        }
    }
74
75
76

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
77
        setStyle(STYLE_NO_TITLE, R.style.ShareDialogStyle)
78
79
    }

80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
    private val networkCallback = object : ConnectivityManager.NetworkCallback() {
        override fun onLost(network: Network?) {
            reloadDevices()
        }

        override fun onAvailable(network: Network?) {
            reloadDevices()
        }
    }

    private fun reloadDevices() {
        context?.let {
            val fxaAccountManager = it.components.backgroundServices.accountManager
            lifecycleScope.launch {
                val refreshDevicesAsync =
                    fxaAccountManager.authenticatedAccount()?.deviceConstellation()
                        ?.refreshDevicesAsync()
                refreshDevicesAsync?.await()
                val devicesShareOptions = buildDeviceList(fxaAccountManager)
                shareToAccountDevicesView.setSharetargets(devicesShareOptions)
            }
        }
    }

    override fun onDetach() {
        connectivityManager?.unregisterNetworkCallback(networkCallback)
        super.onDetach()
    }

109
110
111
112
113
    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
114
115
        val view = inflater.inflate(R.layout.fragment_share, container, false)
        val args = ShareFragmentArgs.fromBundle(arguments!!)
116
        check(!(args.url == null && args.tabs.isNullOrEmpty())) { "URL and tabs cannot both be null." }
117

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

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

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

        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)
        }
148
        shareToAppsView = ShareToAppsView(view.appsShareLayout, shareInteractor)
149

150
        return view
151
    }
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167

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

        lifecycleScope.launch {
            val devicesShareOptions = devicesListDeferred.await()
            shareToAccountDevicesView.setSharetargets(devicesShareOptions)
            val appsToShareTo = appsListDeferred.await()
            shareToAppsView.setSharetargets(appsToShareTo)
        }
    }

    private fun getIntentActivities(shareIntent: Intent, context: Context): List<ResolveInfo>? {
        return context.packageManager.queryIntentActivities(shareIntent, 0)
    }

168
169
170
171
    private fun buildAppsList(
        intentActivities: List<ResolveInfo>?,
        context: Context
    ): List<AppShareOption> {
172
173
174
175
176
177
178
        return intentActivities?.map { resolveInfo ->
            AppShareOption(
                resolveInfo.loadLabel(context.packageManager).toString(),
                resolveInfo.loadIcon(context.packageManager),
                resolveInfo.activityInfo.packageName,
                resolveInfo.activityInfo.name
            )
179
        }?.filter { it.packageName != context.packageName }.orEmpty()
180
181
    }

182
    @Suppress("ReturnCount")
183
184
185
    private fun buildDeviceList(accountManager: FxaAccountManager): List<SyncShareOption> {
        val list = mutableListOf<SyncShareOption>()

186
187
188
189
190
191
        val activeNetwork = connectivityManager?.activeNetworkInfo
        if (activeNetwork?.isConnected != true) {
            list.add(SyncShareOption.Offline)
            return list
        }

192
193
194
195
196
        if (accountManager.authenticatedAccount() == null) {
            list.add(SyncShareOption.SignIn)
            return list
        }

197
198
199
200
201
202
203
204
205
        if (accountManager.accountNeedsReauth()) {
            list.add(SyncShareOption.Reconnect)
            return list
        }

        accountManager.authenticatedAccount()?.deviceConstellation()?.state()
            ?.otherDevices?.let { devices ->
            val shareableDevices =
                devices.filter { it.capabilities.contains(DeviceCapability.SEND_TAB) }
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224

            if (shareableDevices.isEmpty()) {
                list.add(SyncShareOption.AddNewDevice)
            }

            val shareOptions = shareableDevices.map {
                when (it.deviceType) {
                    DeviceType.MOBILE -> SyncShareOption.Mobile(it.displayName, it)
                    else -> SyncShareOption.Desktop(it.displayName, it)
                }
            }
            list.addAll(shareOptions)

            if (shareableDevices.size > 1) {
                list.add(SyncShareOption.SendAll(shareableDevices))
            }
        }
        return list
    }
225
226
227
228

    companion object {
        const val SHOW_PAGE_ALPHA = 0.6f
    }
229
}
230
231

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