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 @@