Skip to content
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
5 changes: 5 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ Every change — no matter how small — must follow this exact sequence:

Direct pushes to `master` are blocked by branch protection. CI enforces the version bump — a PR with the same version as `master` will fail.

**Agent rules:**

- **Never create a branch, commit, or PR unless explicitly asked by the developer.** Make code changes when asked; git operations only on explicit instruction.
- **Do not push** until the developer has tested the changes locally on a dev device and confirmed they are ready.

Choose the bump type based on the nature of the changes:

- **patch** (`npm run bump:patch`) — bug fixes, small tweaks, copy changes
Expand Down
36 changes: 23 additions & 13 deletions NATIVE_CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,29 +19,32 @@ Update this file whenever you add or modify native files.

Location: `android/app/src/main/java/com/pgarr/simplenotepad/widget/`

| File | Description |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `NoteListWidget.kt` | `AppWidgetProvider` — entry point, called by Android on widget add/update. Reads the latest list from SQLite and binds it to the `ListView` via `RemoteViewsService`. |
| `NoteListWidgetService.kt` | `RemoteViewsService` — Android requires a bound service to supply list row views to a widget `ListView`. Instantiates `NoteListRemoteViewsFactory`. |
| `NoteListRemoteViewsFactory.kt` | `RemoteViewsFactory` — builds each list row `RemoteViews`. Sets the checkbox icon (on/off), text, opacity for completed items, and attaches a fill-in `Intent` to each row for tap handling. |
| `WidgetDbHelper.kt` | Opens the app's SQLite database directly using Android's `SQLiteDatabase` API (not expo-sqlite). Provides `getLatestList()` and `toggleItem()`. Parses and writes the `note` column JSON in a format exactly matching the JS `parseListItems` / `stringifyListItems` functions in `db.ts`. |
| `WidgetUpdateReceiver.kt` | `BroadcastReceiver` — receives checkbox tap broadcasts, calls `WidgetDbHelper.toggleItem()`, then calls `notifyAppWidgetViewDataChanged` to re-render the widget list. |
| File | Description |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `NoteListWidget.kt` | `AppWidgetProvider` — entry point, called by Android on widget add/update. Resolves which list to show (via `WidgetDbHelper.resolveList`), binds it to the `ListView`, and sets up the title click (opens the list in the app) and chevron click (opens `WidgetListSelectActivity`). Cleans up prefs on widget delete. |
| `NoteListWidgetService.kt` | `RemoteViewsService` — Android requires a bound service to supply list row views to a widget `ListView`. Instantiates `NoteListRemoteViewsFactory`. |
| `NoteListRemoteViewsFactory.kt` | `RemoteViewsFactory` — builds each list row `RemoteViews`. Sets the checkbox icon (on/off), text, opacity for completed items, and attaches a fill-in `Intent` to each row for tap handling. |
| `WidgetDbHelper.kt` | Opens the app's SQLite database directly using Android's `SQLiteDatabase` API (not expo-sqlite). Provides `getLatestList()`, `getAllLists()`, `getListById()`, `resolveList()`, and `toggleItem()`. Parses and writes the `note` column JSON matching the JS `parseListItems` / `stringifyListItems` functions. |
| `WidgetPrefs.kt` | SharedPreferences helper. Persists the selected list ID per widget instance using the key `"selected_list_<widgetId>"` in the `"widget_prefs"` file. A value of `-1` means "no explicit selection; use latest". |
| `WidgetListSelectActivity.kt` | Dialog-themed `AppCompatActivity` launched by tapping the chevron icon in the widget header. Queries all lists via `WidgetDbHelper.getAllLists()`, shows an `AlertDialog` list picker, saves the selection to `WidgetPrefs`, then refreshes the widget. |
| `WidgetUpdateReceiver.kt` | `BroadcastReceiver` — receives checkbox tap broadcasts, calls `WidgetDbHelper.toggleItem()`, then calls `notifyAppWidgetViewDataChanged` to re-render the widget list. |

#### Resource files

Location: `android/app/src/main/res/`

