parent
f5f0cb8d9c
commit
fe034226a3
|
@ -32,6 +32,7 @@ import kotlinx.coroutines.withContext
|
|||
import mozilla.appservices.places.BookmarkRoot
|
||||
import mozilla.components.browser.session.Session
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import mozilla.components.concept.engine.prompt.ShareData
|
||||
import mozilla.components.feature.accounts.FxaCapability
|
||||
import mozilla.components.feature.accounts.FxaWebChannelFeature
|
||||
import mozilla.components.feature.app.links.AppLinksFeature
|
||||
|
@ -42,6 +43,7 @@ import mozilla.components.feature.downloads.DownloadsFeature
|
|||
import mozilla.components.feature.downloads.manager.FetchDownloadManager
|
||||
import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID
|
||||
import mozilla.components.feature.prompts.PromptFeature
|
||||
import mozilla.components.feature.prompts.share.ShareDelegate
|
||||
import mozilla.components.feature.readerview.ReaderViewFeature
|
||||
import mozilla.components.feature.session.FullScreenFeature
|
||||
import mozilla.components.feature.session.SessionFeature
|
||||
|
@ -58,6 +60,7 @@ import org.mozilla.fenix.Experiments
|
|||
import org.mozilla.fenix.FeatureFlags
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.IntentReceiverActivity
|
||||
import org.mozilla.fenix.NavGraphDirections
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.browser.readermode.DefaultReaderModeController
|
||||
import org.mozilla.fenix.components.FenixSnackbar
|
||||
|
@ -314,6 +317,21 @@ abstract class BaseBrowserFragment : Fragment(), UserInteractionHandler, Session
|
|||
store = store,
|
||||
customTabId = customTabSessionId,
|
||||
fragmentManager = parentFragmentManager,
|
||||
shareDelegate = object : ShareDelegate {
|
||||
override fun showShareSheet(
|
||||
context: Context,
|
||||
shareData: ShareData,
|
||||
onDismiss: () -> Unit,
|
||||
onSuccess: () -> Unit
|
||||
) {
|
||||
val directions = NavGraphDirections.actionGlobalShareFragment(
|
||||
data = arrayOf(shareData),
|
||||
showPage = true,
|
||||
sessionId = getSessionById()?.id
|
||||
)
|
||||
findNavController().navigate(directions)
|
||||
}
|
||||
},
|
||||
onNeedToRequestPermissions = { permissions ->
|
||||
requestPermissions(permissions, REQUEST_CODE_PROMPT_PERMISSIONS)
|
||||
}),
|
||||
|
|
|
@ -9,6 +9,7 @@ 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.net.Uri
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.navigation.NavController
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
@ -21,10 +22,8 @@ import mozilla.components.concept.sync.Device
|
|||
import mozilla.components.concept.sync.TabData
|
||||
import mozilla.components.feature.sendtab.SendTabUseCases
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.FenixSnackbar
|
||||
import org.mozilla.fenix.components.FenixSnackbarPresenter
|
||||
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
|
||||
|
@ -42,6 +41,10 @@ interface ShareController {
|
|||
fun handleShareToDevice(device: Device)
|
||||
fun handleShareToAllDevices(devices: List<Device>)
|
||||
fun handleSignIn()
|
||||
|
||||
enum class Result {
|
||||
DISMISSED, SHARE_ERROR, SUCCESS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -61,17 +64,17 @@ class DefaultShareController(
|
|||
private val sendTabUseCases: SendTabUseCases,
|
||||
private val snackbarPresenter: FenixSnackbarPresenter,
|
||||
private val navController: NavController,
|
||||
private val dismiss: () -> Unit
|
||||
private val dismiss: (ShareController.Result) -> Unit
|
||||
) : ShareController {
|
||||
|
||||
override fun handleReauth() {
|
||||
val directions = ShareFragmentDirections.actionShareFragmentToAccountProblemFragment()
|
||||
navController.nav(R.id.shareFragment, directions)
|
||||
dismiss()
|
||||
dismiss(ShareController.Result.DISMISSED)
|
||||
}
|
||||
|
||||
override fun handleShareClosed() {
|
||||
dismiss()
|
||||
dismiss(ShareController.Result.DISMISSED)
|
||||
}
|
||||
|
||||
override fun handleShareToApp(app: AppShareOption) {
|
||||
|
@ -82,16 +85,14 @@ class DefaultShareController(
|
|||
setClassName(app.packageName, app.activityName)
|
||||
}
|
||||
|
||||
try {
|
||||
val result = try {
|
||||
context.startActivity(intent)
|
||||
ShareController.Result.SUCCESS
|
||||
} catch (e: SecurityException) {
|
||||
context.getRootView()?.let {
|
||||
FenixSnackbar.make(it, Snackbar.LENGTH_LONG)
|
||||
.setText(context.getString(R.string.share_error_snackbar))
|
||||
.show()
|
||||
}
|
||||
snackbarPresenter.present(context.getString(R.string.share_error_snackbar))
|
||||
ShareController.Result.SHARE_ERROR
|
||||
}
|
||||
dismiss()
|
||||
dismiss(result)
|
||||
}
|
||||
|
||||
override fun handleAddNewDevice() {
|
||||
|
@ -112,18 +113,20 @@ class DefaultShareController(
|
|||
context.metrics.track(Event.SignInToSendTab)
|
||||
val directions = ShareFragmentDirections.actionShareFragmentToTurnOnSyncFragment()
|
||||
navController.nav(R.id.shareFragment, directions)
|
||||
dismiss()
|
||||
dismiss(ShareController.Result.DISMISSED)
|
||||
}
|
||||
|
||||
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) {
|
||||
if (shareOperation.invoke().await()) {
|
||||
val result = if (shareOperation.invoke().await()) {
|
||||
showSuccess()
|
||||
ShareController.Result.SUCCESS
|
||||
} else {
|
||||
showFailureWithRetryOption { shareToDevicesWithRetry(shareOperation) }
|
||||
ShareController.Result.DISMISSED
|
||||
}
|
||||
dismiss()
|
||||
dismiss(result)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -161,7 +164,11 @@ class DefaultShareController(
|
|||
|
||||
// Navigation between app fragments uses ShareTab as arguments. SendTabUseCases uses TabData.
|
||||
@VisibleForTesting
|
||||
fun List<ShareData>.toTabData() = map { data ->
|
||||
TabData(data.title.orEmpty(), data.url.orEmpty())
|
||||
internal fun List<ShareData>.toTabData() = map { data ->
|
||||
TabData(title = data.title.orEmpty(), url = data.url ?: data.text?.toDataUri().orEmpty())
|
||||
}
|
||||
|
||||
private fun String.toDataUri(): String {
|
||||
return "data:,${Uri.encode(this)}"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@ import androidx.lifecycle.observe
|
|||
import androidx.navigation.fragment.findNavController
|
||||
import androidx.navigation.fragment.navArgs
|
||||
import kotlinx.android.synthetic.main.fragment_share.view.*
|
||||
import mozilla.components.browser.state.action.ContentAction
|
||||
import mozilla.components.browser.state.selector.findTabOrCustomTab
|
||||
import mozilla.components.concept.engine.prompt.PromptRequest
|
||||
import mozilla.components.feature.sendtab.SendTabUseCases
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.components.FenixSnackbarPresenter
|
||||
|
@ -24,6 +27,7 @@ import org.mozilla.fenix.ext.requireComponents
|
|||
|
||||
class ShareFragment : AppCompatDialogFragment() {
|
||||
|
||||
private val args by navArgs<ShareFragmentArgs>()
|
||||
private val viewModel: ShareViewModel by viewModels {
|
||||
AndroidViewModelFactory(requireActivity().application)
|
||||
}
|
||||
|
@ -37,6 +41,11 @@ class ShareFragment : AppCompatDialogFragment() {
|
|||
viewModel.loadDevicesAndApps()
|
||||
}
|
||||
|
||||
override fun dismiss() {
|
||||
consumePrompt { onDismiss() }
|
||||
super.dismiss()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setStyle(STYLE_NO_TITLE, R.style.ShareDialogStyle)
|
||||
|
@ -48,7 +57,6 @@ class ShareFragment : AppCompatDialogFragment() {
|
|||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(R.layout.fragment_share, container, false)
|
||||
val args by navArgs<ShareFragmentArgs>()
|
||||
val shareData = args.data.toList()
|
||||
|
||||
val accountManager = requireComponents.backgroundServices.accountManager
|
||||
|
@ -59,9 +67,17 @@ class ShareFragment : AppCompatDialogFragment() {
|
|||
shareData = shareData,
|
||||
snackbarPresenter = FenixSnackbarPresenter(activity!!.getRootView()!!),
|
||||
navController = findNavController(),
|
||||
sendTabUseCases = SendTabUseCases(accountManager),
|
||||
dismiss = ::dismiss
|
||||
)
|
||||
sendTabUseCases = SendTabUseCases(accountManager)
|
||||
) { result ->
|
||||
consumePrompt {
|
||||
when (result) {
|
||||
ShareController.Result.DISMISSED -> onDismiss()
|
||||
ShareController.Result.SHARE_ERROR -> onFailure()
|
||||
ShareController.Result.SUCCESS -> onSuccess()
|
||||
}
|
||||
}
|
||||
super.dismiss()
|
||||
}
|
||||
)
|
||||
|
||||
view.shareWrapper.setOnClickListener { shareInteractor.onShareClosed() }
|
||||
|
@ -94,6 +110,25 @@ class ShareFragment : AppCompatDialogFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* If [ShareFragmentArgs.sessionId] is set and the session has a pending Web Share
|
||||
* prompt request, call [consume] then clean up the prompt.
|
||||
*/
|
||||
private fun consumePrompt(
|
||||
consume: PromptRequest.Share.() -> Unit
|
||||
) {
|
||||
val browserStore = requireComponents.core.store
|
||||
args.sessionId
|
||||
?.let { sessionId -> browserStore.state.findTabOrCustomTab(sessionId) }
|
||||
?.let { tab ->
|
||||
val promptRequest = tab.content.promptRequest
|
||||
if (promptRequest is PromptRequest.Share) {
|
||||
consume(promptRequest)
|
||||
browserStore.dispatch(ContentAction.ConsumePromptRequestAction(tab.id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val SHOW_PAGE_ALPHA = 0.6f
|
||||
}
|
||||
|
|
|
@ -63,7 +63,7 @@ class ShareViewModel(application: Application) : AndroidViewModel(application) {
|
|||
|
||||
/**
|
||||
* Load a list of devices and apps into [devicesList] and [appsList].
|
||||
* Should be called when a fragment is attached so the data can be fetched early.
|
||||
* Should be called when the fragment is attached so the data can be fetched early.
|
||||
*/
|
||||
fun loadDevicesAndApps() {
|
||||
val networkRequest = NetworkRequest.Builder().build()
|
||||
|
@ -86,6 +86,9 @@ class ShareViewModel(application: Application) : AndroidViewModel(application) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters the network callback and cleans up.
|
||||
*/
|
||||
override fun onCleared() {
|
||||
connectivityManager?.unregisterNetworkCallback(networkCallback)
|
||||
}
|
||||
|
|
|
@ -562,6 +562,11 @@
|
|||
<action
|
||||
android:id="@+id/action_shareFragment_to_addNewDeviceFragment"
|
||||
app:destination="@id/addNewDeviceFragment" />
|
||||
<argument
|
||||
android:name="sessionId"
|
||||
app:argType="string"
|
||||
app:nullable="true"
|
||||
android:defaultValue="null" />
|
||||
</dialog>
|
||||
<dialog
|
||||
android:id="@+id/quickSettingsSheetDialogFragment"
|
||||
|
|
|
@ -63,7 +63,7 @@ class ShareControllerTest {
|
|||
private val sendTabUseCases = mockk<SendTabUseCases>(relaxed = true)
|
||||
private val snackbarPresenter = mockk<FenixSnackbarPresenter>(relaxed = true)
|
||||
private val navController = mockk<NavController>(relaxed = true)
|
||||
private val dismiss = mockk<() -> Unit>(relaxed = true)
|
||||
private val dismiss = mockk<(ShareController.Result) -> Unit>(relaxed = true)
|
||||
private val controller = DefaultShareController(
|
||||
context, shareData, sendTabUseCases, snackbarPresenter, navController, dismiss
|
||||
)
|
||||
|
@ -77,7 +77,7 @@ class ShareControllerTest {
|
|||
fun `handleShareClosed should call a passed in delegate to close this`() {
|
||||
controller.handleShareClosed()
|
||||
|
||||
verify { dismiss() }
|
||||
verify { dismiss(ShareController.Result.DISMISSED) }
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -95,7 +95,7 @@ class ShareControllerTest {
|
|||
|
||||
testController.handleShareToApp(appShareOption)
|
||||
|
||||
// Check that the Intent used for querying apps has the expected structre
|
||||
// Check that the Intent used for querying apps has the expected structure
|
||||
assertAll {
|
||||
assertThat(shareIntent.isCaptured).isTrue()
|
||||
assertThat(shareIntent.captured.action).isEqualTo(Intent.ACTION_SEND)
|
||||
|
@ -107,7 +107,30 @@ class ShareControllerTest {
|
|||
}
|
||||
verifyOrder {
|
||||
activityContext.startActivity(shareIntent.captured)
|
||||
dismiss()
|
||||
dismiss(ShareController.Result.SUCCESS)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `handleShareToApp should dismiss with an error start when a security exception occurs`() {
|
||||
val appPackageName = "package"
|
||||
val appClassName = "activity"
|
||||
val appShareOption = AppShareOption("app", mockk(), appPackageName, appClassName)
|
||||
val shareIntent = slot<Intent>()
|
||||
// Our share Intent uses `FLAG_ACTIVITY_NEW_TASK` but when resolving the startActivity call
|
||||
// needed for capturing the actual Intent used the `slot` one doesn't have this flag so we
|
||||
// need to use an Activity Context.
|
||||
val activityContext: Context = mockk<Activity>()
|
||||
val testController = DefaultShareController(activityContext, shareData, mockk(), snackbarPresenter, mockk(), dismiss)
|
||||
every { activityContext.startActivity(capture(shareIntent)) } throws SecurityException()
|
||||
every { activityContext.getString(R.string.share_error_snackbar) } returns "Cannot share to this app"
|
||||
|
||||
testController.handleShareToApp(appShareOption)
|
||||
|
||||
verifyOrder {
|
||||
activityContext.startActivity(shareIntent.captured)
|
||||
snackbarPresenter.present("Cannot share to this app")
|
||||
dismiss(ShareController.Result.SHARE_ERROR)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -167,7 +190,7 @@ class ShareControllerTest {
|
|||
R.id.shareFragment,
|
||||
ShareFragmentDirections.actionShareFragmentToTurnOnSyncFragment()
|
||||
)
|
||||
dismiss()
|
||||
dismiss(ShareController.Result.DISMISSED)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -180,7 +203,7 @@ class ShareControllerTest {
|
|||
R.id.shareFragment,
|
||||
ShareFragmentDirections.actionShareFragmentToAccountProblemFragment()
|
||||
)
|
||||
dismiss()
|
||||
dismiss(ShareController.Result.DISMISSED)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -276,4 +299,22 @@ class ShareControllerTest {
|
|||
|
||||
assertThat(tabData).isEqualTo(tabsData)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ShareTab#toTabData creates a data url from text if no url is specified`() {
|
||||
var tabData: List<TabData>
|
||||
val expected = listOf(
|
||||
TabData(title = "title0", url = ""),
|
||||
TabData(title = "title1", url = "data:,Hello%2C%20World!")
|
||||
)
|
||||
|
||||
with(controller) {
|
||||
tabData = listOf(
|
||||
ShareData(title = "title0"),
|
||||
ShareData(title = "title1", text = "Hello, World!")
|
||||
).toTabData()
|
||||
}
|
||||
|
||||
assertThat(tabData).isEqualTo(expected)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user