cordova-plugin-local-notifications-continued 1.2.4

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.
Files changed (67) hide show
  1. package/.github/FUNDING.yml +1 -0
  2. package/.github/ISSUE_TEMPLATE/report_issue.yml +126 -0
  3. package/CHANGELOG.md +358 -0
  4. package/LICENSE +202 -0
  5. package/README.md +1531 -0
  6. package/images/android-actions-with-input-clicked.png +0 -0
  7. package/images/android-actions-with-input-not-clicked.png +0 -0
  8. package/images/android-actions.png +0 -0
  9. package/images/android-alarms-and-reminders-in-app-settings.png +0 -0
  10. package/images/android-alarms-and-reminders-setting.png +0 -0
  11. package/images/android-app-hibernation-notification.png +0 -0
  12. package/images/android-app-hibernation-settings-android-12.png +0 -0
  13. package/images/android-app-hibernation-settings-android-13-14.png +0 -0
  14. package/images/android-app-hibernation-settings-android-15.png +0 -0
  15. package/images/android-attachments-image-folded.png +0 -0
  16. package/images/android-attachments-image-unfolded.png +0 -0
  17. package/images/android-chat.png +0 -0
  18. package/images/android-icon.svg +19 -0
  19. package/images/android-inbox.png +0 -0
  20. package/images/android-notification-example.png +0 -0
  21. package/images/android-progress.png +0 -0
  22. package/images/android-request-permission.png +0 -0
  23. package/images/android-stack.png +0 -0
  24. package/images/apple-icon.svg +1 -0
  25. package/images/cordova-app-notified-logo.png +0 -0
  26. package/images/ios-actions-with-input.png +0 -0
  27. package/images/ios-actions.png +0 -0
  28. package/images/ios-attachments-image-folded.png +0 -0
  29. package/images/ios-attachments-image-unfolded.png +0 -0
  30. package/images/ios-notification.png +0 -0
  31. package/images/ios-request-permission.png +0 -0
  32. package/images/logo.png +0 -0
  33. package/package.json +53 -0
  34. package/plugin.xml +266 -0
  35. package/src/android/ClickActivity.java +72 -0
  36. package/src/android/LocalNotification.java +779 -0
  37. package/src/android/Manager.java +327 -0
  38. package/src/android/Notification.java +844 -0
  39. package/src/android/Options.java +956 -0
  40. package/src/android/OptionsTrigger.java +104 -0
  41. package/src/android/action/Action.java +214 -0
  42. package/src/android/action/ActionGroup.java +135 -0
  43. package/src/android/build/localnotification.gradle +29 -0
  44. package/src/android/receiver/ClearReceiver.java +61 -0
  45. package/src/android/receiver/RestoreReceiver.java +70 -0
  46. package/src/android/receiver/TriggerReceiver.java +70 -0
  47. package/src/android/trigger/TriggerHandler.java +195 -0
  48. package/src/android/trigger/TriggerHandlerAt.java +63 -0
  49. package/src/android/trigger/TriggerHandlerEvery.java +257 -0
  50. package/src/android/trigger/TriggerHandlerIn.java +69 -0
  51. package/src/android/util/AssetUtil.java +385 -0
  52. package/src/android/util/CallbackContextUtil.java +88 -0
  53. package/src/android/util/PluginFileProvider.java +34 -0
  54. package/src/android/xml/shared_files_provider_paths.xml +38 -0
  55. package/src/ios/APPLocalNotification.h +56 -0
  56. package/src/ios/APPLocalNotification.m +650 -0
  57. package/src/ios/APPNotificationCategory.h +28 -0
  58. package/src/ios/APPNotificationCategory.m +107 -0
  59. package/src/ios/APPNotificationContent.h +32 -0
  60. package/src/ios/APPNotificationContent.m +122 -0
  61. package/src/ios/APPNotificationOptions.h +42 -0
  62. package/src/ios/APPNotificationOptions.m +689 -0
  63. package/src/ios/UNNotificationRequest+APPLocalNotification.h +32 -0
  64. package/src/ios/UNNotificationRequest+APPLocalNotification.m +100 -0
  65. package/src/ios/UNUserNotificationCenter+APPLocalNotification.h +60 -0
  66. package/src/ios/UNUserNotificationCenter+APPLocalNotification.m +344 -0
  67. package/www/local-notification.js +1104 -0
