Skip to content

Commit 335890d

Browse files
loloakiraclaude
andcommitted
feat(widget): add interactive notes widget
Add a third home-screen widget, "Interactive notes", that shows notes as a scrollable single column of rounded cards. Each card shows the note title and a multi-line content excerpt (the whole note when it is short). Cards scroll up to the top and bottom edges of the widget, and on Android 12+ the widget uses the system corner radius so the launcher clips them to its shape. Tapping a card opens that specific note; a floating button in the bottom-right creates a new note in the widget's configured category/account. The favorite star is a non-clickable indicator. The configuration screen groups its controls under headings and separates selecting from saving: tapping an entry only selects it, and an "Add widget" button in the bottom-right commits the configuration. "All notes" is selected by default. When an existing widget is reconfigured the button reads "Edit widget" and the stored selection is restored. Sort options are a "favorites on top" toggle and a newest-first / oldest-first order. The widget is reconfigurable via long-press on Android 12+ and provides a live previewLayout for the widget picker. Reuses the existing NotesListWidgetData storage (no database migration) and the existing excerpt generation; sort options are kept in per-widget SharedPreferences. Updates are routed through EXTRA_APPWIDGET_IDS. The two existing widgets are left unchanged. So reconfiguration can persist updated settings, the shared createOrUpdateNoteListWidgetData DAO now uses an upsert (REPLACE) conflict strategy instead of a plain insert that aborted on the existing primary key; this is transparent to the non-reconfigurable widgets, which never hit the conflict. A DAO test covers the upsert. Assisted-by: ClaudeCode:claude-opus-5 Assisted-by: ClaudeCode:claude-opus-5-5 AI-assistant: Claude Code 2.1.226 (Claude Opus 5) AI-assistant: Claude Code 2.1.289 (Claude Opus 5.5) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: loloakira <6253351+loloakira@users.noreply.github.com>
1 parent ca02084 commit 335890d

24 files changed

Lines changed: 1215 additions & 1 deletion

‎app/src/main/AndroidManifest.xml‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,14 @@
159159
</intent-filter>
160160
</activity>
161161

162+
<activity android:name=".widget.interactivelist.InteractiveNoteListWidgetConfigurationActivity"
163+
android:exported="false">
164+
165+
<intent-filter>
166+
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE" />
167+
</intent-filter>
168+
</activity>
169+
162170
<receiver
163171
android:name=".widget.singlenote.SingleNoteWidget"
164172
android:label="@string/widget_single_note_title"
@@ -187,6 +195,20 @@
187195
android:resource="@xml/note_list_widget_provider_info" />
188196
</receiver>
189197

198+
<receiver
199+
android:name=".widget.interactivelist.InteractiveNoteListWidget"
200+
android:label="@string/widget_interactive_note_list_title"
201+
android:exported="false">
202+
203+
<intent-filter>
204+
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
205+
</intent-filter>
206+
207+
<meta-data
208+
android:name="android.appwidget.provider"
209+
android:resource="@xml/interactive_note_list_widget_provider_info" />
210+
</receiver>
211+
190212
<service
191213
android:name=".widget.singlenote.SingleNoteWidgetService"
192214
android:permission="android.permission.BIND_REMOTEVIEWS"
@@ -197,6 +219,11 @@
197219
android:permission="android.permission.BIND_REMOTEVIEWS"
198220
android:exported="false"/>
199221

222+
<service
223+
android:name=".widget.interactivelist.InteractiveNoteListWidgetService"
224+
android:permission="android.permission.BIND_REMOTEVIEWS"
225+
android:exported="false"/>
226+
200227
<service
201228
android:name=".quicksettings.NewNoteTileService"
202229
android:description="@string/action_create"

‎app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import static it.niedermann.owncloud.notes.edit.EditNoteActivity.ACTION_SHORTCUT;
1515
import static it.niedermann.owncloud.notes.shared.util.ApiVersionUtil.getPreferredApiVersion;
1616
import static it.niedermann.owncloud.notes.shared.util.NoteUtil.generateNoteExcerpt;
17+
import static it.niedermann.owncloud.notes.widget.interactivelist.InteractiveNoteListWidget.updateInteractiveNoteListWidgets;
1718
import static it.niedermann.owncloud.notes.widget.notelist.NoteListWidget.updateNoteListWidgets;
1819
import static it.niedermann.owncloud.notes.widget.singlenote.SingleNoteWidget.updateSingleNoteWidgets;
1920

@@ -681,6 +682,7 @@ private void notifyWidgets() {
681682
executor.submit(() -> {
682683
updateSingleNoteWidgets(context);
683684
updateNoteListWidgets(context);
685+
updateInteractiveNoteListWidgets(context);
684686
});
685687
}
686688

‎app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/WidgetNotesListDao.java‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,15 @@
88

99
import androidx.room.Dao;
1010
import androidx.room.Insert;
11+
import androidx.room.OnConflictStrategy;
1112
import androidx.room.Query;
1213

1314
import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData;
1415

