Skip to content

Commit a9044cf

Browse files
For mozilla-mobile#9417 - Add support for sharing actual website images (mozilla-mobile#9420)
Prior to this when the user selected to share an image from the contextual menu the apps would only share the URL, not the actual resource. This patch adds a new `ShareDownloadFeature` that will listen for `AddShareAction` and download, cache locally and then share the Internet resource contained in Action's state. Giving the time needed to actually download these resources this feature is only used for image sharing, not for other types of potentially bigger resource types. This is a breaking change with clients expected to create and register a new instance of the this new feature otherwise the "Share image" from the browser contextual menu will do nothing. Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent 7db67c0 commit a9044cf

File tree

18 files changed

+995
-23
lines changed

18 files changed

+995
-23
lines changed

components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,17 +17,18 @@ import mozilla.components.browser.state.state.EngineState
1717
import mozilla.components.browser.state.state.LoadRequestState
1818
import mozilla.components.browser.state.state.MediaSessionState
1919
import mozilla.components.browser.state.state.MediaState
20-
import mozilla.components.browser.state.state.content.PermissionHighlightsState
2120
import mozilla.components.browser.state.state.ReaderState
21+
import mozilla.components.browser.state.state.SearchState
2222
import mozilla.components.browser.state.state.SecurityInfoState
2323
import mozilla.components.browser.state.state.SessionState
2424
import mozilla.components.browser.state.state.TabSessionState
2525
import mozilla.components.browser.state.state.TrackingProtectionState
26+
import mozilla.components.browser.state.state.UndoHistoryState
2627
import mozilla.components.browser.state.state.WebExtensionState
2728
import mozilla.components.browser.state.state.content.DownloadState
2829
import mozilla.components.browser.state.state.content.FindResultState
29-
import mozilla.components.browser.state.state.SearchState
30-
import mozilla.components.browser.state.state.UndoHistoryState
30+
import mozilla.components.browser.state.state.content.PermissionHighlightsState
31+
import mozilla.components.browser.state.state.content.ShareInternetResourceState
3132
import mozilla.components.browser.state.state.recover.RecoverableTab
3233
import mozilla.components.concept.engine.Engine
3334
import mozilla.components.concept.engine.EngineSession
@@ -38,8 +39,8 @@ import mozilla.components.concept.engine.history.HistoryItem
3839
import mozilla.components.concept.engine.manifest.WebAppManifest
3940
import mozilla.components.concept.engine.media.Media
4041
import mozilla.components.concept.engine.media.RecordingDevice
41-
import mozilla.components.concept.engine.permission.PermissionRequest
4242
import mozilla.components.concept.engine.mediasession.MediaSession
43+
import mozilla.components.concept.engine.permission.PermissionRequest
4344
import mozilla.components.concept.engine.prompt.PromptRequest
4445
import mozilla.components.concept.engine.search.SearchRequest
4546
import mozilla.components.concept.engine.webextension.WebExtensionBrowserAction
@@ -1035,6 +1036,28 @@ sealed class DownloadAction : BrowserAction() {
10351036
data class RestoreDownloadStateAction(val download: DownloadState) : DownloadAction()
10361037
}
10371038

1039+
/**
1040+
* [BrowserAction] implementations related to updating the session state of internet resources to be shared.
1041+
*/
1042+
sealed class ShareInternetResourceAction : BrowserAction() {
1043+
/**
1044+
* Starts the sharing process of an Internet resource.
1045+
*/
1046+
data class AddShareAction(
1047+
val tabId: String,
1048+
val internetResource: ShareInternetResourceState
1049+
) : ShareInternetResourceAction()
1050+
1051+
/**
1052+
* Previous share request is considered completed.
1053+
* File was successfully shared with other apps / user may have aborted the process or the operation
1054+
* may have failed. In either case the previous share request is considered completed.
1055+
*/
1056+
data class ConsumeShareAction(
1057+
val tabId: String
1058+
) : ShareInternetResourceAction()
1059+
}
1060+
10381061
/**
10391062
* [BrowserAction] implementations related to updating [BrowserState.containers]
10401063
*/

