Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
package com.duckduckgo.adblocking.impl.duckplayer.ui

import android.app.Dialog
import android.content.Context
import android.content.res.Configuration
import android.os.Bundle
import android.view.LayoutInflater
Expand All @@ -26,16 +27,36 @@ import com.airbnb.lottie.LottieCompositionFactory
import com.airbnb.lottie.LottieDrawable
import com.duckduckgo.adblocking.impl.R
import com.duckduckgo.adblocking.impl.databinding.ModalDuckPlayerBinding
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.common.ui.applyBottomSystemBarInsetPadding
import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeBucket
import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeProvider
import com.duckduckgo.di.scopes.FragmentScope
import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import dagger.android.support.AndroidSupportInjection
import javax.inject.Inject

@InjectWith(FragmentScope::class)
class DuckPlayerPrimeBottomSheet : BottomSheetDialogFragment() {

private lateinit var binding: ModalDuckPlayerBinding
private val isFromDuckPlayerPage: Boolean by lazy { requireArguments().getBoolean(FROM_DUCK_PLAYER_PAGE) }

override fun getTheme(): Int = R.style.DuckPlayerBottomSheetDialogTheme
@Inject
lateinit var edgeToEdgeProvider: EdgeToEdgeProvider

override fun getTheme(): Int = if (edgeToEdgeProvider.isEnabled(EdgeToEdgeBucket.BOTTOM_SHEETS)) {
R.style.DuckPlayerBottomSheetDialogThemeEdgeToEdge
} else {
R.style.DuckPlayerBottomSheetDialogTheme
}

override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
super.onAttach(context)
}

