Notification.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492
  1. /*
  2. * Apache 2.0 License
  3. *
  4. * Copyright (c) Sebastian Katzer 2017
  5. *
  6. * This file contains Original Code and/or Modifications of Original Code
  7. * as defined in and that are subject to the Apache License
  8. * Version 2.0 (the 'License'). You may not use this file except in
  9. * compliance with the License. Please obtain a copy of the License at
  10. * http://opensource.org/licenses/Apache-2.0/ and read it before using this
  11. * file.
  12. *
  13. * The Original Code and all software distributed under the License are
  14. * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER
  15. * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES,
  16. * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY,
  17. * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT.
  18. * Please see the License for the specific language governing rights and
  19. * limitations under the License.
  20. */
  21. package de.appplant.cordova.plugin.notification;
  22. import android.app.AlarmManager;
  23. import android.app.NotificationManager;
  24. import android.app.PendingIntent;
  25. import android.content.BroadcastReceiver;
  26. import android.content.Context;
  27. import android.content.Intent;
  28. import android.content.SharedPreferences;
  29. import android.net.Uri;
  30. import android.service.notification.StatusBarNotification;
  31. import android.support.v4.app.NotificationCompat;
  32. import android.support.v4.util.ArraySet;
  33. import android.support.v4.util.Pair;
  34. import android.util.Log;
  35. import android.util.SparseArray;
  36. import org.json.JSONException;
  37. import org.json.JSONObject;
  38. import java.util.ArrayList;
  39. import java.util.Date;
  40. import java.util.Iterator;
  41. import java.util.List;
  42. import java.util.Set;
  43. import static android.app.AlarmManager.RTC;
  44. import static android.app.AlarmManager.RTC_WAKEUP;
  45. import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
  46. import static android.os.Build.VERSION.SDK_INT;
  47. import static android.os.Build.VERSION_CODES.M;
  48. import static android.support.v4.app.NotificationCompat.PRIORITY_HIGH;
  49. import static android.support.v4.app.NotificationManagerCompat.IMPORTANCE_MAX;
  50. import static android.support.v4.app.NotificationManagerCompat.IMPORTANCE_MIN;
  51. /**
  52. * Wrapper class around OS notification class. Handles basic operations
  53. * like show, delete, cancel for a single local notification instance.
  54. */
  55. public final class Notification {
  56. // Used to differ notifications by their life cycle state
  57. public enum Type {
  58. ALL, SCHEDULED, TRIGGERED
  59. }
  60. // Extra key for the id
  61. public static final String EXTRA_ID = "NOTIFICATION_ID";
  62. // Extra key for the update flag
  63. public static final String EXTRA_UPDATE = "NOTIFICATION_UPDATE";
  64. // Key for private preferences
  65. static final String PREF_KEY_ID = "NOTIFICATION_ID";
  66. // Key for private preferences
  67. private static final String PREF_KEY_PID = "NOTIFICATION_PID";
  68. // Cache for the builder instances
  69. private static SparseArray<NotificationCompat.Builder> cache = null;
  70. // Application context passed by constructor
  71. private final Context context;
  72. // Notification options passed by JS
  73. private final Options options;
  74. // Builder with full configuration
  75. private final NotificationCompat.Builder builder;
  76. /**
  77. * Constructor
  78. *
  79. * @param context Application context.
  80. * @param options Parsed notification options.
  81. * @param builder Pre-configured notification builder.
  82. */
  83. Notification (Context context, Options options, NotificationCompat.Builder builder) {
  84. this.context = context;
  85. this.options = options;
  86. this.builder = builder;
  87. }
  88. /**
  89. * Constructor
  90. *
  91. * @param context Application context.
  92. * @param options Parsed notification options.
  93. */
  94. public Notification(Context context, Options options) {
  95. this.context = context;
  96. this.options = options;
  97. this.builder = null;
  98. }
  99. /**
  100. * Get application context.
  101. */
  102. public Context getContext() {
  103. return context;
  104. }
  105. /**
  106. * Get notification options.
  107. */
  108. public Options getOptions() {
  109. return options;
  110. }
  111. /**
  112. * Get notification ID.
  113. */
  114. public int getId() {
  115. return options.getId();
  116. }
  117. /**
  118. * If it's a repeating notification.
  119. */
  120. public boolean isRepeating() {
  121. return getOptions().getTrigger().has("every");
  122. }
  123. /**
  124. * If the notifications priority is high or above.
  125. */
  126. public boolean isHighPrio() {
  127. return getOptions().getPrio() >= PRIORITY_HIGH;
  128. }
  129. /**
  130. * Notification type can be one of triggered or scheduled.
  131. */
  132. public Type getType() {
  133. Manager mgr = Manager.getInstance(context);
  134. StatusBarNotification[] toasts = mgr.getActiveNotifications();
  135. int id = getId();
  136. for (StatusBarNotification toast : toasts) {
  137. if (toast.getId() == id) {
  138. return Type.TRIGGERED;
  139. }
  140. }
  141. return Type.SCHEDULED;
  142. }
  143. /**
  144. * Schedule the local notification.
  145. *
  146. * @param request Set of notification options.
  147. * @param receiver Receiver to handle the trigger event.
  148. */
  149. void schedule(Request request, Class<?> receiver) {
  150. List<Pair<Date, Intent>> intents = new ArrayList<Pair<Date, Intent>>();
  151. Set<String> ids = new ArraySet<String>();
  152. AlarmManager mgr = getAlarmMgr();
  153. cancelScheduledAlarms();
  154. do {
  155. Date date = request.getTriggerDate();
  156. Log.d("local-notification", "Next trigger at: " + date);
  157. if (date == null)
  158. continue;
  159. Intent intent = new Intent(context, receiver)
  160. .setAction(PREF_KEY_ID + request.getIdentifier())
  161. .putExtra(Notification.EXTRA_ID, options.getId())
  162. .putExtra(Request.EXTRA_OCCURRENCE, request.getOccurrence());
  163. ids.add(intent.getAction());
  164. intents.add(new Pair<Date, Intent>(date, intent));
  165. }
  166. while (request.moveNext());
  167. if (intents.isEmpty()) {
  168. unpersist();
  169. return;
  170. }
  171. persist(ids);
  172. if (!options.isInfiniteTrigger()) {
  173. Intent last = intents.get(intents.size() - 1).second;
  174. last.putExtra(Request.EXTRA_LAST, true);
  175. }
  176. for (Pair<Date, Intent> pair : intents) {
  177. Date date = pair.first;
  178. long time = date.getTime();
  179. Intent intent = pair.second;
  180. if (!date.after(new Date()) && trigger(intent, receiver))
  181. continue;
  182. PendingIntent pi = PendingIntent.getBroadcast(
  183. context, 0, intent, FLAG_CANCEL_CURRENT);
  184. try {
  185. switch (options.getPrio()) {
  186. case IMPORTANCE_MIN:
  187. mgr.setExact(RTC, time, pi);
  188. break;
  189. case IMPORTANCE_MAX:
  190. if (SDK_INT >= M) {
  191. mgr.setExactAndAllowWhileIdle(RTC_WAKEUP, time, pi);
  192. } else {
  193. mgr.setExact(RTC, time, pi);
  194. }
  195. break;
  196. default:
  197. mgr.setExact(RTC_WAKEUP, time, pi);
  198. break;
  199. }
  200. } catch (Exception ignore) {
  201. // Samsung devices have a known bug where a 500 alarms limit
  202. // can crash the app
  203. }
  204. }
  205. }
  206. /**
  207. * Trigger local notification specified by options.
  208. *
  209. * @param intent The intent to broadcast.
  210. * @param cls The broadcast class.
  211. *
  212. * @return false if the receiver could not be invoked.
  213. */
  214. private boolean trigger (Intent intent, Class<?> cls) {
  215. BroadcastReceiver receiver;
  216. try {
  217. receiver = (BroadcastReceiver) cls.newInstance();
  218. } catch (InstantiationException e) {
  219. return false;
  220. } catch (IllegalAccessException e) {
  221. return false;
  222. }
  223. receiver.onReceive(context, intent);
  224. return true;
  225. }
  226. /**
  227. * Clear the local notification without canceling repeating alarms.
  228. */
  229. public void clear() {
  230. getNotMgr().cancel(getId());
  231. if (isRepeating()) return;
  232. unpersist();
  233. }
  234. /**
  235. * Cancel the local notification.
  236. */
  237. public void cancel() {
  238. cancelScheduledAlarms();
  239. unpersist();
  240. getNotMgr().cancel(getId());
  241. clearCache();
  242. }
  243. /**
  244. * Cancel the scheduled future local notification.
  245. *
  246. * Create an intent that looks similar, to the one that was registered
  247. * using schedule. Making sure the notification id in the action is the
  248. * same. Now we can search for such an intent using the 'getService'
  249. * method and cancel it.
  250. */
  251. private void cancelScheduledAlarms() {
  252. SharedPreferences prefs = getPrefs(PREF_KEY_PID);
  253. String id = options.getIdentifier();
  254. Set<String> actions = prefs.getStringSet(id, null);
  255. if (actions == null)
  256. return;
  257. for (String action : actions) {
  258. Intent intent = new Intent(action);
  259. PendingIntent pi = PendingIntent.getBroadcast(
  260. context, 0, intent, 0);
  261. if (pi != null) {
  262. getAlarmMgr().cancel(pi);
  263. }
  264. }
  265. }
  266. /**
  267. * Present the local notification to user.
  268. */
  269. public void show() {
  270. if (builder == null) return;
  271. if (options.showChronometer()) {
  272. cacheBuilder();
  273. }
  274. grantPermissionToPlaySoundFromExternal();
  275. getNotMgr().notify(getId(), builder.build());
  276. }
  277. /**
  278. * Update the notification properties.
  279. *
  280. * @param updates The properties to update.
  281. * @param receiver Receiver to handle the trigger event.
  282. */
  283. void update (JSONObject updates, Class<?> receiver) {
  284. mergeJSONObjects(updates);
  285. persist(null);
  286. if (getType() != Type.TRIGGERED)
  287. return;
  288. Intent intent = new Intent(context, receiver)
  289. .setAction(PREF_KEY_ID + options.getId())
  290. .putExtra(Notification.EXTRA_ID, options.getId())
  291. .putExtra(Notification.EXTRA_UPDATE, true);
  292. trigger(intent, receiver);
  293. }
  294. /**
  295. * Encode options to JSON.
  296. */
  297. public String toString() {
  298. JSONObject dict = options.getDict();
  299. JSONObject json = new JSONObject();
  300. try {
  301. json = new JSONObject(dict.toString());
  302. } catch (JSONException e) {
  303. e.printStackTrace();
  304. }
  305. return json.toString();
  306. }
  307. /**
  308. * Persist the information of this notification to the Android Shared
  309. * Preferences. This will allow the application to restore the notification
  310. * upon device reboot, app restart, retrieve notifications, aso.
  311. *
  312. * @param ids List of intent actions to persist.
  313. */
  314. private void persist (Set<String> ids) {
  315. String id = options.getIdentifier();
  316. SharedPreferences.Editor editor;
  317. editor = getPrefs(PREF_KEY_ID).edit();
  318. editor.putString(id, options.toString());
  319. editor.apply();
  320. if (ids == null)
  321. return;
  322. editor = getPrefs(PREF_KEY_PID).edit();
  323. editor.putStringSet(id, ids);
  324. editor.apply();
  325. }
  326. /**
  327. * Remove the notification from the Android shared Preferences.
  328. */
  329. private void unpersist () {
  330. String[] keys = { PREF_KEY_ID, PREF_KEY_PID };
  331. String id = options.getIdentifier();
  332. SharedPreferences.Editor editor;
  333. for (String key : keys) {
  334. editor = getPrefs(key).edit();
  335. editor.remove(id);
  336. editor.apply();
  337. }
  338. }
  339. /**
  340. * Since Android 7 the app will crash if an external process has no
  341. * permission to access the referenced sound file.
  342. */
  343. private void grantPermissionToPlaySoundFromExternal() {
  344. if (builder == null)
  345. return;
  346. String sound = builder.getExtras().getString(Options.EXTRA_SOUND);
  347. Uri soundUri = Uri.parse(sound);
  348. context.grantUriPermission(
  349. "com.android.systemui", soundUri,
  350. Intent.FLAG_GRANT_READ_URI_PERMISSION);
  351. }
  352. /**
  353. * Merge two JSON objects.
  354. */
  355. private void mergeJSONObjects (JSONObject updates) {
  356. JSONObject dict = options.getDict();
  357. Iterator it = updates.keys();
  358. while (it.hasNext()) {
  359. try {
  360. String key = (String)it.next();
  361. dict.put(key, updates.opt(key));
  362. } catch (JSONException e) {
  363. e.printStackTrace();
  364. }
  365. }
  366. }
  367. /**
  368. * Caches the builder instance so it can be used later.
  369. */
  370. private void cacheBuilder() {
  371. if (cache == null) {
  372. cache = new SparseArray<NotificationCompat.Builder>();
  373. }
  374. cache.put(getId(), builder);
  375. }
  376. /**
  377. * Find the cached builder instance.
  378. *
  379. * @param key The key under where to look for the builder.
  380. *
  381. * @return null if no builder instance could be found.
  382. */
  383. static NotificationCompat.Builder getCachedBuilder (int key) {
  384. return (cache != null) ? cache.get(key) : null;
  385. }
  386. /**
  387. * Caches the builder instance so it can be used later.
  388. */
  389. private void clearCache () {
  390. if (cache != null) {
  391. cache.delete(getId());
  392. }
  393. }
  394. /**
  395. * Shared private preferences for the application.
  396. */
  397. private SharedPreferences getPrefs (String key) {
  398. return context.getSharedPreferences(key, Context.MODE_PRIVATE);
  399. }
  400. /**
  401. * Notification manager for the application.
  402. */
  403. private NotificationManager getNotMgr () {
  404. return (NotificationManager) context
  405. .getSystemService(Context.NOTIFICATION_SERVICE);
  406. }
  407. /**
  408. * Alarm manager for the application.
  409. */
  410. private AlarmManager getAlarmMgr () {
  411. return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
  412. }
  413. }