support private mode for custom tabs

This commit is contained in:
Fabian Freund
2026-02-21 10:09:32 +01:00
parent 637da88628
commit 7c14e7b3a3
8 changed files with 61 additions and 14 deletions
+1
View File
@@ -12,3 +12,4 @@
build/ build/
pubspec.lock pubspec.lock
CLAUDE.md CLAUDE.md
AGENTS.md
@@ -235,7 +235,9 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
private fun openInBrowser(sessionId: String) { private fun openInBrowser(sessionId: String) {
val activity = requireActivity() val activity = requireActivity()
val store = components.core.store val store = components.core.store
val url = store.state.findCustomTab(sessionId)?.content?.url ?: return val customTab = store.state.findCustomTab(sessionId) ?: return
val url = customTab.content.url
val isPrivateCustomTab = customTab.content.private
sessionFeature?.get()?.release() sessionFeature?.get()?.release()
@@ -243,6 +245,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
val mainIntent = Intent(Intent.ACTION_VIEW, android.net.Uri.parse(url)).apply { val mainIntent = Intent(Intent.ACTION_VIEW, android.net.Uri.parse(url)).apply {
setClassName(activity, "eu.weblibre.gecko.MainActivity") setClassName(activity, "eu.weblibre.gecko.MainActivity")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
putExtra(PRIVATE_BROWSING_MODE, isPrivateCustomTab)
} }
activity.startActivity(mainIntent) activity.startActivity(mainIntent)
@@ -354,6 +357,7 @@ class ExternalAppBrowserFragment : BaseBrowserFragment(), UserInteractionHandler
companion object { companion object {
private const val CUSTOM_TAB_SESSION_ID_KEY = "custom_tab_session_id" private const val CUSTOM_TAB_SESSION_ID_KEY = "custom_tab_session_id"
private const val WEB_APP_MANIFEST_URL_KEY = "web_app_manifest_url" private const val WEB_APP_MANIFEST_URL_KEY = "web_app_manifest_url"
private const val PRIVATE_BROWSING_MODE = "private_browsing_mode"
fun create( fun create(
customTabSessionId: String, customTabSessionId: String,
@@ -11,6 +11,7 @@ import eu.weblibre.flutter_mozilla_components.GlobalComponents
class AuthIntentReceiverActivity : Activity() { class AuthIntentReceiverActivity : Activity() {
companion object { companion object {
private const val TAG = "AuthIntentReceiver" private const val TAG = "AuthIntentReceiver"
private const val PRIVATE_BROWSING_MODE = "private_browsing_mode"
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@@ -32,7 +33,7 @@ class AuthIntentReceiverActivity : Activity() {
val processed = CustomTabIntentProcessor( val processed = CustomTabIntentProcessor(
components.useCases.customTabsUseCases.add, components.useCases.customTabsUseCases.add,
resources, resources,
isPrivate = false, isPrivate = sourceIntent.getBooleanExtra(PRIVATE_BROWSING_MODE, false),
).process(sourceIntent) ).process(sourceIntent)
if (processed) { if (processed) {
@@ -42,6 +42,7 @@ import java.io.File
class IntentReceiverActivity : Activity() { class IntentReceiverActivity : Activity() {
companion object { companion object {
private const val TAG = "IntentReceiverActivity" private const val TAG = "IntentReceiverActivity"
private const val PRIVATE_BROWSING_MODE = "private_browsing_mode"
} }
private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob()) private val coroutineScope = CoroutineScope(Dispatchers.Main + SupervisorJob())
@@ -81,6 +82,9 @@ class IntentReceiverActivity : Activity() {
} }
private fun routeIntent(intent: Intent) { private fun routeIntent(intent: Intent) {
val privateBrowsingMode = intent.getBooleanExtra(PRIVATE_BROWSING_MODE, false)
intent.putExtra(PRIVATE_BROWSING_MODE, privateBrowsingMode)
// Check if this is our custom PWA intent with profile metadata // Check if this is our custom PWA intent with profile metadata
val profileUuid = intent.getStringExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID) val profileUuid = intent.getStringExtra(PwaConstants.EXTRA_PWA_PROFILE_UUID)
val contextId = intent.getStringExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID) val contextId = intent.getStringExtra(PwaConstants.EXTRA_PWA_CONTEXT_ID)
@@ -107,7 +111,7 @@ class IntentReceiverActivity : Activity() {
"CustomTab" to CustomTabIntentProcessor( "CustomTab" to CustomTabIntentProcessor(
components.useCases.customTabsUseCases.add, components.useCases.customTabsUseCases.add,
resources, resources,
isPrivate = false, isPrivate = privateBrowsingMode,
), ),
"PWA" to WebAppIntentProcessor( "PWA" to WebAppIntentProcessor(
components.core.store, components.core.store,
@@ -16,6 +16,7 @@ import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import com.google.android.material.card.MaterialCardView import com.google.android.material.card.MaterialCardView
import com.google.android.material.color.MaterialColors import com.google.android.material.color.MaterialColors
import com.mikepenz.iconics.IconicsDrawable import com.mikepenz.iconics.IconicsDrawable
@@ -46,6 +47,7 @@ class CustomTabToolbar @JvmOverloads constructor(
private val toolbarCard: MaterialCardView private val toolbarCard: MaterialCardView
private val closeButton: ImageButton private val closeButton: ImageButton
private val privateMaskIcon: ImageView
private val securityIcon: ImageView private val securityIcon: ImageView
private val titleText: TextView private val titleText: TextView
private val urlText: TextView private val urlText: TextView
@@ -58,6 +60,7 @@ class CustomTabToolbar @JvmOverloads constructor(
private var urlScope: CoroutineScope? = null private var urlScope: CoroutineScope? = null
private var titleScope: CoroutineScope? = null private var titleScope: CoroutineScope? = null
private var securityScope: CoroutineScope? = null private var securityScope: CoroutineScope? = null
private var isPrivateSession: Boolean = false
var onCloseListener: (() -> Unit)? = null var onCloseListener: (() -> Unit)? = null
var onShareListener: (() -> Unit)? = null var onShareListener: (() -> Unit)? = null
@@ -69,6 +72,7 @@ class CustomTabToolbar @JvmOverloads constructor(
toolbarCard = findViewById(R.id.toolbarCard) toolbarCard = findViewById(R.id.toolbarCard)
closeButton = findViewById(R.id.closeButton) closeButton = findViewById(R.id.closeButton)
privateMaskIcon = findViewById(R.id.privateMaskIcon)
securityIcon = findViewById(R.id.securityIcon) securityIcon = findViewById(R.id.securityIcon)
titleText = findViewById(R.id.titleText) titleText = findViewById(R.id.titleText)
urlText = findViewById(R.id.urlText) urlText = findViewById(R.id.urlText)
@@ -89,9 +93,17 @@ class CustomTabToolbar @JvmOverloads constructor(
this.sessionId = sessionId this.sessionId = sessionId
this.store = store this.store = store
toolbarColor?.let { applyCustomColors(it) } val tab = store.state.findCustomTab(sessionId)
isPrivateSession = tab?.content?.private == true
store.state.findCustomTab(sessionId)?.let { tab -> if (toolbarColor != null) {
applyCustomColors(toolbarColor)
} else {
applyMaterial3Colors()
applyIcons()
}
tab?.let {
updateTitle(tab) updateTitle(tab)
updateUrl(tab) updateUrl(tab)
updateSecurityIcon(tab) updateSecurityIcon(tab)
@@ -118,14 +130,16 @@ class CustomTabToolbar @JvmOverloads constructor(
private fun applyMaterial3Colors() { private fun applyMaterial3Colors() {
val surfaceColor = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface) val surfaceColor = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface)
val onSurfaceColor = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface) val onSurfaceColor = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface)
val onSurfaceVariantColor = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurfaceVariant)
toolbarCard.setCardBackgroundColor(surfaceColor) toolbarCard.setCardBackgroundColor(surfaceColor)
titleText.setTextColor(onSurfaceColor) titleText.setTextColor(onSurfaceColor)
urlText.setTextColor(onSurfaceColor) urlText.setTextColor(onSurfaceVariantColor)
} }
private fun applyIcons() { private fun applyIcons() {
val iconColor = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurfaceVariant) val iconColor = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurfaceVariant)
updatePrivateMaskIcon()
closeButton.setImageDrawable(mdiIcon(CommunityMaterial.Icon.cmd_close, 20, iconColor)) closeButton.setImageDrawable(mdiIcon(CommunityMaterial.Icon.cmd_close, 20, iconColor))
shareButton.setImageDrawable(mdiIcon(CommunityMaterial.Icon3.cmd_share_variant, 18, iconColor)) shareButton.setImageDrawable(mdiIcon(CommunityMaterial.Icon3.cmd_share_variant, 18, iconColor))
openInBrowserButton.setImageDrawable( openInBrowserButton.setImageDrawable(
@@ -145,6 +159,7 @@ class CustomTabToolbar @JvmOverloads constructor(
titleText.setTextColor(textColor) titleText.setTextColor(textColor)
urlText.setTextColor(textColor) urlText.setTextColor(textColor)
updatePrivateMaskIcon()
closeButton.setImageDrawable(mdiIcon(CommunityMaterial.Icon.cmd_close, 20, textColor)) closeButton.setImageDrawable(mdiIcon(CommunityMaterial.Icon.cmd_close, 20, textColor))
shareButton.setImageDrawable(mdiIcon(CommunityMaterial.Icon3.cmd_share_variant, 18, textColor)) shareButton.setImageDrawable(mdiIcon(CommunityMaterial.Icon3.cmd_share_variant, 18, textColor))
openInBrowserButton.setImageDrawable( openInBrowserButton.setImageDrawable(
@@ -242,17 +257,29 @@ class CustomTabToolbar @JvmOverloads constructor(
} }
private fun updateSecurityIcon(tab: CustomTabSessionState) { private fun updateSecurityIcon(tab: CustomTabSessionState) {
val neutralIconColor = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurfaceVariant)
val errorIconColor = MaterialColors.getColor(this, android.R.attr.colorError)
val securityInfoKnown = tab.content.securityInfo.host.isNotBlank() val securityInfoKnown = tab.content.securityInfo.host.isNotBlank()
if (tab.content.loading || !securityInfoKnown) { if (tab.content.loading || !securityInfoKnown) {
val color = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurfaceVariant) securityIcon.setImageDrawable(mdiIcon(CommunityMaterial.Icon2.cmd_lock_open_outline, 16, neutralIconColor))
securityIcon.setImageDrawable(mdiIcon(CommunityMaterial.Icon2.cmd_lock_open_outline, 16, color))
} else if (tab.content.securityInfo.isSecure) { } else if (tab.content.securityInfo.isSecure) {
val color = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurfaceVariant) securityIcon.setImageDrawable(mdiIcon(CommunityMaterial.Icon2.cmd_lock, 16, neutralIconColor))
securityIcon.setImageDrawable(mdiIcon(CommunityMaterial.Icon2.cmd_lock, 16, color))
} else { } else {
val color = MaterialColors.getColor(this, android.R.attr.colorError) securityIcon.setImageDrawable(mdiIcon(CommunityMaterial.Icon2.cmd_lock_open_outline, 16, errorIconColor))
securityIcon.setImageDrawable(mdiIcon(CommunityMaterial.Icon2.cmd_lock_open_outline, 16, color)) }
}
private fun updatePrivateMaskIcon() {
privateMaskIcon.visibility = if (isPrivateSession) View.VISIBLE else View.GONE
if (isPrivateSession) {
val privateMaskColor = ContextCompat.getColor(context, R.color.private_tab_mask_accent)
privateMaskIcon.setImageDrawable(
mdiIcon(CommunityMaterial.Icon.cmd_domino_mask, 16, privateMaskColor)
)
} else {
privateMaskIcon.setImageDrawable(null)
} }
} }
@@ -27,7 +27,6 @@ class CustomTabToolbarFeature(
override fun start() { override fun start() {
val tab = store.state.findCustomTab(sessionId) ?: return val tab = store.state.findCustomTab(sessionId) ?: return
val toolbarColor = tab.config.colorSchemes?.defaultColorSchemeParams?.toolbarColor val toolbarColor = tab.config.colorSchemes?.defaultColorSchemeParams?.toolbarColor
toolbar.bind(sessionId, store, toolbarColor) toolbar.bind(sessionId, store, toolbarColor)
@@ -45,6 +45,15 @@
android:paddingHorizontal="8dp" android:paddingHorizontal="8dp"
android:paddingVertical="6dp"> android:paddingVertical="6dp">
<ImageView
android:id="@+id/privateMaskIcon"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginEnd="6dp"
android:contentDescription="@null"
android:importantForAccessibility="no"
android:visibility="gone" />
<ImageView <ImageView
android:id="@+id/securityIcon" android:id="@+id/securityIcon"
android:layout_width="18dp" android:layout_width="18dp"
@@ -4,4 +4,6 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<color name="icons">#ffffffff</color> <color name="icons">#ffffffff</color>
</resources> <!-- Keep in sync with app/lib/core/design/app_colors.dart privateTabPurple -->
<color name="private_tab_mask_accent">#FF8000D7</color>
</resources>