Skip to content

Commit 2c22a09

Browse files
committed
Added Duck.ai entry points to search widget and updated UI. No logic implemented yet.
1 parent 6906ff5 commit 2c22a09

File tree

24 files changed

+466
-70
lines changed

24 files changed

+466
-70
lines changed

app/src/androidTest/java/com/duckduckgo/widget/SearchAndFavoritesGridCalculatorKtTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,10 @@ class SearchAndFavoritesGridCalculatorKtTest {
9090
return arrayOf(
9191
TestCase(1, 100),
9292
TestCase(1, 172),
93-
TestCase(2, 270),
94-
TestCase(3, 368),
93+
TestCase(1, 270),
94+
TestCase(2, 368),
9595
TestCase(3, 465),
96-
TestCase(4, 466),
96+
TestCase(3, 466),
9797
TestCase(4, 564),
9898
TestCase(4, 662),
9999
TestCase(4, 760),

app/src/main/AndroidManifest.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -538,6 +538,14 @@
538538
</intent-filter>
539539
</receiver>
540540

541+
<receiver
542+
android:name="com.duckduckgo.widget.DuckAiSearchWidgetUpdaterReceiver"
543+
android:exported="false">
544+
<intent-filter>
545+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
546+
</intent-filter>
547+
</receiver>
548+
541549
<receiver
542550
android:name=".remotemessage.SharePromoLinkRMFBroadCastReceiver"
543551
android:exported="false" />
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.widget
18+
19+
import android.appwidget.AppWidgetManager
20+
import android.content.BroadcastReceiver
21+
import android.content.Context
22+
import android.content.Intent
23+
import com.duckduckgo.anvil.annotations.InjectWith
24+
import com.duckduckgo.di.scopes.ReceiverScope
25+
import dagger.android.AndroidInjection
26+
import javax.inject.Inject
27+
28+
@InjectWith(ReceiverScope::class)
29+
class DuckAiSearchWidgetUpdaterReceiver : BroadcastReceiver() {
30+
31+
@Inject
32+
lateinit var widgetUpdater: WidgetUpdater
33+
34+
override fun onReceive(
35+
context: Context,
36+
intent: Intent,
37+
) {
38+
AndroidInjection.inject(this, context)
39+
if (intent.action == AppWidgetManager.ACTION_APPWIDGET_UPDATE) {
40+
widgetUpdater.updateWidgets(context)
41+
}
42+
}
43+
}

app/src/main/java/com/duckduckgo/widget/SearchWidget.kt

Lines changed: 9 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import android.appwidget.AppWidgetProvider
2222
import android.content.Context
2323
import android.content.Intent
2424
import android.os.Bundle
25-
import android.view.View
2625
import android.widget.RemoteViews
2726
import com.duckduckgo.app.browser.R
2827
import com.duckduckgo.app.di.AppCoroutineScope
@@ -33,13 +32,14 @@ import com.duckduckgo.app.systemsearch.SystemSearchActivity
3332
import javax.inject.Inject
3433
import kotlinx.coroutines.CoroutineScope
3534
import kotlinx.coroutines.launch
35+
import logcat.logcat
3636

3737
class SearchWidgetLight : SearchWidget(R.layout.search_widget_light)
3838

3939
open class SearchWidget(val layoutId: Int = R.layout.search_widget_dark) : AppWidgetProvider() {
4040

4141
@Inject
42-
lateinit var voiceSearchWidgetConfigurator: VoiceSearchWidgetConfigurator
42+
lateinit var searchWidgetConfigurator: SearchWidgetConfigurator
4343

4444
@Inject
4545
lateinit var searchWidgetLifecycleDelegate: SearchWidgetLifecycleDelegate
@@ -73,9 +73,11 @@ open class SearchWidget(val layoutId: Int = R.layout.search_widget_dark) : AppWi
7373
appWidgetManager: AppWidgetManager,
7474
appWidgetIds: IntArray,
7575
) {
76+
logcat { "SearchWidget onUpdate" }
7677
// There may be multiple widgets active, so update all of them
7778
for (appWidgetId in appWidgetIds) {
78-
updateAppWidget(context, appWidgetManager, appWidgetId, null)
79+
logcat { "SearchWidget onUpdate called for widget id = $appWidgetId" }
80+
updateAppWidget(context, appWidgetManager, appWidgetId)
7981
}
8082
}
8183

@@ -85,38 +87,25 @@ open class SearchWidget(val layoutId: Int = R.layout.search_widget_dark) : AppWi
8587
appWidgetId: Int,
8688
newOptions: Bundle?,
8789
) {
88-
updateAppWidget(context, appWidgetManager, appWidgetId, newOptions)
90+
logcat { "SearchWidget onAppWidgetOptionsChanged called for widget id = $appWidgetId" }
91+
updateAppWidget(context, appWidgetManager, appWidgetId)
8992
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
9093
}
9194

9295
private fun updateAppWidget(
9396
context: Context,
9497
appWidgetManager: AppWidgetManager,
9598
appWidgetId: Int,
96-
newOptions: Bundle?,
9799
) {
98-
// TODO ANA: Add Duck.ai entry point.
99-
val appWidgetOptions = appWidgetManager.getAppWidgetOptions(appWidgetId)
100-
var portraitWidth = appWidgetOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
101-
102-
if (newOptions != null) {
103-
portraitWidth = appWidgetOptions.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
104-
}
105-
106-
val shouldShowHint = shouldShowSearchBarHint(portraitWidth)
100+
logcat { "SearchWidget updateAppWidget called for widget id = $appWidgetId" }
107101

108102
val views = RemoteViews(context.packageName, layoutId)
109-
views.setViewVisibility(R.id.searchInputBox, if (shouldShowHint) View.VISIBLE else View.INVISIBLE)
110103
views.setOnClickPendingIntent(R.id.widgetContainer, buildPendingIntent(context))
111104

112-
voiceSearchWidgetConfigurator.configureVoiceSearch(context, views, false)
105+
searchWidgetConfigurator.configureWidget(context, views, false)
113106
appWidgetManager.updateAppWidget(appWidgetId, views)
114107
}
115108

116-
private fun shouldShowSearchBarHint(portraitWidth: Int): Boolean {
117-
return portraitWidth > SEARCH_BAR_MIN_HINT_WIDTH_SIZE
118-
}
119-
120109
private fun buildPendingIntent(context: Context): PendingIntent {
121110
val intent = SystemSearchActivity.fromWidget(context)
122111
return PendingIntent.getActivity(
@@ -133,7 +122,6 @@ open class SearchWidget(val layoutId: Int = R.layout.search_widget_dark) : AppWi
133122
}
134123

135124
companion object {
136-
private const val SEARCH_BAR_MIN_HINT_WIDTH_SIZE = 168
137125
private const val SEARCH_WIDGET_REQUEST_CODE = 1530
138126
}
139127
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/*
2+
* Copyright (c) 2025 DuckDuckGo
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.duckduckgo.widget
18+
19+
import android.app.PendingIntent
20+
import android.content.Context
21+
import android.view.View
22+
import android.widget.RemoteViews
23+
import com.duckduckgo.app.browser.R
24+
import com.duckduckgo.app.di.AppCoroutineScope
25+
import com.duckduckgo.app.systemsearch.SystemSearchActivity
26+
import com.duckduckgo.duckchat.api.DuckChat
27+
import com.duckduckgo.voice.api.VoiceSearchAvailability
28+
import javax.inject.Inject
29+
import kotlinx.coroutines.CoroutineScope
30+
import kotlinx.coroutines.runBlocking
31+
import logcat.logcat
32+
33+
class SearchWidgetConfigurator @Inject constructor(
34+
private val voiceSearchAvailability: VoiceSearchAvailability,
35+
private val duckChat: DuckChat,
36+
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
37+
) {
38+
39+
fun configureWidget(
40+
context: Context,
41+
remoteViews: RemoteViews,
42+
fromFavWidget: Boolean,
43+
) {
44+
// TODO ANA: Improve this method to avoid blocking the main thread.
45+
runBlocking {
46+
val voiceSearchEnabled = voiceSearchAvailability.isVoiceSearchAvailable
47+
val duckAiEnabled = duckChat.isEnabled() && duckChat.isDuckChatUserEnabled() && duckChat.wasOpenedBefore()
48+
49+
logcat { "SearchWidgetConfigurator voiceSearchEnabled=$voiceSearchEnabled, duckAiEnabled=$duckAiEnabled" }
50+
remoteViews.setViewVisibility(R.id.voiceSearch, if (voiceSearchEnabled) View.VISIBLE else View.GONE)
51+
remoteViews.setViewVisibility(R.id.duckAi, if (duckAiEnabled) View.VISIBLE else View.GONE)
52+
remoteViews.setViewVisibility(R.id.separator, if (voiceSearchEnabled && duckAiEnabled) View.VISIBLE else View.GONE)
53+
remoteViews.setViewVisibility(R.id.search, if (!voiceSearchEnabled && !duckAiEnabled) View.VISIBLE else View.GONE)
54+
55+
if (voiceSearchEnabled) {
56+
val pendingIntent = buildVoiceSearchPendingIntent(context, fromFavWidget)
57+
remoteViews.setOnClickPendingIntent(R.id.voiceSearch, pendingIntent)
58+
}
59+
60+
if (duckAiEnabled) {
61+
val pendingIntent = buildDuckAiPendingIntent(context)
62+
pendingIntent?.let { remoteViews.setOnClickPendingIntent(R.id.duckAi, it) }
63+
}
64+
}
65+
}
66+
67+
private fun buildVoiceSearchPendingIntent(
68+
context: Context,
69+
fromFavWidget: Boolean,
70+
): PendingIntent {
71+
val intent = if (fromFavWidget) SystemSearchActivity.fromFavWidget(context, true) else SystemSearchActivity.fromWidget(context, true)
72+
return PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
73+
}
74+
75+
private fun buildDuckAiPendingIntent(
76+
context: Context,
77+
): PendingIntent? {
78+
val intent = duckChat.createDuckChatIntent()
79+
if (intent == null) {
80+
return null
81+
}
82+
return PendingIntent.getActivity(context, 2, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
83+
}
84+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!--
2+
~ Copyright (c) 2025 DuckDuckGo
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
-->
16+
17+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
android:width="24dp"
19+
android:height="24dp"
20+
android:viewportWidth="24"
21+
android:viewportHeight="24">
22+
<path
23+
android:fillColor="@color/white84"
24+
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" />
25+
<path
26+
android:fillColor="@color/white84"
27+
android:fillType="evenOdd"
28+
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" />
29+
</vector>
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
<!--
2+
~ Copyright (c) 2025 DuckDuckGo
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
-->
16+
17+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
android:width="24dp"
19+
android:height="24dp"
20+
android:viewportWidth="24"
21+
android:viewportHeight="24">
22+
<path
23+
android:fillColor="@color/black84"
24+
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" />
25+
<path
26+
android:fillColor="@color/black84"
27+
android:fillType="evenOdd"
28+
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" />
29+
</vector>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!--
2+
~ Copyright (c) 2025 DuckDuckGo
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
-->
16+
17+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
android:width="2dp"
19+
android:height="20dp"
20+
android:viewportWidth="2"
21+
android:viewportHeight="20">
22+
<path
23+
android:fillAlpha="0.12"
24+
android:fillColor="#ffffff"
25+
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" />
26+
</vector>
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!--
2+
~ Copyright (c) 2025 DuckDuckGo
3+
~
4+
~ Licensed under the Apache License, Version 2.0 (the "License");
5+
~ you may not use this file except in compliance with the License.
6+
~ You may obtain a copy of the License at
7+
~
8+
~ http://www.apache.org/licenses/LICENSE-2.0
9+
~
10+
~ Unless required by applicable law or agreed to in writing, software
11+
~ distributed under the License is distributed on an "AS IS" BASIS,
12+
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
~ See the License for the specific language governing permissions and
14+
~ limitations under the License.
15+
-->
16+
17+
<vector xmlns:android="http://schemas.android.com/apk/res/android"
18+
android:width="2dp"
19+
android:height="20dp"
20+
android:viewportWidth="2"
21+
android:viewportHeight="20">
22+
<path
23+
android:fillAlpha="0.09"
24+
android:fillColor="#000000"
25+
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" />
26+
</vector>

app/src/main/res/drawable/search_widget_background_black.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@
1616

1717
<shape xmlns:android="http://schemas.android.com/apk/res/android">
1818
<solid android:color="@color/gray90" />
19-
<corners android:radius="@dimen/smallShapeCornerRadius" />
19+
<corners android:radius="@dimen/largeShapeCornerRadius" />
2020
</shape>

0 commit comments

Comments
 (0)