Commit 12352d9d authored by Sebastian Kaspari's avatar Sebastian Kaspari
Browse files

Issue #3616: (Merge day) browser-engine-gecko-nightly (69) -> browser-engine-gecko-beta (69)

parent 1e9b0308
......@@ -11,7 +11,7 @@ internal object GeckoVersions {
/**
* GeckoView Beta Version.
*/
const val beta_version = "68.0.20190612114833"
const val beta_version = "69.0.20190711090037"
/**
* GeckoView Release Version.
......
......@@ -30,7 +30,8 @@ dependencies {
implementation project(':concept-engine')
implementation project(':concept-fetch')
implementation project(':support-ktx')
implementation project(path: ':support-utils')
implementation project(':support-utils')
implementation Dependencies.kotlin_stdlib
implementation Dependencies.kotlin_coroutines
......
......@@ -8,7 +8,6 @@ import android.annotation.SuppressLint
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import mozilla.components.browser.engine.gecko.media.GeckoMediaDelegate
import mozilla.components.browser.engine.gecko.permission.GeckoPermissionRequest
......@@ -19,6 +18,7 @@ import mozilla.components.concept.engine.EngineSessionState
import mozilla.components.concept.engine.HitResult
import mozilla.components.concept.engine.Settings
import mozilla.components.concept.engine.history.HistoryTrackingDelegate
import mozilla.components.concept.engine.manifest.WebAppManifestParser
import mozilla.components.concept.engine.request.RequestInterceptor
import mozilla.components.concept.engine.request.RequestInterceptor.InterceptionResponse
import mozilla.components.concept.storage.VisitType
......@@ -27,6 +27,7 @@ import mozilla.components.support.ktx.kotlin.isEmail
import mozilla.components.support.ktx.kotlin.isGeoLocation
import mozilla.components.support.ktx.kotlin.isPhone
import mozilla.components.support.utils.DownloadUtils
import org.json.JSONObject
import org.mozilla.geckoview.AllowOrDeny
import org.mozilla.geckoview.ContentBlocking
import org.mozilla.geckoview.GeckoResult
......@@ -321,8 +322,9 @@ class GeckoEngineSession(
GeckoResult.fromValue(AllowOrDeny.DENY)
} else {
notifyObservers {
// As the name LoadRequest.isRedirect may imply this flag is about http redirects. The flag
// Unlike the name LoadRequest.isRedirect may imply this flag is not about http redirects. The flag
// is "True if and only if the request was triggered by an HTTP redirect."
// See: https://bugzilla.mozilla.org/show_bug.cgi?id=1545170
onLoadRequest(
url = request.uri,
triggeredByRedirect = request.isRedirect,
......@@ -450,12 +452,10 @@ class GeckoEngineSession(
return GeckoResult.fromValue(false)
}
val result = GeckoResult<Boolean>()
launch {
return launchGeckoResult {
delegate.onVisited(url, visitType)
result.complete(true)
true
}
return result
}
override fun getVisited(
......@@ -468,12 +468,10 @@ class GeckoEngineSession(
val delegate = settings.historyTrackingDelegate ?: return GeckoResult.fromValue(null)
val result = GeckoResult<BooleanArray>()
launch {
val visits: List<Boolean>? = delegate.getVisited(urls.toList())
result.complete(visits?.toBooleanArray())
return launchGeckoResult {
val visits = delegate.getVisited(urls.toList())
visits.toBooleanArray()
}
return result
}
}
......@@ -534,6 +532,13 @@ class GeckoEngineSession(
}
override fun onFocusRequest(session: GeckoSession) = Unit
override fun onWebAppManifest(session: GeckoSession, manifest: JSONObject) {
val parsed = WebAppManifestParser().parse(manifest)
if (parsed is WebAppManifestParser.Result.Success) {
notifyObservers { onWebAppManifestLoaded(parsed.manifest) }
}
}
}
private fun createContentBlockingDelegate() = object : ContentBlocking.Delegate {
......
/* 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.engine.gecko
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.launch
import org.mozilla.geckoview.GeckoResult
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
import kotlin.coroutines.suspendCoroutine
/**
* Wait for a GeckoResult to be complete in a co-routine.
*/
suspend fun <T> GeckoResult<T>.await() = suspendCoroutine<T?> { continuation ->
then({
continuation.resume(it)
GeckoResult<Void>()
}, {
continuation.resumeWithException(it)
GeckoResult<Void>()
})
}
/**
* Create a GeckoResult from a co-routine.
*/
@Suppress("TooGenericExceptionCaught")
fun <T> CoroutineScope.launchGeckoResult(
context: CoroutineContext = EmptyCoroutineContext,
start: CoroutineStart = CoroutineStart.DEFAULT,
block: suspend CoroutineScope.() -> T
) = GeckoResult<T>().apply {
launch(context, start) {
try {
val value = block()
complete(value)
} catch (exception: Throwable) {
completeExceptionally(exception)
}
}
}
......@@ -38,23 +38,15 @@ class GeckoViewFetchClient(
internal var executor: GeckoWebExecutor = GeckoWebExecutor(runtime)
@Throws(IOException::class)
@Suppress("ComplexMethod")
override fun fetch(request: Request): Response {
val webRequest = with(request) {
WebRequest.Builder(url)
.method(method.name)
.addHeadersFrom(request, defaultHeaders)
.addBodyFrom(request)
.cacheMode(if (request.useCaches) CACHE_MODE_DEFAULT else CACHE_MODE_RELOAD)
.build()
}
val webRequest = request.toWebRequest(defaultHeaders)
val readTimeOut = request.readTimeout ?: maxReadTimeOut
val readTimeOutMillis = readTimeOut.let { (timeout, unit) ->
unit.toMillis(timeout)
}
try {
return try {
var fetchFlags = 0
if (request.cookiePolicy == Request.CookiePolicy.OMIT) {
fetchFlags += GeckoWebExecutor.FETCH_FLAGS_ANONYMOUS
......@@ -63,7 +55,7 @@ class GeckoViewFetchClient(
fetchFlags += GeckoWebExecutor.FETCH_FLAGS_NO_REDIRECTS
}
val webResponse = executor.fetch(webRequest, fetchFlags).poll(readTimeOutMillis)
return webResponse?.toResponse() ?: throw IOException("Fetch failed with null response")
webResponse?.toResponse() ?: throw IOException("Fetch failed with null response")
} catch (e: TimeoutException) {
throw SocketTimeoutException()
} catch (e: WebRequestError) {
......@@ -76,6 +68,13 @@ class GeckoViewFetchClient(
}
}
private fun Request.toWebRequest(defaultHeaders: Headers): WebRequest = WebRequest.Builder(url)
.method(method.name)
.addHeadersFrom(this, defaultHeaders)
.addBodyFrom(this)
.cacheMode(if (useCaches) CACHE_MODE_DEFAULT else CACHE_MODE_RELOAD)
.build()
private fun WebRequest.Builder.addHeadersFrom(request: Request, defaultHeaders: Headers): WebRequest.Builder {
defaultHeaders.filter { header ->
request.headers?.contains(header.name) != true
......
......@@ -46,7 +46,7 @@ internal class GeckoMedia(
private class MediaDelegate(
private val media: Media
) : MediaElement.Delegate {
@Suppress("ComplexMethod")
override fun onPlaybackStateChange(mediaElement: MediaElement, mediaState: Int) {
when (mediaState) {
MEDIA_STATE_PLAY -> media.playbackState = Media.PlaybackState.PLAY
......
......@@ -22,13 +22,11 @@ internal class GeckoMediaDelegate(
private val mediaMap: MutableMap<MediaElement, Media> = mutableMapOf()
override fun onMediaAdd(session: GeckoSession, element: MediaElement) {
engineSession.notifyObservers {
val media = GeckoMedia(element)
mediaMap[element] = media
onMediaAdded(media)
val media = GeckoMedia(element).also {
mediaMap[element] = it
}
engineSession.notifyObservers { onMediaAdded(media) }
}
override fun onMediaRemove(session: GeckoSession, element: MediaElement) {
......
......@@ -4,24 +4,21 @@
package mozilla.components.browser.engine.gecko.permission
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.Manifest.permission.CAMERA
import android.Manifest.permission.RECORD_AUDIO
import mozilla.components.concept.engine.permission.Permission
import mozilla.components.concept.engine.permission.PermissionRequest
import org.mozilla.geckoview.GeckoSession.PermissionDelegate
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_AUDIOCAPTURE
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_APPLICATION
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_BROWSER
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_CAMERA
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_MICROPHONE
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_OTHER
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_SCREEN
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.MediaSource.SOURCE_WINDOW
import android.Manifest.permission.ACCESS_COARSE_LOCATION
import android.Manifest.permission.ACCESS_FINE_LOCATION
import android.Manifest.permission.CAMERA
import android.Manifest.permission.RECORD_AUDIO
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_DESKTOP_NOTIFICATION
import org.mozilla.geckoview.GeckoSession.PermissionDelegate.PERMISSION_GEOLOCATION
/**
* Gecko-based implementation of [PermissionRequest].
......@@ -118,26 +115,27 @@ sealed class GeckoPermissionRequest constructor(
}
companion object {
@Suppress("ComplexMethod", "SwitchIntDef")
fun mapPermission(mediaSource: MediaSource): Permission {
fun mapPermission(mediaSource: MediaSource): Permission =
if (mediaSource.type == MediaSource.TYPE_AUDIO) {
return when (mediaSource.source) {
SOURCE_AUDIOCAPTURE -> Permission.ContentAudioCapture(mediaSource.id, mediaSource.name)
SOURCE_MICROPHONE -> Permission.ContentAudioMicrophone(mediaSource.id, mediaSource.name)
SOURCE_OTHER -> Permission.ContentAudioOther(mediaSource.id, mediaSource.name)
else -> Permission.Generic(mediaSource.id, mediaSource.name)
}
mapAudioPermission(mediaSource)
} else {
return when (mediaSource.source) {
SOURCE_CAMERA -> Permission.ContentVideoCamera(mediaSource.id, mediaSource.name)
SOURCE_APPLICATION -> Permission.ContentVideoApplication(mediaSource.id, mediaSource.name)
SOURCE_BROWSER -> Permission.ContentVideoBrowser(mediaSource.id, mediaSource.name)
SOURCE_SCREEN -> Permission.ContentVideoScreen(mediaSource.id, mediaSource.name)
SOURCE_WINDOW -> Permission.ContentVideoWindow(mediaSource.id, mediaSource.name)
SOURCE_OTHER -> Permission.ContentVideoOther(mediaSource.id, mediaSource.name)
else -> Permission.Generic(mediaSource.id, mediaSource.name)
}
mapVideoPermission(mediaSource)
}
@Suppress("SwitchIntDef")
private fun mapAudioPermission(mediaSource: MediaSource) = when (mediaSource.source) {
SOURCE_AUDIOCAPTURE -> Permission.ContentAudioCapture(mediaSource.id, mediaSource.name)
SOURCE_MICROPHONE -> Permission.ContentAudioMicrophone(mediaSource.id, mediaSource.name)
SOURCE_OTHER -> Permission.ContentAudioOther(mediaSource.id, mediaSource.name)
else -> Permission.Generic(mediaSource.id, mediaSource.name)
}
@Suppress("ComplexMethod", "SwitchIntDef")
private fun mapVideoPermission(mediaSource: MediaSource) = when (mediaSource.source) {
SOURCE_CAMERA -> Permission.ContentVideoCamera(mediaSource.id, mediaSource.name)
SOURCE_SCREEN -> Permission.ContentVideoScreen(mediaSource.id, mediaSource.name)
SOURCE_OTHER -> Permission.ContentVideoOther(mediaSource.id, mediaSource.name)
else -> Permission.Generic(mediaSource.id, mediaSource.name)
}
}
}
......
......@@ -132,6 +132,8 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe
val isMultipleFilesSelection = selectionType == GeckoSession.PromptDelegate.FILE_TYPE_MULTIPLE
val captureMode = PromptRequest.File.FacingMode.NONE
val onSelectSingle: (Context, Uri) -> Unit = { context, uri ->
callback.confirm(context, uri.toFileUri(context))
}
......@@ -145,6 +147,7 @@ internal class GeckoPromptDelegate(private val geckoEngineSession: GeckoEngineSe
PromptRequest.File(
mimeTypes ?: emptyArray(),
isMultipleFilesSelection,
captureMode,
onSelectSingle,
onSelectMultiple,
onDismiss
......
......@@ -17,6 +17,7 @@ import mozilla.components.concept.engine.EngineSession.TrackingProtectionPolicy
import mozilla.components.concept.engine.HitResult
import mozilla.components.concept.engine.UnsupportedSettingException
import mozilla.components.concept.engine.history.HistoryTrackingDelegate
import mozilla.components.concept.engine.manifest.WebAppManifest
import mozilla.components.concept.engine.permission.PermissionRequest
import mozilla.components.concept.engine.request.RequestInterceptor
import mozilla.components.concept.storage.VisitType
......@@ -27,6 +28,7 @@ import mozilla.components.support.test.mock
import mozilla.components.support.test.whenever
import mozilla.components.support.utils.ThreadUtils
import mozilla.components.test.ReflectionUtils
import org.json.JSONObject
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNotNull
......@@ -179,8 +181,8 @@ class GeckoEngineSessionTest {
geckoSessionProvider = geckoSessionProvider)
var observedUrl = ""
var observedCanGoBack: Boolean = false
var observedCanGoForward: Boolean = false
var observedCanGoBack = false
var observedCanGoForward = false
engineSession.register(object : EngineSession.Observer {
override fun onLocationChange(url: String) { observedUrl = url }
override fun onNavigationStateChange(canGoBack: Boolean?, canGoForward: Boolean?) {
......@@ -228,13 +230,36 @@ class GeckoEngineSessionTest {
cookie = null)
}
@Test
fun contentDelegateNotifiesObserverAboutWebAppManifest() {
val engineSession = GeckoEngineSession(mock(),
geckoSessionProvider = geckoSessionProvider)
val observer: EngineSession.Observer = mock()
engineSession.register(observer)
val json = JSONObject().apply {
put("name", "Minimal")
put("start_url", "/")
}
val manifest = WebAppManifest(
name = "Minimal",
startUrl = "/"
)
captureDelegates()
contentDelegate.value.onWebAppManifest(mock(), json)
verify(observer).onWebAppManifestLoaded(manifest)
}
@Test
fun permissionDelegateNotifiesObservers() {
val engineSession = GeckoEngineSession(mock(),
geckoSessionProvider = geckoSessionProvider)
var observedContentPermissionRequests: MutableList<PermissionRequest> = mutableListOf()
var observedAppPermissionRequests: MutableList<PermissionRequest> = mutableListOf()
val observedContentPermissionRequests: MutableList<PermissionRequest> = mutableListOf()
val observedAppPermissionRequests: MutableList<PermissionRequest> = mutableListOf()
engineSession.register(object : EngineSession.Observer {
override fun onContentPermissionRequest(permissionRequest: PermissionRequest) {
observedContentPermissionRequests.add(permissionRequest)
......@@ -540,7 +565,7 @@ class GeckoEngineSessionTest {
// Nothing breaks if history delegate isn't configured.
historyDelegate.value.onVisited(geckoSession, "https://www.mozilla.com", null, GeckoSession.HistoryDelegate.VISIT_TOP_LEVEL)
engineSession.job.children.forEach { it.join() }
engineSession.job.children.forEach { it.join() }
engineSession.settings.historyTrackingDelegate = historyTrackingDelegate
......@@ -1644,11 +1669,13 @@ class GeckoEngineSessionTest {
captureDelegates()
var observedUrl: String? = null
var observedTriggeredByRedirect: Boolean? = null
engineSession.register(object : EngineSession.Observer {
override fun onLoadRequest(url: String, triggeredByRedirect: Boolean, triggeredByWebContent: Boolean) {
observedTriggeredByRedirect = triggeredByRedirect
observedUrl = url
}
})
......@@ -1657,11 +1684,13 @@ class GeckoEngineSessionTest {
assertNotNull(observedTriggeredByRedirect)
assertTrue(observedTriggeredByRedirect!!)
assertEquals("sample:about", observedUrl)
navigationDelegate.value.onLoadRequest(
mock(), mockLoadRequest("sample:about", triggeredByRedirect = false))
assertFalse(observedTriggeredByRedirect!!)
assertEquals("sample:about", observedUrl)
}
@Test
......@@ -1758,10 +1787,11 @@ class GeckoEngineSessionTest {
val observer: EngineSession.Observer = mock()
engineSession.register(observer)
val fakeUri = "sample:about"
navigationDelegate.value.onLoadRequest(
mock(), mockLoadRequest("sample:about", triggeredByRedirect = true))
mock(), mockLoadRequest(fakeUri, triggeredByRedirect = true))
verify(observer, never()).onLoadRequest(eq("sample:about"), anyBoolean(), anyBoolean())
verify(observer, never()).onLoadRequest(eq(fakeUri), anyBoolean(), anyBoolean())
}
@Test
......
......@@ -408,7 +408,7 @@ class GeckoEngineTest {
println(version)
assertTrue(version.major >= 68)
assertTrue(version.isAtLeast(68, 0, 0))
assertTrue(version.major >= 69)
assertTrue(version.isAtLeast(69, 0, 0))
}
}
\ No newline at end of file
......@@ -15,7 +15,7 @@ import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.Mockito.verify
import org.mozilla.geckoview.GeckoResult
......@@ -96,8 +96,8 @@ class GeckoEngineViewTest {
engineView.render(engineSession)
verify(geckoView, Mockito.never()).releaseSession()
verify(engineSession, Mockito.never()).unregister(any())
verify(geckoView, never()).releaseSession()
verify(engineSession, never()).unregister(any())
engineView.release()
......
/* 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.engine.gecko
import androidx.test.ext.junit.runners.AndroidJUnit4
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runBlockingTest
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.mozilla.geckoview.GeckoResult
@ExperimentalCoroutinesApi
@RunWith(AndroidJUnit4::class)
class GeckoResultTest {
@Test
fun awaitWithResult() = runBlockingTest {
val result = GeckoResult.fromValue(42).await()
assertEquals(42, result)
}
@Test(expected = IllegalStateException::class)
fun awaitWithException() = runBlockingTest {
GeckoResult.fromException<Unit>(IllegalStateException()).await()
}
@Test
fun fromResult() = runBlockingTest {
val result = launchGeckoResult { 42 }
result.then {
assertEquals(42, it)
GeckoResult.fromValue(null)
}.await()
}
@Test
fun fromException() = runBlockingTest {
val result = launchGeckoResult { throw IllegalStateException() }
result.then({
assertTrue("Invalid branch", false)
GeckoResult.fromValue(null)
}, {
assertTrue(it is IllegalStateException)
GeckoResult.fromValue(null)
}).await()
}
}
......@@ -67,4 +67,21 @@ class GeckoMediaTest {
assertTrue(media.controller is GeckoMediaController)
}
@Test
fun `GeckoMedia observer is notified when its playback state changes`() {
val mediaElement: MediaElement = mock()
val media = GeckoMedia(mediaElement)
val captor = argumentCaptor<MediaElement.Delegate>()
verify(mediaElement).delegate = captor.capture()
val delegate = captor.value
val observer: Media.Observer = mock()
media.register(observer)
delegate.onPlaybackStateChange(mediaElement, MediaElement.MEDIA_STATE_PLAYING)
verify(observer).onPlaybackStateChanged(media, Media.PlaybackState.PLAYING)
}
}
......@@ -114,26 +114,17 @@ class GeckoPermissionRequestTest {
val videoCamera = MockMediaSource("videoCamera", "videoCamera",
MediaSource.SOURCE_CAMERA, MediaSource.TYPE_VIDEO)
val videoBrowser = MockMediaSource("videoBrowser", "videoBrowser",
MediaSource.SOURCE_BROWSER, MediaSource.TYPE_VIDEO)