From a897486f3095ceffd1266343bd5e38e2c113489a Mon Sep 17 00:00:00 2001 From: Sebastian Kaspari Date: Thu, 9 May 2019 14:42:58 -0400 Subject: [PATCH] Issue #2080: Extract website icon resources via WebExtension. --- components/browser/icons/build.gradle | 1 + .../assets/extensions/browser-icons/icons.js | 33 ++- .../extensions/browser-icons/manifest.json | 4 + .../components/browser/icons/BrowserIcons.kt | 11 +- .../icons/extension/AllSessionsObserver.kt | 59 ++++++ .../icons/extension/IconMessageHandler.kt | 140 +++++++++++++ .../icons/extension/IconSessionObserver.kt | 47 +++++ .../extension/AllSessionsObserverTest.kt | 118 +++++++++++ .../icons/extension/IconMessageHandlerTest.kt | 194 ++++++++++++++++++ .../extension/IconSessionObserverTest.kt | 49 +++++ .../org/mozilla/samples/browser/Components.kt | 2 - .../samples/browser/DefaultComponents.kt | 2 + 12 files changed, 650 insertions(+), 10 deletions(-) create mode 100644 components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/AllSessionsObserver.kt create mode 100644 components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessageHandler.kt create mode 100644 components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconSessionObserver.kt create mode 100644 components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/AllSessionsObserverTest.kt create mode 100644 components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt create mode 100644 components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconSessionObserverTest.kt diff --git a/components/browser/icons/build.gradle b/components/browser/icons/build.gradle index 3d2ba4efd7e..6bdab6ec645 100644 --- a/components/browser/icons/build.gradle +++ b/components/browser/icons/build.gradle @@ -37,6 +37,7 @@ dependencies { implementation project(':support-ktx') implementation project(':concept-engine') implementation project(':concept-fetch') + implementation project(':browser-session') implementation Dependencies.kotlin_stdlib implementation Dependencies.kotlin_coroutines diff --git a/components/browser/icons/src/main/assets/extensions/browser-icons/icons.js b/components/browser/icons/src/main/assets/extensions/browser-icons/icons.js index 8ef6198b1d6..fd7155ee570 100644 --- a/components/browser/icons/src/main/assets/extensions/browser-icons/icons.js +++ b/components/browser/icons/src/main/assets/extensions/browser-icons/icons.js @@ -7,13 +7,33 @@ * meta data (e.g. sizes) and passes that to the app code. */ +/** + * Takes a DOMTokenList and returns a String array. + */ +function sizesToList(sizes) { + if (sizes == null) { + return [] + } + + if (!(sizes instanceof DOMTokenList)) { + return [] + } + + var result = [] + for (var size of sizes.values()) { + result.push(size) + } + return result +} + function collect_link_icons(icons, rel) { document.querySelectorAll('link[rel="' + rel + '"]').forEach( function(currentValue, currentIndex, listObj) { icons.push({ 'type': rel, 'href': currentValue.href, - 'sizes': currentValue.sizes + 'sizes': sizesToList(currentValue.sizes), + 'mimeType': currentValue.type }); }) } @@ -57,8 +77,9 @@ collect_meta_property_icons(icons, 'og:image:secure_url') collect_meta_name_icons(icons, 'twitter:image') collect_meta_name_icons(icons, 'msapplication-TileImage') -// TODO: For now we are just logging the icons we found here. We need the Messaging API -// to actually be able to pass them to the Android world. -// https://github.com/mozilla-mobile/android-components/issues/2243 -// https://bugzilla.mozilla.org/show_bug.cgi?id=1518843 -console.log("browser-icons: (" + icons.length + ")", document.location.href, icons) +let message = { + 'url': document.location.href, + 'icons': icons +} + +browser.runtime.sendNativeMessage("MozacBrowserIcons", message); diff --git a/components/browser/icons/src/main/assets/extensions/browser-icons/manifest.json b/components/browser/icons/src/main/assets/extensions/browser-icons/manifest.json index 02e91fc4a16..1c95ea69e68 100644 --- a/components/browser/icons/src/main/assets/extensions/browser-icons/manifest.json +++ b/components/browser/icons/src/main/assets/extensions/browser-icons/manifest.json @@ -8,5 +8,9 @@ "js": ["icons.js"], "run_at": "document_end" } + ], + "permissions": [ + "geckoViewAddons", + "nativeMessaging" ] } diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/BrowserIcons.kt b/components/browser/icons/src/main/java/mozilla/components/browser/icons/BrowserIcons.kt index 772f905d460..f94a809ce19 100644 --- a/components/browser/icons/src/main/java/mozilla/components/browser/icons/BrowserIcons.kt +++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/BrowserIcons.kt @@ -14,6 +14,8 @@ import kotlinx.coroutines.async import mozilla.components.browser.icons.decoder.AndroidIconDecoder import mozilla.components.browser.icons.decoder.ICOIconDecoder import mozilla.components.browser.icons.decoder.IconDecoder +import mozilla.components.browser.icons.extension.AllSessionsObserver +import mozilla.components.browser.icons.extension.IconSessionObserver import mozilla.components.browser.icons.generator.DefaultIconGenerator import mozilla.components.browser.icons.generator.IconGenerator import mozilla.components.browser.icons.loader.DataUriIconLoader @@ -26,6 +28,7 @@ import mozilla.components.browser.icons.preparer.MemoryIconPreparer import mozilla.components.browser.icons.processor.IconProcessor import mozilla.components.browser.icons.processor.MemoryIconProcessor import mozilla.components.browser.icons.utils.MemoryCache +import mozilla.components.browser.session.SessionManager import mozilla.components.concept.engine.Engine import mozilla.components.concept.fetch.Client import mozilla.components.support.base.log.logger.Logger @@ -100,12 +103,16 @@ class BrowserIcons( /** * Installs the "icons" extension in the engine in order to dynamically load icons for loaded websites. */ - fun install(engine: Engine) { + fun install(engine: Engine, sessionManager: SessionManager) { engine.installWebExtension( id = "mozacBrowserIcons", url = "resource://android/assets/extensions/browser-icons/", - onSuccess = { + allowContentMessaging = true, + onSuccess = { extension -> Logger.debug("Installed browser-icons extension") + + AllSessionsObserver.register( + sessionManager, IconSessionObserver(this, sessionManager, extension)) }, onError = { _, throwable -> Logger.error("Could not install browser-icons extension", throwable) diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/AllSessionsObserver.kt b/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/AllSessionsObserver.kt new file mode 100644 index 00000000000..f164538065b --- /dev/null +++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/AllSessionsObserver.kt @@ -0,0 +1,59 @@ +/* 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.icons.extension + +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager + +/** + * This helper will dynamically register the given [Session.Observer] on all [Session]s in the [SessionManager]. It + * automatically registers the observer on added [Session]s and unregisters it if a [Session] gets removed. + */ +internal class AllSessionsObserver internal constructor( + private val sessionManager: SessionManager, + private val observer: Session.Observer +) : SessionManager.Observer { + private val sessions: MutableSet = mutableSetOf() + + init { + sessionManager.all.forEach { registerObserver(it) } + sessionManager.register(this) + } + + override fun onSessionAdded(session: Session) { + registerObserver(session) + } + + override fun onSessionRemoved(session: Session) { + unregisterObserver(session) + } + + override fun onSessionsRestored() { + sessionManager.all.forEach { registerObserver(it) } + } + + override fun onAllSessionsRemoved() { + sessions.toList().forEach { unregisterObserver(it) } + } + + private fun registerObserver(session: Session) { + if (!sessions.contains(session)) { + sessions.add(session) + session.register(observer) + } + } + + private fun unregisterObserver(session: Session) { + if (sessions.contains(session)) { + session.unregister(observer) + sessions.remove(session) + } + } + + companion object { + fun register(sessionManager: SessionManager, observer: Session.Observer) = + AllSessionsObserver(sessionManager, observer) + } +} diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessageHandler.kt b/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessageHandler.kt new file mode 100644 index 00000000000..350012f4a3c --- /dev/null +++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconMessageHandler.kt @@ -0,0 +1,140 @@ +/* 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.icons.extension + +import androidx.annotation.VisibleForTesting +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch +import mozilla.components.browser.icons.BrowserIcons +import mozilla.components.browser.icons.IconRequest +import mozilla.components.browser.session.Session +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.webextension.MessageHandler +import mozilla.components.support.base.log.logger.Logger +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import java.lang.IllegalStateException + +/** + * [MessageHandler] implementation that receives messages from the icons web extensions and performs icon loads. + */ +internal class IconMessageHandler( + private val session: Session, + private val icons: BrowserIcons +) : MessageHandler { + private val scope = CoroutineScope(Dispatchers.Main) + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) // This only exists so that we can wait in tests. + internal var lastJob: Job? = null + + override fun onMessage(message: Any, source: EngineSession?): Any { + if (message is JSONObject) { + message.toIconRequest()?.let { loadRequest(it) } + } else { + throw IllegalStateException("Received unexpected message: $message") + } + + // Needs to return something that is not null and not Unit: + // https://github.com/mozilla-mobile/android-components/issues/2969 + return "" + } + + private fun loadRequest(request: IconRequest) { + lastJob = scope.launch { + val icon = icons.loadIcon(request).await() + + if (session.url == request.url) { + session.icon = icon.bitmap + } + } + } +} + +internal fun JSONObject.toIconRequest(): IconRequest? { + return try { + val url = getString("url") + + val resources = mutableListOf() + + val icons = getJSONArray("icons") + for (i in 0 until icons.length()) { + val resource = icons.getJSONObject(i).toIconResource() + resource?.let { resources.add(it) } + } + + IconRequest(url, resources = resources) + } catch (e: JSONException) { + Logger.warn("Could not parse message from icons extensions", e) + null + } +} + +private fun JSONObject.toIconResource(): IconRequest.Resource? { + try { + val url = getString("href") + val type = getString("type").toResourceType() + ?: return null + val sizes = optJSONArray("sizes").toResourceSizes() + val mimeType = optString("mimeType", null) + + return IconRequest.Resource(url, type, sizes, if (mimeType.isNullOrEmpty()) null else mimeType) + } catch (e: JSONException) { + Logger.warn("Could not parse message from icons extensions", e) + return null + } +} + +@Suppress("ComplexMethod") +private fun String.toResourceType(): IconRequest.Resource.Type? { + return when (this) { + "icon" -> IconRequest.Resource.Type.FAVICON + "shortcut icon" -> IconRequest.Resource.Type.FAVICON + "fluid-icon" -> IconRequest.Resource.Type.FLUID_ICON + "apple-touch-icon" -> IconRequest.Resource.Type.APPLE_TOUCH_ICON + "image_src" -> IconRequest.Resource.Type.IMAGE_SRC + "apple-touch-icon image_src" -> IconRequest.Resource.Type.APPLE_TOUCH_ICON + "apple-touch-icon-precomposed" -> IconRequest.Resource.Type.APPLE_TOUCH_ICON + "og:image" -> IconRequest.Resource.Type.OPENGRAPH + "og:image:url" -> IconRequest.Resource.Type.OPENGRAPH + "og:image:secure_url" -> IconRequest.Resource.Type.OPENGRAPH + "twitter:image" -> IconRequest.Resource.Type.TWITTER + "msapplication-TileImage" -> IconRequest.Resource.Type.MICROSOFT_TILE + else -> null + } +} + +@Suppress("ReturnCount") +private fun JSONArray?.toResourceSizes(): List { + if (this == null) { + return emptyList() + } + + try { + val sizes = mutableListOf() + + for (i in 0 until length()) { + val size = getString(i).split("x") + if (size.size != 2) { + continue + } + + val width = size[0].toInt() + val height = size[1].toInt() + + sizes.add(IconRequest.Resource.Size(width, height)) + } + + return sizes + } catch (e: JSONException) { + Logger.warn("Could not parse message from icons extensions", e) + return emptyList() + } catch (e: NumberFormatException) { + Logger.warn("Could not parse message from icons extensions", e) + return emptyList() + } +} diff --git a/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconSessionObserver.kt b/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconSessionObserver.kt new file mode 100644 index 00000000000..dbdc79ab382 --- /dev/null +++ b/components/browser/icons/src/main/java/mozilla/components/browser/icons/extension/IconSessionObserver.kt @@ -0,0 +1,47 @@ +/* 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.icons.extension + +import mozilla.components.browser.icons.BrowserIcons +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager +import mozilla.components.concept.engine.EngineSession +import mozilla.components.concept.engine.webextension.WebExtension + +private const val EXTENSION_MESSAGING_NAME = "MozacBrowserIcons" + +/** + * [Session.Observer] implementation that will setup the content message handler for communicating with the Web + * Extension for [Session] instances that started loading something. + * + * We only setup the handler for [Session]s that started loading something in order to avoid creating an [EngineSession] + * for all [Session]s - breaking the lazy initialization. + */ +internal class IconSessionObserver( + private val icons: BrowserIcons, + private val sessionManager: SessionManager, + private val extension: WebExtension +) : Session.Observer { + override fun onLoadingStateChanged(session: Session, loading: Boolean) { + if (loading) { + registerMessageHandler(session) + } + } + + private fun registerMessageHandler(session: Session) { + val engineSession = sessionManager.getOrCreateEngineSession(session) + + if (extension.hasContentMessageHandler(engineSession, EXTENSION_MESSAGING_NAME)) { + return + } + + val handler = IconMessageHandler(session, icons) + + extension.registerContentMessageHandler( + sessionManager.getOrCreateEngineSession(session), + EXTENSION_MESSAGING_NAME, + handler) + } +} diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/AllSessionsObserverTest.kt b/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/AllSessionsObserverTest.kt new file mode 100644 index 00000000000..eaa41446c19 --- /dev/null +++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/AllSessionsObserverTest.kt @@ -0,0 +1,118 @@ +/* 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.icons.extension + +import mozilla.components.browser.session.Session +import mozilla.components.browser.session.SessionManager +import mozilla.components.support.test.mock +import org.junit.Test +import org.mockito.Mockito.never +import org.mockito.Mockito.spy +import org.mockito.Mockito.verify + +class AllSessionsObserverTest { + @Test + fun `Observer will be registered on all already existing Sessions`() { + val session1: Session = mock() + val session2: Session = mock() + + val sessionManager = SessionManager(engine = mock()).apply { + add(session1) + add(session2) + } + + val observer: Session.Observer = mock() + + AllSessionsObserver.register(sessionManager, observer) + + verify(session1).register(observer) + verify(session2).register(observer) + } + + @Test + fun `Observer will be registered on added Sessions`() { + val sessionManager = SessionManager(engine = mock()) + + val observer: Session.Observer = mock() + + AllSessionsObserver.register(sessionManager, observer) + + val session1: Session = mock() + val session2: Session = mock() + + sessionManager.add(session1) + sessionManager.add(session2) + + verify(session1).register(observer) + verify(session2).register(observer) + } + + @Test + fun `Observer will be unregistered if Session gets removed`() { + val session1: Session = spy(Session("https://www.mozilla.org")) + val session2: Session = mock() + + val sessionManager = SessionManager(engine = mock()).apply { + add(session1) + add(session2) + } + + val observer: Session.Observer = mock() + + AllSessionsObserver.register(sessionManager, observer) + + sessionManager.remove(session1) + + verify(session1).register(observer) + verify(session1).unregister(observer) + + verify(session2).register(observer) + verify(session2, never()).unregister(observer) + } + + @Test + fun `Observer gets registered when Sessions get restored`() { + val sessionManager = SessionManager(engine = mock()) + + val observer: Session.Observer = mock() + AllSessionsObserver.register(sessionManager, observer) + + val session1: Session = mock() + val session2: Session = mock() + + val snapshot = SessionManager.Snapshot( + sessions = listOf( + SessionManager.Snapshot.Item(session1), + SessionManager.Snapshot.Item(session2)), + selectedSessionIndex = 0) + + sessionManager.restore(snapshot) + + verify(session1).register(observer) + verify(session2).register(observer) + } + + @Test + fun `Observer gets unregistered when all Sessions get removed`() { + val session1: Session = spy(Session("https://www.mozilla.org")) + val session2: Session = spy(Session("https://getpocket.com")) + + val sessionManager = SessionManager(engine = mock()).apply { + add(session1) + add(session2) + } + + val observer: Session.Observer = mock() + AllSessionsObserver.register(sessionManager, observer) + + verify(session1).register(observer) + verify(session2).register(observer) + + sessionManager.removeSessions() + + verify(session1).unregister(observer) + verify(session2).unregister(observer) + } +} diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt b/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt new file mode 100644 index 00000000000..a67a53527cd --- /dev/null +++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconMessageHandlerTest.kt @@ -0,0 +1,194 @@ +/* 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.icons.extension + +import android.graphics.Bitmap +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.async +import kotlinx.coroutines.runBlocking +import mozilla.components.browser.icons.BrowserIcons +import mozilla.components.browser.icons.Icon +import mozilla.components.browser.icons.IconRequest +import mozilla.components.browser.session.Session +import mozilla.components.support.test.any +import mozilla.components.support.test.argumentCaptor +import mozilla.components.support.test.mock +import org.json.JSONObject +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.doReturn +import org.mockito.Mockito.verify +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class IconMessageHandlerTest { + @Test + fun `Complex message (TheVerge) is transformed into IconRequest and loaded`() = runBlocking { + val session: Session = mock() + doReturn("https://www.theverge.com/").`when`(session).url + + val bitmap: Bitmap = mock() + val icon = Icon(bitmap, source = Icon.Source.DOWNLOAD) + val deferredIcon = GlobalScope.async { icon } + + val icons: BrowserIcons = mock() + doReturn(deferredIcon).`when`(icons).loadIcon(any()) + + val handler = IconMessageHandler(session, icons) + + val message = """ + { + "url": "https:\/\/www.theverge.com\/", + "icons": [ + { + "mimeType": "image\/png", + "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7395367\/favicon-16x16.0.png", + "type": "icon", + "sizes": [ + "16x16" + ] + }, + { + "mimeType": "image\/png", + "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7395363\/favicon-32x32.0.png", + "type": "icon", + "sizes": [ + "32x32" + ] + }, + { + "mimeType": "image\/png", + "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7395365\/favicon-96x96.0.png", + "type": "icon", + "sizes": [ + "96x96" + ] + }, + { + "mimeType": "image\/png", + "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7395351\/android-chrome-192x192.0.png", + "type": "icon", + "sizes": [ + "192x192" + ] + }, + { + "mimeType": "", + "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7395361\/favicon-64x64.0.ico", + "type": "shortcut icon", + "sizes": [] + }, + { + "mimeType": "", + "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7395359\/ios-icon.0.png", + "type": "apple-touch-icon", + "sizes": [ + "180x180" + ] + }, + { + "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/9672633\/VergeOG.0_1200x627.0.png", + "type": "og:image" + }, + { + "href": "https:\/\/cdn.vox-cdn.com\/community_logos\/52803\/VER_Logomark_175x92..png", + "type": "twitter:image" + }, + { + "href": "https:\/\/cdn.vox-cdn.com\/uploads\/chorus_asset\/file\/7396113\/221a67c8-a10f-11e6-8fae-983107008690.0.png", + "type": "msapplication-TileImage" + } + ] + } + """.trimIndent() + + handler.onMessage(JSONObject(message), source = null) + + assertNotNull(handler.lastJob) + handler.lastJob!!.join() + + // Examine IconRequest + val captor = argumentCaptor() + verify(icons).loadIcon(captor.capture()) + + val request = captor.value + assertEquals("https://www.theverge.com/", request.url) + assertEquals(9, request.resources.size) + + with(request.resources[0]) { + assertEquals("image/png", mimeType) + assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395367/favicon-16x16.0.png", url) + assertEquals(IconRequest.Resource.Type.FAVICON, type) + assertEquals(1, sizes.size) + assertEquals(IconRequest.Resource.Size(16, 16), sizes[0]) + } + + with(request.resources[1]) { + assertEquals("image/png", mimeType) + assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395363/favicon-32x32.0.png", url) + assertEquals(IconRequest.Resource.Type.FAVICON, type) + assertEquals(1, sizes.size) + assertEquals(IconRequest.Resource.Size(32, 32), sizes[0]) + } + + with(request.resources[2]) { + assertEquals("image/png", mimeType) + assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395365/favicon-96x96.0.png", url) + assertEquals(IconRequest.Resource.Type.FAVICON, type) + assertEquals(1, sizes.size) + assertEquals(IconRequest.Resource.Size(96, 96), sizes[0]) + } + + with(request.resources[3]) { + assertEquals("image/png", mimeType) + assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395351/android-chrome-192x192.0.png", url) + assertEquals(IconRequest.Resource.Type.FAVICON, type) + assertEquals(1, sizes.size) + assertEquals(IconRequest.Resource.Size(192, 192), sizes[0]) + } + + with(request.resources[4]) { + assertNull(mimeType) + assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395361/favicon-64x64.0.ico", url) + assertEquals(IconRequest.Resource.Type.FAVICON, type) + assertEquals(0, sizes.size) + } + + with(request.resources[5]) { + assertNull(mimeType) + assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7395359/ios-icon.0.png", url) + assertEquals(IconRequest.Resource.Type.APPLE_TOUCH_ICON, type) + assertEquals(1, sizes.size) + assertEquals(IconRequest.Resource.Size(180, 180), sizes[0]) + } + + with(request.resources[6]) { + assertNull(mimeType) + assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/9672633/VergeOG.0_1200x627.0.png", url) + assertEquals(IconRequest.Resource.Type.OPENGRAPH, type) + assertEquals(0, sizes.size) + } + + with(request.resources[7]) { + assertNull(mimeType) + assertEquals("https://cdn.vox-cdn.com/community_logos/52803/VER_Logomark_175x92..png", url) + assertEquals(IconRequest.Resource.Type.TWITTER, type) + assertEquals(0, sizes.size) + } + + with(request.resources[8]) { + assertNull(mimeType) + assertEquals("https://cdn.vox-cdn.com/uploads/chorus_asset/file/7396113/221a67c8-a10f-11e6-8fae-983107008690.0.png", url) + assertEquals(IconRequest.Resource.Type.MICROSOFT_TILE, type) + assertEquals(0, sizes.size) + } + + // Loaded icon will be set on session + verify(session).icon = bitmap + } +} diff --git a/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconSessionObserverTest.kt b/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconSessionObserverTest.kt new file mode 100644 index 00000000000..6b33bb9b5ce --- /dev/null +++ b/components/browser/icons/src/test/java/mozilla/components/browser/icons/extension/IconSessionObserverTest.kt @@ -0,0 +1,49 @@ +/* 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.icons.extension + +import mozilla.components.concept.engine.webextension.WebExtension +import mozilla.components.support.test.any +import mozilla.components.support.test.eq +import mozilla.components.support.test.mock +import org.junit.Test +import org.mockito.Mockito.`when` +import org.mockito.Mockito.never +import org.mockito.Mockito.verify + +class IconSessionObserverTest { + @Test + fun `Starting a load will register a message handler`() { + val extension: WebExtension = mock() + `when`(extension.hasContentMessageHandler(any(), any())).thenReturn(false) + + val observer = IconSessionObserver(icons = mock(), sessionManager = mock(), extension = extension) + observer.onLoadingStateChanged(session = mock(), loading = true) + + verify(extension).registerContentMessageHandler(any(), eq("MozacBrowserIcons"), any()) + } + + @Test + fun `Stoping a load will not register a message handler`() { + val extension: WebExtension = mock() + `when`(extension.hasContentMessageHandler(any(), any())).thenReturn(false) + + val observer = IconSessionObserver(icons = mock(), sessionManager = mock(), extension = extension) + observer.onLoadingStateChanged(session = mock(), loading = false) + + verify(extension, never()).registerContentMessageHandler(any(), any(), any()) + } + + @Test + fun `Message handler will not be registered twice`() { + val extension: WebExtension = mock() + `when`(extension.hasContentMessageHandler(any(), any())).thenReturn(true) + + val observer = IconSessionObserver(icons = mock(), sessionManager = mock(), extension = extension) + observer.onLoadingStateChanged(session = mock(), loading = true) + + verify(extension, never()).registerContentMessageHandler(any(), eq("MozacBrowserIcons"), any()) + } +} diff --git a/samples/browser/src/geckoNightly/java/org/mozilla/samples/browser/Components.kt b/samples/browser/src/geckoNightly/java/org/mozilla/samples/browser/Components.kt index 021694cf8d0..f2aa1174c7b 100644 --- a/samples/browser/src/geckoNightly/java/org/mozilla/samples/browser/Components.kt +++ b/samples/browser/src/geckoNightly/java/org/mozilla/samples/browser/Components.kt @@ -17,8 +17,6 @@ class Components(private val applicationContext: Context) : DefaultComponents(ap installWebExtension("mozacBorderify", "resource://android/assets/extensions/borderify/") { ext, throwable -> Log.log(Log.Priority.ERROR, "SampleBrowser", throwable, "Failed to install $ext") } - - icons.install(this) } } } 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 6a57072b54a..035be761af5 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 @@ -73,6 +73,8 @@ open class DefaultComponents(private val applicationContext: Context) { .periodicallyInForeground(interval = 30, unit = TimeUnit.SECONDS) .whenGoingToBackground() .whenSessionsChange() + + icons.install(engine, this) } }