From eb7646f073d4a48c73bb76c2494a614ab5df1983 Mon Sep 17 00:00:00 2001 From: Jonathan Almeida Date: Thu, 23 May 2019 13:48:22 -0400 Subject: [PATCH] Add custom share sheet and send tab support (#2757) * Closes #2751: Add custom app share sheet * Closes #2753: Add send tab devices to share sheet * Closes #2752: Add build flag for send tab * Replace Context.share with ShareFragment --- app/build.gradle | 5 + .../mozilla/fenix/browser/BrowserFragment.kt | 10 +- .../java/org/mozilla/fenix/ext/Context.kt | 1 + .../org/mozilla/fenix/home/HomeFragment.kt | 12 +- .../library/bookmarks/BookmarkFragment.kt | 6 +- .../fenix/library/history/HistoryFragment.kt | 10 +- .../fenix/share/AccountDevicesShareView.kt | 163 ++++++++++++++++++ .../org/mozilla/fenix/share/AppShareView.kt | 129 ++++++++++++++ .../org/mozilla/fenix/share/ShareComponent.kt | 56 ++++++ .../org/mozilla/fenix/share/ShareFragment.kt | 96 +++++++++++ .../org/mozilla/fenix/share/ShareUIView.kt | 54 ++++++ .../main/res/drawable/device_background.xml | 10 ++ .../res/layout/account_share_list_item.xml | 43 +++++ .../main/res/layout/app_share_list_item.xml | 42 +++++ app/src/main/res/layout/component_share.xml | 127 ++++++++++++++ app/src/main/res/layout/fragment_share.xml | 16 ++ app/src/main/res/navigation/nav_graph.xml | 21 +++ app/src/main/res/values/colors.xml | 7 + app/src/main/res/values/strings.xml | 18 ++ 19 files changed, 814 insertions(+), 12 deletions(-) create mode 100644 app/src/main/java/org/mozilla/fenix/share/AccountDevicesShareView.kt create mode 100644 app/src/main/java/org/mozilla/fenix/share/AppShareView.kt create mode 100644 app/src/main/java/org/mozilla/fenix/share/ShareComponent.kt create mode 100644 app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt create mode 100644 app/src/main/java/org/mozilla/fenix/share/ShareUIView.kt create mode 100644 app/src/main/res/drawable/device_background.xml create mode 100644 app/src/main/res/layout/account_share_list_item.xml create mode 100644 app/src/main/res/layout/app_share_list_item.xml create mode 100644 app/src/main/res/layout/component_share.xml create mode 100644 app/src/main/res/layout/fragment_share.xml diff --git a/app/build.gradle b/app/build.gradle index fbdfec7b7..d55ad67ec 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -237,6 +237,11 @@ android.applicationVariants.all { variant -> buildConfigField 'String', 'LEANPLUM_TOKEN', 'null' println("X_X") } + +// ------------------------------------------------------------------------------------------------- +// Feature build flags +// ------------------------------------------------------------------------------------------------- + buildConfigField 'Boolean', 'SEND_TAB_ENABLED', "false" } androidExtensions { diff --git a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt index 0b35d4ea7..1ff213370 100644 --- a/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/browser/BrowserFragment.kt @@ -75,7 +75,6 @@ import org.mozilla.fenix.components.toolbar.ToolbarViewModel import org.mozilla.fenix.customtabs.CustomTabsIntegration import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents -import org.mozilla.fenix.ext.share import org.mozilla.fenix.ext.urlToTrimmedHost import org.mozilla.fenix.home.sessioncontrol.Tab import org.mozilla.fenix.lib.Do @@ -439,7 +438,7 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope { is QuickActionAction.SharePressed -> { requireComponents.analytics.metrics.track(Event.QuickActionSheetShareTapped) getSessionById()?.let { session -> - session.url.apply { requireContext().share(this) } + shareUrl(session.url) } } is QuickActionAction.DownloadsPressed -> { @@ -616,7 +615,7 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope { is ToolbarMenu.Item.RequestDesktop -> sessionUseCases.requestDesktopSite.invoke(action.item.isChecked) ToolbarMenu.Item.Share -> getSessionById()?.let { session -> session.url.apply { - requireContext().share(this) + shareUrl(this) } } ToolbarMenu.Item.NewPrivateTab -> { @@ -766,6 +765,11 @@ class BrowserFragment : Fragment(), BackHandler, CoroutineScope { } } + private fun shareUrl(url: String) { + val directions = BrowserFragmentDirections.actionBrowserFragmentToShareFragment(url) + Navigation.findNavController(view!!).navigate(directions) + } + companion object { private const val REQUEST_CODE_DOWNLOAD_PERMISSIONS = 1 private const val REQUEST_CODE_PROMPT_PERMISSIONS = 2 diff --git a/app/src/main/java/org/mozilla/fenix/ext/Context.kt b/app/src/main/java/org/mozilla/fenix/ext/Context.kt index 452133401..01f83729e 100644 --- a/app/src/main/java/org/mozilla/fenix/ext/Context.kt +++ b/app/src/main/java/org/mozilla/fenix/ext/Context.kt @@ -51,6 +51,7 @@ fun Context.getPreferenceKey(@StringRes resourceId: Int): String = * @param subject of the intent [EXTRA_TEXT] * @return true it is able to share false otherwise. */ +@Deprecated("We are replacing the system share sheet with a custom version. See: [ShareFragment]") fun Context.share(text: String, subject: String = ""): Boolean { return try { val intent = Intent(ACTION_SEND).apply { diff --git a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt index a03672ea0..280c3bc7a 100644 --- a/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/home/HomeFragment.kt @@ -45,7 +45,6 @@ import org.mozilla.fenix.collections.SaveCollectionStep import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.allowUndo import org.mozilla.fenix.ext.requireComponents -import org.mozilla.fenix.ext.share import org.mozilla.fenix.ext.urlToTrimmedHost import org.mozilla.fenix.home.sessioncontrol.CollectionAction import org.mozilla.fenix.home.sessioncontrol.Mode @@ -302,7 +301,7 @@ class HomeFragment : Fragment(), CoroutineScope, AccountObserver { invokePendingDeleteJobs() requireComponents.core.sessionManager.findSessionById(action.sessionId) ?.let { session -> - requireContext().share(session.url) + share(session.url) } } is TabAction.CloseAll -> { @@ -326,7 +325,7 @@ class HomeFragment : Fragment(), CoroutineScope, AccountObserver { val shareText = requireComponents.core.sessionManager.sessions.joinToString("\n") { it.url } - requireContext().share(shareText) + share(shareText) } } } @@ -397,7 +396,7 @@ class HomeFragment : Fragment(), CoroutineScope, AccountObserver { val shareText = action.collection.tabs.joinToString("\n") { it.url } - requireContext().share(shareText) + share(shareText) } is CollectionAction.RemoveTab -> { launch(Dispatchers.IO) { @@ -648,6 +647,11 @@ class HomeFragment : Fragment(), CoroutineScope, AccountObserver { } } + private fun share(text: String) { + val directions = HomeFragmentDirections.actionHomeFragmentToShareFragment(text) + Navigation.findNavController(view!!).navigate(directions) + } + private fun currentMode(): Mode = if (!onboarding.userHasBeenOnboarded()) { val account = requireComponents.backgroundServices.accountManager.authenticatedAccount() if (account == null) { diff --git a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt index 81d79efc4..7e2b4d8ea 100644 --- a/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/bookmarks/BookmarkFragment.kt @@ -46,7 +46,6 @@ import org.mozilla.fenix.components.metrics.Event import org.mozilla.fenix.ext.allowUndo import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents -import org.mozilla.fenix.ext.share import org.mozilla.fenix.ext.urlToTrimmedHost import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.getAutoDisposeObservable @@ -207,7 +206,10 @@ class BookmarkFragment : Fragment(), CoroutineScope, BackHandler, AccountObserve } is BookmarkAction.Share -> { it.item.url?.apply { - requireContext().share(this) + navigation + .navigate( + BookmarkFragmentDirections.actionBookmarkFragmentToShareFragment(this) + ) requireComponents.analytics.metrics.track(Event.ShareBookmark) } } diff --git a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt index 27ea339c3..6bfa43af2 100644 --- a/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt +++ b/app/src/main/java/org/mozilla/fenix/library/history/HistoryFragment.kt @@ -38,7 +38,6 @@ import org.mozilla.fenix.R import org.mozilla.fenix.components.Components import org.mozilla.fenix.ext.components import org.mozilla.fenix.ext.requireComponents -import org.mozilla.fenix.ext.share import org.mozilla.fenix.mvi.ActionBusFactory import org.mozilla.fenix.mvi.getAutoDisposeObservable import org.mozilla.fenix.mvi.getManagedEmitter @@ -182,12 +181,12 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler { R.id.share_history_multi_select -> { val selectedHistory = (historyComponent.uiView as HistoryUIView).getSelected() when { - selectedHistory.size == 1 -> context?.share(selectedHistory.first().url) + selectedHistory.size == 1 -> share(selectedHistory.first().url) selectedHistory.size > 1 -> { val shareText = selectedHistory.joinToString("\n") { it.url } - requireContext().share(shareText) + share(shareText) } } true @@ -283,4 +282,9 @@ class HistoryFragment : Fragment(), CoroutineScope, BackHandler { components.core.historyStorage.deleteVisit(it.url, it.visitedAt) } } + + private fun share(text: String) { + val directions = HistoryFragmentDirections.actionHistoryFragmentToShareFragment(text) + Navigation.findNavController(view!!).navigate(directions) + } } diff --git a/app/src/main/java/org/mozilla/fenix/share/AccountDevicesShareView.kt b/app/src/main/java/org/mozilla/fenix/share/AccountDevicesShareView.kt new file mode 100644 index 000000000..0d93ed036 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/share/AccountDevicesShareView.kt @@ -0,0 +1,163 @@ +/* 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 +import android.util.AttributeSet +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.LinearLayoutManager +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 AccountDevicesShareRecyclerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RecyclerView(context, attrs, defStyleAttr) { + + init { + layoutManager = LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) + } +} + +class AccountDevicesShareAdapter( + private val context: Context, + val actionEmitter: Observer +) : RecyclerView.Adapter() { + + 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 { + val list = mutableListOf() + val accountManager = context.components.backgroundServices.accountManager + + if (accountManager.authenticatedAccount() == null) { + list.add(SyncShareOption.SignIn) + return list + } + + list.add(SyncShareOption.AddNewDevice) + + accountManager.authenticatedAccount()?.deviceConstellation()?.state()?.otherDevices?.let { devices -> + val shareableDevices = devices + .filter { + it.capabilities.contains(DeviceCapability.SEND_TAB) + } + 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 +) : 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.device_icon.apply { + setImageResource(drawableRes) + background.setColorFilter(ContextCompat.getColor(context, colorRes), PorterDuff.Mode.SRC_IN) + drawable.setTint(ContextCompat.getColor(context, R.color.device_foreground)) + } + itemView.device_name.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) : SyncShareOption() + data class Mobile(val name: String, val device: Device) : SyncShareOption() + data class Desktop(val name: String, val device: Device) : SyncShareOption() +} diff --git a/app/src/main/java/org/mozilla/fenix/share/AppShareView.kt b/app/src/main/java/org/mozilla/fenix/share/AppShareView.kt new file mode 100644 index 000000000..13e2e13ec --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/share/AppShareView.kt @@ -0,0 +1,129 @@ +/* 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.util.AttributeSet +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.GridLayoutManager +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.Job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.mozilla.fenix.R +import kotlin.coroutines.CoroutineContext + +class AppShareRecyclerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : RecyclerView(context, attrs, defStyleAttr) { + + init { + layoutManager = GridLayoutManager(context, 2, GridLayoutManager.HORIZONTAL, false) + } +} + +class AppShareAdapter( + private val context: Context, + val actionEmitter: Observer, + private val intentType: String = "text/plain" +) : RecyclerView.Adapter(), CoroutineScope { + + private var job: Job = Job() + override val coroutineContext: CoroutineContext + get() = Dispatchers.IO + job + private var size: Int = 0 + private val shareItems: MutableList = mutableListOf() + + init { + val testIntent = Intent(ACTION_SEND).apply { + type = intentType + flags = FLAG_ACTIVITY_NEW_TASK + } + + 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 + ) + } + + 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 onAttachedToRecyclerView(recyclerView: RecyclerView) { + super.onAttachedToRecyclerView(recyclerView) + job = Job() + } + + override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) { + super.onDetachedFromRecyclerView(recyclerView) + job.cancel() + } +} + +class AppShareItemViewHolder( + itemView: View, + actionEmitter: Observer +) : RecyclerView.ViewHolder(itemView) { + + private var shareItem: ShareItem? = null + + init { + itemView.setOnClickListener { + Log.d("Jonathan", "${shareItem?.name} clicked.") + shareItem?.let { + actionEmitter.onNext(ShareAction.ShareAppClicked(it.packageName)) + } + } + } + + internal fun bind(item: ShareItem) { + shareItem = item + itemView.app_name.text = item.name + itemView.app_icon.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) diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareComponent.kt b/app/src/main/java/org/mozilla/fenix/share/ShareComponent.kt new file mode 100644 index 000000000..e6584d36e --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/share/ShareComponent.kt @@ -0,0 +1,56 @@ +package org.mozilla.fenix.share + +/* 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/. */ + +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) : ShareAction() + data class ShareAppClicked(val packageName: String) : ShareAction() +} + +class ShareComponent( + private val container: ViewGroup, + bus: ActionBusFactory, + viewModelProvider: UIComponentViewModelProvider +) : UIComponent( + 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( + initialState, + reducer +) { + companion object { + val reducer: Reducer = { _, _ -> ShareState } + } +} diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt b/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt new file mode 100644 index 000000000..02d6fb6e9 --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt @@ -0,0 +1,96 @@ +package org.mozilla.fenix.share + +/* 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/. */ + +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.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import kotlinx.android.synthetic.main.fragment_share.view.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import org.mozilla.fenix.FenixViewModelProvider +import org.mozilla.fenix.R +import org.mozilla.fenix.ext.requireComponents +import org.mozilla.fenix.mvi.ActionBusFactory +import org.mozilla.fenix.mvi.getAutoDisposeObservable +import kotlin.coroutines.CoroutineContext + +class ShareFragment : DialogFragment(), CoroutineScope { + override val coroutineContext: CoroutineContext + get() = Dispatchers.Main + job + private lateinit var job: Job + private lateinit var component: ShareComponent + private lateinit var url: String + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setStyle(STYLE_NO_TITLE, R.style.CreateCollectionDialogStyle) + } + + 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!!) + + job = Job() + url = args.url + component = ShareComponent( + view.share_wrapper, + ActionBusFactory.get(this), + FenixViewModelProvider.create( + this, + ShareUIViewModel::class.java + ) { + ShareUIViewModel(ShareState) + } + ) + + return view + } + + override fun onResume() { + super.onResume() + subscribeToActions() + } + + override fun onDestroyView() { + super.onDestroyView() + job.cancel() + } + + private fun subscribeToActions() { + getAutoDisposeObservable().subscribe { + when (it) { + ShareAction.Close -> { + dismiss() + } + ShareAction.AddNewDeviceClicked -> { + requireComponents.useCases.tabsUseCases.addTab.invoke(ADD_NEW_DEVICES_URL, true) + } + is ShareAction.ShareAppClicked -> { + val intent = Intent(ACTION_SEND).apply { + putExtra(EXTRA_TEXT, url) + type = "text/plain" + flags = FLAG_ACTIVITY_NEW_TASK + `package` = it.packageName + } + startActivity(intent) + } + // TODO support other actions in a follow-up issue + } + dismiss() + } + } + + companion object { + const val ADD_NEW_DEVICES_URL = "https://accounts.firefox.com/connect_another_device" + } +} diff --git a/app/src/main/java/org/mozilla/fenix/share/ShareUIView.kt b/app/src/main/java/org/mozilla/fenix/share/ShareUIView.kt new file mode 100644 index 000000000..8ce85ec5f --- /dev/null +++ b/app/src/main/java/org/mozilla/fenix/share/ShareUIView.kt @@ -0,0 +1,54 @@ +/* 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.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import io.reactivex.Observable +import io.reactivex.Observer +import io.reactivex.functions.Consumer +import kotlinx.android.synthetic.main.component_share.* +import org.mozilla.fenix.BuildConfig +import org.mozilla.fenix.R +import org.mozilla.fenix.mvi.UIView + +class ShareUIView( + container: ViewGroup, + actionEmitter: Observer, + changesObservable: Observable +) : UIView( + container, + actionEmitter, + changesObservable +) { + override val view: View = LayoutInflater.from(container.context) + .inflate(R.layout.component_share, container, true) + + init { + val adapter = AppShareAdapter(view.context, actionEmitter).also { + it.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onChanged() { + progress_bar.visibility = View.GONE + intent_handler_recyclerview.visibility = View.VISIBLE + } + }) + } + intent_handler_recyclerview.adapter = adapter + + if (BuildConfig.SEND_TAB_ENABLED) { + account_devices_recyclerview.adapter = AccountDevicesShareAdapter(view.context, actionEmitter) + } else { + send_tab_group.visibility = View.GONE + } + + close_button.setOnClickListener { actionEmitter.onNext(ShareAction.Close) } + } + + override fun updateView() = Consumer { + ShareState + } +} diff --git a/app/src/main/res/drawable/device_background.xml b/app/src/main/res/drawable/device_background.xml new file mode 100644 index 000000000..8cc5ad492 --- /dev/null +++ b/app/src/main/res/drawable/device_background.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/account_share_list_item.xml b/app/src/main/res/layout/account_share_list_item.xml new file mode 100644 index 000000000..6cb3c019e --- /dev/null +++ b/app/src/main/res/layout/account_share_list_item.xml @@ -0,0 +1,43 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/app_share_list_item.xml b/app/src/main/res/layout/app_share_list_item.xml new file mode 100644 index 000000000..de393c34b --- /dev/null +++ b/app/src/main/res/layout/app_share_list_item.xml @@ -0,0 +1,42 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/component_share.xml b/app/src/main/res/layout/component_share.xml new file mode 100644 index 000000000..1904a5554 --- /dev/null +++ b/app/src/main/res/layout/component_share.xml @@ -0,0 +1,127 @@ + + + + +