From f256a07920c314962f22e97f1f75025363c31a13 Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Mon, 15 Dec 2025 15:31:23 +0100 Subject: [PATCH 1/2] Allow SyncBarcodeView consumers to define minimum scanning area --- .../com/duckduckgo/sync/impl/SyncFeature.kt | 3 + .../sync/impl/ui/SyncConnectActivity.kt | 48 +++++- .../sync/impl/ui/qrcode/SyncBarcodeView.kt | 45 +++++- .../res/layout/activity_connect_sync_new.xml | 153 ++++++++++++++++++ .../layout/view_square_decorated_barcode.xml | 15 +- .../res/values/attrs-sync-barcode-view.xml | 24 +++ 6 files changed, 282 insertions(+), 6 deletions(-) create mode 100644 sync/sync-impl/src/main/res/layout/activity_connect_sync_new.xml create mode 100644 sync/sync-impl/src/main/res/values/attrs-sync-barcode-view.xml diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt index b32f80b8c2ce..7c1574ebfa25 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/SyncFeature.kt @@ -65,4 +65,7 @@ interface SyncFeature { @Toggle.DefaultValue(DefaultFeatureValue.TRUE) fun canOverrideThemeSyncSetup(): Toggle + + @Toggle.DefaultValue(DefaultFeatureValue.TRUE) + fun useNewActivityConnectSyncLayout(): Toggle } diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncConnectActivity.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncConnectActivity.kt index c4413b5c1873..d18d0837d802 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncConnectActivity.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/SyncConnectActivity.kt @@ -19,18 +19,23 @@ package com.duckduckgo.sync.impl.ui import android.content.Context import android.content.Intent import android.os.Bundle +import android.view.View +import android.widget.ImageView import androidx.activity.addCallback import androidx.lifecycle.Lifecycle import androidx.lifecycle.flowWithLifecycle import androidx.lifecycle.lifecycleScope import com.duckduckgo.anvil.annotations.InjectWith import com.duckduckgo.common.ui.DuckDuckGoActivity +import com.duckduckgo.common.ui.view.button.DaxButtonGhost import com.duckduckgo.common.ui.view.dialog.TextAlertDialogBuilder import com.duckduckgo.common.ui.view.show -import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.di.scopes.ActivityScope +import com.duckduckgo.mobile.android.databinding.IncludeDefaultToolbarBinding import com.duckduckgo.sync.impl.R +import com.duckduckgo.sync.impl.SyncFeature import com.duckduckgo.sync.impl.databinding.ActivityConnectSyncBinding +import com.duckduckgo.sync.impl.databinding.ActivityConnectSyncNewBinding import com.duckduckgo.sync.impl.ui.EnterCodeActivity.Companion.Code.CONNECT_CODE import com.duckduckgo.sync.impl.ui.SyncConnectViewModel.Command import com.duckduckgo.sync.impl.ui.SyncConnectViewModel.Command.FinishWithError @@ -39,15 +44,21 @@ import com.duckduckgo.sync.impl.ui.SyncConnectViewModel.Command.ReadTextCode import com.duckduckgo.sync.impl.ui.SyncConnectViewModel.Command.ShowError import com.duckduckgo.sync.impl.ui.SyncConnectViewModel.Command.ShowMessage import com.duckduckgo.sync.impl.ui.SyncConnectViewModel.ViewState +import com.duckduckgo.sync.impl.ui.qrcode.SyncBarcodeView import com.duckduckgo.sync.impl.ui.setup.EnterCodeContract import com.duckduckgo.sync.impl.ui.setup.EnterCodeContract.EnterCodeContractOutput import com.google.android.material.snackbar.Snackbar import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach +import javax.inject.Inject @InjectWith(ActivityScope::class) class SyncConnectActivity : DuckDuckGoActivity() { - private val binding: ActivityConnectSyncBinding by viewBinding() + + @Inject + lateinit var syncFeature: SyncFeature + + private lateinit var binding: ConnectSyncBinding private val viewModel: SyncConnectViewModel by bindViewModel() private val enterCodeLauncher = registerForActivityResult( @@ -60,6 +71,15 @@ class SyncConnectActivity : DuckDuckGoActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + binding = if (syncFeature.useNewActivityConnectSyncLayout().isEnabled()) { + val viewBinding = ActivityConnectSyncNewBinding.inflate(layoutInflater) + ConnectSyncBinding.NewBinding(viewBinding) + } else { + val viewBinding = ActivityConnectSyncBinding.inflate(layoutInflater) + ConnectSyncBinding.OldBinding(viewBinding) + } + setContentView(binding.root) setupToolbar(binding.includeToolbar.toolbar) @@ -166,3 +186,27 @@ class SyncConnectActivity : DuckDuckGoActivity() { private const val SOURCE_INTENT_KEY = "source" } } + +private sealed interface ConnectSyncBinding { + val root: View + val includeToolbar: IncludeDefaultToolbarBinding + val qrCodeReader: SyncBarcodeView + val qrCodeImageView: ImageView + val copyCodeButton: DaxButtonGhost + + data class OldBinding(private val binding: ActivityConnectSyncBinding) : ConnectSyncBinding { + override val root: View get() = binding.root + override val includeToolbar: IncludeDefaultToolbarBinding get() = binding.includeToolbar + override val qrCodeReader: SyncBarcodeView get() = binding.qrCodeReader + override val qrCodeImageView: ImageView get() = binding.qrCodeImageView + override val copyCodeButton: DaxButtonGhost get() = binding.copyCodeButton + } + + data class NewBinding(private val binding: ActivityConnectSyncNewBinding) : ConnectSyncBinding { + override val root: View get() = binding.root + override val includeToolbar: IncludeDefaultToolbarBinding get() = binding.includeToolbar + override val qrCodeReader: SyncBarcodeView get() = binding.qrCodeReader + override val qrCodeImageView: ImageView get() = binding.qrCodeImageView + override val copyCodeButton: DaxButtonGhost get() = binding.copyCodeButton + } +} diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/qrcode/SyncBarcodeView.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/qrcode/SyncBarcodeView.kt index 141c74f65534..175f86b7f96d 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/qrcode/SyncBarcodeView.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/qrcode/SyncBarcodeView.kt @@ -30,16 +30,19 @@ import android.view.View import android.widget.FrameLayout import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat +import androidx.core.view.doOnNextLayout import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.findViewTreeViewModelStoreOwner import androidx.lifecycle.lifecycleScope import com.duckduckgo.anvil.annotations.InjectWith +import com.duckduckgo.appbuildconfig.api.AppBuildConfig import com.duckduckgo.common.ui.viewbinding.viewBinding import com.duckduckgo.common.utils.ConflatedJob import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.sync.impl.R +import com.duckduckgo.sync.impl.SyncFeature import com.duckduckgo.sync.impl.databinding.ViewSquareDecoratedBarcodeBinding import com.duckduckgo.sync.impl.ui.qrcode.SquareDecoratedBarcodeViewModel.Command import com.duckduckgo.sync.impl.ui.qrcode.SquareDecoratedBarcodeViewModel.Command.CheckCameraAvailable @@ -48,7 +51,6 @@ import com.duckduckgo.sync.impl.ui.qrcode.SquareDecoratedBarcodeViewModel.Comman import com.duckduckgo.sync.impl.ui.qrcode.SquareDecoratedBarcodeViewModel.Command.RequestPermissions import com.duckduckgo.sync.impl.ui.qrcode.SquareDecoratedBarcodeViewModel.ViewState import dagger.android.support.AndroidSupportInjection -import kotlinx.coroutines.cancel import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import javax.inject.Inject @@ -66,12 +68,30 @@ constructor( ) : FrameLayout(context, attrs, defStyleAttr) { + private val minScanningAreaHeight: Int + + init { + context.obtainStyledAttributes(attrs, R.styleable.SyncBarcodeView).apply { + try { + minScanningAreaHeight = getDimensionPixelSize(R.styleable.SyncBarcodeView_minScanningAreaHeight, MIN_SCANNING_AREA_HEIGHT_NOT_SET) + } finally { + recycle() + } + } + } + @Inject lateinit var viewModelFactory: SquareDecoratedBarcodeViewModel.Factory @Inject lateinit var dispatchers: DispatcherProvider + @Inject + lateinit var appBuildConfig: AppBuildConfig + + @Inject + lateinit var syncFeature: SyncFeature + private val cameraBlockedDrawable by lazy { ContextCompat.getDrawable(context, R.drawable.camera_blocked) } @@ -93,6 +113,8 @@ constructor( AndroidSupportInjection.inject(this) super.onAttachedToWindow() + setupExpandedScanningArea() + findViewTreeLifecycleOwner()?.lifecycle?.addObserver(viewModel) val scope = findViewTreeLifecycleOwner()?.lifecycleScope!! @@ -110,6 +132,23 @@ constructor( } } + private fun setupExpandedScanningArea() { + if (minScanningAreaHeight == MIN_SCANNING_AREA_HEIGHT_NOT_SET) return + binding.barcodeContainer.doOnNextLayout { + val containerHeight = binding.barcodeContainer.height + + if (containerHeight >= minScanningAreaHeight) return@doOnNextLayout + + binding.barcodeView.layoutParams = binding.barcodeView.layoutParams.apply { + height = minScanningAreaHeight + } + + binding.barcodeView.doOnNextLayout { + binding.barcodeView.translationY = -((minScanningAreaHeight - containerHeight) / 2f) + } + } + } + override fun onDetachedFromWindow() { conflatedStateJob.cancel() conflatedCommandJob.cancel() @@ -217,4 +256,8 @@ constructor( } throw IllegalStateException("The ${this.javaClass.simpleName}'s Context is not an Activity.") } + + companion object { + private const val MIN_SCANNING_AREA_HEIGHT_NOT_SET = -1 + } } diff --git a/sync/sync-impl/src/main/res/layout/activity_connect_sync_new.xml b/sync/sync-impl/src/main/res/layout/activity_connect_sync_new.xml new file mode 100644 index 000000000000..c98b8de3c622 --- /dev/null +++ b/sync/sync-impl/src/main/res/layout/activity_connect_sync_new.xml @@ -0,0 +1,153 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/sync/sync-impl/src/main/res/layout/view_square_decorated_barcode.xml b/sync/sync-impl/src/main/res/layout/view_square_decorated_barcode.xml index a3cd63086f62..1b96419e9dc0 100644 --- a/sync/sync-impl/src/main/res/layout/view_square_decorated_barcode.xml +++ b/sync/sync-impl/src/main/res/layout/view_square_decorated_barcode.xml @@ -18,10 +18,19 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> - + android:layout_height="match_parent" + android:clipChildren="true" + android:clipToPadding="true"> + + + + + + + + + + + + + \ No newline at end of file From 1d9fa71ece50a23c2784912a7c1a8fbae278487c Mon Sep 17 00:00:00 2001 From: Cris Barreiro Date: Tue, 16 Dec 2025 11:26:43 +0100 Subject: [PATCH 2/2] Remove unused property --- .../com/duckduckgo/sync/impl/ui/qrcode/SyncBarcodeView.kt | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/qrcode/SyncBarcodeView.kt b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/qrcode/SyncBarcodeView.kt index 175f86b7f96d..893527d2467f 100644 --- a/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/qrcode/SyncBarcodeView.kt +++ b/sync/sync-impl/src/main/java/com/duckduckgo/sync/impl/ui/qrcode/SyncBarcodeView.kt @@ -42,7 +42,6 @@ import com.duckduckgo.common.utils.ConflatedJob import com.duckduckgo.common.utils.DispatcherProvider import com.duckduckgo.di.scopes.ViewScope import com.duckduckgo.sync.impl.R -import com.duckduckgo.sync.impl.SyncFeature import com.duckduckgo.sync.impl.databinding.ViewSquareDecoratedBarcodeBinding import com.duckduckgo.sync.impl.ui.qrcode.SquareDecoratedBarcodeViewModel.Command import com.duckduckgo.sync.impl.ui.qrcode.SquareDecoratedBarcodeViewModel.Command.CheckCameraAvailable @@ -89,9 +88,6 @@ constructor( @Inject lateinit var appBuildConfig: AppBuildConfig - @Inject - lateinit var syncFeature: SyncFeature - private val cameraBlockedDrawable by lazy { ContextCompat.getDrawable(context, R.drawable.camera_blocked) }