Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
f223862
Cache both the feature flag and profile support
0nko May 15, 2026
dafa73a
Add the profile management components
0nko May 15, 2026
0aeae26
Make browser mode data provider function suspending
0nko May 15, 2026
b8c2fe0
Add cookie manager provider and web storage providers
0nko May 15, 2026
399cb16
Update the log message
0nko May 15, 2026
883f11d
Always release the latch, even when there's an exception
0nko May 15, 2026
6e990b8
Add unit tests
0nko May 15, 2026
d9bdf8a
Fix the issue when profiles are lost when migration fails
0nko May 15, 2026
cc458ca
Updates the duck.ai host for cookie migration and add tests
0nko May 15, 2026
d9db7cd
Update the interface docs
0nko May 15, 2026
6ab8cf1
Fix formatting
0nko May 15, 2026
1c55081
Remove the initialize class and make the profile manager observe the …
0nko May 15, 2026
dd68c07
Remove cookie manager provider
0nko May 15, 2026
d7af9ae
Remove WebViewProfileManager and associated classes
0nko May 15, 2026
d92e33a
Add a simplified profile management
0nko May 15, 2026
c473208
Make isAvailable non suspended
0nko May 15, 2026
8ca07c0
Bind the profile to the WebView
0nko May 15, 2026
53e9ef2
Remove unused dependency
0nko May 16, 2026
562ac40
Simplify the availability check and data provider interface
0nko May 16, 2026
e9a92ed
Move WebStorageProvider to app module
0nko May 16, 2026
39ce5ad
Make the profile bind call the very first one
0nko May 16, 2026
cf25965
Fix formatting
0nko May 17, 2026
4d90be3
Rename the interface to WebViewModeInitializer
0nko May 25, 2026
6f986ed
Update the WebViewModeInitializer to return a result
0nko May 26, 2026
d41a0f2
Add the missing else branch
0nko May 26, 2026
466b269
Fix lint issue
0nko May 26, 2026
f2a77af
Capture WebView profile binding failures
cursoragent May 26, 2026
4676907
Check for the non-default mode instead of fire mode specifically when…
0nko May 26, 2026
73bf380
Close current tab if Fire mode unsupported and mode is FIRE
0nko May 27, 2026
ab49d52
Fix the failing unit tests
0nko May 27, 2026
5817366
Consolidate the error handling
0nko May 27, 2026
2de1809
Fix the failing tests
0nko May 27, 2026
ae0a70d
Update the interface docs
0nko May 27, 2026
513ec83
Destroy the webview when closing the tab
0nko May 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ import com.duckduckgo.browser.ui.browsermenu.BrowserMenuBottomSheet
import com.duckduckgo.browser.ui.browsermenu.VpnMenuState
import com.duckduckgo.browser.ui.newtab.hatch.NewTabReturnHatchView
import com.duckduckgo.browsermode.api.BrowserMode
import com.duckduckgo.browsermode.api.WebViewModeInitializer
import com.duckduckgo.common.ui.DuckDuckGoActivity
import com.duckduckgo.common.ui.DuckDuckGoFragment
import com.duckduckgo.common.ui.store.BrowserAppTheme
Expand Down Expand Up @@ -441,6 +442,9 @@ class BrowserTabFragment :
@Inject
lateinit var webChromeClient: BrowserChromeClient

@Inject
lateinit var webViewModeInitializer: WebViewModeInitializer

@Inject
lateinit var viewModelFactory: FragmentViewModelFactory

Expand Down Expand Up @@ -3991,6 +3995,19 @@ class BrowserTabFragment :
).findViewById<DuckDuckGoWebView>(R.id.browserWebView)

