This commit is contained in:
1 parent
fe9e75a4e7
commit
bbfcbe74af
16 files changed
+920
-17
No files matched your search
@@ -9,7 +9,9 @@ android {
|
||||
|
||||
apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle"
|
||||
dependencies {
|
||||
|
||||
implementation project(':capacitor-community-text-to-speech')
|
||||
implementation project(':capacitor-app')
|
||||
implementation project(':capacitor-local-notifications')
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,22 @@
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/file_paths"></meta-data>
|
||||
</provider>
|
||||
|
||||
<!-- Ongoing focus timer (#1) -->
|
||||
<service
|
||||
android:name=".TimerService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="specialUse">
|
||||
<property
|
||||
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||
android:value="Ongoing focus timer keeping step countdown alive while the app is in background." />
|
||||
</service>
|
||||
</application>
|
||||
|
||||
<!-- Permissions -->
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
</manifest>
|
||||
@@ -1,5 +1,13 @@
|
||||
package app.mindlist.foss;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.getcapacitor.BridgeActivity;
|
||||
|
||||
public class MainActivity extends BridgeActivity {}
|
||||
public class MainActivity extends BridgeActivity {
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
registerPlugin(TimerPlugin.class);
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package app.mindlist.foss;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Build;
|
||||
|
||||
import com.getcapacitor.JSObject;
|
||||
import com.getcapacitor.Plugin;
|
||||
import com.getcapacitor.PluginCall;
|
||||
import com.getcapacitor.PluginMethod;
|
||||
import com.getcapacitor.annotation.CapacitorPlugin;
|
||||
|
||||
/** JS bridge for TimerService (#1). All methods are safe no-ops on web. */
|
||||
@CapacitorPlugin(name = "TimerService")
|
||||
public class TimerPlugin extends Plugin {
|
||||
|
||||
@PluginMethod
|
||||
public void start(PluginCall call) {
|
||||
String title = call.getString("title", "Mindlist focus");
|
||||
String text = call.getString("text", "");
|
||||
Context ctx = getContext();
|
||||
Intent i = new Intent(ctx, TimerService.class);
|
||||
i.putExtra(TimerService.EXTRA_TITLE, title);
|
||||
i.putExtra(TimerService.EXTRA_TEXT, text);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ctx.startForegroundService(i);
|
||||
} else {
|
||||
ctx.startService(i);
|
||||
}
|
||||
call.resolve(new JSObject());
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void update(PluginCall call) {
|
||||
String title = call.getString("title", "Mindlist focus");
|
||||
String text = call.getString("text", "");
|
||||
Context ctx = getContext();
|
||||
Intent i = new Intent(ctx, TimerService.class);
|
||||
i.setAction(TimerService.ACTION_UPDATE);
|
||||
i.putExtra(TimerService.EXTRA_TITLE, title);
|
||||
i.putExtra(TimerService.EXTRA_TEXT, text);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ctx.startForegroundService(i);
|
||||
} else {
|
||||
ctx.startService(i);
|
||||
}
|
||||
call.resolve(new JSObject());
|
||||
}
|
||||
|
||||
@PluginMethod
|
||||
public void stop(PluginCall call) {
|
||||
Context ctx = getContext();
|
||||
Intent i = new Intent(ctx, TimerService.class);
|
||||
i.setAction(TimerService.ACTION_STOP);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
ctx.startForegroundService(i);
|
||||
} else {
|
||||
ctx.startService(i);
|
||||
}
|
||||
call.resolve(new JSObject());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package app.mindlist.foss;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.os.Build;
|
||||
import android.os.IBinder;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
/**
|
||||
* Ongoing foreground service that keeps the focus timer alive while the
|
||||
* app is minimized or the phone is locked (#1). Shows a persistent
|
||||
* notification with the current step + remaining time.
|
||||
*/
|
||||
public class TimerService extends Service {
|
||||
public static final String CHANNEL_ID = "mindlist_timer";
|
||||
public static final int NOTIF_ID = 1001;
|
||||
public static final String EXTRA_TITLE = "title";
|
||||
public static final String EXTRA_TEXT = "text";
|
||||
public static final String ACTION_UPDATE = "app.mindlist.foss.UPDATE_TIMER";
|
||||
public static final String ACTION_STOP = "app.mindlist.foss.STOP_TIMER";
|
||||
|
||||
private String title = "Mindlist focus";
|
||||
private String text = "";
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
createChannel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (intent != null) {
|
||||
String action = intent.getAction();
|
||||
if (ACTION_STOP.equals(action)) {
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
if (intent.hasExtra(EXTRA_TITLE)) title = intent.getStringExtra(EXTRA_TITLE);
|
||||
if (intent.hasExtra(EXTRA_TEXT)) text = intent.getStringExtra(EXTRA_TEXT);
|
||||
}
|
||||
Notification notif = buildNotification();
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
startForeground(NOTIF_ID, notif, ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(NOTIF_ID, notif);
|
||||
} else {
|
||||
startForeground(NOTIF_ID, notif);
|
||||
}
|
||||
// NOT_STICKY: scheduled alarms (not the service) are the source of
|
||||
// truth — a kill-restarted zombie with stale text is worse than none.
|
||||
// Known gap: reboot drops pending alarms (no BOOT receiver yet).
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
private void createChannel() {
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm == null) return;
|
||||
NotificationChannel ch = new NotificationChannel(
|
||||
CHANNEL_ID, "Focus timer", NotificationManager.IMPORTANCE_LOW);
|
||||
ch.setDescription("Ongoing focus timer while steps run");
|
||||
ch.setSound(null, null);
|
||||
nm.createNotificationChannel(ch);
|
||||
}
|
||||
|
||||
private Notification buildNotification() {
|
||||
Intent open = new Intent(this, MainActivity.class);
|
||||
open.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
PendingIntent pi = PendingIntent.getActivity(
|
||||
this, 0, open, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
Intent stop = new Intent(this, TimerService.class);
|
||||
stop.setAction(ACTION_STOP);
|
||||
PendingIntent stopPi = PendingIntent.getService(
|
||||
this, 1, stop, PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
int icon = getResources().getIdentifier("ic_stat_timer", "drawable", getPackageName());
|
||||
if (icon == 0) icon = android.R.drawable.ic_dialog_info;
|
||||
|
||||
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setContentTitle(title)
|
||||
.setContentText(text)
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(text))
|
||||
.setSmallIcon(icon)
|
||||
.setContentIntent(pi)
|
||||
.addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop", stopPi)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp" android:height="24dp"
|
||||
android:viewportWidth="512" android:viewportHeight="512">
|
||||
<!-- monochrome silhouette of the v3 logo for status-bar icons -->
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M 256 124 A 132 132 0 1 0 360 180"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="36"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M 210 270 l 34 34 l 64 -76"
|
||||
android:strokeColor="#FFFFFF"
|
||||
android:strokeWidth="42"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" />
|
||||
</vector>
|
||||
@@ -1,3 +1,12 @@
|
||||
// DO NOT EDIT THIS FILE! IT IS GENERATED EACH TIME "capacitor update" IS RUN
|
||||
include ':capacitor-android'
|
||||
project(':capacitor-android').projectDir = new File('../node_modules/@capacitor/android/capacitor')
|
||||
|
||||
include ':capacitor-community-text-to-speech'
|
||||
project(':capacitor-community-text-to-speech').projectDir = new File('../node_modules/@capacitor-community/text-to-speech/android')
|
||||
|
||||
include ':capacitor-app'
|
||||
project(':capacitor-app').projectDir = new File('../node_modules/@capacitor/app/android')
|
||||
|
||||
include ':capacitor-local-notifications'
|
||||
project(':capacitor-local-notifications').projectDir = new File('../node_modules/@capacitor/local-notifications/android')
|
||||
Reference in new issue
Block a user