1516
@Dao
1617
public interface WidgetNotesListDao {
1718

18-
@Insert
19+
@Insert(onConflict = OnConflictStrategy.REPLACE)
1920
void createOrUpdateNoteListWidgetData(NotesListWidgetData data);
2021

2122
@Query("DELETE FROM NOTESLISTWIDGETDATA WHERE id = :appWidgetId")
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
* Nextcloud Notes - Android Client
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
package it.niedermann.owncloud.notes.widget.interactivelist
8+
9+
import android.app.PendingIntent
10+
import android.appwidget.AppWidgetManager
11+
import android.appwidget.AppWidgetProvider
12+
import android.content.ComponentName
13+
import android.content.Context
14+
import android.content.Intent
15+
import android.content.res.ColorStateList
16+
import android.os.Build
17+
import android.os.Bundle
18+
import android.widget.RemoteViews
19+
import androidx.core.net.toUri
20+
import it.niedermann.owncloud.notes.R
21+
import it.niedermann.owncloud.notes.branding.BrandingUtil
22+
import it.niedermann.owncloud.notes.edit.EditNoteActivity
23+
import it.niedermann.owncloud.notes.persistence.NotesRepository
24+
import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData
25+
import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType
26+
import it.niedermann.owncloud.notes.shared.model.NavigationCategory
27+
import it.niedermann.owncloud.notes.shared.util.WidgetUtil
28+
import java.util.concurrent.ExecutorService
29+
import java.util.concurrent.Executors
30+
31+
class InteractiveNoteListWidget : AppWidgetProvider() {
32+
private val executor: ExecutorService = Executors.newCachedThreadPool()
33+
34+
override fun onUpdate(
35+
context: Context,
36+
appWidgetManager: AppWidgetManager,
37+
appWidgetIds: IntArray
38+
) {
39+
super.onUpdate(context, appWidgetManager, appWidgetIds)
40+
updateAppWidget(context, appWidgetManager, appWidgetIds)
41+
}
42+
43+
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
44+
super.onDeleted(context, appWidgetIds)
45+
val repo = NotesRepository.getInstance(context)
46+
47+
for (appWidgetId in appWidgetIds) {
48+
executor.execute {
49+
repo.removeNoteListWidget(appWidgetId)
50+
InteractiveWidgetPreferences.remove(context, appWidgetId)
51+
}
52+
}
53+
}
54+
55+
companion object {
56+
fun updateAppWidget(context: Context, awm: AppWidgetManager, appWidgetIds: IntArray) {
57+
val repo = NotesRepository.getInstance(context)
58+
appWidgetIds.forEach { appWidgetId ->
59+
val data = repo.getNoteListWidgetData(appWidgetId) ?: return@forEach
60+
updateSingleWidget(context, awm, appWidgetId, data)
61+
}
62+
}
63+
64+
private fun updateSingleWidget(
65+
context: Context,
66+
awm: AppWidgetManager,
67+
appWidgetId: Int,
68+
data: NotesListWidgetData
69+
) {
70+
val serviceIntent = Intent(context, InteractiveNoteListWidgetService::class.java).apply {
71+
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
72+
setData(toUri(Intent.URI_INTENT_SCHEME).toUri())
73+
}
74+
75+
val openTemplateIntent = Intent(context, EditNoteActivity::class.java).apply {
76+
setPackage(context.packageName)
77+
}
78+
val openTemplateFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
79+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
80+
} else {
81+
PendingIntent.FLAG_UPDATE_CURRENT
82+
}
83+
val openTemplatePendingIntent =
84+
PendingIntent.getActivity(context, 0, openTemplateIntent, openTemplateFlags)
85+
86+
val createNotePendingIntent =
87+
PendingIntent.getActivity(
88+
context,
89+
appWidgetId,
90+
createNoteIntent(context, data),
91+
WidgetUtil.pendingIntentFlagCompat(PendingIntent.FLAG_UPDATE_CURRENT)
92+
)
93+
94+
val views = RemoteViews(context.packageName, R.layout.widget_interactive_note_list).apply {
95+
setRemoteAdapter(R.id.interactive_note_list_lv, serviceIntent)
96+
setPendingIntentTemplate(R.id.interactive_note_list_lv, openTemplatePendingIntent)
97+
setEmptyView(
98+
R.id.interactive_note_list_lv,
99+
R.id.interactive_note_list_placeholder_tv
100+
)
101+
setOnClickPendingIntent(R.id.interactive_create_note_button, createNotePendingIntent)
102+
103+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
104+
setColorStateList(
105+
R.id.interactive_create_note_button,
106+
"setBackgroundTintList",
107+
ColorStateList.valueOf(BrandingUtil.readBrandMainColor(context))
108+
)
109+
}
110+
}
111+
112+
awm.run {
113+
updateAppWidget(appWidgetId, views)
114+
notifyAppWidgetViewDataChanged(intArrayOf(appWidgetId), R.id.interactive_note_list_lv)
115+
}
116+
}
117+
118+
private fun createNoteIntent(context: Context, data: NotesListWidgetData): Intent {
119+
val navigationCategory = if (data.mode == NotesListWidgetData.MODE_DISPLAY_STARRED) {
120+
NavigationCategory(ENavigationCategoryType.FAVORITES)
121+
} else {
122+
NavigationCategory(data.accountId, data.category)
123+
}
124+
125+
val bundle = Bundle().apply {
126+
putSerializable(EditNoteActivity.PARAM_CATEGORY, navigationCategory)
127+
putLong(EditNoteActivity.PARAM_ACCOUNT_ID, data.accountId)
128+
}
129+
130+
return Intent(context, EditNoteActivity::class.java).apply {
131+
setPackage(context.packageName)
132+
putExtras(bundle)
133+
setData("interactive-create://${data.id}".toUri())
134+
}
135+
}
136+
137+
@JvmStatic
138+
fun updateInteractiveNoteListWidgets(context: Context) {
139+
val appWidgetIds = AppWidgetManager.getInstance(context)
140+
.getAppWidgetIds(ComponentName(context, InteractiveNoteListWidget::class.java))
141+
142+
val intent = Intent(context, InteractiveNoteListWidget::class.java).apply {
143+
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
144+
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
145+
}
146+
context.sendBroadcast(intent)
147+
}
148+
}
149+
}

0 commit comments

Comments
 (0)