components/browser/state/src/main/java/mozilla/components/browser/state/reducer/BrowserStateReducer.kt

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,16 @@ import mozilla.components.browser.state.action.CustomTabListAction
1212
import mozilla.components.browser.state.action.DownloadAction
1313
import mozilla.components.browser.state.action.EngineAction
1414
import mozilla.components.browser.state.action.InitAction
15+
import mozilla.components.browser.state.action.LastAccessAction
1516
import mozilla.components.browser.state.action.MediaAction
17+
import mozilla.components.browser.state.action.MediaSessionAction
1618
import mozilla.components.browser.state.action.ReaderAction
19+
import mozilla.components.browser.state.action.RecentlyClosedAction
20+
import mozilla.components.browser.state.action.RestoreCompleteAction
1721
import mozilla.components.browser.state.action.SearchAction
22+
import mozilla.components.browser.state.action.ShareInternetResourceAction
1823
import mozilla.components.browser.state.action.SystemAction
1924
import mozilla.components.browser.state.action.TabListAction
20-
import mozilla.components.browser.state.action.LastAccessAction
21-
import mozilla.components.browser.state.action.MediaSessionAction
22-
import mozilla.components.browser.state.action.RecentlyClosedAction
23-
import mozilla.components.browser.state.action.RestoreCompleteAction
2425
import mozilla.components.browser.state.action.TrackingProtectionAction
2526
import mozilla.components.browser.state.action.UndoAction
2627
import mozilla.components.browser.state.action.WebExtensionAction
@@ -59,6 +60,7 @@ internal object BrowserStateReducer {
5960
is CrashAction -> CrashReducer.reduce(state, action)
6061
is LastAccessAction -> LastAccessReducer.reduce(state, action)
6162
is UndoAction -> UndoReducer.reduce(state, action)
63+
is ShareInternetResourceAction -> ShareInternetResourceStateReducer.reduce(state, action)
6264
}
6365
}
6466
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.browser.state.reducer
6+
7+
import androidx.annotation.VisibleForTesting
8+
import mozilla.components.browser.state.action.ShareInternetResourceAction
9+
import mozilla.components.browser.state.state.BrowserState
10+
import mozilla.components.browser.state.state.ContentState
11+
12+
internal object ShareInternetResourceStateReducer {
13+
fun reduce(state: BrowserState, action: ShareInternetResourceAction): BrowserState {
14+
return when (action) {
15+
is ShareInternetResourceAction.AddShareAction -> updateTheContentState(state, action.tabId) {
16+
it.copy(share = action.internetResource)
17+
}
18+
is ShareInternetResourceAction.ConsumeShareAction -> updateTheContentState(state, action.tabId) {
19+
it.copy(share = null)
20+
}
21+
}
22+
}
23+
}
24+
25+
@VisibleForTesting
26+
internal inline fun updateTheContentState(
27+
state: BrowserState,
28+
tabId: String,
29+
crossinline update: (ContentState) -> ContentState
30+
): BrowserState {
31+
return state.updateTabState(tabId) { current ->
32+
current.createCopy(content = update(current.content))
33+
}
34+
}

