Skip to content

Commit

Permalink
Issue mozilla-mobile#2080: Extract website icon resources via WebExte…
Browse files Browse the repository at this point in the history
…nsion.
  • Loading branch information
pocmo committed May 10, 2019
1 parent 05854ec commit c54ddf6
Show file tree
Hide file tree
Showing 12 changed files with 648 additions and 10 deletions.
1 change: 1 addition & 0 deletions components/browser/icons/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,29 @@
* 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 []
}

return Array.from(sizes)
}

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
});
})
}
Expand Down Expand Up @@ -57,8 +73,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);
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,9 @@
"js": ["icons.js"],
"run_at": "document_end"
}
],
"permissions": [
"geckoViewAddons",
"nativeMessaging"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Session> = 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)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/* 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) {
// Only update the icon of the session if we are still on this page. The user may have navigated
// away by the time the icon is loaded.
session.icon = icon.bitmap
}
}
}
}

internal fun JSONObject.toIconRequest(): IconRequest? {
return try {
val url = getString("url")

val resources = mutableListOf<IconRequest.Resource>()

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<IconRequest.Resource.Size> {
if (this == null) {
return emptyList()
}

try {
val sizes = mutableListOf<IconRequest.Resource.Size>()

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()
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading

0 comments on commit c54ddf6

Please sign in to comment.