Skip to content

Commit 71bd89a

Browse files
authored
Add WebView dev settings screen for internal builds (#6094)
Task/Issue URL: https://app.asana.com/1/137249556945/project/488551667048375/task/1209643196618443?focus=true ### Description Adds an internal dev settings screen for `WebView` debug info and accessing tooling. ### Steps to test this PR - [x] Install `internal` variant - [x] Visit `Settings -> System WebView` - [x] Check things work as you'd expect (e.g., no crashes even when Play Store not installed) Co-authored-by: Craig Russell <[email protected]>
1 parent 7d3f979 commit 71bd89a

File tree

8 files changed

+411
-3
lines changed

8 files changed

+411
-3
lines changed

app/src/internal/AndroidManifest.xml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
<?xml version="1.0" encoding="utf-8"?>
2-
<manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"
3-
package="com.duckduckgo.app.browser">
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
3+
xmlns:tools="http://schemas.android.com/tools"
4+
package="com.duckduckgo.app.browser">
45

56
<application>
7+
<activity
8+
android:name=".webview.WebViewDevSettingsActivity"
9+
android:exported="true"
10+
android:parentActivityName="com.duckduckgo.app.settings.SettingsActivity"
11+
android:label="@string/webview_dev_settings_label"
12+
/>
613
<activity
714
android:name="com.duckduckgo.app.dev.settings.DevSettingsActivity"
815
android:label="@string/devSettingsTitle"
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.webview
18+
19+
/*
20+
* Copyright (c) 2023 DuckDuckGo
21+
*
22+
* Licensed under the Apache License, Version 2.0 (the "License");
23+
* you may not use this file except in compliance with the License.
24+
* You may obtain a copy of the License at
25+
*
26+
* http://www.apache.org/licenses/LICENSE-2.0
27+
*
28+
* Unless required by applicable law or agreed to in writing, software
29+
* distributed under the License is distributed on an "AS IS" BASIS,
30+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
31+
* See the License for the specific language governing permissions and
32+
* limitations under the License.
33+
*/
34+
35+
import android.content.Context
36+
import com.duckduckgo.anvil.annotations.PriorityKey
37+
import com.duckduckgo.app.browser.R
38+
import com.duckduckgo.di.scopes.AppScope
39+
import com.duckduckgo.internal.features.api.InternalFeaturePlugin
40+
import com.duckduckgo.internal.features.api.InternalFeaturePlugin.Companion.WEB_VIEW_DEV_SETTINGS_PRIO_KEY
41+
import com.duckduckgo.navigation.api.GlobalActivityStarter
42+
import com.squareup.anvil.annotations.ContributesMultibinding
43+
import javax.inject.Inject
44+
45+
@ContributesMultibinding(AppScope::class)
46+
@PriorityKey(WEB_VIEW_DEV_SETTINGS_PRIO_KEY)
47+
class WebViewDevSettings @Inject constructor(
48+
private val globalActivityStarter: GlobalActivityStarter,
49+
private val context: Context,
50+
) : InternalFeaturePlugin {
51+
52+
override fun internalFeatureTitle(): String {
53+
return context.getString(R.string.webview_internal_feature_title)
54+
}
55+
56+
override fun internalFeatureSubtitle(): String {
57+
return context.getString(R.string.webview_internal_feature_subtitle)
58+
}
59+
60+
override fun onInternalFeatureClicked(activityContext: Context) {
61+
globalActivityStarter.start(activityContext, WebViewDevSettingsScreen)
62+
}
63+
}
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.webview
18+
19+
import android.content.Intent
20+
import android.os.Bundle
21+
import android.widget.Toast
22+
import androidx.activity.result.ActivityResultLauncher
23+
import androidx.activity.result.contract.ActivityResultContracts
24+
import androidx.core.net.toUri
25+
import androidx.lifecycle.Lifecycle
26+
import androidx.lifecycle.flowWithLifecycle
27+
import androidx.lifecycle.lifecycleScope
28+
import com.duckduckgo.anvil.annotations.ContributeToActivityStarter
29+
import com.duckduckgo.anvil.annotations.InjectWith
30+
import com.duckduckgo.app.browser.R
31+
import com.duckduckgo.app.browser.databinding.ActivityWebViewDevSettingsBinding
32+
import com.duckduckgo.app.browser.webview.WebViewDevSettingsViewModel.ViewState
33+
import com.duckduckgo.app.clipboard.ClipboardInteractor
34+
import com.duckduckgo.common.ui.DuckDuckGoActivity
35+
import com.duckduckgo.common.ui.viewbinding.viewBinding
36+
import com.duckduckgo.common.utils.playstore.PlayStoreAndroidUtils.Companion.PLAY_STORE_PACKAGE
37+
import com.duckduckgo.di.scopes.ActivityScope
38+
import com.duckduckgo.navigation.api.GlobalActivityStarter
39+
import javax.inject.Inject
40+
import kotlinx.coroutines.flow.launchIn
41+
import kotlinx.coroutines.flow.onEach
42+
43+
@InjectWith(ActivityScope::class)
44+
@ContributeToActivityStarter(WebViewDevSettingsScreen::class)
45+
class WebViewDevSettingsActivity : DuckDuckGoActivity() {
46+
47+
@Inject
48+
lateinit var viewModel: WebViewDevSettingsViewModel
49+
50+
@Inject
51+
lateinit var clipboardManager: ClipboardInteractor
52+
53+
private val binding: ActivityWebViewDevSettingsBinding by viewBinding()
54+
55+
private lateinit var webViewDebugLauncher: ActivityResultLauncher<Intent>
56+
57+
override fun onCreate(savedInstanceState: Bundle?) {
58+
super.onCreate(savedInstanceState)
59+
setContentView(binding.root)
60+
setupToolbar(binding.includeToolbar.toolbar)
61+
registerActivityResultLauncher()
62+
configureListeners()
63+
observeViewModel()
64+
}
65+
66+
private fun observeViewModel() {
67+
viewModel.viewState()
68+
.flowWithLifecycle(lifecycle, Lifecycle.State.CREATED)
69+
.onEach { viewState ->
70+
updateViews(viewState)
71+
}.launchIn(lifecycleScope)
72+
}
73+
74+
private fun updateViews(viewState: ViewState) {
75+
binding.webViewPackage.setSecondaryText(viewState.webViewPackage)
76+
binding.webViewVersion.setSecondaryText(viewState.webViewVersion)
77+
}
78+
79+
override fun onStart() {
80+
super.onStart()
81+
viewModel.start()
82+
}
83+
84+
private fun configureListeners() {
85+
binding.webViewDevTools.setOnClickListener {
86+
launchExternalIntentForAction(Intent(WEBVIEW_DEV_TOOLS_INTENT_ACTION), getString(R.string.webview_dev_ui_not_available))
87+
}
88+
binding.webViewVersion.setClickListener {
89+
clipboardManager.copyToClipboard(viewModel.viewState().value.webViewVersion, false)
90+
}
91+
binding.webViewPackage.setClickListener {
92+
clipboardManager.copyToClipboard(viewModel.viewState().value.webViewPackage, false)
93+
}
94+
binding.webViewPlayStore.setClickListener {
95+
val intent = Intent(Intent.ACTION_VIEW).also {
96+
it.setData(PLAYSTORE_WEBVIEW_INTENT_URI.toUri())
97+
it.setPackage(PLAY_STORE_PACKAGE)
98+
}
99+
launchExternalIntentForAction(intent, getString(R.string.webview_play_store_unavailable))
100+
}
101+
}
102+
103+
private fun registerActivityResultLauncher() {
104+
webViewDebugLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
105+
}
106+
}
107+
108+
private fun launchExternalIntentForAction(intent: Intent, errorIfCannotLaunch: String) {
109+
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
110+
111+
if (intent.resolveActivity(packageManager) != null) {
112+
startActivity(intent)
113+
} else {
114+
Toast.makeText(this, errorIfCannotLaunch, Toast.LENGTH_SHORT).show()
115+
}
116+
}
117+
118+
companion object {
119+
private const val PLAYSTORE_WEBVIEW_INTENT_URI = "market://details?id=com.google.android.webview"
120+
private const val WEBVIEW_DEV_TOOLS_INTENT_ACTION = "com.android.webview.SHOW_DEV_UI"
121+
private const val PLAY_STORE_PACKAGE = "com.android.vending"
122+
}
123+
}
124+
125+
data object WebViewDevSettingsScreen : GlobalActivityStarter.ActivityParams {
126+
private fun readResolve(): Any = WebViewDevSettingsScreen
127+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.webview
18+
19+
import androidx.lifecycle.ViewModel
20+
import androidx.lifecycle.viewModelScope
21+
import com.duckduckgo.anvil.annotations.ContributesViewModel
22+
import com.duckduckgo.di.scopes.ActivityScope
23+
import javax.inject.Inject
24+
import kotlinx.coroutines.flow.MutableStateFlow
25+
import kotlinx.coroutines.flow.StateFlow
26+
import kotlinx.coroutines.launch
27+
28+
@ContributesViewModel(ActivityScope::class)
29+
class WebViewDevSettingsViewModel @Inject constructor(
30+
private val webViewInformationExtractor: WebViewInformationExtractor,
31+
) : ViewModel() {
32+
33+
data class ViewState(
34+
val webViewVersion: String = "unknown",
35+
val webViewPackage: String = "unknown",
36+
)
37+
38+
private val viewState = MutableStateFlow(ViewState())
39+
40+
fun viewState(): StateFlow<ViewState> {
41+
return viewState
42+
}
43+
44+
fun start() {
45+
viewModelScope.launch {
46+
val webViewData = webViewInformationExtractor.extract()
47+
48+
viewState.emit(
49+
currentViewState().copy(
50+
webViewVersion = webViewData.webViewVersion,
51+
webViewPackage = webViewData.webViewPackageName,
52+
),
53+
)
54+
}
55+
}
56+
57+
private fun currentViewState(): ViewState {
58+
return viewState.value
59+
}
60+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.app.browser.webview
18+
19+
import android.content.Context
20+
import androidx.webkit.WebViewCompat
21+
import com.duckduckgo.app.browser.webview.WebViewInformationExtractor.WebViewData
22+
import com.duckduckgo.di.scopes.AppScope
23+
import com.squareup.anvil.annotations.ContributesBinding
24+
import javax.inject.Inject
25+
26+
interface WebViewInformationExtractor {
27+
28+
data class WebViewData(
29+
val webViewVersion: String,
30+
val webViewPackageName: String,
31+
)
32+
33+
fun extract(): WebViewData
34+
}
35+
36+
@ContributesBinding(AppScope::class)
37+
class InternalWebViewInformationExtractor @Inject constructor(
38+
private val context: Context,
39+
) : WebViewInformationExtractor {
40+
41+
override fun extract(): WebViewData {
42+
return WebViewCompat.getCurrentWebViewPackage(context)?.let {
43+
WebViewData(
44+
webViewVersion = it.versionName ?: UNKNOWN,
45+
webViewPackageName = it.packageName ?: UNKNOWN,
46+
)
47+
} ?: unknownWebView
48+
}
49+
50+
companion object {
51+
private const val UNKNOWN = "unknown"
52+
53+
private val unknownWebView = WebViewData(
54+
webViewVersion = UNKNOWN,
55+
webViewPackageName = UNKNOWN,
56+
)
57+
}
58+
}

0 commit comments

Comments
 (0)