components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import mozilla.components.browser.state.state.content.DownloadState
99
import mozilla.components.browser.state.state.content.FindResultState
1010
import mozilla.components.browser.state.state.content.HistoryState
1111
import mozilla.components.browser.state.state.content.PermissionHighlightsState
12+
import mozilla.components.browser.state.state.content.ShareInternetResourceState
1213
import mozilla.components.concept.engine.HitResult
1314
import mozilla.components.concept.engine.manifest.WebAppManifest
1415
import mozilla.components.concept.engine.media.RecordingDevice
@@ -33,6 +34,7 @@ import mozilla.components.concept.engine.window.WindowRequest
3334
* be used as a preview in e.g. a tab switcher.
3435
* @property icon the icon of the page currently loaded by this session.
3536
* @property download Last unhandled download request.
37+
* @property share Last unhandled request to share an internet resource that first needs to be downloaded.
3638
* @property hitResult the target of the latest long click operation.
3739
* @property promptRequest the last received [PromptRequest].
3840
* @property findResults the list of results of the latest "find in page" operation.
@@ -70,6 +72,7 @@ data class ContentState(
7072
val thumbnail: Bitmap? = null,
7173
val icon: Bitmap? = null,
7274
val download: DownloadState? = null,
75+
val share: ShareInternetResourceState? = null,
7376
val hitResult: HitResult? = null,
7477
val promptRequest: PromptRequest? = null,
7578
val findResults: List<FindResultState> = emptyList(),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.browser.state.state.content
6+
7+
import mozilla.components.concept.fetch.Response
8+
9+
/**
10+
* Value type that represents an Internet resource selected to be shared.
11+
*
12+
* @property url The full url to the content that should be shared.
13+
* @property contentType Content type (MIME type) to indicate the media type of the resource.
14+
* @property private Indicates if the share operation initiated from a private session.
15+
* @property response A response object associated with this request, when provided can be
16+
* used instead of performing a manual a download.
17+
*/
18+
data class ShareInternetResourceState(
19+
val url: String,
20+
val contentType: String? = null,
21+
val private: Boolean = false,
22+
val response: Response? = null
23+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/* This Source Code Form is subject to the terms of the Mozilla Public
2+
* License, v. 2.0. If a copy of the MPL was not distributed with this
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
package mozilla.components.browser.state.reducer
6+
7+
import mozilla.components.browser.state.action.ShareInternetResourceAction
8+
import mozilla.components.browser.state.state.BrowserState
9+
import mozilla.components.browser.state.state.ContentState
10+
import mozilla.components.browser.state.state.TabSessionState
11+
import mozilla.components.browser.state.state.content.ShareInternetResourceState
12+
import mozilla.components.concept.fetch.Response
13+
import mozilla.components.support.test.mock
14+
import org.junit.Assert.assertEquals
15+
import org.junit.Assert.assertFalse
16+
import org.junit.Assert.assertNotNull
17+
import org.junit.Assert.assertNull
18+
import org.junit.Assert.assertTrue
19+
import org.junit.Test
20+
21+
class ShareInternetResourceStateReducerTest {
22+
23+
@Test
24+
fun `reduce - AddShareAction should add the internetResource in the ContentState`() {
25+
val reducer = ShareInternetResourceStateReducer
26+
val state = BrowserState(tabs = listOf(TabSessionState("tabId", ContentState("contentStateUrl"))))
27+
val response: Response = mock()
28+
val action = ShareInternetResourceAction.AddShareAction(
29+
"tabId",
30+
ShareInternetResourceState("internetResourceUrl", "type", true, response)
31+
)
32+
33+
assertNull(state.tabs[0].content.share)
34+
35+
val result = reducer.reduce(state, action)
36+
37+
val shareState = result.tabs[0].content.share!!
38+
assertEquals("internetResourceUrl", shareState.url)
39+
assertEquals("type", shareState.contentType)
40+
assertTrue(shareState.private)
41+
assertEquals(response, shareState.response)
42+
}
43+
44+
@Test
45+
fun `reduce - ConsumeShareAction should remove the ShareInternetResourceState ContentState`() {
46+
val reducer = ShareInternetResourceStateReducer
47+
val shareState: ShareInternetResourceState = mock()
48+
val state = BrowserState(
49+
tabs = listOf(
50+
TabSessionState("tabId", ContentState("contentStateUrl", share = shareState))
51+
)
52+
)
53+
val action = ShareInternetResourceAction.ConsumeShareAction("tabId")
54+
55+
assertNotNull(state.tabs[0].content.share)
56+
57+
val result = reducer.reduce(state, action)
58+
59+
assertNull(result.tabs[0].content.share)
60+
}
61+
62+
@Test
63+
fun `updateTheContentState will return a new BrowserState with updated ContentState`() {
64+
val initialContentState = ContentState("emptyStateUrl")
65+
val browserState = BrowserState(tabs = listOf(TabSessionState("tabId", initialContentState)))
66+
67+
val result = updateTheContentState(browserState, "tabId") { it.copy(url = "updatedUrl") }
68+
69+
assertFalse(browserState == result)
70+
assertEquals("updatedUrl", result.tabs[0].content.url)
71+
}
72+
}

components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuCandidate.kt

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@ import android.view.View
1212
import com.google.android.material.snackbar.Snackbar
1313
import mozilla.components.browser.state.state.SessionState
1414
import mozilla.components.browser.state.state.content.DownloadState
15+
import mozilla.components.browser.state.state.content.ShareInternetResourceState
1516
import mozilla.components.concept.engine.HitResult
16-
import mozilla.components.feature.tabs.TabsUseCases
1717
import mozilla.components.feature.app.links.AppLinksUseCases
1818
import mozilla.components.feature.contextmenu.ContextMenuCandidate.Companion.MAX_TITLE_LENGTH
19+
import mozilla.components.feature.tabs.TabsUseCases
1920
import mozilla.components.support.ktx.android.content.addContact
2021
import mozilla.components.support.ktx.android.content.share
2122
import mozilla.components.support.ktx.kotlin.stripMailToProtocol
@@ -69,7 +70,7 @@ data class ContextMenuCandidate(
6970
createCopyLinkCandidate(context, snackBarParentView, snackbarDelegate),
7071
createDownloadLinkCandidate(context, contextMenuUseCases),
7172
createShareLinkCandidate(context),
72-
createShareImageCandidate(context),
73+
createShareImageCandidate(context, contextMenuUseCases),
7374
createOpenImageInNewTabCandidate(
7475
context,
7576
tabsUseCases,
@@ -335,12 +336,20 @@ data class ContextMenuCandidate(
335336
*/
336337
fun createShareImageCandidate(
337338
context: Context,
338-
action: (SessionState, HitResult) -> Unit = { _, hitResult -> context.share(hitResult.src) }
339+
contextMenuUseCases: ContextMenuUseCases
339340
) = ContextMenuCandidate(
340341
id = "mozac.feature.contextmenu.share_image",
341342
label = context.getString(R.string.mozac_feature_contextmenu_share_image),
342343
showFor = { _, hitResult -> hitResult.isImage() },
343-
action = action
344+
action = { tab, hitResult ->
345+
contextMenuUseCases.injectShareFromInternet(
346+
tab.id,
347+
ShareInternetResourceState(
348+
url = hitResult.src,
349+
private = tab.content.private
350+
)
351+
)
352+
}
344353
)
345354

346355
/**

components/feature/contextmenu/src/main/java/mozilla/components/feature/contextmenu/ContextMenuUseCases.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
package mozilla.components.feature.contextmenu
66

77
import mozilla.components.browser.state.action.ContentAction
8+
import mozilla.components.browser.state.action.ShareInternetResourceAction
89
import mozilla.components.browser.state.state.content.DownloadState
10+
import mozilla.components.browser.state.state.content.ShareInternetResourceState
911
import mozilla.components.browser.state.store.BrowserStore
1012
import mozilla.components.concept.engine.HitResult
1113

@@ -44,6 +46,21 @@ class ContextMenuUseCases(
4446
}
4547
}
4648

49+
/**
50+
* Usecase allowing adding a new [ShareInternetResourceState] to the [BrowserStore]
51+
*/
52+
class InjectShareInternetResourceUseCase(
53+
private val store: BrowserStore
54+
) {
55+
/**
56+
* Adds a specific [ShareInternetResourceState] to the [BrowserStore].
57+
*/
58+
operator fun invoke(tabId: String, internetResource: ShareInternetResourceState) {
59+
store.dispatch(ShareInternetResourceAction.AddShareAction(tabId, internetResource))
60+
}
61+
}
62+
4763
val consumeHitResult = ConsumeHitResultUseCase(store)
4864
val injectDownload = InjectDownloadUseCase(store)
65+
val injectShareFromInternet = InjectShareInternetResourceUseCase(store)
4966
}

components/feature/contextmenu/src/test/java/mozilla/components/feature/contextmenu/ContextMenuCandidateTest.kt

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@ import mozilla.components.browser.state.selector.selectedTab
1717
import mozilla.components.browser.state.state.BrowserState
1818
import mozilla.components.browser.state.state.ContentState
1919
import mozilla.components.browser.state.state.TabSessionState
20+
import mozilla.components.browser.state.state.content.ShareInternetResourceState
2021
import mozilla.components.browser.state.state.createTab
2122
import mozilla.components.browser.state.store.BrowserStore
2223
import mozilla.components.concept.engine.HitResult
2324
import mozilla.components.feature.app.links.AppLinkRedirect
2425
import mozilla.components.feature.app.links.AppLinksUseCases
2526
import mozilla.components.feature.tabs.TabsUseCases
2627
import mozilla.components.support.test.any
28+
import mozilla.components.support.test.argumentCaptor
2729
import mozilla.components.support.test.eq
2830
import mozilla.components.support.test.libstate.ext.waitUntilIdle
2931
import mozilla.components.support.test.mock
@@ -693,10 +695,16 @@ class ContextMenuCandidateTest {
693695

694696
@Test
695697
fun `Candidate 'Share image'`() {
698+
val store = BrowserStore(initialState = BrowserState(
699+
tabs = listOf(TabSessionState("123", ContentState(url = "https://www.mozilla.org")))
700+
))
696701
val context = spy(testContext)
697702

698-
val shareImage = ContextMenuCandidate.createShareImageCandidate(context)
699-
703+
val usecases = spy(ContextMenuUseCases(store))
704+
val shareUsecase: ContextMenuUseCases.InjectShareInternetResourceUseCase = mock()
705+
doReturn(shareUsecase).`when`(usecases).injectShareFromInternet
706+
val shareImage = ContextMenuCandidate.createShareImageCandidate(context, usecases)
707+
val shareStateCaptor = argumentCaptor<ShareInternetResourceState>()
700708
// showFor
701709

702710
assertTrue(shareImage.showFor(
@@ -713,16 +721,14 @@ class ContextMenuCandidateTest {
713721

714722
// action
715723

716-
val store = BrowserStore(initialState = BrowserState(
717-
tabs = listOf(TabSessionState("123", ContentState("https://www.mozilla.org")))
718-
))
719-
720724
shareImage.action.invoke(
721725
store.state.tabs.first(),
722726
HitResult.IMAGE_SRC("https://firefox.com", "https://getpocket.com")
723727
)
724728

725-
verify(context).startActivity(any())
729+
verify(shareUsecase).invoke(eq("123"), shareStateCaptor.capture())
730+
assertEquals("https://firefox.com", shareStateCaptor.value.url)
731+
assertEquals(store.state.tabs.first().content.private, shareStateCaptor.value.private)
726732
}
727733

728734
@Test

0 commit comments

Comments
 (0)