Skip to content

Commit 686ed6e

Browse files
authored
Custom Tab: New custom tab design (#7185)
Task/Issue URL: https://app.asana.com/1/137249556945/project/1207418217763355/task/1211972481043749?focus=true ### Description This PR adds a new custom tab design (see [Figma](https://www.figma.com/design/BOHDESHODUXK7wSRNBOHdu/%F0%9F%A4%96-Android-Components?m=auto&node-id=21483-32206&t=3Vkry2YESBRxJEsC-1)). ### Steps to test this PR For custom toolbar colors, [follow this task to install ](https://app.asana.com/1/137249556945/project/1207137509162935/task/1207025963579423?focus=true)a test app to launch a custom tab with parameters. (Donwload [here](https://app.asana.com/app/asana/-/get_asset?asset_id=1207025963579427&force_download)) _Default light mode style (using the new tracker animation)_ - [x] Go to Settings -> Developer settings -> Cust tabs - [x] Enter som URL and load a custom tab - [x] Verify the tab loads correctly, the colors are correct and the buttons work - [x] Verify the new tracker animation (tracker count) is shown correctly _Default dark mode style (using the new tracker animation)_ - [x] Change the device to use dark mode - [x] Go to Settings -> Developer settings -> Cust tabs - [x] Enter som URL and load a custom tab - [x] Verify the tab loads correctly, the colors are correct and the buttons work - [x] Verify the tracker animation (tracker count) is shown correctly _Old tracker animation_ - [x] Before running the app, remove `@Toggle.InternalAlwaysEnabled` from `AddressBarTrackersAnimationFeatureToggle` - [x] Rebuild the app - [x] In developer settings, disable `addressBarTrackersAnimation -> feature` in the FF settings - [x] Open a custom tab - [x] Verify the old tracker animation is shown correctly - [x] Change to dark/light mode - [x] Open a custom tab and verify the tracker animation is shown correctly _Custom color_ - [x] Launch the custom tab test app mentioned above - [x] Select "Customized UI Chrome Custom Tab" - [x] (Optional) Set Toolbar color - [x] Tap on Open custom tab - [x] Verify a colored variant of a custom tab is opened - [x] Verify the "address bar" background uses a lighter version of the main custom toolbar color - [x] Verify the tracker animation is displayed correctly - [x] (Optional) Repeat with different colors _Bottom position_ - [x] In the app, set the omnibar position to BOTTOM - [x] Launch a custom tab - [x] Verify the new custom tab is laid out correctly - [x] Verify the animation works correctly _Old custom tab_ - [x] Before running the app, remove `@Toggle.InternalAlwaysEnabled` from `newCustomTab` in `AndroidBrowserConfigFeature` - [x] Rebuild the app - [x] In developer settings, disable `newCustomTab` FF - [x] Launch a custom tab - [x] Verify the old custom tab UI is used ### UI changes | Before | After | | ------ | ----- | <img width="1080" height="2400" alt="image" src="https://github.com/user-attachments/assets/0cff2a40-0a27-4872-86c1-7b63d7053b43" />|<img width="1080" height="2400" alt="image" src="https://github.com/user-attachments/assets/85dbc053-bd44-43fb-9df7-b43aeaecf4c8" />|
1 parent 7d3f2e9 commit 686ed6e

File tree

18 files changed

+922
-84
lines changed

18 files changed

+922
-84
lines changed

android-design-system/design-system/src/main/res/values/widgets.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,11 @@
478478
<item name="cardCornerRadius">@dimen/largeShapeCornerRadius</item>
479479
</style>
480480

481+
<style name="Widget.DuckDuckGo.CustomToolbarCardView" parent="Widget.MaterialComponents.CardView">
482+
<item name="cardBackgroundColor">?attr/daxColorSurface</item>
483+
<item name="cardCornerRadius">@dimen/largeShapeCornerRadius</item>
484+
</style>
485+
481486
<style name="Widget.DuckDuckGo.TabCardView" parent="Widget.MaterialComponents.CardView">
482487
<item name="cardBackgroundColor">?attr/daxColorWindow</item>
483488
</style>

app/src/androidTest/java/com/duckduckgo/app/browser/omnibar/animations/LottiePrivacyShieldAnimationHelperTest.kt

Lines changed: 244 additions & 14 deletions
Large diffs are not rendered by default.

app/src/internal/java/com/duckduckgo/app/dev/settings/customtabs/CustomTabsInternalSettingsActivity.kt

Lines changed: 106 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,23 @@
1616

1717
package com.duckduckgo.app.dev.settings.customtabs
1818

19+
import android.app.AlertDialog
1920
import android.content.ActivityNotFoundException
2021
import android.content.Context
2122
import android.content.Intent
2223
import android.content.pm.PackageManager
24+
import android.graphics.Color
25+
import android.graphics.drawable.ColorDrawable
2326
import android.net.Uri
2427
import android.os.Bundle
28+
import android.view.ViewGroup
29+
import android.widget.GridLayout
2530
import android.widget.Toast
2631
import androidx.activity.result.ActivityResultLauncher
2732
import androidx.activity.result.contract.ActivityResultContracts
33+
import androidx.browser.customtabs.CustomTabColorSchemeParams
2834
import androidx.browser.customtabs.CustomTabsIntent
35+
import androidx.core.view.setPadding
2936
import com.duckduckgo.anvil.annotations.InjectWith
3037
import com.duckduckgo.app.browser.R
3138
import com.duckduckgo.app.browser.databinding.ActivityCustomTabsInternalSettingsBinding
@@ -71,6 +78,14 @@ class CustomTabsInternalSettingsActivity : DuckDuckGoActivity() {
7178
}
7279
}
7380

