Skip to content
GitLab
Menu
Projects
Groups
Snippets
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
Matthew Finkel
fenix
Commits
fe034226
Unverified
Commit
fe034226
authored
Dec 10, 2019
by
Tiger Oakes
Committed by
GitHub
Dec 10, 2019
Browse files
For #5783 - Web Share with Fenix share sheet (#6883)
parent
f5f0cb8d
Changes
6
Hide whitespace changes
Inline
Side-by-side
app/src/main/java/org/mozilla/fenix/browser/BaseBrowserFragment.kt
View file @
fe034226
...
...
@@ -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
)
}),
...
...
app/src/main/java/org/mozilla/fenix/share/ShareController.kt
View file @
fe034226
...
...
@@ -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)}"
}
}
app/src/main/java/org/mozilla/fenix/share/ShareFragment.kt
View file @
fe034226
...
...
@@ -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
}
...
...
app/src/main/java/org/mozilla/fenix/share/ShareViewModel.kt
View file @
fe034226
...
...
@@ -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
)
}
...
...
app/src/main/res/navigation/nav_graph.xml
View file @
fe034226
...
...
@@ -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"
...
...
app/src/test/java/org/mozilla/fenix/share/ShareControllerTest.kt
View file @
fe034226
...
...
@@ -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 struct
u
re
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
)
}
}
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment