Skip to content

Commit dcadc52

Browse files
committed
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). 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. 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 Signed-off-by: loloakira <6253351+loloakira@users.noreply.github.com> AI-assistant: Claude Code 2.1.226 (Claude Opus 5)
1 parent 7715ba1 commit dcadc52

25 files changed

Lines changed: 1231 additions & 2 deletions

REUSE.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ SPDX-PackageSupplier = "Nextcloud Android team <android@nextcloud.com>"
66
SPDX-PackageDownloadLocation = "https://github.com/nextcloud/notes-android"
77

88
[[annotations]]
9-
path = ["app/schemas/it.niedermann.owncloud.notes.persistence.NotesDatabase/**.json", "app/src/main/res/values-**/strings.xml", "renovate.json5", "**/.gitignore", ".idea/**", "scripts/analysis/findbugs-results.txt", "scripts/analysis/lint-results.txt", "app/src/test/resources/robolectric.properties", "app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker", "fastlane/metadata/**", "app/src/main/res/mipmap-**dpi/ic_launcher.png", "app/src/main/ic_launcher-web.png", ".bundle/config", "app/src/main/res/drawable-**dpi/ic_widget_create.png", "app/src/main/res/drawable-mdpi/context_based_formatting.png", "app/src/main/res/drawable/note_list_widget_preview.webp", "app/src/main/res/drawable/single_note_widget.webp", "app/src/qa/res/drawable/ic_launcher_foreground.xml"]
9+
path = ["app/schemas/it.niedermann.owncloud.notes.persistence.NotesDatabase/**.json", "app/src/main/res/values-**/strings.xml", "renovate.json5", "**/.gitignore", ".idea/**", "scripts/analysis/findbugs-results.txt", "scripts/analysis/lint-results.txt", "app/src/test/resources/robolectric.properties", "app/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker", "fastlane/metadata/**", "app/src/main/res/mipmap-**dpi/ic_launcher.png", "app/src/main/ic_launcher-web.png", ".bundle/config", "app/src/main/res/drawable-**dpi/ic_widget_create.png", "app/src/main/res/drawable-mdpi/context_based_formatting.png", "app/src/main/res/drawable/note_list_widget_preview.webp", "app/src/main/res/drawable/single_note_widget.webp", "app/src/main/res/drawable/interactive_note_list_widget_preview.webp", "app/src/qa/res/drawable/ic_launcher_foreground.xml"]
1010
precedence = "aggregate"
1111
SPDX-FileCopyrightText = "2016-2025 Nextcloud GmbH and Nextcloud contributors"
1212
SPDX-License-Identifier = "GPL-3.0-or-later"

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: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
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.util.Log
19+
import android.widget.RemoteViews
20+
import androidx.core.net.toUri
21+
import it.niedermann.owncloud.notes.R
22+
import it.niedermann.owncloud.notes.branding.BrandingUtil
23+
import it.niedermann.owncloud.notes.edit.EditNoteActivity
24+
import it.niedermann.owncloud.notes.persistence.NotesRepository
25+
import it.niedermann.owncloud.notes.persistence.entity.NotesListWidgetData
26+
import it.niedermann.owncloud.notes.shared.model.ENavigationCategoryType
27+
import it.niedermann.owncloud.notes.shared.model.NavigationCategory
28+
import it.niedermann.owncloud.notes.shared.util.WidgetUtil
29+
import java.util.concurrent.ExecutorService
30+
import java.util.concurrent.Executors
31+
32+
class InteractiveNoteListWidget : AppWidgetProvider() {
33+
private val executor: ExecutorService = Executors.newCachedThreadPool()
34+
35+
override fun onUpdate(
36+
context: Context,
37+
appWidgetManager: AppWidgetManager,
38+
appWidgetIds: IntArray
39+
) {
40+
super.onUpdate(context, appWidgetManager, appWidgetIds)
41+
updateAppWidget(context, appWidgetManager, appWidgetIds)
42+
}
43+
44+
override fun onReceive(context: Context, intent: Intent) {
45+
super.onReceive(context, intent)
46+
val awm = AppWidgetManager.getInstance(context)
47+
48+
if (intent.action == null) {
49+
Log.w(TAG, "Intent action is null")
50+
return
51+
}
52+
53+
if (intent.action != AppWidgetManager.ACTION_APPWIDGET_UPDATE) {
54+
Log.w(TAG, "Intent action is not ACTION_APPWIDGET_UPDATE")
55+
return
56+
}
57+
58+
val appWidgetIds = if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID)) {
59+
intArrayOf(
60+
intent.getIntExtra(
61+
AppWidgetManager.EXTRA_APPWIDGET_ID,
62+
AppWidgetManager.INVALID_APPWIDGET_ID
63+
)
64+
)
65+
} else {
66+
awm.getAppWidgetIds(ComponentName(context, InteractiveNoteListWidget::class.java))
67+
}
68+
69+
updateAppWidget(context, awm, appWidgetIds)
70+
}
71+
72+
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
73+
super.onDeleted(context, appWidgetIds)
74+
val repo = NotesRepository.getInstance(context)
75+
76+
for (appWidgetId in appWidgetIds) {
77+
executor.execute {
78+
repo.removeNoteListWidget(appWidgetId)
79+
InteractiveWidgetPreferences.remove(context, appWidgetId)
80+
}
81+
}
82+
}
83+
84+
companion object {
85+
private val TAG: String = InteractiveNoteListWidget::class.java.simpleName
86+
87+
fun updateAppWidget(context: Context, awm: AppWidgetManager, appWidgetIds: IntArray) {
88+
val repo = NotesRepository.getInstance(context)
89+
appWidgetIds.forEach { appWidgetId ->
90+
val data = repo.getNoteListWidgetData(appWidgetId) ?: return@forEach
91+
updateSingleWidget(context, awm, appWidgetId, data)
92+
}
93+
}
94+
95+
private fun updateSingleWidget(
96+
context: Context,
97+
awm: AppWidgetManager,
98+
appWidgetId: Int,
99+
data: NotesListWidgetData
100+
) {
101+
val serviceIntent = Intent(context, InteractiveNoteListWidgetService::class.java).apply {
102+
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
103+
setData(toUri(Intent.URI_INTENT_SCHEME).toUri())
104+
}
105+
106+
val openTemplateIntent = Intent(context, EditNoteActivity::class.java).apply {
107+
setPackage(context.packageName)
108+
}
109+
val openTemplateFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
110+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE
111+
} else {
112+
PendingIntent.FLAG_UPDATE_CURRENT
113+
}
114+
val openTemplatePendingIntent =
115+
PendingIntent.getActivity(context, 0, openTemplateIntent, openTemplateFlags)
116+
117+
val createNotePendingIntent =
118+
PendingIntent.getActivity(
119+
context,
120+
appWidgetId,
121+
createNoteIntent(context, data),
122+
WidgetUtil.pendingIntentFlagCompat(PendingIntent.FLAG_UPDATE_CURRENT)
123+
)
124+
125+
val views = RemoteViews(context.packageName, R.layout.widget_interactive_note_list).apply {
126+
setRemoteAdapter(R.id.interactive_note_list_lv, serviceIntent)
127+
setPendingIntentTemplate(R.id.interactive_note_list_lv, openTemplatePendingIntent)
128+
setEmptyView(
129+
R.id.interactive_note_list_lv,
130+
R.id.interactive_note_list_placeholder_tv
131+
)
132+
setOnClickPendingIntent(R.id.interactive_create_note_button, createNotePendingIntent)
133+
134+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
135+
setColorStateList(
136+
R.id.interactive_create_note_button,
137+
"setBackgroundTintList",
138+
ColorStateList.valueOf(BrandingUtil.readBrandMainColor(context))
139+
)
140+
}
141+
}
142+
143+
awm.run {
144+
updateAppWidget(appWidgetId, views)
145+
notifyAppWidgetViewDataChanged(intArrayOf(appWidgetId), R.id.interactive_note_list_lv)
146+
}
147+
}
148+
149+
private fun createNoteIntent(context: Context, data: NotesListWidgetData): Intent {
150+
val navigationCategory = if (data.mode == NotesListWidgetData.MODE_DISPLAY_STARRED) {
151+
NavigationCategory(ENavigationCategoryType.FAVORITES)
152+
} else {
153+
NavigationCategory(data.accountId, data.category)
154+
}
155+
156+
val bundle = Bundle().apply {
157+
putSerializable(EditNoteActivity.PARAM_CATEGORY, navigationCategory)
158+
putLong(EditNoteActivity.PARAM_ACCOUNT_ID, data.accountId)
159+
}
160+
161+
return Intent(context, EditNoteActivity::class.java).apply {
162+
setPackage(context.packageName)
163+
putExtras(bundle)
164+
setData("interactive-create://${data.id}".toUri())
165+
}
166+
}
167+
168+
@JvmStatic
169+
fun updateInteractiveNoteListWidgets(context: Context) {
170+
val intent = Intent(context, InteractiveNoteListWidget::class.java).apply {
171+
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
172+
}
173+
context.sendBroadcast(intent)
174+
}
175+
}
176+
}

0 commit comments

Comments
 (0)