81+
binding.colorPicker.setOnClickListener {
82+
showColorPicker()
83+
}
84+
85+
binding.clearColor.setOnClickListener {
86+
clearColor()
87+
}
88+
7489
binding.defaultBrowser.setOnClickListener {
7590
launchDefaultAppActivityForResult(defaultAppActivityResultLauncher)
7691
}
@@ -95,8 +110,98 @@ class CustomTabsInternalSettingsActivity : DuckDuckGoActivity() {
95110
}
96111
}
97112

113+
private fun clearColor() {
114+
binding.toolbarColorInput.text = ""
115+
Toast.makeText(this, "Custom color cleared", Toast.LENGTH_SHORT).show()
116+
}
117+
118+
private fun validateColorFormat(colorText: String): Boolean {
119+
return try {
120+
if (!colorText.startsWith("#")) {
121+
false
122+
} else {
123+
Color.parseColor(colorText)
124+
true
125+
}
126+
} catch (e: IllegalArgumentException) {
127+
false
128+
}
129+
}
130+
131+
private fun showColorPicker() {
132+
val presetColors = listOf(
133+
"#DE5833" to "DuckDuckGo Orange",
134+
"#F44336" to "Red",
135+
"#E91E63" to "Pink",
136+
"#9C27B0" to "Purple",
137+
"#673AB7" to "Deep Purple",
138+
"#3F51B5" to "Indigo",
139+
"#2196F3" to "Blue",
140+
"#03A9F4" to "Light Blue",
141+
"#00BCD4" to "Cyan",
142+
"#009688" to "Teal",
143+
"#4CAF50" to "Green",
144+
"#8BC34A" to "Light Green",
145+
"#CDDC39" to "Lime",
146+
"#FFEB3B" to "Yellow",
147+
"#FFC107" to "Amber",
148+
"#FF9800" to "Orange",
149+
"#FFFFFF" to "White",
150+
"#795548" to "Brown",
151+
"#607D8B" to "Blue Grey",
152+
"#000000" to "Black",
153+
)
154+
155+
val gridLayout = GridLayout(this).apply {
156+
columnCount = 4
157+
setPadding(32)
158+
}
159+
160+
val colorSize = resources.displayMetrics.density * 56
161+
162+
val dialog = AlertDialog.Builder(this)
163+
.setTitle("Pick a Color")
164+
.setView(gridLayout)
165+
.setNegativeButton("Cancel", null)
166+
.create()
167+
168+
presetColors.forEach { (colorHex, colorName) ->
169+
val colorView = android.view.View(this).apply {
170+
layoutParams = ViewGroup.MarginLayoutParams(colorSize.toInt(), colorSize.toInt()).apply {
171+
setMargins(8, 8, 8, 8)
172+
}
173+
background = ColorDrawable(Color.parseColor(colorHex))
174+
contentDescription = colorName
175+
setOnClickListener {
176+
binding.toolbarColorInput.text = colorHex
177+
Toast.makeText(this@CustomTabsInternalSettingsActivity, "Selected: $colorName", Toast.LENGTH_SHORT).show()
178+
dialog.dismiss()
179+
}
180+
}
181+
gridLayout.addView(colorView)
182+
}
183+
184+
dialog.show()
185+
}
186+
98187
private fun openCustomTab(url: String) {
99-
val customTabsIntent = CustomTabsIntent.Builder().build()
188+
val builder = CustomTabsIntent.Builder()
189+
190+
// Apply custom toolbar color if available and valid
191+
val color = binding.toolbarColorInput.text
192+
if (color.isNotEmpty() && validateColorFormat(color)) {
193+
try {
194+
val color = Color.parseColor(color)
195+
val colorSchemeParams = CustomTabColorSchemeParams.Builder()
196+
.setToolbarColor(color)
197+
.build()
198+
builder.setDefaultColorSchemeParams(colorSchemeParams)
199+
} catch (e: IllegalArgumentException) {
200+
logcat(WARN) { "Failed to parse color: $color" }
201+
}
202+
}
203+
204+
val customTabsIntent = builder.build()
100205
kotlin.runCatching {
101206
customTabsIntent.launchUrl(this, Uri.parse(url))
102207
}.onFailure {

app/src/internal/res/layout/activity_custom_tabs_internal_settings.xml

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,31 @@
5252
app:layout_constraintStart_toStartOf="parent"
5353
app:layout_constraintEnd_toEndOf="parent" />
5454

55+
<com.duckduckgo.common.ui.view.text.DaxTextInput
56+
android:id="@+id/toolbarColorInput"
57+
android:layout_width="0dp"
58+
android:layout_height="wrap_content"
59+
android:layout_marginEnd="@dimen/keyline_3"
60+
android:layout_marginStart="@dimen/keyline_3"
61+
android:hint="@string/customTabsToolbarColorHint"
62+
app:editable="true"
63+
android:layout_marginTop="@dimen/keyline_2"
64+
app:layout_constraintTop_toBottomOf="@id/urlInput"
65+
app:layout_constraintStart_toStartOf="parent"
66+
app:layout_constraintEnd_toEndOf="parent" />
67+
68+
<com.duckduckgo.common.ui.view.button.DaxButtonSecondary
69+
android:id="@+id/colorPicker"
70+
android:layout_width="0dp"
71+
android:layout_height="wrap_content"
72+
android:text="@string/customTabsColorPicker"
73+
android:layout_marginEnd="@dimen/keyline_3"
74+
android:layout_marginStart="@dimen/keyline_3"
75+
android:layout_marginTop="@dimen/keyline_2"
76+
app:layout_constraintTop_toBottomOf="@id/toolbarColorInput"
77+
app:layout_constraintStart_toStartOf="parent"
78+
app:layout_constraintEnd_toEndOf="parent" />
79+
5580
<com.duckduckgo.common.ui.view.button.DaxButtonPrimary
5681
android:id="@+id/load"
5782
android:layout_width="0dp"
@@ -60,15 +85,27 @@
6085
android:layout_marginEnd="@dimen/keyline_3"
6186
android:layout_marginStart="@dimen/keyline_3"
6287
android:layout_marginTop="@dimen/keyline_2"
63-
app:layout_constraintTop_toBottomOf="@id/urlInput"
88+
app:layout_constraintTop_toBottomOf="@id/colorPicker"
89+
app:layout_constraintStart_toStartOf="parent"
90+
app:layout_constraintEnd_toEndOf="parent" />
91+
92+
<com.duckduckgo.common.ui.view.button.DaxButtonSecondary
93+
android:id="@+id/clearColor"
94+
android:layout_width="0dp"
95+
android:layout_height="wrap_content"
96+
android:text="@string/customTabsClearColor"
97+
android:layout_marginEnd="@dimen/keyline_3"
98+
android:layout_marginStart="@dimen/keyline_3"
99+
android:layout_marginTop="@dimen/keyline_2"
100+
app:layout_constraintTop_toBottomOf="@id/load"
64101
app:layout_constraintStart_toStartOf="parent"
65102
app:layout_constraintEnd_toEndOf="parent" />
66103

67104
<com.duckduckgo.common.ui.view.listitem.TwoLineListItem
68105
android:id="@+id/defaultBrowser"
69106
android:layout_width="match_parent"
70107
android:layout_height="wrap_content"
71-
app:layout_constraintTop_toBottomOf="@id/load"
108+
app:layout_constraintTop_toBottomOf="@id/clearColor"
72109
app:layout_constraintStart_toStartOf="parent"
73110
app:layout_constraintEnd_toEndOf="parent"
74111
app:primaryText="@string/customTabsDefaultBrowserTitle" />

app/src/internal/res/values/donottranslate.xml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,12 @@
9898
<string name="customTabsTitle">Custom Tabs</string>
9999
<string name="customTabsLoad">Load Custom Tab</string>
100100
<string name="customTabsUrlHint">Add your URL here</string>
101+
<string name="customTabsToolbarColorHint">Toolbar color (e.g., #FF5733 or leave empty for default)</string>
101102
<string name="customTabsInvalidUrl">Invalid URL</string>
102103
<string name="customTabsEmptyUrl">Enter a URL</string>
104+
<string name="customTabsInvalidColor">Invalid color format. Use hex format like #FF5733</string>
105+
<string name="customTabsClearColor">Clear Custom Color</string>
106+
<string name="customTabsColorPicker">Pick a Color</string>
103107
<string name="customTabsDefaultBrowserTitle">Default Browser</string>
104108

105109
<!-- Notifications -->

app/src/main/java/com/duckduckgo/app/browser/BrowserTabFragment.kt

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1298,10 +1298,46 @@ class BrowserTabFragment :
12981298
requireActivity().window.navigationBarColor = customTabToolbarColor
12991299
requireActivity().window.statusBarColor = customTabToolbarColor
13001300

1301+
// Update status bar icon colors based on toolbar color luminance
1302+
updateStatusBarIconColors(customTabToolbarColor)
1303+
13011304
browserNavigationBarIntegration.configureCustomTab()
13021305
}
13031306
}
13041307

1308+
@Suppress("DEPRECATION")
1309+
private fun updateStatusBarIconColors(backgroundColor: Int) {
1310+
val window = requireActivity().window
1311+
val decorView = window.decorView
1312+
1313+
// Calculate luminance to determine if background is light or dark
1314+
val luminance = androidx.core.graphics.ColorUtils.calculateLuminance(backgroundColor)
1315+
val isLightBackground = luminance > 0.5
1316+
1317+
if (Build.VERSION.SDK_INT >= 30) {
1318+
// Use WindowInsetsController for Android R and above
1319+
window.insetsController?.let { controller ->
1320+
controller.setSystemBarsAppearance(
1321+
if (isLightBackground) {
1322+
android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
1323+
} else {
1324+
0
1325+
},
1326+
android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS,
1327+
)
1328+
}
1329+
} else {
1330+
// Use systemUiVisibility for Android M to Q
1331+
var flags = decorView.systemUiVisibility
1332+
flags = if (isLightBackground) {
1333+
flags or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
1334+
} else {
1335+
flags and View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR.inv()
1336+
}
1337+
decorView.systemUiVisibility = flags
1338+
}
1339+
}
1340+
13051341
private fun recreatePopupMenu() {
13061342
popupMenu.dismiss()
13071343
createPopupMenu()

app/src/main/java/com/duckduckgo/app/browser/omnibar/OmnibarFeatureRepository.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,16 +44,21 @@ open class OmnibarFeatureRepository @Inject constructor(
4444
@AppCoroutineScope private val coroutineScope: CoroutineScope,
4545
) : OmnibarRepository, MainProcessLifecycleObserver {
4646
private var isSplitOmnibarFlagEnabled: Boolean = false
47+
private var isNewCustomTabFlagEnabled: Boolean = false
4748

4849
override val omnibarType: OmnibarType
4950
get() = settingsDataStore.omnibarType
5051

5152
override val isSplitOmnibarAvailable: Boolean
5253
get() = isSplitOmnibarFlagEnabled
5354

55+
override val isNewCustomTabEnabled: Boolean
56+
get() = isNewCustomTabFlagEnabled
57+
5458
override fun onStart(owner: LifecycleOwner) {
5559
coroutineScope.launch(dispatcherProvider.io()) {
5660
isSplitOmnibarFlagEnabled = browserFeatures.splitOmnibar().isEnabled()
61+
isNewCustomTabFlagEnabled = browserFeatures.newCustomTab().isEnabled()
5762

5863
resetOmnibarTypeIfNecessary()
5964
}

0 commit comments

Comments
 (0)