Commit 09587014 authored by Mugurell's avatar Mugurell Committed by Jeff Boek
Browse files

For #4007 - Refactor AppShareView in standalone Share Views

In an effort to respect the initial MVI architecture I've broken the
complex `AppShareView` in 3 separate Views
- `ShareCloseView`
- `ShareToAccountDevicesView`
- `ShareToAppsView`
They are standalone Views (extending LayoutContainer) which know nothing about
each other or their parent and so offer their container the possibility to
order or display them in any form later.
According to the lib-state contract they are only responsible to
- inflate themselves in their injected containerView
- render a certain state (to be added in later commits)
- delegate all user interaction to an associated Interactor
parent 5e19741d
/* 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
import android.content.Context
import android.graphics.PorterDuff.Mode.SRC_IN
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import kotlinx.android.synthetic.main.account_share_list_item.view.*
import mozilla.components.concept.sync.Device
import mozilla.components.concept.sync.DeviceCapability
import mozilla.components.concept.sync.DeviceType
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.components
class AccountDevicesShareAdapter(
private val context: Context,
val actionEmitter: Observer<ShareAction>
) : RecyclerView.Adapter<AccountDeviceViewHolder>() {
private val devices = buildDeviceList()
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountDeviceViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(AccountDeviceViewHolder.LAYOUT_ID, parent, false)
return AccountDeviceViewHolder(view, actionEmitter)
}
override fun getItemCount(): Int = devices.size
override fun onBindViewHolder(holder: AccountDeviceViewHolder, position: Int) {
holder.bind(devices[position])
}
private fun buildDeviceList(): List<SyncShareOption> {
val list = mutableListOf<SyncShareOption>()
val accountManager = context.components.backgroundServices.accountManager
if (accountManager.authenticatedAccount() == null) {
list.add(SyncShareOption.SignIn)
return list
}
accountManager.authenticatedAccount()?.deviceConstellation()?.state()?.otherDevices?.let { devices ->
val shareableDevices = devices.filter { it.capabilities.contains(DeviceCapability.SEND_TAB) }
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
}
}
class AccountDeviceViewHolder(
itemView: View,
actionEmitter: Observer<ShareAction>
) : RecyclerView.ViewHolder(itemView) {
private val context: Context = itemView.context
private var action: ShareAction? = null
init {
itemView.setOnClickListener {
action?.let { actionEmitter.onNext(it) }
}
}
fun bind(option: SyncShareOption) {
val (name, drawableRes, colorRes) = when (option) {
SyncShareOption.SignIn -> {
action = ShareAction.SignInClicked
Triple(
context.getText(R.string.sync_sign_in),
R.drawable.mozac_ic_sync,
R.color.default_share_background
)
}
SyncShareOption.AddNewDevice -> {
action = ShareAction.AddNewDeviceClicked
Triple(
context.getText(R.string.sync_connect_device),
R.drawable.mozac_ic_new,
R.color.default_share_background
)
}
is SyncShareOption.SendAll -> {
action = ShareAction.SendAllClicked(option.devices)
Triple(
context.getText(R.string.sync_send_to_all),
R.drawable.mozac_ic_select_all,
R.color.default_share_background
)
}
is SyncShareOption.Mobile -> {
action = ShareAction.ShareDeviceClicked(option.device)
Triple(
option.name,
R.drawable.mozac_ic_device_mobile,
R.color.device_type_mobile_background
)
}
is SyncShareOption.Desktop -> {
action = ShareAction.ShareDeviceClicked(option.device)
Triple(
option.name,
R.drawable.mozac_ic_device_desktop,
R.color.device_type_desktop_background
)
}
}
itemView.deviceIcon.apply {
setImageResource(drawableRes)
background.setColorFilter(ContextCompat.getColor(context, colorRes), SRC_IN)
drawable.setTint(ContextCompat.getColor(context, R.color.device_foreground))
}
itemView.deviceName.text = name
}
companion object {
const val LAYOUT_ID = R.layout.account_share_list_item
}
}
sealed class SyncShareOption {
object SignIn : SyncShareOption()
object AddNewDevice : SyncShareOption()
data class SendAll(val devices: List<Device>) : SyncShareOption()
data class Mobile(val name: String, val device: Device) : SyncShareOption()
data class Desktop(val name: String, val device: Device) : SyncShareOption()
}
/* 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
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_SEND
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.graphics.drawable.Drawable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import io.reactivex.Observer
import kotlinx.android.synthetic.main.app_share_list_item.view.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.mozilla.fenix.R
class AppShareAdapter(
private val context: Context,
val actionEmitter: Observer<ShareAction>,
private val intentType: String = "text/plain"
) : RecyclerView.Adapter<AppShareItemViewHolder>() {
private var scope = CoroutineScope(Dispatchers.IO)
private var size: Int = 0
private val shareItems: MutableList<ShareItem> = mutableListOf()
init {
val testIntent = Intent(ACTION_SEND).apply {
type = intentType
flags = FLAG_ACTIVITY_NEW_TASK
}
scope.launch {
val activities = context.packageManager.queryIntentActivities(testIntent, 0)
val items = activities.map { resolveInfo ->
ShareItem(
resolveInfo.loadLabel(context.packageManager).toString(),
resolveInfo.loadIcon(context.packageManager),
resolveInfo.activityInfo.packageName,
resolveInfo.activityInfo.name
)
}
size = activities.size
shareItems.addAll(items)
// Notify adapter on the UI thread when the dataset is populated.
withContext(Dispatchers.Main) {
notifyDataSetChanged()
}
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AppShareItemViewHolder {
val view = LayoutInflater.from(parent.context)
.inflate(AppShareItemViewHolder.LAYOUT_ID, parent, false)
return AppShareItemViewHolder(view, actionEmitter)
}
override fun getItemCount(): Int = size
override fun onBindViewHolder(holder: AppShareItemViewHolder, position: Int) {
holder.bind(shareItems[position])
}
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
super.onDetachedFromRecyclerView(recyclerView)
scope.cancel()
}
}
class AppShareItemViewHolder(
itemView: View,
actionEmitter: Observer<ShareAction>
) : RecyclerView.ViewHolder(itemView) {
private var shareItem: ShareItem? = null
init {
itemView.setOnClickListener {
shareItem?.let {
actionEmitter.onNext(ShareAction.ShareAppClicked(it))
}
}
}
internal fun bind(item: ShareItem) {
shareItem = item
itemView.appName.text = item.name
itemView.appIcon.setImageDrawable(item.icon)
}
companion object {
const val LAYOUT_ID = R.layout.app_share_list_item
}
}
data class ShareItem(val name: String, val icon: Drawable, val packageName: String, val activityName: String)
/* 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
import android.view.LayoutInflater
import android.view.ViewGroup
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.main.share_close.*
import org.mozilla.fenix.R
/**
* Callbacks for possible user interactions on the [ShareCloseView]
*/
interface ShareCloseInteractor {
fun onShareClosed()
}
class ShareCloseView(
override val containerView: ViewGroup,
private val interactor: ShareCloseInteractor
) : LayoutContainer {
init {
LayoutInflater.from(containerView.context)
.inflate(R.layout.share_close, containerView, true)
closeButton.setOnClickListener { interactor.onShareClosed() }
}
}
/* 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
import android.view.ViewGroup
import mozilla.components.concept.sync.Device
import org.mozilla.fenix.mvi.Action
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.Change
import org.mozilla.fenix.mvi.Reducer
import org.mozilla.fenix.mvi.UIComponent
import org.mozilla.fenix.mvi.UIComponentViewModelBase
import org.mozilla.fenix.mvi.UIComponentViewModelProvider
import org.mozilla.fenix.mvi.ViewState
object ShareState : ViewState
sealed class ShareChange : Change
sealed class ShareAction : Action {
object Close : ShareAction()
object SignInClicked : ShareAction()
object AddNewDeviceClicked : ShareAction()
data class ShareDeviceClicked(val device: Device) : ShareAction()
data class SendAllClicked(val devices: List<Device>) : ShareAction()
data class ShareAppClicked(val item: ShareItem) : ShareAction()
}
class ShareComponent(
private val container: ViewGroup,
bus: ActionBusFactory,
viewModelProvider: UIComponentViewModelProvider<ShareState, ShareChange>
) : UIComponent<ShareState, ShareAction, ShareChange>(
bus.getManagedEmitter(ShareAction::class.java),
bus.getSafeManagedObservable(ShareChange::class.java),
viewModelProvider
) {
override fun initView() = ShareUIView(container, actionEmitter, changesObservable)
init {
bind()
}
}
class ShareUIViewModel(
initialState: ShareState
) : UIComponentViewModelBase<ShareState, ShareChange>(
initialState,
reducer
) {
companion object {
val reducer: Reducer<ShareState, ShareChange> = { _, _ -> ShareState }
}
}
......@@ -4,35 +4,25 @@
package org.mozilla.fenix.share
import android.content.Intent
import android.content.Intent.ACTION_SEND
import android.content.Intent.EXTRA_TEXT
import android.content.Intent.FLAG_ACTIVITY_NEW_TASK
import android.os.Bundle
import android.os.Parcelable
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatDialogFragment
import kotlinx.android.parcel.Parcelize
import kotlinx.android.synthetic.main.fragment_share.view.*
import mozilla.components.concept.sync.DeviceEventOutgoing
import mozilla.components.concept.sync.OAuthAccount
import org.mozilla.fenix.FenixViewModelProvider
import org.mozilla.fenix.HomeActivity
import org.mozilla.fenix.R
import org.mozilla.fenix.ext.nav
import org.mozilla.fenix.ext.requireComponents
import org.mozilla.fenix.mvi.ActionBusFactory
import org.mozilla.fenix.mvi.getAutoDisposeObservable
class ShareFragment : AppCompatDialogFragment() {
interface TabsSharedCallback {
fun onTabsShared(tabsSize: Int)
}
private lateinit var component: ShareComponent
private lateinit var shareInteractor: ShareInteractor
private lateinit var shareCloseView: ShareCloseView
private lateinit var shareToAccountDevicesView: ShareToAccountDevicesView
private lateinit var shareToAppsView: ShareToAppsView
private var tabs: Array<ShareTab> = emptyArray()
override fun onCreate(savedInstanceState: Bundle?) {
......@@ -53,92 +43,13 @@ class ShareFragment : AppCompatDialogFragment() {
tabs = args.tabs ?: arrayOf(ShareTab(args.url!!, args.title ?: ""))
component = ShareComponent(
view.shareWrapper,
ActionBusFactory.get(this),
FenixViewModelProvider.create(
this,
ShareUIViewModel::class.java
) {
ShareUIViewModel(ShareState)
}
)
shareInteractor = ShareInteractor()
return view
}
override fun onResume() {
super.onResume()
subscribeToActions()
}
@SuppressWarnings("ComplexMethod")
private fun subscribeToActions() {
getAutoDisposeObservable<ShareAction>().subscribe {
when (it) {
ShareAction.Close -> {
dismiss()
}
ShareAction.SignInClicked -> {
val directions =
ShareFragmentDirections.actionShareFragmentToTurnOnSyncFragment()
nav(R.id.shareFragment, directions)
dismiss()
}
ShareAction.AddNewDeviceClicked -> {
context?.let {
AlertDialog.Builder(it).apply {
setMessage(R.string.sync_connect_device_dialog)
setPositiveButton(R.string.sync_confirmation_button) { dialog, _ -> dialog.cancel() }
create()
}.show()
}
}
is ShareAction.ShareDeviceClicked -> {
val authAccount =
requireComponents.backgroundServices.accountManager.authenticatedAccount()
authAccount?.run {
sendSendTab(this, it.device.id, tabs)
}
dismiss()
}
is ShareAction.SendAllClicked -> {
val authAccount =
requireComponents.backgroundServices.accountManager.authenticatedAccount()
authAccount?.run {
it.devices.forEach { device ->
sendSendTab(this, device.id, tabs)
}
}
dismiss()
}
is ShareAction.ShareAppClicked -> {
val shareText = tabs.joinToString("\n") { tab -> tab.url }
shareCloseView = ShareCloseView(view.closeSharingLayout, shareInteractor)
shareToAccountDevicesView = ShareToAccountDevicesView(view.devicesShareLayout, shareInteractor)
shareToAppsView = ShareToAppsView(view.appsShareLayout, shareInteractor)
val intent = Intent(ACTION_SEND).apply {
putExtra(EXTRA_TEXT, shareText)
type = "text/plain"
flags = FLAG_ACTIVITY_NEW_TASK
setClassName(it.item.packageName, it.item.activityName)
}
startActivity(intent)
dismiss()
}
}
}
}
private fun sendSendTab(account: OAuthAccount, deviceId: String, tabs: Array<ShareTab>) {
account.run {
tabs.forEach { tab ->
deviceConstellation().sendEventToDeviceAsync(
deviceId,
DeviceEventOutgoing.SendTab(tab.title, tab.url)
)
}
}
(activity as? HomeActivity)?.onTabsShared(tabs.size)
return view
}
}
......
/* 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
import mozilla.components.concept.sync.Device
import org.mozilla.fenix.share.listadapters.Application
/**
* Interactor for the share screen.
*/
class ShareInteractor : ShareCloseInteractor, ShareToAccountDevicesInteractor, ShareToAppsInteractor {
override fun onShareClosed() {
TODO("not yet!? implemented")
}
override fun onSignIn() {
TODO("not yet!? implemented")
}
override fun onAddNewDevice() {
TODO("not yet!? implemented")
}
override fun onShareToDevice(device: Device) {
TODO("not yet!? implemented")
}
override fun onShareToAllDevices(devices: List<Device>) {
TODO("not yet!? implemented")
}
override fun onShareToApp(appToShareTo: Application) {
TODO("not yet!? implemented")
}
}
/* 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
import android.view.LayoutInflater
import android.view.ViewGroup
import kotlinx.android.extensions.LayoutContainer
import mozilla.components.concept.sync.Device
import org.mozilla.fenix.R
/**
* Callbacks for possible user interactions on the [ShareToAccountDevicesView]
*/
interface ShareToAccountDevicesInteractor {
fun onSignIn()
fun onAddNewDevice()
fun onShareToDevice(device: Device)
fun onShareToAllDevices(devices: List<Device>)
}
class ShareToAccountDevicesView(
override val containerView: ViewGroup,
private val interactor: ShareToAccountDevicesInteractor
) : LayoutContainer {
init {
LayoutInflater.from(containerView.context)
.inflate(R.layout.share_to_account_devices, containerView, true)
}
}
/* 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
import android.view.LayoutInflater
import android.view.ViewGroup
import kotlinx.android.extensions.LayoutContainer
import org.mozilla.fenix.R
import org.mozilla.fenix.share.listadapters.Application
/**
* Callbacks for possible user interactions on the [ShareCloseView]
*/
interface ShareToAppsInteractor {
fun onShareToApp(appToShareTo: Application)
}
class ShareToAppsView(
override val containerView: ViewGroup,
private val interactor: ShareToAppsInteractor
) : LayoutContainer {
init {
LayoutInflater.from(containerView.context)
.inflate(R.layout.share_to_apps, containerView, true)
}