Skip to content
This repository has been archived by the owner on Feb 20, 2023. It is now read-only.

Commit

Permalink
Fixes #4528 - Prevent share menu from jumping
Browse files Browse the repository at this point in the history
Plus a bunch of docs and refactoring
  • Loading branch information
NotWoods authored and ekager committed Nov 15, 2019
1 parent 6b9a0d0 commit 333ff8c
Show file tree
Hide file tree
Showing 17 changed files with 250 additions and 218 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import kotlinx.android.synthetic.main.fragment_add_new_device.*
import org.mozilla.fenix.R
import org.mozilla.fenix.settings.SupportUtils

/**
* Fragment to add a new device. Tabs can be shared to devices after they are added.
*/
class AddNewDeviceFragment : Fragment(R.layout.fragment_add_new_device) {

override fun onResume() {
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/org/mozilla/fenix/share/ShareCloseView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class ShareCloseView(
override val containerView: ViewGroup,
private val interactor: ShareCloseInteractor
) : LayoutContainer {

val adapter = ShareTabsAdapter()

init {
Expand All @@ -36,6 +37,6 @@ class ShareCloseView(
}

fun setTabs(tabs: List<ShareTab>) {
adapter.setTabs(tabs)
adapter.submitList(tabs)
}
}
13 changes: 7 additions & 6 deletions app/src/main/java/org/mozilla/fenix/share/ShareController.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import org.mozilla.fenix.components.metrics.Event
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.metrics
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.share.listadapters.AppShareOption
import org.mozilla.fenix.share.listadapters.AndroidShareOption

/**
* [ShareFragment] controller.
Expand All @@ -36,7 +36,7 @@ import org.mozilla.fenix.share.listadapters.AppShareOption
interface ShareController {
fun handleReauth()
fun handleShareClosed()
fun handleShareToApp(app: AppShareOption)
fun handleShareToApp(app: AndroidShareOption.App)
fun handleAddNewDevice()
fun handleShareToDevice(device: Device)
fun handleShareToAllDevices(devices: List<Device>)
Expand Down Expand Up @@ -72,7 +72,7 @@ class DefaultShareController(
dismiss()
}

override fun handleShareToApp(app: AppShareOption) {
override fun handleShareToApp(app: AndroidShareOption.App) {
val intent = Intent(ACTION_SEND).apply {
putExtra(EXTRA_TEXT, getShareText())
type = "text/plain"
Expand Down Expand Up @@ -116,9 +116,10 @@ class DefaultShareController(
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) {
when (shareOperation.invoke().await()) {
true -> showSuccess()
false -> showFailureWithRetryOption { shareToDevicesWithRetry(shareOperation) }
if (shareOperation.invoke().await()) {
showSuccess()
} else {
showFailureWithRetryOption { shareToDevicesWithRetry(shareOperation) }
}
dismiss()
}
Expand Down
173 changes: 90 additions & 83 deletions app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,25 +17,27 @@ import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.WorkerThread
import androidx.appcompat.app.AppCompatDialogFragment
import androidx.core.content.getSystemService
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_share.view.*
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import mozilla.components.concept.sync.DeviceCapability
import mozilla.components.concept.sync.DeviceType
import mozilla.components.feature.sendtab.SendTabUseCases
import mozilla.components.service.fxa.manager.FxaAccountManager
import org.mozilla.fenix.R
import org.mozilla.fenix.components.FenixSnackbarPresenter
import org.mozilla.fenix.ext.components
import org.mozilla.fenix.ext.getRootView
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.share.listadapters.AppShareOption
import org.mozilla.fenix.share.listadapters.AndroidShareOption
import org.mozilla.fenix.share.listadapters.SyncShareOption

@Suppress("TooManyFunctions")
Expand All @@ -44,20 +46,39 @@ class ShareFragment : AppCompatDialogFragment() {
private lateinit var shareCloseView: ShareCloseView
private lateinit var shareToAccountDevicesView: ShareToAccountDevicesView
private lateinit var shareToAppsView: ShareToAppsView
private lateinit var appsListDeferred: Deferred<List<AppShareOption>>
private lateinit var appsListDeferred: Deferred<List<AndroidShareOption>>
private lateinit var devicesListDeferred: Deferred<List<SyncShareOption>>
private var connectivityManager: ConnectivityManager? = null

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)
}
}
}
}

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

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

// Start preparing the data as soon as we have a valid Context
appsListDeferred = lifecycleScope.async(Dispatchers.IO) {
appsListDeferred = lifecycleScope.async(IO) {
val shareIntent = Intent(ACTION_SEND).apply {
type = "text/plain"
flags = FLAG_ACTIVITY_NEW_TASK
Expand All @@ -66,53 +87,29 @@ class ShareFragment : AppCompatDialogFragment() {
buildAppsList(shareAppsActivities, context)
}

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

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

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()
}

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

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

val tabs = args.tabs?.toList() ?: listOf(ShareTab(args.url!!, args.title.orEmpty()))
Expand Down Expand Up @@ -153,74 +150,84 @@ class ShareFragment : AppCompatDialogFragment() {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)

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

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

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

/**
* Returns a list of apps that can be shared to.
* @param intentActivities List of activities from [getIntentActivities].
*/
@WorkerThread
private fun buildAppsList(
intentActivities: List<ResolveInfo>?,
context: Context
): List<AppShareOption> {
return intentActivities?.map { resolveInfo ->
AppShareOption(
resolveInfo.loadLabel(context.packageManager).toString(),
resolveInfo.loadIcon(context.packageManager),
resolveInfo.activityInfo.packageName,
resolveInfo.activityInfo.name
)
}?.filter { it.packageName != context.packageName }.orEmpty()
): 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
)
}
}