webView?.let {
val bindResult = webViewModeInitializer.bind(it, browserMode)
if (bindResult.isFailure) {
if (browserMode != BrowserMode.REGULAR) {
bindResult.exceptionOrNull()?.message?.let {
message ->
logcat(ERROR) { message }
}
this.closeCurrentTab()
destroyWebView()
return
}
Comment thread
cursor[bot] marked this conversation as resolved.
}

it.webViewClient = webViewClient
it.webChromeClient = webChromeClient
it.clearSslPreferences()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright (c) 2026 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.browser.mode

import android.annotation.SuppressLint
import android.webkit.WebStorage
import androidx.annotation.UiThread
import androidx.webkit.ProfileStore
import com.duckduckgo.browsermode.api.BrowserMode
import com.duckduckgo.browsermode.api.BrowserModeDataProvider
import com.duckduckgo.browsermode.api.FireModeAvailability
import com.duckduckgo.browsermode.api.profileName
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesTo
import dagger.Binds
import dagger.Module
import dagger.SingleInstanceIn
import javax.inject.Inject

/**
* Resolves a per-mode [WebStorage]. Callers must invoke [forMode] on the main thread when
* `MULTI_PROFILE` is supported on the device — `ProfileStore` is tied to the WebView thread.
* Falls back to the default shared [WebStorage] when MultiProfile is unsupported.
*/
@SuppressLint("RequiresFeature")
@SingleInstanceIn(AppScope::class)
class WebStorageProvider @Inject constructor(
private val fireModeAvailability: FireModeAvailability,
) : BrowserModeDataProvider<WebStorage> {

@UiThread
override fun forMode(mode: BrowserMode): WebStorage {
if (!fireModeAvailability.isAvailable()) return WebStorage.getInstance()
return ProfileStore.getInstance().getOrCreateProfile(mode.profileName).webStorage
}
}

@ContributesTo(AppScope::class)
@Module
abstract class WebStorageProviderModule {
@Binds
abstract fun bindWebStorageProvider(impl: WebStorageProvider): BrowserModeDataProvider<WebStorage>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2026 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.app.browser.mode

import androidx.test.ext.junit.runners.AndroidJUnit4
import com.duckduckgo.browsermode.api.BrowserMode
import com.duckduckgo.browsermode.api.FireModeAvailability
import org.junit.Assert.assertNotNull
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.whenever

@RunWith(AndroidJUnit4::class)
class WebStorageProviderTest {

private val fireModeAvailability: FireModeAvailability = mock()
private val testee = WebStorageProvider(fireModeAvailability)

@Test
fun `forMode returns default WebStorage when multi-profile is unavailable`() {
whenever(fireModeAvailability.isAvailable()).thenReturn(false)

assertNotNull(testee.forMode(BrowserMode.REGULAR))
assertNotNull(testee.forMode(BrowserMode.FIRE))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright (c) 2026 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.browsermode.api

import androidx.webkit.Profile

/**
* Stable mapping from a [BrowserMode] to its WebView profile name.
*
* [REGULAR][BrowserMode.REGULAR] shares the default WebView profile;
* [FIRE][BrowserMode.FIRE] gets its own isolated profile.
*/
val BrowserMode.profileName: String
get() = when (this) {
BrowserMode.REGULAR -> Profile.DEFAULT_PROFILE_NAME
BrowserMode.FIRE -> "Fire"
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,5 @@ package com.duckduckgo.browsermode.api
*/
interface FireModeAvailability {
/** Returns true if the prerequisites are satisfied, false otherwise **/
suspend fun isAvailable(): Boolean
fun isAvailable(): Boolean
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2026 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.browsermode.api

import android.webkit.WebView

/**
* Binds a [WebView] to the WebView profile associated with a [BrowserMode], isolating cookies
* and origin-keyed storage (LocalStorage, IndexedDB) between modes.
*
* Must be called once per [WebView] before any other WebView API call, including `loadUrl`. The
* binding is permanent for the lifetime of the [WebView] — it cannot be changed once the
* [WebView] has been used.
*
* Fails on devices that do not support the `MULTI_PROFILE` WebView feature, regardless of the
* requested [BrowserMode].
*/
interface WebViewModeInitializer {

/**
* Must be called on the main thread.
*
* @param webView a freshly-created [WebView] that has not yet been used.
* @param mode the [BrowserMode] whose profile should back this [WebView].
* @return [Result.success] when the profile is bound to the [WebView]; [Result.failure] when
* the `MULTI_PROFILE` WebView feature is unsupported on this device, or when the underlying
* profile binding call throws.
*/
fun bind(webView: WebView, mode: BrowserMode): Result<Unit>
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,30 +16,43 @@

package com.duckduckgo.browsermode.impl

import com.duckduckgo.app.browser.api.WebViewCapabilityChecker
import com.duckduckgo.app.browser.api.WebViewCapabilityChecker.WebViewCapability.MultiProfile
import androidx.lifecycle.LifecycleOwner
import androidx.webkit.WebViewFeature
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver
import com.duckduckgo.browsermode.api.FireModeAvailability
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.anvil.annotations.ContributesMultibinding
import dagger.SingleInstanceIn
import kotlinx.coroutines.withContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import javax.inject.Inject

@SingleInstanceIn(AppScope::class)
@ContributesBinding(AppScope::class)
@ContributesBinding(AppScope::class, boundType = FireModeAvailability::class)
@ContributesMultibinding(AppScope::class, boundType = MainProcessLifecycleObserver::class)
class RealFireModeAvailability @Inject constructor(
private val fireModeFeature: FireModeFeature,
private val webViewCapabilityChecker: WebViewCapabilityChecker,
private val dispatchers: DispatcherProvider,
) : FireModeAvailability {
@param:AppCoroutineScope private val appScope: CoroutineScope,
) : FireModeAvailability, MainProcessLifecycleObserver {

@Volatile
private var multiProfileSupported: Boolean? = null
private var cachedAvailability: Boolean? = null
Comment thread
CDRussell marked this conversation as resolved.

override fun onCreate(owner: LifecycleOwner) {
appScope.launch(dispatchers.io()) { computeAndCache() }
}

override fun isAvailable(): Boolean = cachedAvailability ?: computeAndCache()

override suspend fun isAvailable(): Boolean = withContext(dispatchers.io()) {
if (!fireModeFeature.fireTabs().isEnabled()) return@withContext false
multiProfileSupported ?: webViewCapabilityChecker.isSupported(MultiProfile).also {
multiProfileSupported = it
}
private fun computeAndCache(): Boolean {
cachedAvailability?.let { return it }
val value = WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE) &&
fireModeFeature.fireTabs().isEnabled()
cachedAvailability = value
return value
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (c) 2026 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.browsermode.impl

import android.annotation.SuppressLint
import android.webkit.WebView
import androidx.webkit.ProfileStore
import androidx.webkit.WebViewCompat
import androidx.webkit.WebViewFeature
import com.duckduckgo.browsermode.api.BrowserMode
import com.duckduckgo.browsermode.api.WebViewModeInitializer
import com.duckduckgo.browsermode.api.profileName
import com.duckduckgo.di.scopes.AppScope
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject

@SingleInstanceIn(AppScope::class)
@ContributesBinding(AppScope::class)
class RealWebViewModeInitializer @Inject constructor(
private val webViewProfileBinder: WebViewProfileBinder,
) : WebViewModeInitializer {
Comment thread
cursor[bot] marked this conversation as resolved.
override fun bind(webView: WebView, mode: BrowserMode): Result<Unit> {
if (!WebViewFeature.isFeatureSupported(WebViewFeature.MULTI_PROFILE)) {
return Result.failure(
IllegalStateException(
"Attempting to bind a WebView profile to " +
"{${mode.name}} mode when MULTI_PROFILE feature is not available.",
),
)
}
return runCatching {
webViewProfileBinder.bind(webView, mode.profileName)
}
}
}

interface WebViewProfileBinder {
fun bind(webView: WebView, profileName: String)
}

@ContributesBinding(AppScope::class)
@SuppressLint("RequiresFeature")
class AndroidXWebViewProfileBinder @Inject constructor() : WebViewProfileBinder {
override fun bind(webView: WebView, profileName: String) {
ProfileStore.getInstance().getOrCreateProfile(profileName)
WebViewCompat.setProfile(webView, profileName)
}
Comment thread
cursor[bot] marked this conversation as resolved.
}
Loading
Loading