| File | Description |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `layout/widget_note_list.xml` | Root widget layout. Contains a `TextView` for the list title and a `ListView` for the items. |
| File | Description |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `layout/widget_note_list.xml` | Root widget layout. Contains a horizontal header row (list title `TextView` + chevron `ImageView` for the list picker) and a `ListView` for the items. |
| `drawable/widget_chevron_down.xml` | Lucide-style chevron-down vector icon (16×16dp) used in the widget header to indicate the list selector. |
| `layout/widget_list_item.xml` | Single row layout. Contains an `ImageView` acting as a checkbox (swapped between `checkbox_on_background` / `checkbox_off_background` drawables) and a `TextView` for item text. Note: real `CheckBox` views cannot be used interactively in `RemoteViews`. |
| `xml/note_list_widget_info.xml` | `AppWidgetProviderInfo` — declares widget minimum size (250×180dp), resize behaviour, update interval, and initial layout. |

### AndroidManifest.xml modifications

File: `android/app/src/main/AndroidManifest.xml`

Three entries added inside `<application>`:
Four entries added inside `<application>`:

```xml
<!-- Widget provider -->
Expand Down Expand Up @@ -70,6 +73,13 @@ Three entries added inside `<application>`:
android:name=".widget.NoteListWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS"
android:exported="false" />

<!-- Dialog activity for picking which list the widget displays -->
<activity
android:name=".widget.WidgetListSelectActivity"
android:theme="@style/Theme.Widget.ListSelect"
android:exported="false"
android:excludeFromRecents="true" />
```

---
Expand All @@ -83,7 +93,7 @@ The widget reads from the same database and table as the main app.
| Database file | `notes.db` — **must match** the string passed to `openDatabaseAsync()` in JS. Defined in `WidgetDbHelper.kt` as `DB_NAME`. |
| Table | `content` |
| Relevant columns | `id`, `title`, `note` (JSON string), `type` (0 = note, 1 = list) |
| Widget reads | `SELECT * FROM content WHERE type = 1 ORDER BY id DESC LIMIT 1` |
| Widget reads | `SELECT * FROM content WHERE type = 1 ORDER BY id DESC LIMIT 1` (latest/fallback); `SELECT ... WHERE id = ?` (selected) |
| Widget writes | `UPDATE content SET note = ? WHERE id = ? AND type = 1` |