@Suppress("ReturnCount")
/**
* 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.
*/
private fun buildDeviceList(accountManager: FxaAccountManager): List<SyncShareOption> {
val list = mutableListOf<SyncShareOption>()

val activeNetwork = connectivityManager?.activeNetworkInfo
if (activeNetwork?.isConnected != true) {
list.add(SyncShareOption.Offline)
return list
}

if (accountManager.authenticatedAccount() == null) {
list.add(SyncShareOption.SignIn)
return list
}

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) }
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)
}

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

val shareOptions = shareableDevices.map {
when (it.deviceType) {
DeviceType.MOBILE -> SyncShareOption.Mobile(it.displayName, it)
else -> SyncShareOption.Desktop(it.displayName, it)
if (shareableDevices.size > 1) {
// Show send all button if there are multiple devices
list.add(SyncShareOption.SendAll(shareableDevices))
}
}
list.addAll(shareOptions)

if (shareableDevices.size > 1) {
list.add(SyncShareOption.SendAll(shareableDevices))
list
}
}
return list
}

companion object {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/org/mozilla/fenix/share/ShareInteractor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
package org.mozilla.fenix.share

import mozilla.components.concept.sync.Device
import org.mozilla.fenix.share.listadapters.AppShareOption
import org.mozilla.fenix.share.listadapters.AndroidShareOption

/**
* Interactor for the share screen.
Expand Down Expand Up @@ -37,7 +37,7 @@ class ShareInteractor(
controller.handleShareToAllDevices(devices)
}

override fun onShareToApp(appToShareTo: AppShareOption) {
override fun onShareToApp(appToShareTo: AndroidShareOption.App) {
controller.handleShareToApp(appToShareTo)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,18 +26,19 @@ interface ShareToAccountDevicesInteractor {

class ShareToAccountDevicesView(
override val containerView: ViewGroup,
private val interactor: ShareToAccountDevicesInteractor
interactor: ShareToAccountDevicesInteractor
) : LayoutContainer {

private val adapter = AccountDevicesShareAdapter(interactor)

init {
LayoutInflater.from(containerView.context)
.inflate(R.layout.share_to_account_devices, containerView, true)

devicesList.adapter = AccountDevicesShareAdapter(interactor)
devicesList.adapter = adapter
}

fun setSharetargets(targets: List<SyncShareOption>) {
with(devicesList.adapter as AccountDevicesShareAdapter) {
updateData(targets)
}
fun setShareTargets(targets: List<SyncShareOption>) {
adapter.submitList(targets)
}
}
Loading

0 comments on commit 333ff8c

Please sign in to comment.