rn-persistent-timer 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +607 -0
- package/android/.gradle/7.4.2/checksums/checksums.lock +0 -0
- package/android/.gradle/7.4.2/fileChanges/last-build.bin +0 -0
- package/android/.gradle/7.4.2/fileHashes/fileHashes.lock +0 -0
- package/android/.gradle/7.4.2/gc.properties +0 -0
- package/android/.gradle/vcs-1/gc.properties +0 -0
- package/android/build.gradle +15 -0
- package/android/src/main/java/com/rnpersistenttimer/RNPersistentTimerModule.java +164 -0
- package/android/src/main/java/com/rnpersistenttimer/RNPersistentTimerPackage.java +27 -0
- package/android/src/main/java/com/rnpersistenttimer/TimerForegroundService.java +280 -0
- package/ios/RNPersistentTimer.h +10 -0
- package/ios/RNPersistentTimer.m +221 -0
- package/lib/commonjs/NativeTimerModule.js +46 -0
- package/lib/commonjs/NativeTimerModule.js.map +1 -0
- package/lib/commonjs/PersistentTimerManager.js +337 -0
- package/lib/commonjs/PersistentTimerManager.js.map +1 -0
- package/lib/commonjs/index.js +76 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/usePersistentTimer.js +159 -0
- package/lib/commonjs/usePersistentTimer.js.map +1 -0
- package/lib/commonjs/utils.js +112 -0
- package/lib/commonjs/utils.js.map +1 -0
- package/lib/module/NativeTimerModule.js +40 -0
- package/lib/module/NativeTimerModule.js.map +1 -0
- package/lib/module/PersistentTimerManager.js +329 -0
- package/lib/module/PersistentTimerManager.js.map +1 -0
- package/lib/module/index.js +17 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/usePersistentTimer.js +153 -0
- package/lib/module/usePersistentTimer.js.map +1 -0
- package/lib/module/utils.js +100 -0
- package/lib/module/utils.js.map +1 -0
- package/lib/typescript/NativeTimerModule.d.ts +31 -0
- package/lib/typescript/NativeTimerModule.d.ts.map +1 -0
- package/lib/typescript/PersistentTimerManager.d.ts +37 -0
- package/lib/typescript/PersistentTimerManager.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +7 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +167 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/lib/typescript/usePersistentTimer.d.ts +16 -0
- package/lib/typescript/usePersistentTimer.d.ts.map +1 -0
- package/lib/typescript/utils.d.ts +36 -0
- package/lib/typescript/utils.d.ts.map +1 -0
- package/package.json +98 -0
- package/src/NativeTimerModule.ts +73 -0
- package/src/PersistentTimerManager.ts +410 -0
- package/src/index.ts +41 -0
- package/src/types.ts +198 -0
- package/src/usePersistentTimer.tsx +173 -0
- package/src/utils.ts +91 -0
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Android: RNPersistentTimerModule.java
|
|
2
|
+
// Exposes native methods to JS and manages the ForegroundService
|
|
3
|
+
|
|
4
|
+
package com.rnpersistenttimer;
|
|
5
|
+
|
|
6
|
+
import android.content.Intent;
|
|
7
|
+
import android.os.Build;
|
|
8
|
+
|
|
9
|
+
import androidx.annotation.NonNull;
|
|
10
|
+
|
|
11
|
+
import com.facebook.react.bridge.Arguments;
|
|
12
|
+
import com.facebook.react.bridge.Promise;
|
|
13
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
14
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
15
|
+
import com.facebook.react.bridge.ReactMethod;
|
|
16
|
+
import com.facebook.react.bridge.ReadableMap;
|
|
17
|
+
import com.facebook.react.bridge.WritableArray;
|
|
18
|
+
import com.facebook.react.bridge.WritableMap;
|
|
19
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule;
|
|
20
|
+
|
|
21
|
+
import java.util.ArrayList;
|
|
22
|
+
import java.util.HashMap;
|
|
23
|
+
import java.util.List;
|
|
24
|
+
import java.util.Map;
|
|
25
|
+
|
|
26
|
+
public class RNPersistentTimerModule extends ReactContextBaseJavaModule {
|
|
27
|
+
|
|
28
|
+
public static final String MODULE_NAME = "RNPersistentTimer";
|
|
29
|
+
private static ReactApplicationContext reactContext;
|
|
30
|
+
|
|
31
|
+
// Active timers map: timerId -> start timestamp (ms)
|
|
32
|
+
private static final Map<String, Long> activeTimers = new HashMap<>();
|
|
33
|
+
private static final Map<String, Long> elapsedAtPause = new HashMap<>();
|
|
34
|
+
|
|
35
|
+
public RNPersistentTimerModule(ReactApplicationContext context) {
|
|
36
|
+
super(context);
|
|
37
|
+
reactContext = context;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@NonNull
|
|
41
|
+
@Override
|
|
42
|
+
public String getName() {
|
|
43
|
+
return MODULE_NAME;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── JS-Exposed Methods ────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
@ReactMethod
|
|
49
|
+
public void startTimer(ReadableMap options, Promise promise) {
|
|
50
|
+
try {
|
|
51
|
+
String timerId = options.getString("timerId");
|
|
52
|
+
String mode = options.hasKey("mode") ? options.getString("mode") : "stopwatch";
|
|
53
|
+
int duration = options.hasKey("duration") ? options.getInt("duration") : 0;
|
|
54
|
+
int elapsed = options.hasKey("elapsed") ? options.getInt("elapsed") : 0;
|
|
55
|
+
long startedAt = options.hasKey("startedAt") ? (long) options.getDouble("startedAt") : System.currentTimeMillis();
|
|
56
|
+
boolean showNotif = !options.hasKey("showNotification") || options.getBoolean("showNotification");
|
|
57
|
+
|
|
58
|
+
// Record start
|
|
59
|
+
activeTimers.put(timerId, startedAt - (elapsed * 1000L));
|
|
60
|
+
|
|
61
|
+
// Start foreground service
|
|
62
|
+
Intent serviceIntent = new Intent(reactContext, TimerForegroundService.class);
|
|
63
|
+
serviceIntent.putExtra("timerId", timerId);
|
|
64
|
+
serviceIntent.putExtra("mode", mode);
|
|
65
|
+
serviceIntent.putExtra("duration", duration);
|
|
66
|
+
serviceIntent.putExtra("elapsed", elapsed);
|
|
67
|
+
serviceIntent.putExtra("startedAt", startedAt);
|
|
68
|
+
serviceIntent.putExtra("showNotification", showNotif);
|
|
69
|
+
serviceIntent.setAction(TimerForegroundService.ACTION_START);
|
|
70
|
+
|
|
71
|
+
// Notification extras
|
|
72
|
+
if (options.hasKey("notification")) {
|
|
73
|
+
ReadableMap notif = options.getMap("notification");
|
|
74
|
+
if (notif != null) {
|
|
75
|
+
serviceIntent.putExtra("notif_title", notif.hasKey("title") ? notif.getString("title") : "Timer Running");
|
|
76
|
+
serviceIntent.putExtra("notif_body", notif.hasKey("body") ? notif.getString("body") : "{time}");
|
|
77
|
+
serviceIntent.putExtra("notif_color", notif.hasKey("color") ? notif.getString("color") : "#2196F3");
|
|
78
|
+
serviceIntent.putExtra("notif_icon", notif.hasKey("icon") ? notif.getString("icon") : "ic_notification");
|
|
79
|
+
serviceIntent.putExtra("notif_actions", notif.hasKey("showActions") && notif.getBoolean("showActions"));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
84
|
+
reactContext.startForegroundService(serviceIntent);
|
|
85
|
+
} else {
|
|
86
|
+
reactContext.startService(serviceIntent);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
promise.resolve(null);
|
|
90
|
+
} catch (Exception e) {
|
|
91
|
+
promise.reject("TIMER_START_ERROR", e.getMessage(), e);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@ReactMethod
|
|
96
|
+
public void stopTimer(String timerId, Promise promise) {
|
|
97
|
+
try {
|
|
98
|
+
activeTimers.remove(timerId);
|
|
99
|
+
elapsedAtPause.remove(timerId);
|
|
100
|
+
|
|
101
|
+
Intent serviceIntent = new Intent(reactContext, TimerForegroundService.class);
|
|
102
|
+
serviceIntent.setAction(TimerForegroundService.ACTION_STOP);
|
|
103
|
+
serviceIntent.putExtra("timerId", timerId);
|
|
104
|
+
reactContext.startService(serviceIntent);
|
|
105
|
+
|
|
106
|
+
promise.resolve(null);
|
|
107
|
+
} catch (Exception e) {
|
|
108
|
+
promise.reject("TIMER_STOP_ERROR", e.getMessage(), e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@ReactMethod
|
|
113
|
+
public void getElapsed(String timerId, Promise promise) {
|
|
114
|
+
Long startedAt = activeTimers.get(timerId);
|
|
115
|
+
if (startedAt == null) {
|
|
116
|
+
Long paused = elapsedAtPause.get(timerId);
|
|
117
|
+
promise.resolve(paused != null ? paused.intValue() : 0);
|
|
118
|
+
} else {
|
|
119
|
+
long elapsed = (System.currentTimeMillis() - startedAt) / 1000;
|
|
120
|
+
promise.resolve((int) elapsed);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
@ReactMethod
|
|
125
|
+
public void getActiveTimers(Promise promise) {
|
|
126
|
+
WritableArray arr = Arguments.createArray();
|
|
127
|
+
for (String id : activeTimers.keySet()) {
|
|
128
|
+
arr.pushString(id);
|
|
129
|
+
}
|
|
130
|
+
promise.resolve(arr);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
@ReactMethod
|
|
134
|
+
public void cancelAll(Promise promise) {
|
|
135
|
+
activeTimers.clear();
|
|
136
|
+
elapsedAtPause.clear();
|
|
137
|
+
Intent serviceIntent = new Intent(reactContext, TimerForegroundService.class);
|
|
138
|
+
serviceIntent.setAction(TimerForegroundService.ACTION_CANCEL_ALL);
|
|
139
|
+
reactContext.startService(serviceIntent);
|
|
140
|
+
promise.resolve(null);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@ReactMethod
|
|
144
|
+
public void isKilledStateSupported(Promise promise) {
|
|
145
|
+
// Android supports killed-state via WorkManager + AsyncStorage restore
|
|
146
|
+
promise.resolve(true);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ─── Event Emitter to JS ─────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
public static void sendEvent(String eventName, WritableMap params) {
|
|
152
|
+
if (reactContext != null && reactContext.hasActiveReactInstance()) {
|
|
153
|
+
reactContext
|
|
154
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class)
|
|
155
|
+
.emit(eventName, params);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Required for NativeEventEmitter
|
|
160
|
+
@ReactMethod
|
|
161
|
+
public void addListener(String eventName) {}
|
|
162
|
+
@ReactMethod
|
|
163
|
+
public void removeListeners(Integer count) {}
|
|
164
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// RNPersistentTimerPackage.java — React Native Package Registration
|
|
2
|
+
|
|
3
|
+
package com.rnpersistenttimer;
|
|
4
|
+
|
|
5
|
+
import com.facebook.react.ReactPackage;
|
|
6
|
+
import com.facebook.react.bridge.NativeModule;
|
|
7
|
+
import com.facebook.react.bridge.ReactApplicationContext;
|
|
8
|
+
import com.facebook.react.uimanager.ViewManager;
|
|
9
|
+
|
|
10
|
+
import java.util.ArrayList;
|
|
11
|
+
import java.util.Collections;
|
|
12
|
+
import java.util.List;
|
|
13
|
+
|
|
14
|
+
public class RNPersistentTimerPackage implements ReactPackage {
|
|
15
|
+
|
|
16
|
+
@Override
|
|
17
|
+
public List<NativeModule> createNativeModules(ReactApplicationContext reactContext) {
|
|
18
|
+
List<NativeModule> modules = new ArrayList<>();
|
|
19
|
+
modules.add(new RNPersistentTimerModule(reactContext));
|
|
20
|
+
return modules;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
@Override
|
|
24
|
+
public List<ViewManager> createViewManagers(ReactApplicationContext reactContext) {
|
|
25
|
+
return Collections.emptyList();
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// Android: TimerForegroundService.java
|
|
2
|
+
// Keeps the timer alive in background with a persistent notification
|
|
3
|
+
|
|
4
|
+
package com.rnpersistenttimer;
|
|
5
|
+
|
|
6
|
+
import android.app.Notification;
|
|
7
|
+
import android.app.NotificationChannel;
|
|
8
|
+
import android.app.NotificationManager;
|
|
9
|
+
import android.app.PendingIntent;
|
|
10
|
+
import android.app.Service;
|
|
11
|
+
import android.content.Intent;
|
|
12
|
+
import android.graphics.Color;
|
|
13
|
+
import android.os.Build;
|
|
14
|
+
import android.os.Handler;
|
|
15
|
+
import android.os.IBinder;
|
|
16
|
+
import android.os.Looper;
|
|
17
|
+
|
|
18
|
+
import androidx.annotation.Nullable;
|
|
19
|
+
import androidx.core.app.NotificationCompat;
|
|
20
|
+
|
|
21
|
+
import com.facebook.react.bridge.Arguments;
|
|
22
|
+
import com.facebook.react.bridge.WritableMap;
|
|
23
|
+
|
|
24
|
+
import java.util.HashMap;
|
|
25
|
+
import java.util.Map;
|
|
26
|
+
import java.util.Locale;
|
|
27
|
+
|
|
28
|
+
public class TimerForegroundService extends Service {
|
|
29
|
+
|
|
30
|
+
public static final String ACTION_START = "ACTION_START";
|
|
31
|
+
public static final String ACTION_STOP = "ACTION_STOP";
|
|
32
|
+
public static final String ACTION_CANCEL_ALL = "ACTION_CANCEL_ALL";
|
|
33
|
+
public static final String ACTION_PAUSE = "ACTION_PAUSE";
|
|
34
|
+
public static final String ACTION_RESUME = "ACTION_RESUME";
|
|
35
|
+
|
|
36
|
+
private static final String CHANNEL_ID = "rn_persistent_timer_channel";
|
|
37
|
+
private static final String CHANNEL_NAME = "Timer Notifications";
|
|
38
|
+
private static final int NOTIF_ID = 7771;
|
|
39
|
+
|
|
40
|
+
// Active timers: timerId -> startedAtMs (adjusted for elapsed)
|
|
41
|
+
private final Map<String, Long> timerStartTimes = new HashMap<>();
|
|
42
|
+
private final Map<String, Integer> timerDurations = new HashMap<>();
|
|
43
|
+
private final Map<String, String> timerModes = new HashMap<>();
|
|
44
|
+
|
|
45
|
+
private Handler handler;
|
|
46
|
+
private Runnable tickRunnable;
|
|
47
|
+
private boolean running = false;
|
|
48
|
+
|
|
49
|
+
// Notification config (last started timer drives the notification)
|
|
50
|
+
private String notifTitle = "Timer Running";
|
|
51
|
+
private String notifBody = "{time}";
|
|
52
|
+
private String notifColor = "#2196F3";
|
|
53
|
+
private boolean showActions = true;
|
|
54
|
+
|
|
55
|
+
@Override
|
|
56
|
+
public void onCreate() {
|
|
57
|
+
super.onCreate();
|
|
58
|
+
handler = new Handler(Looper.getMainLooper());
|
|
59
|
+
createNotificationChannel();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@Override
|
|
63
|
+
public int onStartCommand(Intent intent, int flags, int startId) {
|
|
64
|
+
if (intent == null) return START_STICKY;
|
|
65
|
+
|
|
66
|
+
String action = intent.getAction();
|
|
67
|
+
if (action == null) return START_STICKY;
|
|
68
|
+
|
|
69
|
+
switch (action) {
|
|
70
|
+
case ACTION_START:
|
|
71
|
+
handleStart(intent);
|
|
72
|
+
break;
|
|
73
|
+
case ACTION_STOP:
|
|
74
|
+
handleStop(intent.getStringExtra("timerId"));
|
|
75
|
+
break;
|
|
76
|
+
case ACTION_CANCEL_ALL:
|
|
77
|
+
handleCancelAll();
|
|
78
|
+
break;
|
|
79
|
+
case ACTION_PAUSE:
|
|
80
|
+
// handled from notification button
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
return START_STICKY; // Restart if system kills service
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
private void handleStart(Intent intent) {
|
|
87
|
+
String timerId = intent.getStringExtra("timerId");
|
|
88
|
+
String mode = intent.getStringExtra("mode");
|
|
89
|
+
int elapsed = intent.getIntExtra("elapsed", 0);
|
|
90
|
+
long startedAt = intent.getLongExtra("startedAt", System.currentTimeMillis());
|
|
91
|
+
int duration = intent.getIntExtra("duration", 0);
|
|
92
|
+
boolean showNotif = intent.getBooleanExtra("showNotification", true);
|
|
93
|
+
|
|
94
|
+
notifTitle = intent.getStringExtra("notif_title") != null ? intent.getStringExtra("notif_title") : notifTitle;
|
|
95
|
+
notifBody = intent.getStringExtra("notif_body") != null ? intent.getStringExtra("notif_body") : notifBody;
|
|
96
|
+
notifColor = intent.getStringExtra("notif_color") != null ? intent.getStringExtra("notif_color") : notifColor;
|
|
97
|
+
showActions = intent.getBooleanExtra("notif_actions", true);
|
|
98
|
+
|
|
99
|
+
// Adjust startedAt for already-elapsed seconds
|
|
100
|
+
long adjustedStart = startedAt - ((long) elapsed * 1000);
|
|
101
|
+
timerStartTimes.put(timerId, adjustedStart);
|
|
102
|
+
timerModes.put(timerId, mode != null ? mode : "stopwatch");
|
|
103
|
+
timerDurations.put(timerId, duration);
|
|
104
|
+
|
|
105
|
+
if (!running) {
|
|
106
|
+
running = true;
|
|
107
|
+
Notification notification = showNotif ? buildNotification("00:00:00") : buildSilentNotification();
|
|
108
|
+
startForeground(NOTIF_ID, notification);
|
|
109
|
+
startTicking();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
private void handleStop(String timerId) {
|
|
114
|
+
if (timerId == null) return;
|
|
115
|
+
timerStartTimes.remove(timerId);
|
|
116
|
+
timerModes.remove(timerId);
|
|
117
|
+
timerDurations.remove(timerId);
|
|
118
|
+
|
|
119
|
+
if (timerStartTimes.isEmpty()) {
|
|
120
|
+
stopTicking();
|
|
121
|
+
stopForeground(true);
|
|
122
|
+
stopSelf();
|
|
123
|
+
running = false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private void handleCancelAll() {
|
|
128
|
+
timerStartTimes.clear();
|
|
129
|
+
timerModes.clear();
|
|
130
|
+
timerDurations.clear();
|
|
131
|
+
stopTicking();
|
|
132
|
+
stopForeground(true);
|
|
133
|
+
stopSelf();
|
|
134
|
+
running = false;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private void startTicking() {
|
|
138
|
+
tickRunnable = new Runnable() {
|
|
139
|
+
@Override
|
|
140
|
+
public void run() {
|
|
141
|
+
if (!running) return;
|
|
142
|
+
|
|
143
|
+
long now = System.currentTimeMillis();
|
|
144
|
+
String displayTime = "00:00:00";
|
|
145
|
+
|
|
146
|
+
for (Map.Entry<String, Long> entry : timerStartTimes.entrySet()) {
|
|
147
|
+
String timerId = entry.getKey();
|
|
148
|
+
long startedAt = entry.getValue();
|
|
149
|
+
String mode = timerModes.getOrDefault(timerId, "stopwatch");
|
|
150
|
+
int duration = timerDurations.getOrDefault(timerId, 0);
|
|
151
|
+
|
|
152
|
+
long elapsedSeconds = (now - startedAt) / 1000;
|
|
153
|
+
|
|
154
|
+
int displaySeconds;
|
|
155
|
+
if ("countdown".equals(mode)) {
|
|
156
|
+
displaySeconds = (int) Math.max(0, duration - elapsedSeconds);
|
|
157
|
+
if (displaySeconds == 0) {
|
|
158
|
+
// Timer completed
|
|
159
|
+
WritableMap params = Arguments.createMap();
|
|
160
|
+
params.putString("timerId", timerId);
|
|
161
|
+
RNPersistentTimerModule.sendEvent("RNPersistentTimer_complete", params);
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
displaySeconds = (int) elapsedSeconds;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
displayTime = formatSeconds(displaySeconds);
|
|
168
|
+
|
|
169
|
+
// Emit tick to JS
|
|
170
|
+
WritableMap params = Arguments.createMap();
|
|
171
|
+
params.putString("timerId", timerId);
|
|
172
|
+
params.putInt("elapsed", (int) elapsedSeconds);
|
|
173
|
+
params.putInt("remaining", "countdown".equals(mode) ? (int) Math.max(0, duration - elapsedSeconds) : -1);
|
|
174
|
+
RNPersistentTimerModule.sendEvent("RNPersistentTimer_tick", params);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Update notification with last timer's display time
|
|
178
|
+
updateNotification(displayTime);
|
|
179
|
+
|
|
180
|
+
handler.postDelayed(this, 1000);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
handler.post(tickRunnable);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private void stopTicking() {
|
|
187
|
+
if (tickRunnable != null) {
|
|
188
|
+
handler.removeCallbacks(tickRunnable);
|
|
189
|
+
tickRunnable = null;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ─── Notification ─────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
private void createNotificationChannel() {
|
|
196
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
197
|
+
NotificationChannel channel = new NotificationChannel(
|
|
198
|
+
CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_LOW
|
|
199
|
+
);
|
|
200
|
+
channel.setDescription("Persistent timer notifications");
|
|
201
|
+
channel.setSound(null, null);
|
|
202
|
+
NotificationManager nm = getSystemService(NotificationManager.class);
|
|
203
|
+
if (nm != null) nm.createNotificationChannel(channel);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private Notification buildNotification(String timeStr) {
|
|
208
|
+
Intent openApp = getPackageManager().getLaunchIntentForPackage(getPackageName());
|
|
209
|
+
PendingIntent pendingIntent = PendingIntent.getActivity(
|
|
210
|
+
this, 0, openApp,
|
|
211
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
|
212
|
+
? PendingIntent.FLAG_IMMUTABLE : 0
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
int colorInt;
|
|
216
|
+
try { colorInt = Color.parseColor(notifColor); }
|
|
217
|
+
catch (Exception e) { colorInt = Color.parseColor("#2196F3"); }
|
|
218
|
+
|
|
219
|
+
String body = notifBody.replace("{time}", timeStr);
|
|
220
|
+
|
|
221
|
+
NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
|
|
222
|
+
.setContentTitle(notifTitle)
|
|
223
|
+
.setContentText(body)
|
|
224
|
+
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
|
225
|
+
.setColor(colorInt)
|
|
226
|
+
.setOngoing(true)
|
|
227
|
+
.setOnlyAlertOnce(true)
|
|
228
|
+
.setSilent(true)
|
|
229
|
+
.setContentIntent(pendingIntent);
|
|
230
|
+
|
|
231
|
+
if (showActions) {
|
|
232
|
+
Intent pauseIntent = new Intent(this, TimerForegroundService.class);
|
|
233
|
+
pauseIntent.setAction(ACTION_PAUSE);
|
|
234
|
+
PendingIntent pausePending = PendingIntent.getService(
|
|
235
|
+
this, 1, pauseIntent,
|
|
236
|
+
Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
|
|
237
|
+
? PendingIntent.FLAG_IMMUTABLE : 0
|
|
238
|
+
);
|
|
239
|
+
builder.addAction(android.R.drawable.ic_media_pause, "Pause", pausePending);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return builder.build();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
private Notification buildSilentNotification() {
|
|
246
|
+
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
|
247
|
+
.setContentTitle("Timer")
|
|
248
|
+
.setSmallIcon(android.R.drawable.ic_dialog_info)
|
|
249
|
+
.setOngoing(true)
|
|
250
|
+
.setSilent(true)
|
|
251
|
+
.setPriority(NotificationCompat.PRIORITY_MIN)
|
|
252
|
+
.build();
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
private void updateNotification(String timeStr) {
|
|
256
|
+
Notification notification = buildNotification(timeStr);
|
|
257
|
+
NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
|
|
258
|
+
if (nm != null) nm.notify(NOTIF_ID, notification);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private static String formatSeconds(int totalSeconds) {
|
|
262
|
+
int h = totalSeconds / 3600;
|
|
263
|
+
int m = (totalSeconds % 3600) / 60;
|
|
264
|
+
int s = totalSeconds % 60;
|
|
265
|
+
return String.format(Locale.US, "%02d:%02d:%02d", h, m, s);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
@Nullable
|
|
269
|
+
@Override
|
|
270
|
+
public IBinder onBind(Intent intent) {
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
@Override
|
|
275
|
+
public void onDestroy() {
|
|
276
|
+
stopTicking();
|
|
277
|
+
running = false;
|
|
278
|
+
super.onDestroy();
|
|
279
|
+
}
|
|
280
|
+
}
|