override fun onCreateView(
inflater: LayoutInflater,
Expand All @@ -59,6 +80,9 @@ class DuckPlayerPrimeBottomSheet : BottomSheetDialogFragment() {
binding.closeButton.setOnClickListener {
dismiss()
}
if (edgeToEdgeProvider.isEnabled(EdgeToEdgeBucket.BOTTOM_SHEETS)) {
binding.root.applyBottomSystemBarInsetPadding()
}
return binding.root
}

Expand Down
13 changes: 12 additions & 1 deletion ad-blocking/ad-blocking-impl/src/main/res/values/styles.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,22 @@
~ limitations under the License.
-->

<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<style name="DuckPlayerBottomSheetDialogTheme" parent="@style/ThemeOverlay.MaterialComponents.BottomSheetDialog">
<item name="bottomSheetStyle">@style/DuckPlayerBottomSheetStyle</item>
</style>

<!-- Opt-in variant used when EdgeToEdgeBucket.BOTTOM_SHEETS is enabled: draws the sheet behind the
transparent system bars (edge-to-edge, Android 15+); content clears the nav bar via
View.applyBottomSystemBarInsetPadding() and Material adds the top status-bar inset. -->
<style name="DuckPlayerBottomSheetDialogThemeEdgeToEdge" parent="DuckPlayerBottomSheetDialogTheme">
<item name="enableEdgeToEdge">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:enforceNavigationBarContrast" tools:targetApi="q">false</item>
<item name="paddingBottomSystemWindowInsets">false</item>
</style>

<style name="DuckPlayerBottomSheetStyle" parent="Widget.MaterialComponents.BottomSheet.Modal">
<item name="backgroundTint">@android:color/transparent</item>
</style>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@

package com.duckduckgo.common.ui

import android.view.View
import android.widget.FrameLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import com.google.android.material.bottomsheet.BottomSheetDialog
import com.google.android.material.shape.CornerFamily
import com.google.android.material.shape.MaterialShapeDrawable
Expand All @@ -36,3 +40,23 @@ fun BottomSheetDialog.setRoundCorners() {
.build()
bottomSheet?.background = shapeDrawable
}

/**
* Pads [this]'s bottom by the navigation-bar inset (on top of its existing bottom padding) so a bottom sheet's
* content clears the gesture pill / button bar under edge-to-edge (Android 15+, targetSdk 35), where a
* BottomSheetDialog's own window is edge-to-edge regardless of the host activity. Only call this when the sheet
* is actually using an `.EdgeToEdge` theme (see `EdgeToEdgeBucket.BOTTOM_SHEETS`), otherwise the inset is 0 and
* the padding never changes. Call on the view that owns the sheet's rounded background (not necessarily
* `binding.root` - e.g. when the root is a backgroundless `NestedScrollView` wrapping a styled `ConstraintLayout`,
* pad that inner view instead, or the reserved clearance won't be covered by the sheet's background). Call in
* onViewCreated / onCreateView. The original bottom padding is captured once, so repeated inset dispatches never
* accumulate.
*/
fun View.applyBottomSystemBarInsetPadding() {
val initialBottom = paddingBottom
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
view.updatePadding(bottom = initialBottom + insets.getInsets(WindowInsetsCompat.Type.systemBars()).bottom)
insets
}
ViewCompat.requestApplyInsets(this)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?><!--
~ 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.
-->

<resources>

<!--
Landscape override: reserve the display cutout as a dark letterbox for edge-to-edge splash-themed
screens. The base splash theme (AndroidX Theme.SplashScreen) forces "shortEdges", which persists over
the runtime cutout handling, so we set "never" declaratively here where there is no timing race.

Lives in values-land-v27 because android:windowLayoutInDisplayCutoutMode requires API 27 (module minSdk
is 26). Devices below API 27 have no display cutout, so they correctly fall back to the empty base
subclass in values/.
-->
<style name="Theme.DuckDuckGo.SplashScreen.EdgeToEdge" parent="Theme.DuckDuckGo.SplashScreen">
<item name="android:windowLayoutInDisplayCutoutMode">never</item>
</style>

</resources>
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,15 @@
<item name="postSplashScreenTheme">@style/Theme.DuckDuckGo.Light</item>
</style>

<!--
Splash-screen theme variant for edge-to-edge screens (e.g. SystemSearchActivity,
WidgetThemeConfiguration). Portrait keeps the splash defaults; the values-land override sets
layoutInDisplayCutoutMode to "never" so in landscape the cutout renders as a dark letterbox
instead of the window background bleeding into it (the base splash theme forces "shortEdges",
which otherwise overrides the runtime cutout handling).
-->
<style name="Theme.DuckDuckGo.SplashScreen.EdgeToEdge" parent="Theme.DuckDuckGo.SplashScreen" />

<style name="Theme.DuckDuckGo.Splash" parent="Theme.SplashScreen">
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_dax_splash_screen_icon</item>
</style>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,22 @@
<item name="bottomSheetStyle">@style/Widget.DuckDuckGo.ModalBottomSheetStyle</item>
</style>

<!-- Opt-in variant used when EdgeToEdgeBucket.BOTTOM_SHEETS is enabled: draws the sheet behind the
transparent system bars (edge-to-edge, Android 15+). Content clears the nav bar via
View.applyBottomSystemBarInsetPadding(); Material adds the top status-bar inset.
`enableEdgeToEdge` can only be read from the theme at dialog construction (no runtime setter),
so callers pick this style vs. the plain one above based on the toggle. -->
<style name="Widget.DuckDuckGo.BottomSheetDialog.EdgeToEdge" parent="Widget.DuckDuckGo.BottomSheetDialog">
<item name="enableEdgeToEdge">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:enforceNavigationBarContrast" tools:targetApi="q">false</item>
<!-- Material's own bottom auto-padding (default true) would double up with our manual,
toggle-gated View.applyBottomSystemBarInsetPadding() now that the window is genuinely
edge-to-edge. Disable it; top/left/right auto-padding stay on (nothing manual replaces them). -->
<item name="paddingBottomSystemWindowInsets">false</item>
</style>

<!-- Workaround is necessary if we want to show the drag handle in our bottom sheets -->
<!-- See https://github.com/material-components/material-components-android/issues/3178 -->
<style name="Widget.MaterialComponents.BottomSheet.DragHandle" parent="Widget.Material3.BottomSheet.DragHandle">
Expand All @@ -479,6 +495,16 @@
<item name="bottomSheetStyle">@style/Widget.DuckDuckGo.ModalBottomSheetCollapsedStyle</item>
</style>

<!-- Opt-in variant used when EdgeToEdgeBucket.BOTTOM_SHEETS is enabled: draw behind the transparent
system bars; content clears the nav bar via View.applyBottomSystemBarInsetPadding(). -->
<style name="Widget.DuckDuckGo.BottomSheetDialogCollapsed.EdgeToEdge" parent="Widget.DuckDuckGo.BottomSheetDialogCollapsed">
<item name="enableEdgeToEdge">true</item>
<item name="android:statusBarColor">@android:color/transparent</item>
<item name="android:navigationBarColor">@android:color/transparent</item>
<item name="android:enforceNavigationBarContrast" tools:targetApi="q">false</item>
<item name="paddingBottomSystemWindowInsets">false</item>
</style>
Comment thread
aibrahim- marked this conversation as resolved.

<style name="Widget.DuckDuckGo.ModalBottomSheetCollapsedStyle" parent="Widget.MaterialComponents.BottomSheet.Modal">
<item name="backgroundTint">@android:color/transparent</item>
<item name="behavior_skipCollapsed">true</item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class ManageRecentAppsProtectionActivity :
private fun configureEdgeToEdgeInsets() {
edgeToEdgeHandler.applyHorizontalSystemBarInsets(binding.rootContainer)
edgeToEdgeHandler.applyStatusBarInsets(binding.includeToolbar.appBarLayout)
edgeToEdgeHandler.applyNavigationBarInsets(binding.contentScrollView, drawBehindGestureNav = true)
edgeToEdgeHandler.applyScrollableNavigationBarInsets(binding.contentScrollView)
}

override fun onDestroy() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ class TrackingProtectionExclusionListActivity :
private fun configureEdgeToEdgeInsets() {
edgeToEdgeHandler.applyHorizontalSystemBarInsets(binding.rootContainer)
edgeToEdgeHandler.applyStatusBarInsets(binding.includeToolbar.appBarLayout)
edgeToEdgeHandler.applyNavigationBarInsets(binding.excludedAppsRecycler, drawBehindGestureNav = true)
edgeToEdgeHandler.applyScrollableNavigationBarInsets(binding.excludedAppsRecycler)
}

override fun onDestroy() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ import android.view.ViewGroup
import androidx.core.text.HtmlCompat
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.appbuildconfig.api.AppBuildConfig
import com.duckduckgo.common.ui.applyBottomSystemBarInsetPadding
import com.duckduckgo.common.ui.store.AppTheme
import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeBucket
import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeProvider
import com.duckduckgo.common.utils.extensions.getSerializable
import com.duckduckgo.di.scopes.FragmentScope
import com.duckduckgo.mobile.android.vpn.R
Expand All @@ -47,10 +50,16 @@ class AlwaysOnAlertDialogFragment : BottomSheetDialogFragment() {
@Inject lateinit var appBuildConfig: AppBuildConfig

@Inject lateinit var appTheme: AppTheme

@Inject lateinit var edgeToEdgeProvider: EdgeToEdgeProvider
private lateinit var listener: Listener
private lateinit var fragmentType: FragmentType

override fun getTheme(): Int = com.duckduckgo.mobile.android.R.style.Widget_DuckDuckGo_BottomSheetDialogCollapsed
override fun getTheme(): Int = if (edgeToEdgeProvider.isEnabled(EdgeToEdgeBucket.BOTTOM_SHEETS)) {
com.duckduckgo.mobile.android.R.style.Widget_DuckDuckGo_BottomSheetDialogCollapsed_EdgeToEdge
} else {
com.duckduckgo.mobile.android.R.style.Widget_DuckDuckGo_BottomSheetDialogCollapsed
}

override fun onAttach(context: Context) {
AndroidSupportInjection.inject(this)
Expand All @@ -65,6 +74,9 @@ class AlwaysOnAlertDialogFragment : BottomSheetDialogFragment() {
return ContentVpnAlwaysOnAlertBinding.inflate(inflater, container, false).apply {
fragmentType = requireArguments().getSerializable<FragmentType>(ARGUMENT_FRAGMENT_TYPE) ?: FragmentType.ALWAYS_ON
configureViews(this)
if (edgeToEdgeProvider.isEnabled(EdgeToEdgeBucket.BOTTOM_SHEETS)) {
root.applyBottomSystemBarInsetPadding()
}
}.root
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class DeviceShieldAppTrackersInfo : DuckDuckGoActivity() {
private fun configureEdgeToEdgeInsets() {
edgeToEdgeHandler.applyHorizontalSystemBarInsets(binding.bookmarkRootView)
edgeToEdgeHandler.applyStatusBarInsets(binding.includeToolbar.appBarLayout)
edgeToEdgeHandler.applyNavigationBarInsets(binding.contentScrollView, drawBehindGestureNav = true)
edgeToEdgeHandler.applyScrollableNavigationBarInsets(binding.contentScrollView)
}

override fun onSupportNavigateUp(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ class AppTPCompanyTrackersActivity : DuckDuckGoActivity() {
private fun configureEdgeToEdgeInsets() {
edgeToEdgeHandler.applyHorizontalSystemBarInsets(binding.root)
edgeToEdgeHandler.applyStatusBarInsets(binding.includeToolbar.root)
edgeToEdgeHandler.applyNavigationBarInsets(binding.contentScrollView, drawBehindGestureNav = true)
edgeToEdgeHandler.applyScrollableNavigationBarInsets(binding.contentScrollView)
}

private fun observeViewModel() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.common.ui.DuckDuckGoFragment
import com.duckduckgo.common.ui.recyclerviewext.StickyHeadersLinearLayoutManager
import com.duckduckgo.common.utils.FragmentViewModelFactory
import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeBucket
import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeHandler
import com.duckduckgo.common.utils.edgetoedge.EdgeToEdgeProvider
import com.duckduckgo.di.scopes.FragmentScope
import com.duckduckgo.mobile.android.vpn.R
import com.duckduckgo.mobile.android.vpn.apps.ui.TrackingProtectionExclusionListActivity
Expand Down Expand Up @@ -58,6 +61,12 @@ class DeviceShieldActivityFeedFragment : DuckDuckGoFragment() {
@Inject
lateinit var trackerFeedAdapter: TrackerFeedAdapter

@Inject
lateinit var edgeToEdgeProvider: EdgeToEdgeProvider

@Inject
lateinit var edgeToEdgeHandler: EdgeToEdgeHandler

private val viewModel: DeviceShieldActivityFeedViewModel by bindViewModel()
private lateinit var binding: ViewDeviceShieldActivityFeedBinding

Expand All @@ -83,6 +92,13 @@ class DeviceShieldActivityFeedFragment : DuckDuckGoFragment() {
adapter = trackerFeedAdapter
}

// This fragment is reused both full-screen (DeviceShieldMostRecentActivity) and nested inside the
// dashboard's ScrollView (DeviceShieldTrackerActivity). Only reserve the navigation-bar inset when we're
// the top-level scroller; when nested in a scrolling ancestor, that outer scroller already handles it.
if (edgeToEdgeProvider.isEnabled(EdgeToEdgeBucket.VPN) && !binding.activityRecyclerView.hasVerticalScrollAncestor()) {
edgeToEdgeHandler.applyScrollableNavigationBarInsets(binding.activityRecyclerView)
}

lifecycleScope.launch {
viewModel.getMostRecentTrackers(
TimeWindow(
Expand Down Expand Up @@ -194,6 +210,21 @@ class DeviceShieldActivityFeedFragment : DuckDuckGoFragment() {

private inline fun <reified V : ViewModel> bindViewModel() = lazy { ViewModelProvider(this, viewModelFactory).get(V::class.java) }

private fun View.hasVerticalScrollAncestor(): Boolean {
var ancestor = parent
while (ancestor is View) {
if (
ancestor is android.widget.ScrollView ||
ancestor is androidx.core.widget.NestedScrollView ||
ancestor is androidx.recyclerview.widget.RecyclerView
) {
return true
}
ancestor = (ancestor as View).parent
}
return false
}

companion object {
private val defaultConfig = ActivityFeedConfig(
maxRows = Int.MAX_VALUE,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,8 @@ class DeviceShieldMostRecentActivity : DuckDuckGoActivity() {
private fun configureEdgeToEdgeInsets() {
edgeToEdgeHandler.applyHorizontalSystemBarInsets(binding.root)
edgeToEdgeHandler.applyStatusBarInsets(binding.includeToolbar.appBarLayout)
edgeToEdgeHandler.applyNavigationBarInsets(binding.activityList, drawBehindGestureNav = true)
// Bottom navigation-bar insets are applied inside DeviceShieldActivityFeedFragment (on its RecyclerView),
// because padding this FragmentContainerView host wouldn't reliably inset the fragment's scroll view.
}

override fun onBackPressed() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,7 @@ class DeviceShieldTrackerActivity :
private fun configureEdgeToEdgeInsets() {
edgeToEdgeHandler.applyHorizontalSystemBarInsets(binding.root)
edgeToEdgeHandler.applyStatusBarInsets(binding.includeToolbar.root)
edgeToEdgeHandler.applyNavigationBarInsets(binding.contentScrollView, drawBehindGestureNav = true)
edgeToEdgeHandler.applyScrollableNavigationBarInsets(binding.contentScrollView)
}

private fun bindViews() {
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@
android:exported="true"
android:label="@string/appName"
android:roundIcon="${appIconRound}"
android:theme="@style/Theme.DuckDuckGo.SplashScreen"
android:theme="@style/Theme.DuckDuckGo.SplashScreen.EdgeToEdge"
android:launchMode="singleTask"
android:stateNotNeeded="true">

Expand Down Expand Up @@ -496,7 +496,7 @@
android:exported="false"
android:label="@string/widgetConfigurationActivityTitle"
android:roundIcon="${appIconRound}"
android:theme="@style/Theme.DuckDuckGo.SplashScreen">
android:theme="@style/Theme.DuckDuckGo.SplashScreen.EdgeToEdge">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
</intent-filter>
Expand Down
Loading
Loading