Commit 42b1ceb4 authored by Gabriel Luong's avatar Gabriel Luong
Browse files

Closes #6352: Add contextId to BrowserState

parent 2f745060
......@@ -141,10 +141,11 @@ class GeckoEngine(
/**
* See [Engine.createSession].
*/
override fun createSession(private: Boolean): EngineSession {
override fun createSession(private: Boolean, contextId: String?): EngineSession {
ThreadUtils.assertOnUiThread()
val speculativeSession = this.speculativeSession?.let { speculativeSession ->
if (speculativeSession.geckoSession.settings.usePrivateMode == private) {
if (speculativeSession.geckoSession.settings.usePrivateMode == private &&
speculativeSession.geckoSession.settings.contextId == contextId) {
speculativeSession
} else {
speculativeSession.close()
......@@ -153,7 +154,7 @@ class GeckoEngine(
this.speculativeSession = null
}
}
return speculativeSession ?: GeckoEngineSession(runtime, private, defaultSettings)
return speculativeSession ?: GeckoEngineSession(runtime, private, defaultSettings, contextId)
}
/**
......@@ -166,11 +167,12 @@ class GeckoEngine(
/**
* See [Engine.speculativeCreateSession].
*/
override fun speculativeCreateSession(private: Boolean) {
override fun speculativeCreateSession(private: Boolean, contextId: String?) {
ThreadUtils.assertOnUiThread()
if (this.speculativeSession?.geckoSession?.settings?.usePrivateMode != private) {
if (this.speculativeSession?.geckoSession?.settings?.usePrivateMode != private ||
this.speculativeSession?.geckoSession?.settings?.contextId != contextId) {
this.speculativeSession?.geckoSession?.close()
this.speculativeSession = GeckoEngineSession(runtime, private, defaultSettings)
this.speculativeSession = GeckoEngineSession(runtime, private, defaultSettings, contextId)
}
}
......
......@@ -52,9 +52,11 @@ class GeckoEngineSession(
private val runtime: GeckoRuntime,
private val privateMode: Boolean = false,
private val defaultSettings: Settings? = null,
contextId: String? = null,
private val geckoSessionProvider: () -> GeckoSession = {
val settings = GeckoSessionSettings.Builder()
.usePrivateMode(privateMode)
.contextId(contextId)
.build()
GeckoSession(settings)
},
......
......@@ -136,6 +136,28 @@ class GeckoEngineTest {
assertFalse(regularSpeculativeSession.geckoSession.settings.usePrivateMode)
}
@Test
fun `createSession with contextId`() {
val engine = GeckoEngine(context, runtime = runtime)
// Create a speculative session with a context id and consume it
engine.speculativeCreateSession(private = false, contextId = "1")
assertNotNull(engine.speculativeSession)
var newSpeculativeSession = engine.speculativeSession!!
assertSame(newSpeculativeSession, engine.createSession(private = false, contextId = "1"))
assertNull(engine.speculativeSession)
// Create a regular speculative session and make sure it is not returned
// if a session with a context id is requested instead.
engine.speculativeCreateSession(private = false)
assertNotNull(engine.speculativeSession)
newSpeculativeSession = engine.speculativeSession!!
assertNotSame(newSpeculativeSession, engine.createSession(private = false, contextId = "1"))
// Make sure previous (never used) speculative session is now closed
assertFalse(newSpeculativeSession.geckoSession.isOpen)
assertNull(engine.speculativeSession)
}
@Test
fun name() {
assertEquals("Gecko", GeckoEngine(context, runtime = runtime).name())
......
......@@ -141,10 +141,11 @@ class GeckoEngine(
/**
* See [Engine.createSession].
*/
override fun createSession(private: Boolean): EngineSession {
override fun createSession(private: Boolean, contextId: String?): EngineSession {
ThreadUtils.assertOnUiThread()
val speculativeSession = this.speculativeSession?.let { speculativeSession ->
if (speculativeSession.geckoSession.settings.usePrivateMode == private) {
if (speculativeSession.geckoSession.settings.usePrivateMode == private &&
speculativeSession.geckoSession.settings.contextId == contextId) {
speculativeSession
} else {
speculativeSession.close()
......@@ -153,7 +154,7 @@ class GeckoEngine(
this.speculativeSession = null
}
}
return speculativeSession ?: GeckoEngineSession(runtime, private, defaultSettings)
return speculativeSession ?: GeckoEngineSession(runtime, private, defaultSettings, contextId)
}
/**
......@@ -166,11 +167,12 @@ class GeckoEngine(
/**
* See [Engine.speculativeCreateSession].
*/
override fun speculativeCreateSession(private: Boolean) {
override fun speculativeCreateSession(private: Boolean, contextId: String?) {
ThreadUtils.assertOnUiThread()
if (this.speculativeSession?.geckoSession?.settings?.usePrivateMode != private) {
if (this.speculativeSession?.geckoSession?.settings?.usePrivateMode != private ||
this.speculativeSession?.geckoSession?.settings?.contextId != contextId) {
this.speculativeSession?.geckoSession?.close()
this.speculativeSession = GeckoEngineSession(runtime, private, defaultSettings)
this.speculativeSession = GeckoEngineSession(runtime, private, defaultSettings, contextId)
}
}
......
......@@ -52,9 +52,11 @@ class GeckoEngineSession(
private val runtime: GeckoRuntime,
private val privateMode: Boolean = false,
private val defaultSettings: Settings? = null,
contextId: String? = null,
private val geckoSessionProvider: () -> GeckoSession = {
val settings = GeckoSessionSettings.Builder()
.usePrivateMode(privateMode)
.contextId(contextId)
.build()
GeckoSession(settings)
},
......
......@@ -136,6 +136,28 @@ class GeckoEngineTest {
assertFalse(regularSpeculativeSession.geckoSession.settings.usePrivateMode)
}
@Test
fun `createSession with contextId`() {
val engine = GeckoEngine(context, runtime = runtime)
// Create a speculative session with a context id and consume it
engine.speculativeCreateSession(private = false, contextId = "1")
assertNotNull(engine.speculativeSession)
var newSpeculativeSession = engine.speculativeSession!!
assertSame(newSpeculativeSession, engine.createSession(private = false, contextId = "1"))
assertNull(engine.speculativeSession)
// Create a regular speculative session and make sure it is not returned
// if a session with a context id is requested instead.
engine.speculativeCreateSession(private = false)
assertNotNull(engine.speculativeSession)
newSpeculativeSession = engine.speculativeSession!!
assertNotSame(newSpeculativeSession, engine.createSession(private = false, contextId = "1"))
// Make sure previous (never used) speculative session is now closed
assertFalse(newSpeculativeSession.geckoSession.isOpen)
assertNull(engine.speculativeSession)
}
@Test
fun name() {
assertEquals("Gecko", GeckoEngine(context, runtime = runtime).name())
......
......@@ -137,10 +137,11 @@ class GeckoEngine(
/**
* See [Engine.createSession].
*/
override fun createSession(private: Boolean): EngineSession {
override fun createSession(private: Boolean, contextId: String?): EngineSession {
ThreadUtils.assertOnUiThread()
val speculativeSession = this.speculativeSession?.let { speculativeSession ->
if (speculativeSession.geckoSession.settings.usePrivateMode == private) {
if (speculativeSession.geckoSession.settings.usePrivateMode == private &&
speculativeSession.geckoSession.settings.contextId == contextId) {
speculativeSession
} else {
speculativeSession.close()
......@@ -149,7 +150,7 @@ class GeckoEngine(
this.speculativeSession = null
}
}
return speculativeSession ?: GeckoEngineSession(runtime, private, defaultSettings)
return speculativeSession ?: GeckoEngineSession(runtime, private, defaultSettings, contextId)
}
/**
......@@ -162,11 +163,12 @@ class GeckoEngine(
/**
* See [Engine.speculativeCreateSession].
*/
override fun speculativeCreateSession(private: Boolean) {
override fun speculativeCreateSession(private: Boolean, contextId: String?) {
ThreadUtils.assertOnUiThread()
if (this.speculativeSession?.geckoSession?.settings?.usePrivateMode != private) {
if (this.speculativeSession?.geckoSession?.settings?.usePrivateMode != private ||
this.speculativeSession?.geckoSession?.settings?.contextId != contextId) {
this.speculativeSession?.geckoSession?.close()
this.speculativeSession = GeckoEngineSession(runtime, private, defaultSettings)
this.speculativeSession = GeckoEngineSession(runtime, private, defaultSettings, contextId)
}
}
......
......@@ -52,9 +52,11 @@ class GeckoEngineSession(
private val runtime: GeckoRuntime,
private val privateMode: Boolean = false,
private val defaultSettings: Settings? = null,
contextId: String? = null,
private val geckoSessionProvider: () -> GeckoSession = {
val settings = GeckoSessionSettings.Builder()
.usePrivateMode(privateMode)
.contextId(contextId)
.build()
GeckoSession(settings)
},
......
......@@ -128,6 +128,28 @@ class GeckoEngineTest {
assertFalse(regularSpeculativeSession.geckoSession.settings.usePrivateMode)
}
@Test
fun `createSession with contextId`() {
val engine = GeckoEngine(context, runtime = runtime)
// Create a speculative session with a context id and consume it
engine.speculativeCreateSession(private = false, contextId = "1")
assertNotNull(engine.speculativeSession)
var newSpeculativeSession = engine.speculativeSession!!
assertSame(newSpeculativeSession, engine.createSession(private = false, contextId = "1"))
assertNull(engine.speculativeSession)
// Create a regular speculative session and make sure it is not returned
// if a session with a context id is requested instead.
engine.speculativeCreateSession(private = false)
assertNotNull(engine.speculativeSession)
newSpeculativeSession = engine.speculativeSession!!
assertNotSame(newSpeculativeSession, engine.createSession(private = false, contextId = "1"))
// Make sure previous (never used) speculative session is now closed
assertFalse(newSpeculativeSession.geckoSession.isOpen)
assertNull(engine.speculativeSession)
}
@Test
fun name() {
assertEquals("Gecko", GeckoEngine(context, runtime = runtime).name())
......
......@@ -31,6 +31,7 @@ class SystemEngine(
init {
initDefaultUserAgent(context)
}
/**
* Creates a new WebView-based EngineView implementation.
*/
......@@ -41,11 +42,15 @@ class SystemEngine(
/**
* Creates a new WebView-based EngineSession implementation.
*/
override fun createSession(private: Boolean): EngineSession {
override fun createSession(private: Boolean, contextId: String?): EngineSession {
if (private) {
// TODO Implement private browsing: https://github.com/mozilla-mobile/android-components/issues/649
throw UnsupportedOperationException("Private browsing is not supported in ${this::class.java.simpleName}")
} else if (contextId != null) {
throw UnsupportedOperationException(
"Contextual identities are not supported in ${this::class.java.simpleName}")
}
return SystemEngineSession(context, defaultSettings)
}
......
......@@ -43,6 +43,12 @@ class SystemEngineTest {
// Private browsing not yet supported
fail("Expected UnsupportedOperationException")
} catch (e: UnsupportedOperationException) { }
try {
engine.createSession(false, "1")
// Contextual identities not yet supported
fail("Expected UnsupportedOperationException")
} catch (e: UnsupportedOperationException) { }
}
@Test
......
......@@ -273,7 +273,7 @@ class LegacySessionManager(
fun getOrCreateEngineSession(session: Session = selectedSessionOrThrow): EngineSession {
getEngineSession(session)?.let { return it }
return engine.createSession(session.private).apply {
return engine.createSession(session.private, session.contextId).apply {
var restored = false
session.engineSessionHolder.engineSessionState?.let { state ->
restored = restoreState(state)
......
......@@ -54,6 +54,7 @@ class Session(
val private: Boolean = false,
val source: Source = Source.NONE,
val id: String = UUID.randomUUID().toString(),
val contextId: String? = null,
delegate: Observable<Observer> = ObserverRegistry()
) : Observable<Session.Observer> by delegate {
/**
......
......@@ -22,7 +22,8 @@ fun Session.toTabSessionState(): TabSessionState {
toContentState(),
toTrackingProtectionState(),
parentId = parentId,
readerState = toReaderState()
readerState = toReaderState(),
contextId = contextId
)
}
......@@ -30,8 +31,15 @@ fun Session.toTabSessionState(): TabSessionState {
* Creates a matching [CustomTabSessionState] from a custom tab [Session].
*/
fun Session.toCustomTabSessionState(): CustomTabSessionState {
val config = customTabConfig ?: throw IllegalStateException("Session is not a custom tab session")
return CustomTabSessionState(id, toContentState(), toTrackingProtectionState(), config)
val config =
customTabConfig ?: throw IllegalStateException("Session is not a custom tab session")
return CustomTabSessionState(
id,
toContentState(),
toTrackingProtectionState(),
config,
contextId = contextId
)
}
private fun Session.toContentState(): ContentState {
......
......@@ -8,6 +8,7 @@ import androidx.annotation.VisibleForTesting
import mozilla.components.browser.session.Session
import mozilla.components.browser.session.SessionManager
import mozilla.components.concept.engine.Engine
import mozilla.components.support.ktx.android.org.json.tryGetString
import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
......@@ -94,6 +95,7 @@ internal fun serializeSession(session: Session): JSONObject {
put(Keys.SESSION_PARENT_UUID_KEY, session.parentId ?: "")
put(Keys.SESSION_TITLE, session.title)
put(Keys.SESSION_READER_MODE_KEY, session.readerMode)
put(Keys.SESSION_CONTEXT_ID_KEY, session.contextId)
}
}
......@@ -114,7 +116,8 @@ internal fun deserializeSession(json: JSONObject, restoreId: Boolean, restorePar
json.getString(Keys.SESSION_UUID_KEY)
} else {
UUID.randomUUID().toString()
}
},
json.tryGetString(Keys.SESSION_CONTEXT_ID_KEY)
)
if (restoreParentId) {
session.parentId = json.getString(Keys.SESSION_PARENT_UUID_KEY).takeIf { it != "" }
......@@ -131,6 +134,7 @@ private object Keys {
const val SESSION_SOURCE_KEY = "source"
const val SESSION_URL_KEY = "url"
const val SESSION_UUID_KEY = "uuid"
const val SESSION_CONTEXT_ID_KEY = "contextId"
const val SESSION_PARENT_UUID_KEY = "parentUuid"
const val SESSION_READER_MODE_KEY = "readerMode"
const val SESSION_TITLE = "title"
......
......@@ -41,14 +41,17 @@ class SessionManagerMigrationTest {
assertTrue(store.state.tabs.isEmpty())
sessionManager.add(Session("https://www.mozilla.org", private = true))
sessionManager.add(Session("https://www.firefox.com", contextId = "1"))
assertEquals(1, sessionManager.sessions.size)
assertEquals(1, store.state.tabs.size)
val tab = store.state.tabs[0]
assertEquals(2, sessionManager.sessions.size)
assertEquals(2, store.state.tabs.size)
assertEquals("https://www.mozilla.org", tab.content.url)
assertTrue(tab.content.private)
assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
assertEquals("https://www.firefox.com", store.state.tabs[1].content.url)
assertTrue(store.state.tabs[0].content.private)
assertFalse(store.state.tabs[1].content.private)
assertNull(store.state.tabs[0].contextId)
assertEquals("1", store.state.tabs[1].contextId)
}
@Test
......@@ -77,6 +80,8 @@ class SessionManagerMigrationTest {
assertEquals(3, store.state.tabs.size)
assertEquals("https://www.mozilla.org", store.state.tabs[0].content.url)
assertEquals("https://www.firefox.com", store.state.tabs[1].content.url)
assertNull(store.state.tabs[0].contextId)
assertNull(store.state.tabs[1].contextId)
}
@Test
......@@ -158,7 +163,7 @@ class SessionManagerMigrationTest {
val sessionManager = SessionManager(engine = mock(), store = store)
sessionManager.add(Session("https://www.mozilla.org"))
sessionManager.add(Session("https://www.firefox.com"))
sessionManager.add(Session("https://www.firefox.com", contextId = "1"))
sessionManager.add(Session("https://www.getpocket.com").apply { customTabConfig = mock() })
assertEquals(2, sessionManager.sessions.size)
......@@ -202,7 +207,7 @@ class SessionManagerMigrationTest {
val sessionManager = SessionManager(engine = mock(), store = store)
sessionManager.add(Session("https://www.mozilla.org"))
sessionManager.add(Session("https://www.firefox.com"))
sessionManager.add(Session("https://www.firefox.com", contextId = "1"))
sessionManager.select(sessionManager.sessions[1])
......@@ -211,6 +216,8 @@ class SessionManagerMigrationTest {
assertEquals("https://www.firefox.com", sessionManager.selectedSessionOrThrow.url)
assertEquals("https://www.firefox.com", selectedTab.content.url)
assertEquals("1", sessionManager.selectedSessionOrThrow.contextId)
assertEquals("1", selectedTab.contextId)
}
@Test
......
......@@ -60,6 +60,27 @@ class SessionManagerTest {
assertEquals(session2.id, manager.sessions[3].parentId)
}
@Test
fun `session can be added by specifying contextId`() {
val manager = SessionManager(mock())
val session1 = Session("https://www.mozilla.org")
val session2 = Session("https://www.firefox.com", contextId = "1")
val session3 = Session("https://wiki.mozilla.org", contextId = "2")
manager.add(session1)
manager.add(session2, true)
manager.add(session3)
assertEquals(3, manager.size)
assertEquals("https://www.firefox.com", manager.selectedSessionOrThrow.url)
assertEquals("1", manager.selectedSessionOrThrow.contextId)
manager.select(session3)
assertEquals("https://wiki.mozilla.org", manager.selectedSessionOrThrow.url)
assertEquals("2", manager.selectedSessionOrThrow.contextId)
}
@Test(expected = IllegalArgumentException::class)
fun `session manager throws exception if parent is not in session manager`() {
val parent = Session("https://www.mozilla.org")
......@@ -114,7 +135,7 @@ class SessionManagerTest {
val engineSession1 = mock(EngineSession::class.java)
val engineSession2 = mock(EngineSession::class.java)
`when`(engine.name()).thenReturn("gecko")
`when`(engine.createSession(anyBoolean())).thenReturn(engineSession1)
`when`(engine.createSession(anyBoolean(), any())).thenReturn(engineSession1)
val session1 = Session("http://www.mozilla.org")
val session2 = Session("http://www.firefox.com")
......@@ -132,7 +153,7 @@ class SessionManagerTest {
verify(engineSession1).markActiveForWebExtensions(true)
// Selecting a new session should mark the new session as active and the previous one as inactive
`when`(engine.createSession(anyBoolean())).thenReturn(engineSession2)
`when`(engine.createSession(anyBoolean(), any())).thenReturn(engineSession2)
verify(engineSession2, never()).markActiveForWebExtensions(true)
manager.getOrCreateEngineSession(session2)
manager.select(session2)
......@@ -141,7 +162,7 @@ class SessionManagerTest {
verify(engineSession2).markActiveForWebExtensions(true)
// Removing the selected session should mark it as inactive and the new selection as active
`when`(engine.createSession(anyBoolean())).thenReturn(engineSession1)
`when`(engine.createSession(anyBoolean(), any())).thenReturn(engineSession1)
manager.remove(session2)
assertEquals("http://www.mozilla.org", manager.selectedSessionOrThrow.url)
verify(engineSession2).markActiveForWebExtensions(false)
......@@ -313,6 +334,7 @@ class SessionManagerTest {
add(Session("https://www.mozilla.org"))
add(session)
add(Session("https://www.firefox.com"))
add(Session("https://www.wikipedia.org", contextId = "1"))
}
val item = manager.createSessionSnapshot(session)
......@@ -324,11 +346,16 @@ class SessionManagerTest {
manager.restore(SessionManager.Snapshot.singleItem(item), updateSelection = false)
assertEquals(3, manager.size)
assertEquals(4, manager.size)
assertEquals("https://www.mozilla.org", manager.selectedSessionOrThrow.url)
assertEquals("https://getpocket.com", manager.sessions[0].url)
assertEquals("https://www.mozilla.org", manager.sessions[1].url)
assertEquals("https://www.firefox.com", manager.sessions[2].url)
assertEquals("https://www.wikipedia.org", manager.sessions[3].url)
assertNull(manager.sessions[0].contextId)
assertNull(manager.sessions[1].contextId)
assertNull(manager.sessions[2].contextId)
assertEquals("1", manager.sessions[3].contextId)
verify(observer).onSessionsRestored()
}
......@@ -344,7 +371,7 @@ class SessionManagerTest {
listOf(
SessionManager.Snapshot.Item(session = Session("https://www.firefox.com")),
SessionManager.Snapshot.Item(session = Session("https://www.wikipedia.org")),
SessionManager.Snapshot.Item(session = Session("https://getpocket.com"))
SessionManager.Snapshot.Item(session = Session("https://getpocket.com", contextId = "1"))
),
selectedSessionIndex = 1
)
......@@ -358,6 +385,10 @@ class SessionManagerTest {
assertEquals("https://www.wikipedia.org", manager.sessions[1].url)
assertEquals("https://getpocket.com", manager.sessions[2].url)
assertEquals("https://www.mozilla.org", manager.sessions[3].url)
assertNull(manager.sessions[0].contextId)
assertNull(manager.sessions[1].contextId)
assertNull(manager.sessions[3].contextId)
assertEquals("1", manager.sessions[2].contextId)
}
@Test
......
......@@ -409,6 +409,17 @@ class SessionTest {
assertEquals(session1.hashCode(), session2.hashCode())
}
@Test
fun `session returns context ID`() {
val session1 = Session("https://www.mozilla.org")
val session2 = Session("https://www.mozilla.org", contextId = "1")
val session3 = Session("https://www.mozilla.org", contextId = "2")
assertNull(session1.contextId)
assertEquals("1", session2.contextId)
assertEquals("2", session3.contextId)
}
@Test
fun `HitResult will be set on Session`() {
val hitResult: HitResult = mock()
......