The JSON format of the `note` column for lists is an array of `{ "checked": boolean, "text": string }` objects, matching `parseListItems` / `stringifyListItems` in `db.ts`.
Expand All @@ -94,7 +104,7 @@ The JSON format of the `note` column for lists is an array of `{ "checked": bool

- **Stale app state:** If the user taps a checkbox in the widget while the app is open on the same list, the app's in-memory state will be stale until it re-fetches. Add a re-fetch in your list screen's `useEffect` on `AppState` change event to handle this.
- **Concurrent writes:** The widget and the app both write to the same SQLite file. SQLite WAL mode (enabled in migrations) handles concurrent reads safely, but avoid triggering widget updates and app writes simultaneously. In practice this is unlikely for a notepad app.
- **Which list is shown:** Currently the widget always shows the list with the highest `id`. There is no per-widget configuration (pinning a specific list). This could be added via `AppWidgetConfigureActivity` in a future iteration.
- **Per-widget list selection:** The chevron icon in the widget header opens a list picker. The selection is stored in `SharedPreferences` (`"widget_prefs"` file, key `"selected_list_<widgetId>"`). If the selected list is later deleted from the app, the widget automatically falls back to the most recently created list and clears the stale preference.
- **Update interval:** Set to 30 minutes (`updatePeriodMillis="1800000"`) in `note_list_widget_info.xml`. Android batches and throttles this — do not rely on it for real-time updates. The widget updates immediately on checkbox tap via the broadcast receiver.

---
Expand Down
4 changes: 2 additions & 2 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ android {
applicationId 'com.pgarr.simplenotepad'
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 21
versionName "1.4.4"
versionCode 22
versionName "1.5.0"

buildConfigField "String", "REACT_NATIVE_RELEASE_LEVEL", "\"${findProperty('reactNativeReleaseLevel') ?: 'stable'}\""
}
Expand Down
5 changes: 5 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,10 @@
</intent-filter>
</receiver>
<service android:name=".widget.NoteListWidgetService" android:permission="android.permission.BIND_REMOTEVIEWS" android:exported="false"/>
<activity
android:name=".widget.WidgetListSelectActivity"
android:theme="@style/Theme.Widget.ListSelect"
android:exported="false"
android:excludeFromRecents="true" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,17 @@ import com.pgarr.simplenotepad.R

class NoteListRemoteViewsFactory(
private val context: Context,
@Suppress("UNUSED_PARAMETER") private val listId: Int
private val widgetId: Int
) : RemoteViewsService.RemoteViewsFactory {

private var items: List<WidgetListItem> = emptyList()
private var listTitle: String = ""
/** Id of the list rows are from — always aligned with [onDataSetChanged] (latest list). */
private var boundListId: Int = -1

override fun onCreate() {}

override fun onDataSetChanged() {
val list = WidgetDbHelper.getLatestList(context)
val list = WidgetDbHelper.resolveList(context, widgetId)
if (list == null) {
items = emptyList()
listTitle = ""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ class NoteListWidget : AppWidgetProvider() {
}
}

override fun onDeleted(context: Context, appWidgetIds: IntArray) {
super.onDeleted(context, appWidgetIds)
for (widgetId in appWidgetIds) {
WidgetPrefs.clearSelectedListId(context, widgetId)
}
}

companion object {
fun refreshAllWidgets(context: Context) {
val appWidgetManager = AppWidgetManager.getInstance(context)
Expand All @@ -42,15 +49,15 @@ class NoteListWidget : AppWidgetProvider() {
appWidgetManager: AppWidgetManager,
widgetId: Int
) {
val latestList = WidgetDbHelper.getLatestList(context)
val displayList = WidgetDbHelper.resolveList(context, widgetId)

val rv = RemoteViews(context.packageName, R.layout.widget_note_list)
rv.setTextViewText(R.id.widget_title, latestList?.title ?: "No lists")

rv.setTextViewText(R.id.widget_title, displayList?.title ?: "No lists")

val listDeepLink =
if (latestList != null) {
Uri.parse("simple-notepad:///list/${latestList.id}")
if (displayList != null) {
Uri.parse("simple-notepad:///list/${displayList.id}")
} else {
Uri.parse("simple-notepad:///")
}
Expand All @@ -59,7 +66,7 @@ class NoteListWidget : AppWidgetProvider() {
setClass(context, MainActivity::class.java)
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
val openListFlags =
val pendingIntentFlags =
PendingIntent.FLAG_UPDATE_CURRENT or
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE
Expand All @@ -71,12 +78,25 @@ class NoteListWidget : AppWidgetProvider() {
context,
widgetId + 10_000,
openListIntent,
openListFlags
pendingIntentFlags
)
rv.setOnClickPendingIntent(R.id.widget_title, openListPendingIntent)


val selectListIntent = Intent(context, WidgetListSelectActivity::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
data = Uri.parse("widget://select/$widgetId")
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val selectListPendingIntent = PendingIntent.getActivity(
context,
widgetId + 20_000,
selectListIntent,
pendingIntentFlags
)
rv.setOnClickPendingIntent(R.id.widget_dropdown_btn, selectListPendingIntent)

val serviceIntent = Intent(context, NoteListWidgetService::class.java).apply {
putExtra("list_id", latestList?.id ?: -1)
putExtra("widget_id", widgetId)
data = android.net.Uri.parse("widget://list/$widgetId")
}
rv.setRemoteAdapter(R.id.widget_list_view, serviceIntent)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
package com.pgarr.simplenotepad.widget

import android.appwidget.AppWidgetManager
import android.content.Intent
import android.widget.RemoteViewsService

class NoteListWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
val listId = intent.getIntExtra("list_id", -1)
return NoteListRemoteViewsFactory(applicationContext, listId)
val widgetId = intent.getIntExtra("widget_id", AppWidgetManager.INVALID_APPWIDGET_ID)
return NoteListRemoteViewsFactory(applicationContext, widgetId)
}
}
Loading
Loading