From f631ed25aaa02cf48bf88c7c921692ba5bd102ee Mon Sep 17 00:00:00 2001 From: Sebastian Kaspari Date: Mon, 2 Sep 2019 15:33:03 +0200 Subject: [PATCH 1/4] Issue #4279: feature-intent: Remove deprecated IntentProcessor. --- .../feature/intent/IntentProcessor.kt | 104 ------- .../feature/intent/IntentProcessorTest.kt | 267 ------------------ 2 files changed, 371 deletions(-) delete mode 100644 components/feature/intent/src/main/java/mozilla/components/feature/intent/IntentProcessor.kt delete mode 100644 components/feature/intent/src/test/java/mozilla/components/feature/intent/IntentProcessorTest.kt diff --git a/components/feature/intent/src/main/java/mozilla/components/feature/intent/IntentProcessor.kt b/components/feature/intent/src/main/java/mozilla/components/feature/intent/IntentProcessor.kt deleted file mode 100644 index 505cf3a5d0d..00000000000 --- a/components/feature/intent/src/main/java/mozilla/components/feature/intent/IntentProcessor.kt +++ /dev/null @@ -1,104 +0,0 @@ -/* 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 mozilla.components.feature.intent - -import android.content.Context -import android.content.Intent -import kotlinx.coroutines.runBlocking -import mozilla.components.browser.session.SessionManager -import mozilla.components.browser.session.intent.EXTRA_SESSION_ID -import mozilla.components.feature.customtabs.CustomTabIntentProcessor -import mozilla.components.feature.search.SearchUseCases -import mozilla.components.feature.session.SessionUseCases - -typealias IntentHandler = (Intent) -> Boolean - -/** - * Processor for intents which should trigger session-related actions. - * - * @property sessionUseCases A reference to [SessionUseCases] used to load URLs. - * @property sessionManager The application's [SessionManager]. - * @property searchUseCases A reference to [SearchUseCases] to be used for ACTION_SEND - * intents if the provided text is not a URL. - * @property useDefaultHandlers Whether or not the built-in handlers should be used. - * @property openNewTab Whether a processed intent should open a new tab or - * open URLs in the currently selected tab. - * @property isPrivate Whether a processed intent should open a new tab as private - * - * @deprecated Use individual intent processors instead. - */ -@Deprecated("Use individual processors such as TabIntentProcessor instead.") -class IntentProcessor( - private val sessionUseCases: SessionUseCases, - private val sessionManager: SessionManager, - private val searchUseCases: SearchUseCases, - private val context: Context, - private val useDefaultHandlers: Boolean = true, - private val openNewTab: Boolean = true, - private val isPrivate: Boolean = false -) { - - private val defaultHandlers by lazy { - val tabIntentProcessor = TabIntentProcessor( - sessionManager, - sessionUseCases.loadUrl, - if (isPrivate) searchUseCases.newPrivateTabSearch else searchUseCases.newTabSearch, - openNewTab, - isPrivate - ) - val customTabIntentProcessor = CustomTabIntentProcessor( - sessionManager, - sessionUseCases.loadUrl, - context.resources - ) - val viewHandlers = listOf(customTabIntentProcessor, tabIntentProcessor) - - mutableMapOf( - Intent.ACTION_VIEW to { intent -> - runBlocking { viewHandlers.any { it.process(intent) } } - }, - Intent.ACTION_SEND to { intent -> - runBlocking { tabIntentProcessor.process(intent) } - } - ) - } - - private val handlers = if (useDefaultHandlers) defaultHandlers else mutableMapOf() - - /** - * Processes the given intent by invoking the registered handler. - * - * @param intent the intent to process - * @return true if the intent was processed, otherwise false. - */ - fun process(intent: Intent): Boolean { - val action = intent.action ?: return false - return handlers[action]?.invoke(intent) ?: false - } - - /** - * Registers the given handler to be invoked for intents with the given action. If a - * handler is already present it will be overwritten. - * - * @param action the intent action which should trigger the provided handler. - * @param handler the intent handler to be registered. - */ - fun registerHandler(action: String, handler: IntentHandler) { - handlers[action] = handler - } - - /** - * Removes the registered handler for the given intent action, if present. - * - * @param action the intent action for which the handler should be removed. - */ - fun unregisterHandler(action: String) { - handlers.remove(action) - } - - companion object { - const val ACTIVE_SESSION_ID = EXTRA_SESSION_ID - } -} diff --git a/components/feature/intent/src/test/java/mozilla/components/feature/intent/IntentProcessorTest.kt b/components/feature/intent/src/test/java/mozilla/components/feature/intent/IntentProcessorTest.kt deleted file mode 100644 index 0fdd9df1cb4..00000000000 --- a/components/feature/intent/src/test/java/mozilla/components/feature/intent/IntentProcessorTest.kt +++ /dev/null @@ -1,267 +0,0 @@ -/* 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 mozilla.components.feature.intent - -import android.content.Intent -import androidx.browser.customtabs.CustomTabsIntent -import androidx.test.ext.junit.runners.AndroidJUnit4 -import mozilla.components.browser.search.SearchEngine -import mozilla.components.browser.search.SearchEngineManager -import mozilla.components.browser.session.Session -import mozilla.components.browser.session.Session.Source -import mozilla.components.browser.session.SessionManager -import mozilla.components.concept.engine.Engine -import mozilla.components.concept.engine.EngineSession -import mozilla.components.concept.engine.EngineSession.LoadUrlFlags -import mozilla.components.feature.search.SearchUseCases -import mozilla.components.feature.session.SessionUseCases -import mozilla.components.support.test.any -import mozilla.components.support.test.mock -import mozilla.components.support.test.robolectric.testContext -import mozilla.components.support.test.whenever -import org.junit.Assert.assertEquals -import org.junit.Assert.assertFalse -import org.junit.Assert.assertNotNull -import org.junit.Assert.assertTrue -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.ArgumentMatchers.eq -import org.mockito.Mockito.doReturn -import org.mockito.Mockito.never -import org.mockito.Mockito.spy -import org.mockito.Mockito.verify -import org.mockito.Mockito.verifyZeroInteractions - -@Suppress("Deprecation") -@RunWith(AndroidJUnit4::class) -class IntentProcessorTest { - - private val sessionManager = mock() - private val session = mock() - private val engineSession = mock() - private val sessionUseCases = SessionUseCases(sessionManager) - private val searchEngineManager = mock() - private val searchUseCases = SearchUseCases(testContext, searchEngineManager, sessionManager) - - @Before - fun setup() { - whenever(sessionManager.selectedSession).thenReturn(session) - whenever(sessionManager.getOrCreateEngineSession(session)).thenReturn(engineSession) - } - - @Test - fun processWithDefaultHandlers() { - val engine = mock() - val sessionManager = spy(SessionManager(engine)) - val useCases = SessionUseCases(sessionManager) - val handler = - IntentProcessor(useCases, sessionManager, searchUseCases, testContext) - val intent = mock() - whenever(intent.action).thenReturn(Intent.ACTION_VIEW) - - val engineSession = mock() - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession()) - - whenever(intent.dataString).thenReturn("") - handler.process(intent) - verify(engineSession, never()).loadUrl("") - - whenever(intent.dataString).thenReturn("http://mozilla.org") - handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", LoadUrlFlags.external()) - - val session = sessionManager.all[0] - assertNotNull(session) - assertEquals("http://mozilla.org", session.url) - assertEquals(Source.ACTION_VIEW, session.source) - } - - @Test - fun processWithDefaultHandlersUsingSelectedSession() { - val handler = IntentProcessor( - sessionUseCases, - sessionManager, - searchUseCases, - testContext, - useDefaultHandlers = true, - openNewTab = false - ) - val intent = mock() - whenever(intent.action).thenReturn(Intent.ACTION_VIEW) - whenever(intent.dataString).thenReturn("http://mozilla.org") - - handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", LoadUrlFlags.external()) - } - - @Test - fun processWithDefaultHandlersHavingNoSelectedSession() { - whenever(sessionManager.selectedSession).thenReturn(null) - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession()) - - val handler = IntentProcessor( - sessionUseCases, - sessionManager, - searchUseCases, - testContext, - useDefaultHandlers = true, - openNewTab = false - ) - val intent = mock() - whenever(intent.action).thenReturn(Intent.ACTION_VIEW) - whenever(intent.dataString).thenReturn("http://mozilla.org") - - handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", LoadUrlFlags.external()) - } - - @Test - fun processWithoutDefaultHandlers() { - val handler = IntentProcessor( - sessionUseCases, - sessionManager, - searchUseCases, - testContext, - useDefaultHandlers = false - ) - val intent = mock() - whenever(intent.action).thenReturn(Intent.ACTION_VIEW) - whenever(intent.dataString).thenReturn("http://mozilla.org") - - handler.process(intent) - verifyZeroInteractions(engineSession) - } - - @Test - fun processWithCustomHandlers() { - val handler = IntentProcessor( - sessionUseCases, - sessionManager, - searchUseCases, - testContext, - useDefaultHandlers = false - ) - val intent = mock() - whenever(intent.action).thenReturn(Intent.ACTION_SEND) - - var handlerInvoked = false - handler.registerHandler(Intent.ACTION_SEND) { - handlerInvoked = true - true - } - - handler.process(intent) - assertTrue(handlerInvoked) - - handlerInvoked = false - handler.unregisterHandler(Intent.ACTION_SEND) - - handler.process(intent) - assertFalse(handlerInvoked) - } - - @Test - fun processCustomTabIntentWithDefaultHandlers() { - val engine = mock() - val sessionManager = spy(SessionManager(engine)) - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession()) - val useCases = SessionUseCases(sessionManager) - - val handler = IntentProcessor(useCases, sessionManager, searchUseCases, testContext) - - val intent = mock() - whenever(intent.action).thenReturn(Intent.ACTION_VIEW) - whenever(intent.hasExtra(CustomTabsIntent.EXTRA_SESSION)).thenReturn(true) - whenever(intent.dataString).thenReturn("http://mozilla.org") - whenever(intent.putExtra(any(), any())).thenReturn(intent) - - handler.process(intent) - verify(sessionManager).add(anySession(), eq(false), eq(null), eq(null)) - verify(engineSession).loadUrl("http://mozilla.org", LoadUrlFlags.external()) - - val customTabSession = sessionManager.all[0] - assertNotNull(customTabSession) - assertEquals("http://mozilla.org", customTabSession.url) - assertEquals(Source.CUSTOM_TAB, customTabSession.source) - assertNotNull(customTabSession.customTabConfig) - } - - @Test - fun `load URL on ACTION_SEND if text contains URL`() { - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession()) - - val handler = IntentProcessor(sessionUseCases, sessionManager, searchUseCases, testContext) - - val intent = mock() - whenever(intent.action).thenReturn(Intent.ACTION_SEND) - - whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("http://mozilla.org") - handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.org", LoadUrlFlags.external()) - - whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("see http://getpocket.com") - handler.process(intent) - verify(engineSession).loadUrl("http://getpocket.com", LoadUrlFlags.external()) - - whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("see http://mozilla.com and http://getpocket.com") - handler.process(intent) - verify(engineSession).loadUrl("http://mozilla.com", LoadUrlFlags.external()) - - whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("checkout the Tweet: http://tweets.mozilla.com") - handler.process(intent) - verify(engineSession).loadUrl("http://tweets.mozilla.com", LoadUrlFlags.external()) - - whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn("checkout the Tweet: HTTP://tweets.mozilla.com") - handler.process(intent) - verify(engineSession).loadUrl("http://tweets.mozilla.com", LoadUrlFlags.external()) - } - - @Test - fun `perform search on ACTION_SEND if text (no URL) provided`() { - val engine = mock() - val sessionManager = spy(SessionManager(engine)) - doReturn(engineSession).`when`(sessionManager).getOrCreateEngineSession(anySession()) - - val searchUseCases = SearchUseCases(testContext, searchEngineManager, sessionManager) - val sessionUseCases = SessionUseCases(sessionManager) - - val searchTerms = "mozilla android" - val searchUrl = "http://search-url.com?$searchTerms" - - val handler = IntentProcessor(sessionUseCases, sessionManager, searchUseCases, testContext) - - val intent = mock() - whenever(intent.action).thenReturn(Intent.ACTION_SEND) - whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn(searchTerms) - - val searchEngine = mock() - whenever(searchEngine.buildSearchUrl(searchTerms)).thenReturn(searchUrl) - whenever(searchEngineManager.getDefaultSearchEngine(testContext)).thenReturn(searchEngine) - - handler.process(intent) - verify(engineSession).loadUrl(searchUrl) - assertEquals(searchUrl, sessionManager.selectedSession?.url) - assertEquals(searchTerms, sessionManager.selectedSession?.searchTerms) - } - - @Test - fun `processor handles ACTION_SEND with empty text`() { - val handler = IntentProcessor(sessionUseCases, sessionManager, searchUseCases, testContext) - - val intent = mock() - whenever(intent.action).thenReturn(Intent.ACTION_SEND) - whenever(intent.getStringExtra(Intent.EXTRA_TEXT)).thenReturn(" ") - - val processed = handler.process(intent) - assertFalse(processed) - } - - @Suppress("UNCHECKED_CAST") - private fun anySession(): T { - any() - return null as T - } -} From 75986cce66d7e5c2bb0f1ff75f75f7c4011b2d51 Mon Sep 17 00:00:00 2001 From: Sebastian Kaspari Date: Mon, 2 Sep 2019 17:34:22 +0200 Subject: [PATCH 2/4] Issue #4257, #4279: Move intent related code from browser-session to feature-intent. --- components/feature/customtabs/build.gradle | 1 + .../components/feature/customtabs/CustomTabIntentProcessor.kt | 4 ++-- .../feature/customtabs/CustomTabIntentProcessorTest.kt | 2 +- components/feature/intent/build.gradle | 1 - .../components/feature/intent/ext}/IntentExtensions.kt | 2 +- .../components/feature/intent/processing}/IntentProcessor.kt | 2 +- .../feature/intent/{ => processing}/TabIntentProcessor.kt | 3 +-- .../components/feature/intent/ext}/IntentExtensionsTest.kt | 2 +- .../feature/intent/{ => processing}/TabIntentProcessorTest.kt | 2 +- components/feature/pwa/build.gradle | 1 + .../feature/pwa/intent/TrustedWebActivityIntentProcessor.kt | 4 ++-- .../components/feature/pwa/intent/WebAppIntentProcessor.kt | 4 ++-- .../feature/pwa/intent/WebAppIntentProcessorTest.kt | 2 +- docs/changelog.md | 3 +++ .../main/java/org/mozilla/samples/browser/BrowserActivity.kt | 2 +- .../java/org/mozilla/samples/browser/DefaultComponents.kt | 2 +- 16 files changed, 20 insertions(+), 17 deletions(-) rename components/{browser/session/src/main/java/mozilla/components/browser/session/intent => feature/intent/src/main/java/mozilla/components/feature/intent/ext}/IntentExtensions.kt (96%) rename components/{browser/session/src/main/java/mozilla/components/browser/session/intent => feature/intent/src/main/java/mozilla/components/feature/intent/processing}/IntentProcessor.kt (92%) rename components/feature/intent/src/main/java/mozilla/components/feature/intent/{ => processing}/TabIntentProcessor.kt (97%) rename components/{browser/session/src/test/java/mozilla/components/browser/session/intent => feature/intent/src/test/java/mozilla/components/feature/intent/ext}/IntentExtensionsTest.kt (96%) rename components/feature/intent/src/test/java/mozilla/components/feature/intent/{ => processing}/TabIntentProcessorTest.kt (99%) diff --git a/components/feature/customtabs/build.gradle b/components/feature/customtabs/build.gradle index 61283c99c4f..c62ecfeb616 100644 --- a/components/feature/customtabs/build.gradle +++ b/components/feature/customtabs/build.gradle @@ -28,6 +28,7 @@ dependencies { implementation project(':concept-engine') implementation project(':concept-fetch') implementation project(':feature-session') + implementation project(':feature-intent') implementation project(':support-base') implementation project(':support-ktx') implementation project(':support-utils') diff --git a/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt b/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt index 857d1b2527c..bcb9798bf10 100644 --- a/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt +++ b/components/feature/customtabs/src/main/java/mozilla/components/feature/customtabs/CustomTabIntentProcessor.kt @@ -9,9 +9,9 @@ import android.content.Intent.ACTION_VIEW import android.content.res.Resources import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager -import mozilla.components.browser.session.intent.IntentProcessor -import mozilla.components.browser.session.intent.putSessionId import mozilla.components.concept.engine.EngineSession +import mozilla.components.feature.intent.ext.putSessionId +import mozilla.components.feature.intent.processing.IntentProcessor import mozilla.components.feature.session.SessionUseCases import mozilla.components.support.utils.SafeIntent import mozilla.components.support.utils.toSafeIntent diff --git a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt index f37d2741fda..4443fe2d9da 100644 --- a/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt +++ b/components/feature/customtabs/src/test/java/mozilla/components/feature/customtabs/CustomTabIntentProcessorTest.kt @@ -12,10 +12,10 @@ import kotlinx.coroutines.test.runBlockingTest import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session.Source import mozilla.components.browser.session.SessionManager -import mozilla.components.browser.session.intent.EXTRA_SESSION_ID import mozilla.components.concept.engine.Engine import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.engine.EngineSession.LoadUrlFlags +import mozilla.components.feature.intent.ext.EXTRA_SESSION_ID import mozilla.components.feature.session.SessionUseCases import mozilla.components.support.test.any import mozilla.components.support.test.mock diff --git a/components/feature/intent/build.gradle b/components/feature/intent/build.gradle index 3533d41b597..2faf5927b15 100644 --- a/components/feature/intent/build.gradle +++ b/components/feature/intent/build.gradle @@ -26,7 +26,6 @@ android { dependencies { implementation project(':concept-engine') implementation project(':browser-session') - implementation project(':feature-customtabs') implementation project(':feature-search') implementation project(':feature-session') implementation project(':support-utils') diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/intent/IntentExtensions.kt b/components/feature/intent/src/main/java/mozilla/components/feature/intent/ext/IntentExtensions.kt similarity index 96% rename from components/browser/session/src/main/java/mozilla/components/browser/session/intent/IntentExtensions.kt rename to components/feature/intent/src/main/java/mozilla/components/feature/intent/ext/IntentExtensions.kt index d7081eb224c..806d79c5f95 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/intent/IntentExtensions.kt +++ b/components/feature/intent/src/main/java/mozilla/components/feature/intent/ext/IntentExtensions.kt @@ -2,7 +2,7 @@ * 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 mozilla.components.browser.session.intent +package mozilla.components.feature.intent.ext import android.content.Intent import mozilla.components.support.utils.SafeIntent diff --git a/components/browser/session/src/main/java/mozilla/components/browser/session/intent/IntentProcessor.kt b/components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/IntentProcessor.kt similarity index 92% rename from components/browser/session/src/main/java/mozilla/components/browser/session/intent/IntentProcessor.kt rename to components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/IntentProcessor.kt index 29ce4c95968..9b729f0ae5e 100644 --- a/components/browser/session/src/main/java/mozilla/components/browser/session/intent/IntentProcessor.kt +++ b/components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/IntentProcessor.kt @@ -2,7 +2,7 @@ * 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 mozilla.components.browser.session.intent +package mozilla.components.feature.intent.processing import android.content.Intent diff --git a/components/feature/intent/src/main/java/mozilla/components/feature/intent/TabIntentProcessor.kt b/components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/TabIntentProcessor.kt similarity index 97% rename from components/feature/intent/src/main/java/mozilla/components/feature/intent/TabIntentProcessor.kt rename to components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/TabIntentProcessor.kt index 1b2dabd5ef4..059cc14fd97 100644 --- a/components/feature/intent/src/main/java/mozilla/components/feature/intent/TabIntentProcessor.kt +++ b/components/feature/intent/src/main/java/mozilla/components/feature/intent/processing/TabIntentProcessor.kt @@ -2,7 +2,7 @@ * 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 mozilla.components.feature.intent +package mozilla.components.feature.intent.processing import android.content.Intent import android.content.Intent.ACTION_SEND @@ -11,7 +11,6 @@ import android.content.Intent.EXTRA_TEXT import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session.Source import mozilla.components.browser.session.SessionManager -import mozilla.components.browser.session.intent.IntentProcessor import mozilla.components.concept.engine.EngineSession.LoadUrlFlags import mozilla.components.feature.search.SearchUseCases import mozilla.components.feature.session.SessionUseCases diff --git a/components/browser/session/src/test/java/mozilla/components/browser/session/intent/IntentExtensionsTest.kt b/components/feature/intent/src/test/java/mozilla/components/feature/intent/ext/IntentExtensionsTest.kt similarity index 96% rename from components/browser/session/src/test/java/mozilla/components/browser/session/intent/IntentExtensionsTest.kt rename to components/feature/intent/src/test/java/mozilla/components/feature/intent/ext/IntentExtensionsTest.kt index b6b33b44985..a7a5a9bc400 100644 --- a/components/browser/session/src/test/java/mozilla/components/browser/session/intent/IntentExtensionsTest.kt +++ b/components/feature/intent/src/test/java/mozilla/components/feature/intent/ext/IntentExtensionsTest.kt @@ -2,7 +2,7 @@ * 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 mozilla.components.browser.session.intent +package mozilla.components.feature.intent.ext import android.content.Intent import androidx.test.ext.junit.runners.AndroidJUnit4 diff --git a/components/feature/intent/src/test/java/mozilla/components/feature/intent/TabIntentProcessorTest.kt b/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt similarity index 99% rename from components/feature/intent/src/test/java/mozilla/components/feature/intent/TabIntentProcessorTest.kt rename to components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt index 797999418c3..42fc9f77197 100644 --- a/components/feature/intent/src/test/java/mozilla/components/feature/intent/TabIntentProcessorTest.kt +++ b/components/feature/intent/src/test/java/mozilla/components/feature/intent/processing/TabIntentProcessorTest.kt @@ -2,7 +2,7 @@ * 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 mozilla.components.feature.intent +package mozilla.components.feature.intent.processing import android.content.Intent import androidx.test.ext.junit.runners.AndroidJUnit4 diff --git a/components/feature/pwa/build.gradle b/components/feature/pwa/build.gradle index 67a8ea8e241..23d026cc97f 100644 --- a/components/feature/pwa/build.gradle +++ b/components/feature/pwa/build.gradle @@ -34,6 +34,7 @@ dependencies { implementation project(':concept-engine') implementation project(':concept-fetch') implementation project(':feature-customtabs') + implementation project(':feature-intent') implementation project(':feature-session') implementation project(':support-base') implementation project(':support-ktx') diff --git a/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt b/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt index 9478d0872d0..f79ee18fe62 100644 --- a/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt +++ b/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/TrustedWebActivityIntentProcessor.kt @@ -17,8 +17,6 @@ import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch import mozilla.components.browser.session.Session import mozilla.components.browser.session.SessionManager -import mozilla.components.browser.session.intent.IntentProcessor -import mozilla.components.browser.session.intent.putSessionId import mozilla.components.browser.state.state.CustomTabConfig.Companion.EXTRA_ADDITIONAL_TRUSTED_ORIGINS import mozilla.components.concept.engine.EngineSession import mozilla.components.concept.fetch.Client @@ -26,6 +24,8 @@ import mozilla.components.feature.customtabs.createCustomTabConfigFromIntent import mozilla.components.feature.customtabs.feature.OriginVerifierFeature import mozilla.components.feature.customtabs.isTrustedWebActivityIntent import mozilla.components.feature.customtabs.store.CustomTabsServiceStore +import mozilla.components.feature.intent.ext.putSessionId +import mozilla.components.feature.intent.processing.IntentProcessor import mozilla.components.feature.pwa.ext.toOrigin import mozilla.components.feature.session.SessionUseCases import mozilla.components.support.utils.SafeIntent diff --git a/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessor.kt b/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessor.kt index 0a75185443f..f589b88f97e 100644 --- a/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessor.kt +++ b/components/feature/pwa/src/main/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessor.kt @@ -8,9 +8,9 @@ import android.content.Intent import mozilla.components.browser.session.Session import mozilla.components.browser.session.Session.Source import mozilla.components.browser.session.SessionManager -import mozilla.components.browser.session.intent.IntentProcessor -import mozilla.components.browser.session.intent.putSessionId import mozilla.components.concept.engine.EngineSession +import mozilla.components.feature.intent.ext.putSessionId +import mozilla.components.feature.intent.processing.IntentProcessor import mozilla.components.feature.pwa.ManifestStorage import mozilla.components.feature.pwa.ext.putWebAppManifest import mozilla.components.feature.pwa.ext.toCustomTabConfig diff --git a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt index 81107a30963..5232ae6526c 100644 --- a/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt +++ b/components/feature/pwa/src/test/java/mozilla/components/feature/pwa/intent/WebAppIntentProcessorTest.kt @@ -10,8 +10,8 @@ import androidx.core.net.toUri import androidx.test.ext.junit.runners.AndroidJUnit4 import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runBlockingTest -import mozilla.components.browser.session.intent.getSessionId import mozilla.components.concept.engine.manifest.WebAppManifest +import mozilla.components.feature.intent.ext.getSessionId import mozilla.components.feature.pwa.ManifestStorage import mozilla.components.feature.pwa.ext.getWebAppManifest import mozilla.components.feature.pwa.intent.WebAppIntentProcessor.Companion.ACTION_VIEW_PWA diff --git a/docs/changelog.md b/docs/changelog.md index 90197959690..57476140e00 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -38,6 +38,9 @@ permalink: /changelog/ * **service-telemetry** * This component is now deprecated. Please use the [Glean SDK](https://mozilla.github.io/glean/book/index.html) instead. This library will not be removed until all projects using it start using the Glean SDK. +* **browser-session**, **feature-intent** + * ⚠️ **This is a breaking change**: Moved `Intent` related code from `browser-session` to `feature-intent`. + # 10.0.1 * [Commits](https://github.com/mozilla-mobile/android-components/compare/v10.0.0...v10.0.1) diff --git a/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt b/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt index 7acbd749a89..f2e67cbe976 100644 --- a/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt +++ b/samples/browser/src/main/java/org/mozilla/samples/browser/BrowserActivity.kt @@ -11,7 +11,7 @@ import android.util.AttributeSet import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment -import mozilla.components.browser.session.intent.getSessionId +import mozilla.components.feature.intent.ext.getSessionId import mozilla.components.browser.tabstray.BrowserTabsTray import mozilla.components.concept.engine.EngineView import mozilla.components.concept.tabstray.TabsTray diff --git a/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt b/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt index 109d88d7f86..70cfbf4283f 100644 --- a/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt +++ b/samples/browser/src/main/java/org/mozilla/samples/browser/DefaultComponents.kt @@ -30,7 +30,7 @@ import mozilla.components.concept.engine.DefaultSettings import mozilla.components.concept.engine.Engine import mozilla.components.feature.customtabs.CustomTabIntentProcessor import mozilla.components.feature.customtabs.store.CustomTabsServiceStore -import mozilla.components.feature.intent.TabIntentProcessor +import mozilla.components.feature.intent.processing.TabIntentProcessor import mozilla.components.feature.media.MediaFeature import mozilla.components.feature.media.RecordingDevicesNotificationFeature import mozilla.components.feature.media.state.MediaStateMachine From 49f0532bf62c0f08578f5edcdf8f43236e2826fa Mon Sep 17 00:00:00 2001 From: Sebastian Kaspari Date: Tue, 3 Sep 2019 12:20:09 +0200 Subject: [PATCH 3/4] Issue #4284: Add DownloadState to browser-state. --- .../browser/state/action/BrowserAction.kt | 11 +++++ .../state/reducer/ContentStateReducer.kt | 10 +++++ .../browser/state/state/ContentState.kt | 5 ++- .../state/state/content/DownloadState.kt | 28 +++++++++++++ .../browser/state/action/ContentActionTest.kt | 40 +++++++++++++++++++ 5 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 components/browser/state/src/main/java/mozilla/components/browser/state/state/content/DownloadState.kt diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt index f3cf8a4ddd1..803fdf71a00 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/action/BrowserAction.kt @@ -12,6 +12,7 @@ import mozilla.components.browser.state.state.SecurityInfoState import mozilla.components.browser.state.state.SessionState import mozilla.components.browser.state.state.TabSessionState import mozilla.components.browser.state.state.TrackingProtectionState +import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.concept.engine.content.blocking.Tracker import mozilla.components.lib.state.Action @@ -166,6 +167,16 @@ sealed class ContentAction : BrowserAction() { * Updates the thumbnail of the [ContentState] with the given [sessionId]. */ data class UpdateThumbnailAction(val sessionId: String, val thumbnail: Bitmap) : ContentAction() + + /** + * Updates the [DownloadState] of the [ContentState] with the given [sessionId]. + */ + data class UpdateDownloadAction(val sessionId: String, val download: DownloadState) : ContentAction() + + /** + * Removes the [DownloadState] of the [ContentState] with the given [sessionId]. + */ + data class ConsumeDownloadAction(val sessionId: String, val download: DownloadState) : ContentAction() } /** diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt index b3b0ec4d5cb..328db62cc71 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/reducer/ContentStateReducer.kt @@ -45,6 +45,16 @@ internal object ContentStateReducer { is ContentAction.UpdateThumbnailAction -> updateContentState(state, action.sessionId) { it.copy(thumbnail = action.thumbnail) } + is ContentAction.UpdateDownloadAction -> updateContentState(state, action.sessionId) { + it.copy(download = action.download) + } + is ContentAction.ConsumeDownloadAction -> updateContentState(state, action.sessionId) { + if (action.download == it.download) { + it.copy(download = null) + } else { + it + } + } } } } diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt index 345a316ce24..ed4cecc2e49 100644 --- a/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/state/ContentState.kt @@ -5,6 +5,7 @@ package mozilla.components.browser.state.state import android.graphics.Bitmap +import mozilla.components.browser.state.state.content.DownloadState /** * Value type that represents the state of the content within a [SessionState]. @@ -21,6 +22,7 @@ import android.graphics.Bitmap * @property thumbnail the last generated [Bitmap] of this session's content, to * be used as a preview in e.g. a tab switcher. * @property icon the icon of the page currently loaded by this session. + * @property download Last unhandled download request. */ data class ContentState( val url: String, @@ -31,5 +33,6 @@ data class ContentState( val searchTerms: String = "", val securityInfo: SecurityInfoState = SecurityInfoState(), val thumbnail: Bitmap? = null, - val icon: Bitmap? = null + val icon: Bitmap? = null, + val download: DownloadState? = null ) diff --git a/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/DownloadState.kt b/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/DownloadState.kt new file mode 100644 index 00000000000..8a78ca0ed2c --- /dev/null +++ b/components/browser/state/src/main/java/mozilla/components/browser/state/state/content/DownloadState.kt @@ -0,0 +1,28 @@ +/* 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 mozilla.components.browser.state.state.content + +import android.os.Environment + +/** + * Value type that represents a download request. + * + * @property url The full url to the content that should be downloaded. + * @property fileName A canonical filename for this download. + * @property contentType Content type (MIME type) to indicate the media type of the download. + * @property contentLength The file size reported by the server. + * @property userAgent The user agent to be used for the download. + * @property destinationDirectory The matching destination directory for this type of download. + * @property referrerUrl The site that linked to this download. + */ +data class DownloadState( + val url: String, + val fileName: String? = null, + val contentType: String? = null, + val contentLength: Long? = null, + val userAgent: String? = null, + val destinationDirectory: String = Environment.DIRECTORY_DOWNLOADS, + val referrerUrl: String? = null +) diff --git a/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt b/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt index fcbfb366382..f20dc02264c 100644 --- a/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt +++ b/components/browser/state/src/test/java/mozilla/components/browser/state/action/ContentActionTest.kt @@ -9,10 +9,12 @@ import mozilla.components.browser.state.selector.findCustomTab import mozilla.components.browser.state.state.BrowserState import mozilla.components.browser.state.state.SecurityInfoState import mozilla.components.browser.state.state.TabSessionState +import mozilla.components.browser.state.state.content.DownloadState import mozilla.components.browser.state.state.createCustomTab import mozilla.components.browser.state.state.createTab import mozilla.components.browser.state.store.BrowserStore import mozilla.components.support.test.ext.joinBlocking +import mozilla.components.support.test.mock import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertNotEquals @@ -251,4 +253,42 @@ class ContentActionTest { assertNotEquals("I am a custom tab", tab.content.title) assertNotEquals("I am a custom tab", otherTab.content.title) } + + @Test + fun `UpdateDownloadAction updates download`() { + assertNull(tab.content.download) + + val download1: DownloadState = mock() + + store.dispatch( + ContentAction.UpdateDownloadAction(tab.id, download1) + ).joinBlocking() + + assertEquals(download1, tab.content.download) + + val download2: DownloadState = mock() + + store.dispatch( + ContentAction.UpdateDownloadAction(tab.id, download2) + ).joinBlocking() + + assertEquals(download2, tab.content.download) + } + + @Test + fun `ConsumeDownloadAction removes download`() { + val download: DownloadState = mock() + + store.dispatch( + ContentAction.UpdateDownloadAction(tab.id, download) + ).joinBlocking() + + assertEquals(download, tab.content.download) + + store.dispatch( + ContentAction.ConsumeDownloadAction(tab.id, download) + ).joinBlocking() + + assertNull(tab.content.download) + } } \ No newline at end of file From 5dc78f68661ed725a925ef71ee6de6a4c851c187 Mon Sep 17 00:00:00 2001 From: Sebastian Kaspari Date: Mon, 2 Sep 2019 14:01:11 +0200 Subject: [PATCH 4/4] Add blog posting explaining browser-state plans. --- docs/_posts/2019-09-02-browser-state.md | 64 +++++++++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 docs/_posts/2019-09-02-browser-state.md diff --git a/docs/_posts/2019-09-02-browser-state.md b/docs/_posts/2019-09-02-browser-state.md new file mode 100644 index 00000000000..e61642fc3c1 --- /dev/null +++ b/docs/_posts/2019-09-02-browser-state.md @@ -0,0 +1,64 @@ +--- +layout: post +title: "✨ browser-state: Better state management for apps and components" +date: 2019-09-02 10:30:00 +0200 +author: sebastian +--- + +We have been working on a new component called `browser-state` to eventually replace `browser-session`. Now we are ready to start migrating components from `browser-session` to `browser-state`. This blog posting explains why we want to decommission `browser-session`, describes how `browser-state` works and what our migration plans are. + +## What's the problem with `browser-session`? + +For maintaining the global browser state (e.g. "What tabs are open? What URLs are they pointing to?") the *Android Components* project provides the `browser-session` component. The initial implementation of `browser-session` was a clean, generic re-implementation of what we had developed (more organically) for [Firefox Focus](https://github.com/mozilla-mobile/focus-android). + +In 2018 we [noticed some flaws](https://github.com/mozilla-mobile/android-components/issues/400) in the design of `browser-session`. Those flaws came down to being able to observe the state while being able to modify it at the same time ("mutable state"). This unintended behavior could lead to "event order issues" and observers not really seeing a particular state change. Luckily back then we hadn't seen those issues causing any problems in our apps yet. + +We looked at [multiple ways to prevent those side effects](https://github.com/mozilla-mobile/android-components/pull/453) but that turned out to be almost impossible as long as the state is mutable. After more brainstorming, researching and prototyping we came up with a new design for a completely new component called `browser-state` to eventually replace `browser-session`. + +In 2019 we completed and tweaked the design of the new `browser-state` component until we felt that it was ready to be used in other components. + +## A closer look at `browser-state` + +Concepts used in the `browser-state` component are similar to [Redux](https://redux.js.org/) - a state container library for JavaScript. The Redux documentation is a great way to get familiar with some of the concepts: + * [Core Concepts](https://redux.js.org/introduction/core-concepts) + * [Three Principles](https://redux.js.org/introduction/three-principles) + +### BrowserState + +The global state of the browser is represented by an instance of an immutable data class: `BrowserState` ([API](https://mozac.org/api/mozilla.components.browser.state.state/-browser-state/)). Since it is immutable, an instance of this data class can be observed and processed without any side effects changing it. A state change is represented by the creation of a new `BrowserState` instance. + +### BrowserStore + +The `BrowserStore` ([API](https://mozac.org/api/mozilla.components.browser.state.store/-browser-store/)) is the single source of truth. It holds the current `BrowserState` instance and components, as well as app code, can observe it in order to always receive the latest `BrowserState`. The only way to change the state is by dispatching a `BrowserAction` ([API](https://mozac.org/api/mozilla.components.browser.state.action/-browser-action.html)) on the store. A dispatched `BrowserAction` will be processed internally and a new `BrowserState` object will be emitted by the store. + +## How are we going to migrate apps and components to `browser-state`? + +The `browser-session` component is at the heart of many components and most apps using our components. It is obvious that we cannot migrate all components and apps from `browser-session` to `browser-state` from one *Android Components* release to the next one. Therefore the *Android Components* team made it possible to use `browser-state` and `browser-session` simultaneously and keep the state in both components synchronized. + +```kotlin +val store = BrowserStore() + +// Passing the BrowserStore instance to SessionManager makes sure that both +// components will be kept in sync. +val sessionManager = SessionManager(engine, store) +``` + +With the ability to use both components simultaneously, the *Android Components* team will start migrating components over from `browser-session` to `browser-state`. As part of this work the *Android Components* team will extend and add to `BrowserState` to eventually reach feature parity with the state in `SessionManager` and `Session`. The only thing that may be different for app teams is that some components may require a `BrowserStore` instance instead of a `SessionManager` instance after migration. + +```kotlin +// Before the migration +feature = ToolbarFeature( + layout.toolbar, + components.sessionManager, + components.sessionUseCases.loadUrl, + components.defaultSearchUseCase) + +// After the migration +feature = ToolbarFeature( + layout.toolbar, + components.store, + components.sessionUseCases.loadUrl, + components.defaultSearchUseCase) +``` + +Once the migration of components is largely done, the *Android Components* team will start to help the app teams to plan migrating app code from `browser-session` to `browser-state`. \ No newline at end of file