Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
package net.cozic.joplin.widgets.recents;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.widget.RemoteViews;
import android.widget.RemoteViewsService;

import net.cozic.joplin.R;

import java.util.List;

public class RecentsWidgetService extends RemoteViewsService {
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new RecentsWidgetDataViewsFactory(getApplicationContext(), intent);
}

private static class RecentsWidgetDataViewsFactory implements RemoteViewsService.RemoteViewsFactory {
private List<RecentsWidgetData.NoteItem> notes;
private Context context;
private RecentsWidgetData recentsWidgetData;

public RecentsWidgetDataViewsFactory(Context context, Intent intent) {
this.context = context;
recentsWidgetData = new RecentsWidgetData(context);
}

@Override
public void onCreate() {
notes = recentsWidgetData.readRecents();
}

@Override
public void onDataSetChanged() {
notes = recentsWidgetData.readRecents();
}

@Override
public RemoteViews getViewAt(int position) {
RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.recents_widget_item);
rv.setTextViewText(R.id.recents_widget_item, notes.get(position).getTitle());
rv.setOnClickFillInIntent(R.id.recents_widget_item, fillInIntent(notes.get(position).getId()));
return rv;
}

private Intent fillInIntent(String noteId) {
Bundle extras = new Bundle();
extras.putString(RecentsWidgetProvider.NOTE_ID, noteId);
Intent intent = new Intent();
intent.putExtras(extras);
return intent;
}

@Override
public void onDestroy() {
notes.clear();
}

@Override
public int getCount() {
return notes.size();
}

@Override
public RemoteViews getLoadingView() {
return null;
}

@Override
public int getViewTypeCount() {
return 1;
}

@Override
public boolean hasStableIds() {
return true;
}

@Override
public long getItemId(int position) {
return notes.get(position).hashCode();
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?android:attr/colorBackground">

<ListView
android:id="@+id/list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:loopViews="true" />

<TextView
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:text="@string/no_recent_notes_found"
android:textColor="?android:attr/textColorPrimary"
android:textSize="@dimen/recents_title_text_size"
android:textStyle="bold" />
</FrameLayout>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/recents_widget_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorPrimary"
android:padding="@dimen/recents_title_padding"
android:textSize="@dimen/recents_title_text_size"
android:textStyle="bold" />
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<resources>
<dimen name="recents_title_text_size">16sp</dimen>
<dimen name="recents_title_padding">8dp</dimen>
</resources>
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<resources>
<string name="app_name">Joplin</string>
<string name="default_notification_channel_id">net.cozic.joplin.notification</string>
<string name="no_recent_notes_found">No recent notes found</string>
laurent22 marked this conversation as resolved.
Show resolved Hide resolved
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="40dp"
android:minHeight="40dp"
android:initialLayout="@layout/recents_widget"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen" />
13 changes: 12 additions & 1 deletion packages/app-mobile/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import setupQuickActions from './setupQuickActions';
import PluginAssetsLoader from './PluginAssetsLoader';
import AlarmService from '@joplin/lib/services/AlarmService';
import Alarm from '@joplin/lib/models/Alarm';
import eventManager from '@joplin/lib/eventManager';
import time from '@joplin/lib/time';
import Logger, { TargetType } from '@joplin/lib/Logger';
import BaseModel from '@joplin/lib/BaseModel';
import BaseModel, { ModelType } from '@joplin/lib/BaseModel';
import BaseService from '@joplin/lib/services/BaseService';
import ResourceService from '@joplin/lib/services/ResourceService';
import KvStore from '@joplin/lib/services/KvStore';
Expand All @@ -20,6 +21,7 @@ import PoorManIntervals from '@joplin/lib/PoorManIntervals';
import reducer from '@joplin/lib/reducer';
import ShareExtension from './utils/ShareExtension';
import handleShared from './utils/shareHandler';
import { updateRecentsWidgetWithDebounce } from './utils/WidgetUtils';
import uuid from '@joplin/lib/uuid';
import { loadKeychainServiceAndSettings } from '@joplin/lib/services/SettingUtils';
import KeychainServiceDriverMobile from '@joplin/lib/services/keychain/KeychainServiceDriver.mobile';
Expand Down Expand Up @@ -754,6 +756,8 @@ class AppComponent extends React.Component {

Linking.addEventListener('url', this.handleOpenURL_);

eventManager.on('itemChange', this.onItemChange_);

BackButtonService.initialize(this.backButtonHandler_);

AlarmService.setInAppNotificationHandler(async (alarmId: string) => {
Expand Down Expand Up @@ -810,6 +814,13 @@ class AppComponent extends React.Component {
return false;
}

private onItemChange_({ itemType }: {itemType: ModelType}) {
if (itemType !== ModelType.Note) {
return;
}
updateRecentsWidgetWithDebounce();
Copy link
Owner

Choose a reason for hiding this comment

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

Is there any way to detect if the widget is installed, and if it's not, to skip this call?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Seems the only way is to store the widget status in shared preferences and use that. However, I don't think this is a good idea:

  • It complicates the update flow, as the widget would have no data when first added, so we'd need to expose some way for the widget to trigger these queries and I'm not sure how to cleanly do that.
  • I'm not sure if we're saving much, if anything, since React would still need to use native code to read/write shared preferences just to check if a widget is active.

}

private async handleShareData() {
const sharedData = await ShareExtension.data();
if (sharedData) {
Expand Down
20 changes: 20 additions & 0 deletions packages/app-mobile/utils/RecentsWidget.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
const { NativeModules, Platform } = require('react-native');

export interface NoteItem {
id: string;
title: string;
}

interface WidgetData {
notes?: NoteItem[];
}

export const RecentsWidget = (Platform.OS === 'android' && NativeModules.RecentsWidget) ?
{
read: async (): Promise<WidgetData> => JSON.parse(await NativeModules.RecentsWidget.read()),
write: async (data: WidgetData) => NativeModules.RecentsWidget.write(JSON.stringify(data)),
} :
{
read: async (): Promise<WidgetData> => ({}),
write: async (_: WidgetData) => {},
};
32 changes: 32 additions & 0 deletions packages/app-mobile/utils/WidgetUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { RecentsWidget } from './RecentsWidget';
import Note from '@joplin/lib/models/Note';
import { reg } from '@joplin/lib/registry';
import shim from '@joplin/lib/shim';

const MAX_COUNT = 10;
const DEBOUNCE_TIMEOUT = 5000;

export async function updateRecentsWidget() {
reg.logger().info('updating recents widget');
const recents = await Note.all({
fields: ['id', 'title'],
order: [{ by: 'updated_time', dir: 'DESC' }],
limit: MAX_COUNT,
});
return RecentsWidget.write({
notes: recents,
});
}

let hasUpdateScheduled = false;

export function updateRecentsWidgetWithDebounce() {
if (hasUpdateScheduled) {
return;
}
hasUpdateScheduled = true;
shim.setTimeout(async () => {
await updateRecentsWidget();
hasUpdateScheduled = false;
}, DEBOUNCE_TIMEOUT);
}