diff --git a/app/src/androidTest/java/com/duckduckgo/widget/SearchAndFavoritesGridCalculatorKtTest.kt b/app/src/androidTest/java/com/duckduckgo/widget/SearchAndFavoritesGridCalculatorKtTest.kt
index abbb3c76db96..bf0e598c8807 100644
--- a/app/src/androidTest/java/com/duckduckgo/widget/SearchAndFavoritesGridCalculatorKtTest.kt
+++ b/app/src/androidTest/java/com/duckduckgo/widget/SearchAndFavoritesGridCalculatorKtTest.kt
@@ -90,10 +90,10 @@ class SearchAndFavoritesGridCalculatorKtTest {
return arrayOf(
TestCase(1, 100),
TestCase(1, 172),
- TestCase(2, 270),
- TestCase(3, 368),
+ TestCase(1, 270),
+ TestCase(2, 368),
TestCase(3, 465),
- TestCase(4, 466),
+ TestCase(3, 466),
TestCase(4, 564),
TestCase(4, 662),
TestCase(4, 760),
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 77f45a963542..78f94dddb5cb 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -538,6 +538,14 @@
+
+
+
+
+
+
diff --git a/app/src/main/java/com/duckduckgo/widget/DuckAiSearchWidgetUpdaterReceiver.kt b/app/src/main/java/com/duckduckgo/widget/DuckAiSearchWidgetUpdaterReceiver.kt
new file mode 100644
index 000000000000..b823e65b351f
--- /dev/null
+++ b/app/src/main/java/com/duckduckgo/widget/DuckAiSearchWidgetUpdaterReceiver.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (c) 2025 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.
+ */
+
+package com.duckduckgo.widget
+
+import android.appwidget.AppWidgetManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import com.duckduckgo.anvil.annotations.InjectWith
+import com.duckduckgo.di.scopes.ReceiverScope
+import dagger.android.AndroidInjection
+import javax.inject.Inject
+
+@InjectWith(ReceiverScope::class)
+class DuckAiSearchWidgetUpdaterReceiver : BroadcastReceiver() {
+
+ @Inject
+ lateinit var widgetUpdater: WidgetUpdater
+
+ override fun onReceive(
+ context: Context,
+ intent: Intent,
+ ) {
+ AndroidInjection.inject(this, context)
+ if (intent.action == AppWidgetManager.ACTION_APPWIDGET_UPDATE) {
+ widgetUpdater.updateWidgets(context)
+ }
+ }
+}
diff --git a/app/src/main/java/com/duckduckgo/widget/SearchWidget.kt b/app/src/main/java/com/duckduckgo/widget/SearchWidget.kt
index c05cef3a8752..08c664a0b0ed 100644
--- a/app/src/main/java/com/duckduckgo/widget/SearchWidget.kt
+++ b/app/src/main/java/com/duckduckgo/widget/SearchWidget.kt
@@ -22,7 +22,6 @@ import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.os.Bundle
-import android.view.View
import android.widget.RemoteViews
import com.duckduckgo.app.browser.R
import com.duckduckgo.app.di.AppCoroutineScope
@@ -33,13 +32,14 @@ import com.duckduckgo.app.systemsearch.SystemSearchActivity
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
+import logcat.logcat
class SearchWidgetLight : SearchWidget(R.layout.search_widget_light)
open class SearchWidget(val layoutId: Int = R.layout.search_widget_dark) : AppWidgetProvider() {
@Inject
- lateinit var voiceSearchWidgetConfigurator: VoiceSearchWidgetConfigurator
+ lateinit var searchWidgetConfigurator: SearchWidgetConfigurator
@Inject
lateinit var searchWidgetLifecycleDelegate: SearchWidgetLifecycleDelegate
@@ -73,9 +73,11 @@ open class SearchWidget(val layoutId: Int = R.layout.search_widget_dark) : AppWi
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray,
) {
+ logcat { "SearchWidget onUpdate" }
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
- updateAppWidget(context, appWidgetManager, appWidgetId, null)
+ logcat { "SearchWidget onUpdate called for widget id = $appWidgetId" }
+ updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
@@ -85,7 +87,8 @@ open class SearchWidget(val layoutId: Int = R.layout.search_widget_dark) : AppWi
appWidgetId: Int,
newOptions: Bundle?,
) {
- updateAppWidget(context, appWidgetManager, appWidgetId, newOptions)
+ logcat { "SearchWidget onAppWidgetOptionsChanged called for widget id = $appWidgetId" }
+ updateAppWidget(context, appWidgetManager, appWidgetId)
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
}
@@ -93,27 +96,21 @@ open class SearchWidget(val layoutId: Int = R.layout.search_widget_dark) : AppWi
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
- newOptions: Bundle?,
) {
- val appWidgetOptions = appWidgetManager.getAppWidgetOptions(appWidgetId)
- var portraitWidth = appWidgetOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
-
- if (newOptions != null) {
- portraitWidth = appWidgetOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
- }
-
- val shouldShowHint = shouldShowSearchBarHint(portraitWidth)
+ logcat { "SearchWidget updateAppWidget called for widget id = $appWidgetId" }
val views = RemoteViews(context.packageName, layoutId)
- views.setViewVisibility(R.id.searchInputBox, if (shouldShowHint) View.VISIBLE else View.INVISIBLE)
views.setOnClickPendingIntent(R.id.widgetContainer, buildPendingIntent(context))
- voiceSearchWidgetConfigurator.configureVoiceSearch(context, views, false)
- appWidgetManager.updateAppWidget(appWidgetId, views)
- }
-
- private fun shouldShowSearchBarHint(portraitWidth: Int): Boolean {
- return portraitWidth > SEARCH_BAR_MIN_HINT_WIDTH_SIZE
+ appCoroutineScope.launch {
+ searchWidgetConfigurator.populateRemoteViews(
+ context = context,
+ remoteViews = views,
+ fromFavWidget = false,
+ )
+ appWidgetManager.updateAppWidget(appWidgetId, views)
+ logcat { "SearchWidget updateAppWidget completed for widget id = $appWidgetId" }
+ }
}
private fun buildPendingIntent(context: Context): PendingIntent {
@@ -132,7 +129,6 @@ open class SearchWidget(val layoutId: Int = R.layout.search_widget_dark) : AppWi
}
companion object {
- private const val SEARCH_BAR_MIN_HINT_WIDTH_SIZE = 168
private const val SEARCH_WIDGET_REQUEST_CODE = 1530
}
}
diff --git a/app/src/main/java/com/duckduckgo/widget/SearchWidgetConfigurator.kt b/app/src/main/java/com/duckduckgo/widget/SearchWidgetConfigurator.kt
new file mode 100644
index 000000000000..6d1b1dbff9ff
--- /dev/null
+++ b/app/src/main/java/com/duckduckgo/widget/SearchWidgetConfigurator.kt
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2025 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.
+ */
+
+package com.duckduckgo.widget
+
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.view.View
+import android.widget.RemoteViews
+import com.duckduckgo.app.browser.R
+import com.duckduckgo.app.systemsearch.SystemSearchActivity
+import com.duckduckgo.common.utils.DispatcherProvider
+import com.duckduckgo.duckchat.api.DuckChat
+import com.duckduckgo.voice.api.VoiceSearchAvailability
+import javax.inject.Inject
+import kotlinx.coroutines.withContext
+import logcat.logcat
+
+class SearchWidgetConfigurator @Inject constructor(
+ private val voiceSearchAvailability: VoiceSearchAvailability,
+ private val duckChat: DuckChat,
+ private val dispatcherProvider: DispatcherProvider,
+) {
+
+ suspend fun populateRemoteViews(
+ context: Context,
+ remoteViews: RemoteViews,
+ fromFavWidget: Boolean,
+ ) {
+ val voiceSearchEnabled = withContext(dispatcherProvider.io()) {
+ voiceSearchAvailability.isVoiceSearchAvailable
+ }
+ val duckAiIntent = withContext(dispatcherProvider.io()) {
+ duckChat.createDuckChatIntent()
+ }
+ val duckAiEnabled = duckAiIntent != null
+
+ logcat { "SearchWidgetConfigurator voiceSearchEnabled=$voiceSearchEnabled, duckAiEnabled=$duckAiEnabled" }
+
+ withContext(dispatcherProvider.main()) {
+ remoteViews.setViewVisibility(R.id.voiceSearch, if (voiceSearchEnabled) View.VISIBLE else View.GONE)
+ remoteViews.setViewVisibility(R.id.duckAi, if (duckAiEnabled) View.VISIBLE else View.GONE)
+ remoteViews.setViewVisibility(R.id.separator, if (voiceSearchEnabled && duckAiEnabled) View.VISIBLE else View.GONE)
+ remoteViews.setViewVisibility(R.id.search, if (!voiceSearchEnabled && !duckAiEnabled) View.VISIBLE else View.GONE)
+
+ if (voiceSearchEnabled) {
+ val pendingIntent = buildVoiceSearchPendingIntent(context, fromFavWidget)
+ remoteViews.setOnClickPendingIntent(R.id.voiceSearch, pendingIntent)
+ }
+
+ if (duckAiEnabled) {
+ val pendingIntent = buildDuckAiPendingIntent(context, duckAiIntent!!)
+ remoteViews.setOnClickPendingIntent(R.id.duckAi, pendingIntent)
+ }
+ }
+ }
+
+ private fun buildVoiceSearchPendingIntent(
+ context: Context,
+ fromFavWidget: Boolean,
+ ): PendingIntent {
+ val intent = if (fromFavWidget) SystemSearchActivity.fromFavWidget(context, true) else SystemSearchActivity.fromWidget(context, true)
+ return PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ }
+
+ private fun buildDuckAiPendingIntent(
+ context: Context,
+ intent: Intent,
+ ): PendingIntent {
+ return PendingIntent.getActivity(context, 2, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
+ }
+}
diff --git a/app/src/main/res/drawable/ic_duck_ai_24_dark.xml b/app/src/main/res/drawable/ic_duck_ai_24_dark.xml
new file mode 100644
index 000000000000..0b0b182056e4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_duck_ai_24_dark.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_duck_ai_24_light.xml b/app/src/main/res/drawable/ic_duck_ai_24_light.xml
new file mode 100644
index 000000000000..1edfccc52de6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_duck_ai_24_light.xml
@@ -0,0 +1,29 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_vertical_separator_dark.xml b/app/src/main/res/drawable/ic_vertical_separator_dark.xml
new file mode 100644
index 000000000000..aca264b34690
--- /dev/null
+++ b/app/src/main/res/drawable/ic_vertical_separator_dark.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_vertical_separator_light.xml b/app/src/main/res/drawable/ic_vertical_separator_light.xml
new file mode 100644
index 000000000000..7f7833b73b23
--- /dev/null
+++ b/app/src/main/res/drawable/ic_vertical_separator_light.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/app/src/main/res/drawable/search_widget_background_black.xml b/app/src/main/res/drawable/search_widget_background_black.xml
index e13fc8835fa8..8fd505c1d40f 100644
--- a/app/src/main/res/drawable/search_widget_background_black.xml
+++ b/app/src/main/res/drawable/search_widget_background_black.xml
@@ -16,5 +16,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/search_widget_background_white.xml b/app/src/main/res/drawable/search_widget_background_white.xml
index 8fa546166c10..571661ebcd04 100644
--- a/app/src/main/res/drawable/search_widget_background_white.xml
+++ b/app/src/main/res/drawable/search_widget_background_white.xml
@@ -16,5 +16,5 @@
-
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/search_widget_dark.xml b/app/src/main/res/layout/search_widget_dark.xml
index 12f6a9e6afd9..94b3a3ab8ccf 100644
--- a/app/src/main/res/layout/search_widget_dark.xml
+++ b/app/src/main/res/layout/search_widget_dark.xml
@@ -1,5 +1,5 @@
-
-
-
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ tools:ignore="DeprecatedWidgetInXml, InvalidColorAttribute">
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_ddg_logo"
+ tools:ignore="ContentDescription" />
+ android:textColor="@color/white60"
+ tools:ignore="ContentDescription" />
+ android:visibility="gone"
+ tools:ignore="ContentDescription" />
+
+
+
+
+ android:src="@drawable/ic_find_search_24_dark"
+ android:visibility="visible"
+ tools:ignore="ContentDescription" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/search_widget_light.xml b/app/src/main/res/layout/search_widget_light.xml
index 39cebb35ed1f..ad256e173311 100644
--- a/app/src/main/res/layout/search_widget_light.xml
+++ b/app/src/main/res/layout/search_widget_light.xml
@@ -1,5 +1,5 @@
-
-
-
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ tools:ignore="DeprecatedWidgetInXml, InvalidColorAttribute">
+ android:layout_gravity="center_vertical"
+ android:src="@drawable/ic_ddg_logo"
+ tools:ignore="ContentDescription" />
+ android:textColor="@color/black60"
+ tools:ignore="ContentDescription" />
+ android:visibility="gone"
+ tools:ignore="ContentDescription" />
+
+
+
+
+ android:src="@drawable/ic_find_search_24_light"
+ android:visibility="visible"
+ tools:ignore="ContentDescription" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/search_widget_info.xml b/app/src/main/res/xml/search_widget_info.xml
index 2d14cac592c1..2d55bb7c8e65 100644
--- a/app/src/main/res/xml/search_widget_info.xml
+++ b/app/src/main/res/xml/search_widget_info.xml
@@ -20,7 +20,7 @@
+
+
+
+
diff --git a/common/common-ui/src/main/res/drawable/ic_find_search_24_light.xml b/common/common-ui/src/main/res/drawable/ic_find_search_24_light.xml
new file mode 100644
index 000000000000..915eae7da768
--- /dev/null
+++ b/common/common-ui/src/main/res/drawable/ic_find_search_24_light.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
diff --git a/common/common-ui/src/main/res/values/design-system-dimensions.xml b/common/common-ui/src/main/res/values/design-system-dimensions.xml
index 07de8fda2a9e..0417e5a43074 100644
--- a/common/common-ui/src/main/res/values/design-system-dimensions.xml
+++ b/common/common-ui/src/main/res/values/design-system-dimensions.xml
@@ -141,7 +141,7 @@
16dp
64dp
78dp
- 46dp
+ 48dp
16dp
20dp
4dp
diff --git a/common/common-ui/src/main/res/values/widgets.xml b/common/common-ui/src/main/res/values/widgets.xml
index ac25bfdde003..a103956bef86 100644
--- a/common/common-ui/src/main/res/values/widgets.xml
+++ b/common/common-ui/src/main/res/values/widgets.xml
@@ -558,6 +558,8 @@
- horizontal
- center
- 5dp
+ - 12dp
+ - 12dp
- 2dp
@@ -571,30 +573,26 @@