Skip to content

[Implementation] Widget 1: Search + Duck.ai #6576

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
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 @@ -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),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We changed some of the dimens, so I updated the test to match the current expected behavior.

TestCase(4, 564),
TestCase(4, 662),
TestCase(4, 760),
Expand Down
8 changes: 8 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -538,6 +538,14 @@
</intent-filter>
</receiver>

<receiver
android:name="com.duckduckgo.widget.DuckAiSearchWidgetUpdaterReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
</receiver>

<receiver
android:name=".remotemessage.SharePromoLinkRMFBroadCastReceiver"
android:exported="false" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
39 changes: 17 additions & 22 deletions app/src/main/java/com/duckduckgo/widget/SearchWidget.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -85,36 +87,30 @@ 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)
}

private fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle?,
) {
// TODO ANA: Add Duck.ai entry point.
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)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed all this as in the new requirements we don't hide the hint.

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 {
Expand All @@ -133,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
}
}
Original file line number Diff line number Diff line change
@@ -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()
}
Comment on lines +44 to +49
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Could also do this but NBD

val (voiceSearchEnabled, duckAiIntent) = withContext(dispatcherProvider.io()) {
    voiceSearchAvailability.isVoiceSearchAvailable to 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)
}
}
29 changes: 29 additions & 0 deletions app/src/main/res/drawable/ic_duck_ai_24_dark.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!--
~ 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.
-->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/white84"
android:pathData="M12.382,6.284C12.282,5.886 11.717,5.886 11.618,6.284L11.247,7.767C10.953,8.942 10.036,9.86 8.861,10.154L7.377,10.524C6.98,10.624 6.98,11.189 7.377,11.288L8.861,11.659C10.036,11.953 10.953,12.87 11.247,14.046L11.618,15.529C11.717,15.927 12.282,15.927 12.382,15.529L12.753,14.046C13.047,12.87 13.964,11.953 15.139,11.659L16.623,11.288C17.02,11.189 17.02,10.624 16.623,10.524L15.139,10.154C13.964,9.86 13.047,8.942 12.753,7.767L12.382,6.284Z" />
<path
android:fillColor="@color/white84"
android:fillType="evenOdd"
android:pathData="M4.268,21.916C7.588,21.346 13.405,20.243 15.769,19.158C19.424,17.833 22,14.638 22,10.906C22,5.987 17.523,2 12,2C6.477,2 2,5.987 2,10.906C2,13.374 3.127,15.607 4.947,17.22C5.346,17.574 5.422,18.187 5.072,18.59L3.455,20.454C2.903,21.091 3.438,22.058 4.268,21.916ZM15.143,17.795L15.199,17.769L15.257,17.748C18.424,16.6 20.5,13.901 20.5,10.906C20.5,6.974 16.862,3.5 12,3.5C7.137,3.5 3.5,6.974 3.5,10.906C3.5,12.898 4.406,14.737 5.942,16.098C6.881,16.93 7.157,18.475 6.206,19.572L5.717,20.136C7.062,19.89 8.586,19.592 10.05,19.266C12.225,18.781 14.124,18.263 15.143,17.795Z" />
</vector>
29 changes: 29 additions & 0 deletions app/src/main/res/drawable/ic_duck_ai_24_light.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<!--
~ 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.
-->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@color/black84"
android:pathData="M12.382,6.284C12.282,5.886 11.717,5.886 11.618,6.284L11.247,7.767C10.953,8.942 10.036,9.86 8.861,10.154L7.377,10.524C6.98,10.624 6.98,11.189 7.377,11.288L8.861,11.659C10.036,11.953 10.953,12.87 11.247,14.046L11.618,15.529C11.717,15.927 12.282,15.927 12.382,15.529L12.753,14.046C13.047,12.87 13.964,11.953 15.139,11.659L16.623,11.288C17.02,11.189 17.02,10.624 16.623,10.524L15.139,10.154C13.964,9.86 13.047,8.942 12.753,7.767L12.382,6.284Z" />
<path
android:fillColor="@color/black84"
android:fillType="evenOdd"
android:pathData="M4.268,21.916C7.588,21.346 13.405,20.243 15.769,19.158C19.424,17.833 22,14.638 22,10.906C22,5.987 17.523,2 12,2C6.477,2 2,5.987 2,10.906C2,13.374 3.127,15.607 4.947,17.22C5.346,17.574 5.422,18.187 5.072,18.59L3.455,20.454C2.903,21.091 3.438,22.058 4.268,21.916ZM15.143,17.795L15.199,17.769L15.257,17.748C18.424,16.6 20.5,13.901 20.5,10.906C20.5,6.974 16.862,3.5 12,3.5C7.137,3.5 3.5,6.974 3.5,10.906C3.5,12.898 4.406,14.737 5.942,16.098C6.881,16.93 7.157,18.475 6.206,19.572L5.717,20.136C7.062,19.89 8.586,19.592 10.05,19.266C12.225,18.781 14.124,18.263 15.143,17.795Z" />
</vector>
26 changes: 26 additions & 0 deletions app/src/main/res/drawable/ic_vertical_separator_dark.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!--
~ 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.
-->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="2dp"
android:height="20dp"
android:viewportWidth="2"
android:viewportHeight="20">
<path
android:fillAlpha="0.12"
android:fillColor="#ffffff"
android:pathData="M0.5,0.5C0.5,0.224 0.724,0 1,0C1.276,0 1.5,0.224 1.5,0.5V19.5C1.5,19.776 1.276,20 1,20C0.724,20 0.5,19.776 0.5,19.5V0.5Z" />
</vector>
26 changes: 26 additions & 0 deletions app/src/main/res/drawable/ic_vertical_separator_light.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!--
~ 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.
-->

<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="2dp"
android:height="20dp"
android:viewportWidth="2"
android:viewportHeight="20">
<path
android:fillAlpha="0.09"
android:fillColor="#000000"
android:pathData="M0.5,0.5C0.5,0.224 0.724,0 1,0C1.276,0 1.5,0.224 1.5,0.5V19.5C1.5,19.776 1.276,20 1,20C0.724,20 0.5,19.776 0.5,19.5V0.5Z" />
</vector>
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@

<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/gray90" />
<corners android:radius="@dimen/smallShapeCornerRadius" />
<corners android:radius="@dimen/largeShapeCornerRadius" />
</shape>
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,5 @@

<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid android:color="@color/white" />
<corners android:radius="@dimen/smallShapeCornerRadius" />
<corners android:radius="@dimen/largeShapeCornerRadius" />
</shape>
Loading
Loading