@@ -0,0 +1,844 @@
1
+ /*
2
+ * Apache 2.0 License
3
+ *
4
+ * Copyright (c) Sebastian Katzer 2017
5
+ * Copyright (c) Manuel Beck 2024
6
+ *
7
+ * This file contains Original Code and/or Modifications of Original Code
8
+ * as defined in and that are subject to the Apache License
9
+ * Version 2.0 (the 'License'). You may not use this file except in
10
+ * compliance with the License. Please obtain a copy of the License at
11
+ * http://opensource.org/licenses/Apache-2.0/ and read it before using this
12
+ * file.
13
+ *
14
+ * The Original Code and all software distributed under the License are
15
+ * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
16
+ * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
17
+ * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
18
+ * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
19
+ * Please see the License for the specific language governing rights and
20
+ * limitations under the License.
21
+ */
22
+
23
+ package de.appplant.cordova.plugin.localnotification;
24
+
25
+ import android.app.AlarmManager;
26
+ import android.app.PendingIntent;
27
+ import android.content.BroadcastReceiver;
28
+ import android.content.Context;
29
+ import android.content.Intent;
30
+ import android.content.SharedPreferences;
31
+ import android.graphics.Bitmap;
32
+ import android.net.Uri;
33
+ import android.os.Build;
34
+ import android.os.Bundle;
35
+ import android.os.PowerManager;
36
+ import android.service.notification.StatusBarNotification;
37
+ import androidx.core.app.NotificationCompat;
38
+ import androidx.core.app.NotificationCompat.MessagingStyle;
39
+ import androidx.core.app.NotificationManagerCompat;
40
+ import androidx.collection.ArraySet;
41
+ import androidx.core.util.Pair;
42
+ import android.util.Log;
43
+ import android.util.SparseArray;
44
+
45
+ import org.json.JSONException;
46
+ import org.json.JSONObject;
47
+
48
+ import java.util.ArrayList;
49
+ import java.util.Date;
50
+ import java.util.Iterator;
51
+ import java.util.List;
52
+ import java.util.Set;
53
+
54
+ import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
55
+ import static android.os.Build.VERSION.SDK_INT;
56
+
57
+ import de.appplant.cordova.plugin.localnotification.action.Action;
58
+ import de.appplant.cordova.plugin.localnotification.action.ActionGroup;
59
+ import de.appplant.cordova.plugin.localnotification.receiver.ClearReceiver;
60
+ import de.appplant.cordova.plugin.localnotification.receiver.TriggerReceiver;
61
+ import de.appplant.cordova.plugin.localnotification.trigger.TriggerHandler;
62
+ import de.appplant.cordova.plugin.localnotification.trigger.TriggerHandlerAt;
63
+ import de.appplant.cordova.plugin.localnotification.trigger.TriggerHandlerIn;
64
+ import de.appplant.cordova.plugin.localnotification.trigger.TriggerHandlerEvery;
65
+ import de.appplant.cordova.plugin.localnotification.util.AssetUtil;
66
+
67
+ /**
68
+ * Wrapper class around OS notification class. Handles basic operations
69
+ * like show, delete, cancel for a single local notification instance.
70
+ */
71
+ public class Notification {
72
+
73
+ private static final String TAG = "Notification";
74
+
75
+ // Used to differ notifications by their life cycle state
76
+ public enum Type {
77
+ ALL, SCHEDULED, TRIGGERED
78
+ }
79
+
80
+ // Extra key for the id
81
+ public static String EXTRA_ID = "NOTIFICATION_ID";
82
+
83
+ // Application context passed by constructor
84
+ private Context context;
85
+
86
+ // Notification options passed by JS
87
+ private Options options;
88
+
89
+ /**
90
+ * Trigger handler for trigger.at/in/every
91
+ */
92
+ private TriggerHandler triggerhandler;
93
+
94
+ /**
95
+ * Constructor
96
+ * @param context Application context.
97
+ * @param options Parsed notification options.
98
+ * @param builder Pre-configured notification builder.
99
+ */
100
+ public Notification(Context context, JSONObject options) {
101
+ this.context = context;
102
+ this.options = new Options(context, options);
103
+ OptionsTrigger optionsTrigger = this.options.getOptionsTrigger();
104
+
105
+ // Handle trigger.at
106
+ // Example: trigger: { at: new Date(2017, 10, 27, 15) }
107
+ if (optionsTrigger.getJSON().has("at")) {
108
+ this.triggerhandler = new TriggerHandlerAt(this.options);
109
+
110
+ // Handle trigger.in
111
+ // Example: trigger: { in: 1, unit: 'hour' }
112
+ } else if (optionsTrigger.getJSON().has("in")) {
113
+ this.triggerhandler = new TriggerHandlerIn(this.options);
114
+
115
+ // Handle trigger.every
116
+ // Example:
117
+ // trigger: { every: 'day', count: 5 }
118
+ // trigger: { every: { month: 10, day: 27, hour: 9, minute: 0 } }
119
+ } else if (optionsTrigger.getJSON().has("every")) {
120
+ this.triggerhandler = new TriggerHandlerEvery(this.options);
121
+
122
+ } else {
123
+ throw new IllegalArgumentException("Trigger not property set");
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Get application context.
129
+ */
130
+ public Context getContext() {
131
+ return context;
132
+ }
133
+
134
+ /**
135
+ * Get notification options.
136
+ */
137
+ public Options getOptions() {
138
+ return options;
139
+ }
140
+
141
+ /**
142
+ * Get notification ID.
143
+ */
144
+ public int getId() {
145
+ return options.getId();
146
+ }
147
+
148
+ /**
149
+ * The action identifier in the form NOTIFICATION_ID{notification-id}-{occurrence} like "NOTIFICATION_ID1173-1"
150
+ */
151
+ String getIntentActionIdentifier() {
152
+ return Manager.PREF_KEY_ID + options.getId();
153
+ }
154
+
155
+
156
+ /**
157
+ * Notification type can be one of triggered or scheduled.
158
+ */
159
+ public Type getType() {
160
+ for (StatusBarNotification statusBarNotification : new Manager(context).getActiveNotifications()) {
161
+ if (statusBarNotification.getId() == getId()) {
162
+ return Type.TRIGGERED;
163
+ }
164
+ }
165
+
166
+ return Type.SCHEDULED;
167
+ }
168
+
169
+ /**
170
+ * Schedules the first or next occurrence of the notification. This has also to be called
171
+ * when the notification is scheduled for the first time, so the first occurrence
172
+ * can be calculated.
173
+ * @return true if the notification was scheduled, false otherwise, which can happen,
174
+ * if the notification was triggered directly, an error occured or there is no next trigger date.
175
+ */
176
+ public boolean scheduleNext() {
177
+ Date triggerDate = triggerhandler.getNextTriggerDate();
178
+
179
+ // No next trigger date available, all triggers are done
180
+ // The notification will not be removed from SharedPreferences.
181
+ // This will always do the ClearReceiver and ClickActivity
182
+ if (triggerDate == null) {
183
+ Log.d(TAG, "No next trigger date available" +
184
+ ", notificationId=" + options.getId() +
185
+ ", occurrence=" + triggerhandler.getOccurrence() +
186
+ ", triggerDate=" + triggerhandler.getTriggerDate() +
187
+ ", triggerBaseDate=" + triggerhandler.getBaseDate() +
188
+ ", options=" + options);
189
+
190
+ return false;
191
+ }
192
+
193
+ return schedule();
194
+ }
195
+
196
+ /**
197
+ * Schedules the notification with the current state of the dateTrigger.
198
+ * If the triggerDate is in the past, the notification will be triggered directly.
199
+ * @return true if the notification was scheduled, false otherwise, which can happen,
200
+ * if the notification was triggered directly or an error occured.
201
+ */
202
+ public boolean schedule() {
203
+ Date triggerDate = triggerhandler.getTriggerDate();
204
+
205
+ if (triggerDate == null) {
206
+ Log.e(TAG, "schedule was wrongly called with triggerDate = null, options=" + options);
207
+ return false;
208
+ }
209
+
210
+ // Create channel if not exists
211
+ Manager.createChannel(context, options);
212
+
213
+ // Store notification data for restoration
214
+ // Needed for ClickActivity and ClearReceiver
215
+ storeInSharedPreferences();
216
+
217
+ // Date is in the past, show directly
218
+ if (!triggerDate.after(new Date())) {
219
+ show(false);
220
+ scheduleNext();
221
+ return false;
222
+ }
223
+
224
+ boolean canScheduleExactAlarms = Manager.canScheduleExactAlarms(context);
225
+
226
+ // Intent when the alarm goes off
227
+ Intent alarmFiresIntent = new Intent(context, TriggerReceiver.class)
228
+ // action identifier for the intent like "NOTIFICATION_ID1173"
229
+ .setAction(getIntentActionIdentifier())
230
+ // Notification-ID
231
+ .putExtra(EXTRA_ID, options.getId());
232
+
233
+ Log.d(TAG, "Schedule notification" +
234
+ ", notificationId: " + options.getId() +
235
+ ", canScheduleExactAlarms: " + canScheduleExactAlarms +
236
+ ", intentAction=" + alarmFiresIntent.getAction() +
237
+ ", occurrence: " + triggerhandler.getOccurrence() +
238
+ ", triggerDate: " + triggerDate +
239
+ ", options=" + options);
240
+
241
+ // AlarmManager.set: If there is already an alarm scheduled for the same IntentSender,
242
+ // that previous alarm will first be canceled.
243
+ PendingIntent alarmFiresPendingIntent = PendingIntent.getBroadcast(context, 0, alarmFiresIntent, PendingIntent.FLAG_IMMUTABLE);
244
+
245
+ // A maximum of 500 alarms can be scheduled, catch exception
246
+ try {
247
+ // Execute alarm even when the system is in low-power idle (a.k.a. doze) modes.
248
+ if (options.isAndroidAllowWhileIdle()) {
249
+ if (canScheduleExactAlarms) {
250
+ getAlarmManager().setExactAndAllowWhileIdle(
251
+ options.getAndroidAlarmType(), triggerDate.getTime(), alarmFiresPendingIntent);
252
+ } else {
253
+ getAlarmManager().setAndAllowWhileIdle(
254
+ options.getAndroidAlarmType(), triggerDate.getTime(), alarmFiresPendingIntent);
255
+ }
256
+
257
+ // Execute alarm by RTC or RTC_WAKEUP
258
+ } else {
259
+ if (canScheduleExactAlarms) {
260
+ getAlarmManager().setExact(
261
+ options.getAndroidAlarmType(), triggerDate.getTime(), alarmFiresPendingIntent);
262
+ } else {
263
+ getAlarmManager().set(
264
+ options.getAndroidAlarmType(), triggerDate.getTime(), alarmFiresPendingIntent);
265
+ }
266
+ }
267
+
268
+ return true;
269
+
270
+ // Maximum 500 alarms can be scheduled.
271
+ // If more are scheduled, an exception will be thrown.
272
+ } catch (Exception exception) {
273
+ Log.d(TAG, "Exception occurred during scheduling notification", exception);
274
+ return false;
275
+ }
276
+ }
277
+
278
+ /**
279
+ * Present the notification to the user
280
+ */
281
+ public void show(boolean isUpdate) {
282
+ Log.d(TAG, "Show notification, options=" + options + ", isUpdate=" + isUpdate);
283
+
284
+ NotificationCompat.Builder builder = getBuilder(isUpdate);
285
+
286
+ // Notification should be silent
287
+ if (builder == null) {
288
+ Log.d(TAG, "Notification is silent, don't show it");
289
+ return;
290
+ }
291
+
292
+ // Turn the screen on
293
+ PowerManager.WakeLock wakeLock = options.isAndroidWakeUpScreen() ? Manager.wakeUpScreen(context) : null;
294
+
295
+ NotificationManagerCompat.from(context).notify(
296
+ LocalNotification.getAppName(context), options.getId(), builder.build());
297
+
298
+ // The wake lock has to be released after the notification was shown
299
+ if (wakeLock != null) wakeLock.release();
300
+
301
+ // Fire trigger event if it's not an update and the app is running
302
+ if (!isUpdate && LocalNotification.isAppRunning()) {
303
+ LocalNotification.fireEvent("trigger", this);
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Update the notification properties.
309
+ * @param updates The properties to update.
310
+ */
311
+ public void update(JSONObject updates) {
312
+ Log.d(TAG, "Update notification, options=" + options + ", updates=" + updates);
313
+
314
+ // Update options of notification
315
+ mergeJSONObjects(updates);
316
+
317
+ // Store notification data
318
+ storeInSharedPreferences();
319
+
320
+ // Update triggered notification in status bar
321
+ if (getType() == Type.TRIGGERED) show(true);
322
+ }
323
+
324
+ /**
325
+ * Handle when the notification was clicked.
326
+ */
327
+ public void handleClick() {
328
+ Log.d(TAG, "Handle click, options=" + options);
329
+
330
+ // Fire notification click event to JS
331
+ LocalNotification.fireEvent("click", this);
332
+
333
+ // Clear notification from statusbar if it should not be ongoing
334
+ // This will also remove the notification from the SharedPreferences
335
+ // if it is the last one
336
+ if (!options.isAndroidOngoing()) clear();
337
+
338
+ // Launch the app if required
339
+ if (options.isLaunch()) LocalNotification.launchApp(context);
340
+ }
341
+
342
+ public void handleActionClick(Intent intent, String actionId) {
343
+ options.getActionsGroup().getActionById(actionId).handleClick(intent, this);
344
+ }
345
+
346
+ /**
347
+ * Clears the notification from Statusbar. If the notification was the last one,
348
+ * the notification will be removed from SharedPreferences.
349
+ * The WebView will receive a clear event, if the app is running.
350
+ */
351
+ public void clear() {
352
+ Log.d(TAG, "Clear notification, options=" + options);
353
+
354
+ // Clear the notification from the statusbar if posted
355
+ NotificationManagerCompat.from(context).cancel(LocalNotification.getAppName(context), getId());
356
+
357
+ // If it is the last occurrence remove the notification from SharedPreferences
358
+ if (triggerhandler.isLastOccurrence()) {
359
+ removeFromSharedPreferences();
360
+ }
361
+
362
+ // Inform WebView about the clearing
363
+ if (LocalNotification.isAppRunning()) LocalNotification.fireEvent("clear", this);
364
+ }
365
+
366
+ /**
367
+ * Cancels the notification and removes it from SharedPreferences.
368
+ * The WebView will receive a cancel event, if the app is running.
369
+ */
370
+ public void cancel() {
371
+ Log.d(TAG, "Cancel notification, options=" + options);
372
+
373
+ cancelScheduledAlarm();
374
+
375
+ // Remove saved notification data from the app
376
+ removeFromSharedPreferences();
377
+
378
+ // Clear the notification from the status bar if posted
379
+ NotificationManagerCompat.from(context).cancel(LocalNotification.getAppName(context), getId());
380
+
381
+ // Inform WebView about the canceling
382
+ if (LocalNotification.isAppRunning()) LocalNotification.fireEvent("cancel", this);
383
+ }
384
+
385
+ /**
386
+ * Cancel all alarms for this notification. If the notification is repeating, it can
387
+ * have multiple alarms.
388
+ */
389
+ private void cancelScheduledAlarm() {
390
+ String intentActionId = getIntentActionIdentifier();
391
+ Log.d(TAG, "Cancel PendingIntent, intentActionId=" + intentActionId);
392
+
393
+ // Create similar PendingIntent to cancel the alarm
394
+ PendingIntent pendingIntent = PendingIntent.getBroadcast(
395
+ context, 0,
396
+ // The intent have to be build with the same context, class and action
397
+ new Intent(context, TriggerReceiver.class).setAction(intentActionId),
398
+ // If the PendingIntent could not be found, null will be returned (configured by FLAG_NO_CREATE)
399
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_NO_CREATE);
400
+
401
+ // PendingIntent could not be found
402
+ if (pendingIntent == null) {
403
+ Log.d(TAG, "Could not cancel PendingIntent, intentActionId=" + intentActionId + ", PendingIntent not found");
404
+ return;
405
+ }
406
+
407
+ // Remove any alarms with a matching Intent. Any alarm, of any type, whose Intent matches this one
408
+ // (as defined by Intent#filterEquals), will be canceled.
409
+ // Intent#filterEquals: That is, if their action, data, type, identity, class, and categories are the same.
410
+ // This does not compare any extra data included in the intents.
411
+ getAlarmManager().cancel(pendingIntent);
412
+ }
413
+
414
+ /**
415
+ * Stores the information of this notification in the SharedPreferences.
416
+ * This will allow the application to restore the notification upon device reboot,
417
+ * app restart, retrieve notifications, etc.
418
+ */
419
+ private void storeInSharedPreferences() {
420
+ Log.d(TAG, "Store notification in SharedPreferences" +
421
+ ", notificationId=" + options.getId() +
422
+ ", occurrence=" + triggerhandler.getOccurrence() +
423
+ ", triggerDate=" + triggerhandler.getTriggerDate() +
424
+ ", triggerBaseDate=" + triggerhandler.getBaseDate() +
425
+ ", options=" + options);
426
+
427
+ Manager.getSharedPreferences(context).edit()
428
+ // options as JSON string
429
+ .putString(getSharedPreferencesKeyOptions(), options.toString())
430
+ // occurrence for restoration
431
+ .putInt(getSharedPreferencesKeyOccurrence(), triggerhandler.getOccurrence())
432
+ // trigger base date for restoration
433
+ .putLong(getSharedPreferencesKeyTriggerBaseDate(), triggerhandler.getBaseDate().getTime())
434
+ // calculated triggerDate for restoration
435
+ .putLong(getSharedPreferencesKeyTriggerDate(), triggerhandler.getTriggerDate().getTime())
436
+ .apply();
437
+ }
438
+
439
+ /**
440
+ * Removes the notification data from the SharedPreferences.
441
+ */
442
+ private void removeFromSharedPreferences() {
443
+ Log.d(TAG, "Remove notification from SharedPreferences" +
444
+ ", notificationId=" + options.getId() +
445
+ ", occurrence=" + triggerhandler.getOccurrence() +
446
+ ", triggerDate=" + triggerhandler.getTriggerDate() +
447
+ ", triggerBaseDate=" + triggerhandler.getBaseDate() +
448
+ ", options=" + options);
449
+
450
+ Manager.getSharedPreferences(context).edit()
451
+ .remove(getSharedPreferencesKeyOptions())
452
+ .remove(getSharedPreferencesKeyOccurrence())
453
+ .remove(getSharedPreferencesKeyTriggerBaseDate())
454
+ .remove(getSharedPreferencesKeyTriggerDate())
455
+ .apply();
456
+ }
457
+
458
+ /**
459
+ * Gets a stored notification from the {@link SharedPreferences} by id.
460
+ * If the notification does not exists, null will be returned.
461
+ */
462
+ public static Notification getFromSharedPreferences(Context context, int notificationId) {
463
+ String optionsJSONString = Manager.getSharedPreferences(context).getString(
464
+ getSharedPreferencesKeyOptions(notificationId), null);
465
+
466
+ Log.d(TAG, "Restoring notification from SharedPreferences" +
467
+ ", notificationId=" + notificationId +
468
+ ", options=" + optionsJSONString);
469
+
470
+ // Notification does not exists
471
+ if (optionsJSONString == null) {
472
+ Log.w(TAG, "Could not restore notification from SharedPreferences, options are null" +
473
+ ", notificationId=" + notificationId);
474
+ return null;
475
+ }
476
+
477
+ try {
478
+ // Parse options string to JSONObject
479
+ Notification notification = new Notification(context, new JSONObject(optionsJSONString));
480
+
481
+ // Restore state of dateTrigger
482
+ // Get occurrence
483
+ int occurrence = Manager.getSharedPreferences(context).getInt(
484
+ notification.getSharedPreferencesKeyOccurrence(), 0);
485
+
486
+ // Get triger base date
487
+ long triggerBaseTime = Manager.getSharedPreferences(context).getLong(
488
+ notification.getSharedPreferencesKeyTriggerBaseDate(), 0);
489
+
490
+ // Get triggerDate
491
+ long triggerTime = Manager.getSharedPreferences(context).getLong(
492
+ notification.getSharedPreferencesKeyTriggerDate(), 0);
493
+
494
+ TriggerHandler triggerHandler = notification.getTriggerHandler();
495
+
496
+ // The saving of occurrence, triggerBaseDate and triggerDate exists since version 1.1.4
497
+ // Before only the options were saved
498
+ // Just caclulate the next trigger from the current time
499
+ // Durring the development of version 1.1.4, first only the occurrence was saved,
500
+ // later also the triggerBaseDate and triggerDate, so check this also
501
+ if (occurrence == 0 || triggerBaseTime == 0 || triggerTime == 0) {
502
+ triggerHandler.getNextTriggerDate();
503
+
504
+ // Restore the state of the trigger date
505
+ } else {
506
+ triggerHandler.restoreState(occurrence, new Date(triggerBaseTime), new Date(triggerTime));
507
+ }
508
+
509
+ Log.d(TAG, "Restored trigger date" +
510
+ ", notificationId=" + notificationId +
511
+ ", occurrence=" + triggerHandler.getOccurrence() +
512
+ ", triggerDate=" + triggerHandler.getTriggerDate() +
513
+ ", triggerBaseDate=" + triggerHandler.getBaseDate());
514
+
515
+ return notification;
516
+ } catch (JSONException exception) {
517
+ Log.e(TAG, "Could not parse stored notification options to JSON" +
518
+ ", notificationId=" + notificationId +
519
+ ", jsonString=" + optionsJSONString,
520
+ exception);
521
+ return null;
522
+ }
523
+ }
524
+
525
+ /**
526
+ * Update options
527
+ */
528
+ private void mergeJSONObjects(JSONObject updates) {
529
+ Iterator keys = updates.keys();
530
+
531
+ while (keys.hasNext()) {
532
+ try {
533
+ String key = (String) keys.next();
534
+ options.getJSON().put(key, updates.opt(key));
535
+ } catch (JSONException jsonException) {
536
+ jsonException.printStackTrace();
537
+ }
538
+ }
539
+ }
540
+
541
+ public String getSharedPreferencesKeyOccurrence() {
542
+ return options.getId() + "_occurrence";
543
+ }
544
+
545
+ public String getSharedPreferencesKeyTriggerBaseDate() {
546
+ return options.getId() + "_triggerBaseDate";
547
+ }
548
+
549
+ public String getSharedPreferencesKeyTriggerDate() {
550
+ return options.getId() + "_triggerDate";
551
+ }
552
+
553
+ /**
554
+ * Get the SharedPreferences key for the notification options.
555
+ * @return
556
+ */
557
+ public String getSharedPreferencesKeyOptions() {
558
+ return "" + getSharedPreferencesKeyOptions(options.getId());
559
+ }
560
+
561
+ /**
562
+ * Static version of {@link #getSharedPreferencesKeyOptions()}.
563
+ * To use, when there is no notification instance available.
564
+ * @param notificationId
565
+ * @return
566
+ */
567
+ public static String getSharedPreferencesKeyOptions(int notificationId) {
568
+ return "" + notificationId;
569
+ }
570
+
571
+ public TriggerHandler getTriggerHandler() {
572
+ return triggerhandler;
573
+ }
574
+
575
+ /**
576
+ * Alarm manager for the application.
577
+ */
578
+ private AlarmManager getAlarmManager() {
579
+ return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
580
+ }
581
+
582
+ /**
583
+ * Creates a {@link NotificationCompat.Builder} from options
584
+ * @return The builder instance or null if the notification is silent.
585
+ */
586
+ public NotificationCompat.Builder getBuilder(boolean isUpdate) {
587
+ if (options.isSilent()) return null;
588
+
589
+ Bundle extras = new Bundle();
590
+ extras.putInt(Notification.EXTRA_ID, options.getId());
591
+
592
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(context, options.getAndroidChannelId())
593
+ .setExtras(extras)
594
+ .setOnlyAlertOnce(options.isOnlyAlertOnce())
595
+ .setContentTitle(options.getTitle())
596
+ .setContentText(options.getText())
597
+ // Text that summarizes this notification for accessibility services.
598
+ // Since Android 5, this text is no longer shown on screen, but it is
599
+ // still useful to accessibility services (where it serves as an audible announcement
600
+ // of the notification's appearance).
601
+ .setTicker(options.getText())
602
+ // Since Android 8 shows as a badge count for Launchers that support badging
603
+ // prior to 8 it could be shown in the header
604
+ .setNumber(options.getBadgeNumber())
605
+ .setAutoCancel(options.isAndroidAutoCancel())
606
+ .setOngoing(options.isAndroidOngoing())
607
+ .setColor(options.getColor())
608
+ .setVisibility(options.getVisibility())
609
+ // Show the Notification when date
610
+ .setShowWhen(options.isAndroidShowWhen())
611
+ // Show the Notification#when field as a stopwatch. Instead of presenting when as a timestamp,
612
+ // the notification will show an automatically updating display of the minutes and seconds since
613
+ // when
614
+ .setUsesChronometer(options.isAndroidUsesChronometer())
615
+ .setGroup(options.getGroup())
616
+ .setGroupSummary(options.isGroupSummary());
617
+
618
+ // Specify the duration in milliseconds after which this notification should be canceled
619
+ if (options.getAndroidTimeoutAfter() >= 0) builder.setTimeoutAfter(options.getAndroidTimeoutAfter());
620
+
621
+ // Settings for Android older than 8
622
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
623
+ builder.setDefaults(options.getDefaults());
624
+ builder.setPriority(options.getPriorityByImportance());
625
+ builder.setLights(options.getLedColor(), options.getLedOn(), options.getLedOff());
626
+
627
+ Uri soundUri = options.getSoundUri();
628
+
629
+ // Set sound only, if the notification should not be updated
630
+ if (soundUri != Uri.EMPTY && !isUpdate) {
631
+ // Grant permission to the system to play the sound, needed in Android 7 and 8
632
+ Manager.grantUriPermission(context, soundUri);
633
+ builder.setSound(soundUri);
634
+ }
635
+ }
636
+
637
+ if (options.getProgressBar() != null) {
638
+ builder.setProgress(
639
+ options.getProgressMaxValue(),
640
+ options.getProgressValue(),
641
+ options.isProgressIndeterminate());
642
+ }
643
+
644
+ // Get smallIcon from resources only
645
+ //
646
+ // There exists also a setSmallIcon(IconCompat icon) method, but tested on Android 15, when multiple notifications with the
647
+ // same icon are posted, the app icon will be shown in statusbar instead of the small icon. This does not happen,
648
+ // when using the setSmallIcon(int icon) method.
649
+ builder.setSmallIcon(options.getSmallIcon());
650
+
651
+ Bitmap largeIcon = new AssetUtil(context).getBitmap(options.getAndroidLargeIcon());
652
+
653
+ if (largeIcon != null) {
654
+ if (options.getAndroidLargeIconType().equals(Options.LARGE_ICON_TYPE_CIRCLE)) {
655
+ largeIcon = AssetUtil.getCircleBitmap(largeIcon);
656
+ }
657
+
658
+ builder.setLargeIcon(largeIcon);
659
+ }
660
+
661
+ applyStyle(builder);
662
+ addActions(builder);
663
+
664
+ // Supply a PendingIntent to send when the notification is cleared by the user directly from the notification panel
665
+ setDeleteIntent(builder);
666
+
667
+ // Supply a PendingIntent to send when the notification is clicked
668
+ setContentIntent(builder);
669
+
670
+ return builder;
671
+ }
672
+
673
+ /**
674
+ * Find out and set the notification style.
675
+ * @param builder Notification builder instance.
676
+ */
677
+ private void applyStyle(NotificationCompat.Builder builder) {
678
+ if (applyMessagingStyle(builder)) return;
679
+ if (applyBigPictureStyle(builder)) return;
680
+ if (applyInboxStyle(builder)) return;
681
+ if (applyBigTextStyle(builder)) return;
682
+ }
683
+
684
+ /**
685
+ * Apply messaging style
686
+ * @param builder Notification builder instance
687
+ * @return true if the messaging style was applied
688
+ */
689
+ private boolean applyMessagingStyle(NotificationCompat.Builder builder) {
690
+ MessagingStyle.Message[] messages = options.getAndroidMessages();
691
+ if (messages == null) return false;
692
+
693
+ // Find if there is a notification already displayed with this ID.
694
+ StatusBarNotification activeNotification = new Manager(context).getActiveNotification(options.getId());
695
+
696
+ MessagingStyle style = activeNotification != null ?
697
+ // If the notification was already displayed, extract the MessagingStyle to add the message
698
+ MessagingStyle.extractMessagingStyleFromNotification(activeNotification.getNotification()) :
699
+ // No active notification, create a new style
700
+ new NotificationCompat.MessagingStyle("");
701
+
702
+ // Add the new messages to the style
703
+ for (MessagingStyle.Message message : messages) {
704
+ style.addMessage(message);
705
+ }
706
+
707
+ String title = options.getTitle();
708
+
709
+ // Add the count of messages to the title if there is more than 1.
710
+ Integer messagesCount = style.getMessages().size();
711
+
712
+ if (messagesCount > 1 && options.getTitleCount() != null && !options.getTitleCount().trim().isEmpty()) {
713
+ title += " " + options.getTitleCount().replace("%n%", "" + messagesCount);
714
+ }
715
+
716
+ style.setConversationTitle(title);
717
+
718
+ // Use the style.
719
+ builder.setStyle(style);
720
+
721
+ return true; // style applied
722
+ }
723
+
724
+ /**
725
+ * Apply big picture style. Only uses the first attachment.
726
+ * @param builder Notification builder instance
727
+ * @return true if the big picture style was applied
728
+ */
729
+ private boolean applyBigPictureStyle(NotificationCompat.Builder builder) {
730
+ List<Bitmap> attachmentsPictures = options.getAttachments();
731
+ if (attachmentsPictures == null || attachmentsPictures.size() == 0) return false;
732
+
733
+ builder.setStyle(new NotificationCompat.BigPictureStyle(builder)
734
+ .setSummaryText(options.getSummary() != null ? options.getSummary() : options.getText())
735
+ .bigPicture(attachmentsPictures.get(0))
736
+ );
737
+
738
+ return true; // style applied
739
+ }
740
+
741
+ /**
742
+ * Apply inbox style
743
+ * @param builder Notification builder instance
744
+ * @return true if the inbox style was applied
745
+ */
746
+ private boolean applyInboxStyle(NotificationCompat.Builder builder) {
747
+ if (!options.getText().contains("\n")) return false;
748
+
749
+ NotificationCompat.InboxStyle style = new NotificationCompat.InboxStyle(builder)
750
+ .setSummaryText(options.getSummary());
751
+
752
+ for (String line : options.getText().split("\n")) {
753
+ style.addLine(line);
754
+ }
755
+
756
+ builder.setStyle(style);
757
+ return true; // style applied
758
+ }
759
+
760
+ /**
761
+ * Apply big text style
762
+ * @param builder Notification builder instance
763
+ * @return true if the big text style was applied
764
+ */
765
+ private boolean applyBigTextStyle(NotificationCompat.Builder builder) {
766
+ if (options.getSummary() == null && options.getText().length() < 45) return false;
767
+ builder.setStyle(new NotificationCompat.BigTextStyle(builder)
768
+ .setSummaryText(options.getSummary())
769
+ .bigText(options.getText()));
770
+ return true; // style applied
771
+ }
772
+
773
+ /**
774
+ * Supply a PendingIntent to send when the notification is cleared by the user directly
775
+ * from the notification panel. For example, this intent is sent when the user clicks the
776
+ * "Clear all" button, or the individual "X" buttons on notifications.
777
+ * This intent is not sent when the application calls NotificationManager.cancel(int).
778
+ */
779
+ private void setDeleteIntent(NotificationCompat.Builder builder) {
780
+ Intent intent = new Intent(context, ClearReceiver.class)
781
+ .setAction(options.getId().toString())
782
+ .putExtra(Notification.EXTRA_ID, options.getId());
783
+
784
+ builder.setDeleteIntent(PendingIntent.getBroadcast(
785
+ context, 0, intent, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT));
786
+ }
787
+
788
+ /**
789
+ * Supply a PendingIntent to send when the notification is clicked.
790
+ * Will bring the app to foreground.
791
+ */
792
+ private void setContentIntent(NotificationCompat.Builder builder) {
793
+ // Route content tap to a transparent activity to avoid trampoline and still run plugin logic
794
+ Intent clickIntent = new Intent(context, ClickActivity.class)
795
+ .putExtra(Notification.EXTRA_ID, options.getId())
796
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
797
+
798
+ PendingIntent contentPendingIntent = PendingIntent.getActivity(
799
+ context,
800
+ Manager.getRandomRequestCode(),
801
+ clickIntent,
802
+ PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
803
+
804
+ builder.setContentIntent(contentPendingIntent);
805
+ }
806
+
807
+ /**
808
+ * Add actions to the builder if there are any.
809
+ */
810
+ private void addActions(NotificationCompat.Builder builder) {
811
+ ActionGroup actionGroup = options.getActionsGroup();
812
+ if (actionGroup == null) return;
813
+
814
+ for (Action action : actionGroup.getActions()) {
815
+ addAction(builder, action);
816
+ }
817
+ }
818
+
819
+ /**
820
+ * Add an action to the builder.
821
+ */
822
+ private void addAction(NotificationCompat.Builder builder, Action action) {
823
+ // Route content tap to a transparent activity to avoid trampoline and still run plugin logic
824
+ Intent actionClickIntent = new Intent(context, ClickActivity.class)
825
+ .putExtra(Notification.EXTRA_ID, options.getId())
826
+ .putExtra(Action.EXTRA_ID, action.getId())
827
+ .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
828
+
829
+ PendingIntent actionClickPendingIntent = PendingIntent.getActivity(
830
+ context,
831
+ Manager.getRandomRequestCode(),
832
+ actionClickIntent,
833
+ // Input actions need FLAG_MUTABLE, otherwise it would give the error
834
+ // "PendingIntents attached to actions with remote inputs must be mutable"
835
+ (action.isInput() ? PendingIntent.FLAG_MUTABLE : PendingIntent.FLAG_IMMUTABLE) | PendingIntent.FLAG_UPDATE_CURRENT);
836
+
837
+ NotificationCompat.Action.Builder actionBuilder = new NotificationCompat.Action.Builder(
838
+ action.getIcon(), action.getTitle(), actionClickPendingIntent);
839
+
840
+ if (action.isInput()) actionBuilder.addRemoteInput(action.buildRemoteInput());
841
+
842
+ builder.addAction(actionBuilder.build());
843
+ }
844
+ }