diff --git a/README.md b/README.md index 741a515..9fa88fa 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ - إذاعات قرآن ورقية شرعية واذكار الخ... - صور متنوعة لأحاديث وأذكار وأدعية بشكل عشوائي - معلمة الدرر | دليلك لمعرفة وتحسين مستواك في العلوم الشرعية +- المتبقي على شهر رمضان الكريم # صور الشاشة diff --git a/config.xml b/config.xml index 78d8c66..5a4ae6e 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - + التقوى تطبيق إسلامي سهل الإستخدام و جامع للكثير من الميزات التي يحتاجها المسلم في يومه Altaqwaa @@ -10,6 +10,7 @@ + @@ -39,13 +40,27 @@ + + + + + + + + - + + + + + + + diff --git a/my-plugins/cordova-plugin-local-notification/README.md b/my-plugins/cordova-plugin-local-notification/README.md new file mode 100644 index 0000000..8573a65 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/README.md @@ -0,0 +1,517 @@ +#### Important notice +If the app is in background, it must not be launched but put in foreground. +To avoid launching the app in this case, add the following in your config.xml file: +`` + + +> A notification is a message you display to the user outside of your app's normal UI. When you tell the system to issue a notification, it first appears as an icon in the notification area. To see the details of the notification, the user opens the notification drawer. Both the notification area and the notification drawer are system-controlled areas that the user can view at any time. +
+ + +
+ + +### Notification components + +- Header area +- Content area +- Action area + +### How notifications may be noticed + +- Showing a status bar icon +- Appearing on the lock screen +- Playing a sound or vibrating +- Peeking onto the current screen +- Blinking the device's LED + +### Supported platforms + +- Android 4.4+ + +
+
+ +## Basics + +The plugin creates the object `cordova.plugins.notification.local` and is accessible after *deviceready* has been fired. + +```js +cordova.plugins.notification.local.schedule({ + title: 'My first notification', + text: 'Thats pretty easy...', + foreground: true +}); +``` + +

+ +

+ +The plugin allows to schedule multiple notifications at once. + +```js +cordova.plugins.notification.local.schedule([ + { id: 1, title: 'My first notification' }, + { id: 2, title: 'My first notification' } +]); +``` + +## Properties + +A notification does have a set of configurable properties. Not all of them are supported across all platforms. + +| Property | Property | Property | Property | Property | Property | Property | Property | Property | +| :------------ | :------------ | :------------ | :------------ | :------------ | :------------ | :------------ | :------------ | :------------ | +| id | data | timeoutAfter | summary | led | clock | channelName | actions | alarmVolume | +| text | icon | attachments | smallIcon | color | defaults | launch | groupSummary | resetDelay | +| title | silent | progressBar | sticky | vibrate | priority | mediaSession | foreground | autoLaunch | +| sound | trigger | group | autoClear | lockscreen | number | badge | wakeup | channelId | +| iconType | wakeLockTimeout | triggerInApp | fullScreenIntent + +For their default values see: + +```js +cordova.plugins.notification.local.getDefaults(); +``` + +To change some default values: + +```js +cordova.plugins.notification.local.setDefaults({ + led: { color: '#FF00FF', on: 500, off: 500 }, + vibrate: false +}); +``` + +## Actions + +The plugin knows two types of actions: _button_ and _input_. + +```js +cordova.plugins.notification.local.schedule({ + title: 'The big survey', + text: 'Are you a fan of RB Leipzig?', + attachments: ['file://img/rb-leipzig.jpg'], + actions: [ + { id: 'yes', title: 'Yes' }, + { id: 'no', title: 'No' } + ] +}); +``` + +

+ +      + +      + +

+ +### Input + +```js +cordova.plugins.notification.local.schedule({ + title: 'Justin Rhyss', + text: 'Do you want to go see a movie tonight?', + actions: [{ + id: 'reply', + type: 'input', + title: 'Reply', + emptyText: 'Type message', + }, ... ] +}); +``` + +

+ +

+ +It is recommended to pre-define action groups rather then specifying them with each new notification of the same type. + + +```js +cordova.plugins.notification.local.addActions('yes-no', [ + { id: 'yes', title: 'Yes' }, + { id: 'no', title: 'No' } +]); +``` + +Once you have defined an action group, you can reference it when scheduling notifications: + +```js +cordova.plugins.notification.local.schedule({ + title: 'Justin Rhyss', + text: 'Do you want to go see a movie tonight?', + actions: 'yes-no' +}); +``` + +### Properties + +Actions do have a set of configurable properties. Not all of them are supported across all platforms. + +| Property | Type | Android +| :----------- | :----------- | :------ +| id | button+input | x +| title | button+input | x +| launch | button+input | x +| ui | button+input | +| needsAuth | button+input | +| icon | button+input | x +| emptyText | input | x +| submitTitle | input | +| editable | input | x +| choices | input | x +| defaultValue | input | + + +## Triggers + +Notifications may trigger immediately or depend on calendar or location. + +To trigger at a fix date: + +```js +cordova.plugins.notification.local.schedule({ + title: 'Design team meeting', + text: '3:00 - 4:00 PM', + trigger: { at: new Date(2017, 10, 27, 15) } +}); +``` + +Or relative from now: + +```js +cordova.plugins.notification.local.schedule({ + title: 'Design team meeting', + trigger: { in: 1, unit: 'hour' } +}); +``` + +### Repeating + +Repeat relative from now: + +```js +cordova.plugins.notification.local.schedule({ + title: 'Design team meeting', + trigger: { every: 'day', count: 5 } +}); +``` + +Or trigger every time the date matches: + +```js +cordova.plugins.notification.local.schedule({ + title: 'Happy Birthday!!!', + trigger: { every: { month: 10, day: 27, hour: 9, minute: 0 } } +}); +``` + +### Location based + +To trigger when the user enters a region: + +```js +cordova.plugins.notification.local.schedule({ + title: 'Welcome to our office', + trigger: { + type: 'location', + center: [x, y], + radius: 15, + notifyOnEntry: true + } +}); +``` + +### Properties + +The properties depend on the trigger type. Not all of them are supported across all platforms. + +| Type | Property | Type | Value | Android +| :----------- | :------------ | :------ | :--------------- | :------ +| Fix | +| | at | Date | | x +| Timespan | +| | in | Int | | x +| | unit | String | `second` | x +| | unit | String | `minute` | x +| | unit | String | `hour` | x +| | unit | String | `day` | x +| | unit | String | `week` | x +| | unit | String | `month` | x +| | unit | String | `quarter` | x +| | unit | String | `year` | x +| Repeat | +| | count | Int | | x +| | every | String | `minute` | x +| | every | String | `hour` | x +| | every | String | `day` | x +| | every | String | `week` | x +| | every | String | `month` | x +| | every | String | `quarter` | x +| | every | String | `year` | x +| | before | Date | | x +| | firstAt | Date | | x +| Match | +| | count | Int | | x +| | every | Object | `minute` | x +| | every | Object | `hour` | x +| | every | Object | `day` | x +| | every | Object | `weekday` | x +| | every | Object | `weekdayOrdinal` | +| | every | Object | `week` | +| | every | Object | `weekOfMonth` | x +| | every | Object | `month` | x +| | every | Object | `quarter` | +| | every | Object | `year` | x +| | before | Date | | x +| | after | Date | | x +| Location | +| | center | Array | `[lat, long]` | +| | radius | Int | | +| | notifyOnEntry | Boolean | | +| | notifyOnExit | Boolean | | +| | single | Boolean | | + + +## Progress + +Notifications can include an animated progress indicator that shows users the status of an ongoing operation. + +```js +cordova.plugins.notification.local.schedule({ + title: 'Sync in progress', + text: 'Copied 2 of 10 files', + progressBar: { value: 20 } +}); +``` + +

+ +

+ + +## Patterns + +Split the text by line breaks if the message comes from a single person and just too long to show in a single line. + +```js +cordova.plugins.notification.local.schedule({ + title: 'The Big Meeting', + text: '4:15 - 5:15 PM\nBig Conference Room', + smallIcon: 'res://calendar', + icon: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcTzfXKe6Yfjr6rCtR6cMPJB8CqMAYWECDtDqH-eMnerHHuXv9egrw' +}); +``` + +

+ +

+ +### Summarizing + +Instead of displaying multiple notifications, you can create one notification that summarizes them all. + +```js +cordova.plugins.notification.local.schedule({ + id: 15, + title: 'Chat with Irish', + icon: 'http://climberindonesia.com/assets/icon/ionicons-2.0.1/png/512/android-chat.png', + text: [ + { message: 'I miss you' }, + { person: 'Irish', message: 'I miss you more!' }, + { message: 'I always miss you more by 10%' } + ] +}); +``` + +

+ +

+ +To add a new message to the existing chat: + +```js +cordova.plugins.notification.local.update({ + id: 15, + text: [{ person: 'Irish', message: 'Bye bye' }] +}); +``` + +### Grouping + +Your app can present multiple notifications as a single group: + +- A parent notification displays a summary of its child notifications. +- The child notifications are presented without duplicate header information. + +```js +cordova.plugins.notification.local.schedule([ + { id: 0, title: 'Design team meeting', ... }, + { id: 1, summary: 'me@gmail.com', group: 'email', groupSummary: true }, + { id: 2, title: 'Please take all my money', ... group: 'email' }, + { id: 3, title: 'A question regarding this plugin', ... group: 'email' }, + { id: 4, title: 'Wellcome back home', ... group: 'email' } +]); +``` + +

+ +

+ + +## Permissions + +Each platform may require the user to grant permissions first before the app is allowed to schedule notifications. + +```js +cordova.plugins.notification.local.hasPermission(function (granted) { ... }); +``` + +If requesting via plug-in, a system dialog does pop up for the first time. Later its only possible to tweak the settings through the system settings. + +```js +cordova.plugins.notification.local.requestPermission(function (granted) { ... }); +``` + +

+ +

+ +Checking the permissions is done automatically, however it's possible to skip that. + +```js +cordova.plugins.notification.local.schedule(toast, callback, scope, { skipPermission: true }); +``` + + +On Android 8, special permissions are required to exit "do not disturb mode" (in case alarmVolume is defined). +You can check these by using: + +```js +cordova.plugins.notification.local.hasDoNotDisturbPermissions(function (granted) { ... }) +``` + +... and you can request them by using: + +```js +cordova.plugins.notification.local.requestDoNotDisturbPermissions(function (granted) { ... }) +``` + +The only downside to not having these permissions granted is that alarmVolume and vibrate may not be +honored on Android 8+ devices if the device is currently on silent when the notification fires (silent, not vibrate). +In this situation, the notification will fire silently but still appear in the notification bar. + +Also on Android 8, it is helpful for alarms that autolaunch the app with an event, if the app can +ignore battery saving mode (otherwise alarms won't trigger reliably). You can check to see if the app is whitelisted for this with the following method. + +```js +cordova.plugins.notification.local.isIgnoringBatteryOptimizations(function (granted) { ... }) +``` + +... and you can request to be whitelisted by using: + +```js +cordova.plugins.notification.local.requestIgnoreBatteryOptimizations(function (granted) { ... }) +``` + +The request method here will work one of two ways. +1. If you have the REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission defined in the manifest, it will use ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS to explicitly ignore battery optimizations for this app. This is the best overall user experience, but the REQUEST_IGNORE_BATTERY_OPTIMIZATIONS permission seems to be frowned upon and can get your app banned. This plugin does not have this permission in plugin.xml for this reason, so you will need to use the cordova-custom-config plugin to add it to your config.xml. Alternatively, you can use the edit-config tag in the platform section of the config.xml. +```xml + + + +``` +2. If you do not have REQUEST_IGNORE_BATTERY_OPTIMIZATIONS requested, it will launch ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS to show a list of all applications. You will want to put some sort of instructions prior to this to walk the user through this. Also, this action doesn't exist on all Android devices (is missing on Samsung phones), which will make this method simply return false if it can't start the activity. + + +## Events + +The following events are supported: `add`, `trigger`, `click`, `clear`, `cancel`, `update`, `clearall` and `cancelall`. + +```js +cordova.plugins.notification.local.on(event, callback, scope); +``` + +To unsubscribe from events: + +```js +cordova.plugins.notification.local.un(event, callback, scope); +``` + +__Note:__ You have to provide the exact same callback to `cordova.plugins.notification.local.un` as you provided to `cordova.plugins.notification.local.on` to make unsubscribing work. +Hence you should define your callback as a separate function, not inline. If you want to use `this` inside of your callback, you also have to provide `this` as `scope` to `cordova.plugins.notification.local.on`. + +### Custom + +The plugin also fires events specified by actions. + +```js +cordova.plugins.notification.local.schedule({ + title: 'Do you want to go see a movie tonight?', + actions: [{ id: 'yes', title: 'Yes' }] +}); +``` + +The name of the event is the id of the action. + +```js +cordova.plugins.notification.local.on('yes', function (notification, eopts) { ... }); +``` + +### Fire manually + +Not an official interface, however its possible to manually fire events. + +```js +cordova.plugins.notification.local.core.fireEvent(event, args); +``` + + +## Launch Details + +Check the `launchDetails` to find out if the app was launched by clicking on a notification. + +```js +document.addEventListener('deviceready', function () { + console.log(cordova.plugins.notification.local.launchDetails); +}, false); +``` + +It might be possible that the underlying framework like __Ionic__ is not compatible with the launch process defined by cordova. With the result that the plugin fires the click event on app start before the app is able to listen for the events. + +Therefore its possible to fire the queued events manually by defining a global variable. + +```js +window.skipLocalNotificationReady = true +``` + +Once the app and Ionic is ready, you can fire the queued events manually. + +```js +cordova.plugins.notification.local.fireQueuedEvents(); +``` + + +## Methods + +All methods work asynchronous and accept callback methods. +See the sample app for how to use them. + +| Method | Method | Method | Method | Method | Method | +| :------- | :---------------- | :-------------- | :------------- | :------------ | :--------------- | +| schedule | cancelAll | isTriggered | get | removeActions | un | +| update | hasPermission | getType | getAll | hasActions | fireQueuedEvents | +| clear | requestPermission | getIds | getScheduled | getDefaults | requestDoNotDisturbPermissions | +| clearAll | isPresent | getScheduledIds | getTriggered | setDefaults | hasDoNotDisturbPermissions | +| cancel | isScheduled | getTriggeredIds | addActions | on | + +## SetDummyNotification + +This method allows user to trigger runtime permission for Android 13 + +```js +cordova.plugins.notification.local.setDummyNotifications(); +``` \ No newline at end of file diff --git a/my-plugins/cordova-plugin-local-notification/images/android-actions.png b/my-plugins/cordova-plugin-local-notification/images/android-actions.png new file mode 100644 index 0000000..767e280 Binary files /dev/null and b/my-plugins/cordova-plugin-local-notification/images/android-actions.png differ diff --git a/my-plugins/cordova-plugin-local-notification/images/android-chat.png b/my-plugins/cordova-plugin-local-notification/images/android-chat.png new file mode 100644 index 0000000..31b9011 Binary files /dev/null and b/my-plugins/cordova-plugin-local-notification/images/android-chat.png differ diff --git a/my-plugins/cordova-plugin-local-notification/images/android-inbox.png b/my-plugins/cordova-plugin-local-notification/images/android-inbox.png new file mode 100644 index 0000000..59ec735 Binary files /dev/null and b/my-plugins/cordova-plugin-local-notification/images/android-inbox.png differ diff --git a/my-plugins/cordova-plugin-local-notification/images/android-progress.png b/my-plugins/cordova-plugin-local-notification/images/android-progress.png new file mode 100644 index 0000000..278fd76 Binary files /dev/null and b/my-plugins/cordova-plugin-local-notification/images/android-progress.png differ diff --git a/my-plugins/cordova-plugin-local-notification/images/android-reply-2.png b/my-plugins/cordova-plugin-local-notification/images/android-reply-2.png new file mode 100644 index 0000000..eb34af1 Binary files /dev/null and b/my-plugins/cordova-plugin-local-notification/images/android-reply-2.png differ diff --git a/my-plugins/cordova-plugin-local-notification/images/android-reply.png b/my-plugins/cordova-plugin-local-notification/images/android-reply.png new file mode 100644 index 0000000..70d26eb Binary files /dev/null and b/my-plugins/cordova-plugin-local-notification/images/android-reply.png differ diff --git a/my-plugins/cordova-plugin-local-notification/images/android-stack.png b/my-plugins/cordova-plugin-local-notification/images/android-stack.png new file mode 100644 index 0000000..fed9777 Binary files /dev/null and b/my-plugins/cordova-plugin-local-notification/images/android-stack.png differ diff --git a/my-plugins/cordova-plugin-local-notification/images/ios-actions.png b/my-plugins/cordova-plugin-local-notification/images/ios-actions.png new file mode 100644 index 0000000..60fa99c Binary files /dev/null and b/my-plugins/cordova-plugin-local-notification/images/ios-actions.png differ diff --git a/my-plugins/cordova-plugin-local-notification/images/ios-basic.png b/my-plugins/cordova-plugin-local-notification/images/ios-basic.png new file mode 100644 index 0000000..aa33a89 Binary files /dev/null and b/my-plugins/cordova-plugin-local-notification/images/ios-basic.png differ diff --git a/my-plugins/cordova-plugin-local-notification/images/ios-permission.png b/my-plugins/cordova-plugin-local-notification/images/ios-permission.png new file mode 100644 index 0000000..901ba4e Binary files /dev/null and b/my-plugins/cordova-plugin-local-notification/images/ios-permission.png differ diff --git a/my-plugins/cordova-plugin-local-notification/images/windows-actions.png b/my-plugins/cordova-plugin-local-notification/images/windows-actions.png new file mode 100644 index 0000000..cc5e4c0 Binary files /dev/null and b/my-plugins/cordova-plugin-local-notification/images/windows-actions.png differ diff --git a/my-plugins/cordova-plugin-local-notification/package.json b/my-plugins/cordova-plugin-local-notification/package.json new file mode 100644 index 0000000..ecd27ec --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/package.json @@ -0,0 +1,33 @@ +{ + "name": "cordova-plugin-local-notification", + "version": "0.10.0", + "description": "Schedules and queries for local notifications", + "cordova": { + "id": "cordova-plugin-local-notification", + "platforms": [ + "android" + ] + }, + "keywords": [ + "appplant", + "notification", + "local notification", + "user notification", + "ecosystem:cordova", + "cordova-android" + ], + "engines": [ + { + "name": "cordova", + "version": ">=3.6.0" + }, + { + "name": "cordova-android", + "version": ">=6.0.0" + }, + { + "name": "android-sdk", + "version": ">=26" + } + ] +} \ No newline at end of file diff --git a/my-plugins/cordova-plugin-local-notification/plugin.xml b/my-plugins/cordova-plugin-local-notification/plugin.xml new file mode 100644 index 0000000..072ed81 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/plugin.xml @@ -0,0 +1,249 @@ + + + + + + + LocalNotification + + Schedules and queries for local notifications + + appplant, notification, local notification, user notification + + Apache 2.0 + + rn0x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/my-plugins/cordova-plugin-local-notification/src/android/ClearReceiver.java b/my-plugins/cordova-plugin-local-notification/src/android/ClearReceiver.java new file mode 100644 index 0000000..255678f --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/ClearReceiver.java @@ -0,0 +1,42 @@ + + +package de.appplant.cordova.plugin.localnotification; + +import android.os.Bundle; + +import de.appplant.cordova.plugin.notification.Notification; +import de.appplant.cordova.plugin.notification.receiver.AbstractClearReceiver; + +import static de.appplant.cordova.plugin.localnotification.LocalNotification.fireEvent; +import static de.appplant.cordova.plugin.localnotification.LocalNotification.isAppRunning; +import static de.appplant.cordova.plugin.notification.Request.EXTRA_LAST; + +/** + * The clear intent receiver is triggered when the user clears a + * notification manually. It un-persists the cleared notification from the + * shared preferences. + */ +public class ClearReceiver extends AbstractClearReceiver { + + /** + * Called when a local notification was cleared from outside of the app. + * + * @param notification Wrapper around the local notification. + * @param bundle The bundled extras. + */ + @Override + public void onClear (Notification notification, Bundle bundle) { + boolean isLast = bundle.getBoolean(EXTRA_LAST, false); + + if (isLast) { + notification.cancel(); + } else { + notification.clear(); + } + + if (isAppRunning()) { + fireEvent("clear", notification); + } + } + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/ClickReceiver.java b/my-plugins/cordova-plugin-local-notification/src/android/ClickReceiver.java new file mode 100644 index 0000000..f3b8436 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/ClickReceiver.java @@ -0,0 +1,90 @@ + + +package de.appplant.cordova.plugin.localnotification; + +import android.os.Bundle; +import androidx.core.app.RemoteInput; + +import org.json.JSONException; +import org.json.JSONObject; + +import de.appplant.cordova.plugin.notification.Notification; +import de.appplant.cordova.plugin.notification.receiver.AbstractClickReceiver; +import de.appplant.cordova.plugin.notification.util.LaunchUtils; + +import static de.appplant.cordova.plugin.localnotification.LocalNotification.fireEvent; +import static de.appplant.cordova.plugin.notification.Options.EXTRA_LAUNCH; +import static de.appplant.cordova.plugin.notification.Request.EXTRA_LAST; + +/** + * The receiver activity is triggered when a notification is clicked by a user. + * The activity calls the background callback and brings the launch intent + * up to foreground. + */ +public class ClickReceiver extends AbstractClickReceiver { + + /** + * Called when local notification was clicked by the user. + * + * @param notification Wrapper around the local notification. + * @param bundle The bundled extras. + */ + @Override + public void onClick(Notification notification, Bundle bundle) { + String action = getAction(); + JSONObject data = new JSONObject(); + + setTextInput(action, data); + launchAppIf(); + + fireEvent(action, notification, data); + + if (notification.getOptions().isSticky()) + return; + + if (isLast()) { + notification.cancel(); + } else { + notification.clear(); + } + } + + /** + * Set the text if any remote input is given. + * + * @param action The action where to look for. + * @param data The object to extend. + */ + private void setTextInput(String action, JSONObject data) { + Bundle input = RemoteInput.getResultsFromIntent(getIntent()); + + if (input == null) + return; + + try { + data.put("text", input.getCharSequence(action)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + + /** + * Launch app if requested by user. + */ + private void launchAppIf() { + boolean doLaunch = getIntent().getBooleanExtra(EXTRA_LAUNCH, true); + + if (!doLaunch) + return; + + LaunchUtils.launchApp(getApplicationContext()); + } + + /** + * If the notification was the last scheduled one by request. + */ + private boolean isLast() { + return getIntent().getBooleanExtra(EXTRA_LAST, false); + } + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/LocalNotification.java b/my-plugins/cordova-plugin-local-notification/src/android/LocalNotification.java new file mode 100644 index 0000000..7091dc8 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/LocalNotification.java @@ -0,0 +1,811 @@ + + +// codebeat:disable[TOO_MANY_FUNCTIONS] + +package de.appplant.cordova.plugin.localnotification; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.app.KeyguardManager; +import android.app.NotificationManager; +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PermissionInfo; +import android.net.Uri; +import android.os.Bundle; +import android.os.PowerManager; +import android.provider.Settings; +import android.util.Pair; +import android.view.View; + +import android.app.NotificationManager; +import android.app.NotificationChannel; +import android.app.PendingIntent; +import androidx.core.app.NotificationCompat; +import android.content.Intent; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.PluginResult; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +import javax.security.auth.callback.Callback; + +import de.appplant.cordova.plugin.notification.Manager; +import de.appplant.cordova.plugin.notification.Notification; +import de.appplant.cordova.plugin.notification.Options; +import de.appplant.cordova.plugin.notification.Request; +import de.appplant.cordova.plugin.notification.action.ActionGroup; + +import static android.Manifest.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS; +import static android.content.Context.POWER_SERVICE; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.M; +import static de.appplant.cordova.plugin.notification.Notification.Type.SCHEDULED; +import static de.appplant.cordova.plugin.notification.Notification.Type.TRIGGERED; + +/** + * This plugin utilizes the Android AlarmManager in combination with local + * notifications. When a local notification is scheduled the alarm manager takes + * care of firing the event. When the event is processed, a notification is put + * in the Android notification center and status bar. + */ +@SuppressWarnings({ "Convert2Diamond", "Convert2Lambda" }) +public class LocalNotification extends CordovaPlugin { + + // Reference to the web view for static access + private static WeakReference webView = null; + + // Indicates if the device is ready (to receive events) + private static Boolean deviceready = false; + + // Queues all events before deviceready + private static ArrayList eventQueue = new ArrayList(); + + // Launch details + private static Pair launchDetails; + + private static int REQUEST_PERMISSIONS_CALL = 10; + + private static int REQUEST_IGNORE_BATTERY_CALL = 20; + + private CallbackContext callbackContext; + + /** + * Called after plugin construction and fields have been initialized. Prefer to + * use pluginInitialize instead since there is no value in having parameters on + * the initialize() function. + */ + @Override + public void initialize(CordovaInterface cordova, CordovaWebView webView) { + LocalNotification.webView = new WeakReference(webView); + } + + /** + * Called when the activity will start interacting with the user. + * + * @param multitasking Flag indicating if multitasking is turned on for app. + */ + @Override + public void onResume(boolean multitasking) { + super.onResume(multitasking); + deviceready(); + } + + /** + * The final call you receive before your activity is destroyed. + */ + @Override + public void onDestroy() { + deviceready = false; + } + + /** + * Executes the request. + * + * This method is called from the WebView thread. To do a non-trivial amount of + * work, use: cordova.getThreadPool().execute(runnable); + * + * To run on the UI thread, use: cordova.getActivity().runOnUiThread(runnable); + * + * @param action The action to execute. + * @param args The exec() arguments in JSON form. + * @param command The callback context used when calling back into JavaScript. + * + * @return Whether the action was valid. + */ + @Override + public boolean execute(final String action, final JSONArray args, final CallbackContext command) + throws JSONException { + + if (action.equals("launch")) { + launch(command); + return true; + } + + cordova.getThreadPool().execute(new Runnable() { + public void run() { + if (action.equals("ready")) { + deviceready(); + } else if (action.equals("check")) { + check(command); + } else if (action.equals("request")) { + request(command); + } else if (action.equals("actions")) { + actions(args, command); + } else if (action.equals("schedule")) { + schedule(args, command); + } else if (action.equals("update")) { + update(args, command); + } else if (action.equals("cancel")) { + cancel(args, command); + } else if (action.equals("cancelAll")) { + cancelAll(command); + } else if (action.equals("clear")) { + clear(args, command); + } else if (action.equals("clearAll")) { + clearAll(command); + } else if (action.equals("type")) { + type(args, command); + } else if (action.equals("ids")) { + ids(args, command); + } else if (action.equals("notification")) { + notification(args, command); + } else if (action.equals("notifications")) { + notifications(args, command); + } else if (action.equals("hasDoNotDisturbPermissions")) { + hasDoNotDisturbPermissions(command); + } else if (action.equals("requestDoNotDisturbPermissions")) { + requestDoNotDisturbPermissions(command); + } else if (action.equals("isIgnoringBatteryOptimizations")) { + isIgnoringBatteryOptimizations(command); + } else if (action.equals("requestIgnoreBatteryOptimizations")) { + requestIgnoreBatteryOptimizations(command); + } else if (action.equals("dummyNotifications")) { + dummyNotifications(command); + } + } + }); + + return true; + } + + /** + * Required for Android 13 to get the runtime notification permissions. + * + * @param command The callback context used when calling back into JavaScript. + */ + private void dummyNotifications(CallbackContext command) { + + fireEvent("dummyNotifications"); + NotificationManager mNotificationManager; + NotificationCompat.Builder mBuilder; + String NOTIFICATION_CHANNEL_ID = "10004457"; + String notificationMsg = "Test"; + String notificationTitle = "Mdd"; + Context context = cordova.getActivity().getApplicationContext(); + + Intent intentToLaunch = new Intent(context, TriggerReceiver.class); + intentToLaunch.putExtra("Callfrom", "reminders"); + + final PendingIntent resultPendingIntent = PendingIntent.getActivity(context, + 0, intentToLaunch, PendingIntent.FLAG_IMMUTABLE); + + mBuilder = new NotificationCompat.Builder(context,NOTIFICATION_CHANNEL_ID); + mBuilder.setContentIntent(resultPendingIntent); + + mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){ + int importance = NotificationManager.IMPORTANCE_HIGH; + NotificationChannel notificationChannel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, "NOTIFICATION_CHANNEL_NAME", importance); + assert mNotificationManager != null; + mBuilder.setChannelId(NOTIFICATION_CHANNEL_ID); + mNotificationManager.createNotificationChannel(notificationChannel); + } + + command.success(); + } + + /** + * Determine if do not disturb permissions have been granted + * + * @return true if we still need to acquire do not disturb permissions. + */ + private boolean needsDoNotDisturbPermissions() { + Context mContext = this.cordova.getActivity().getApplicationContext(); + + NotificationManager mNotificationManager = (NotificationManager) mContext + .getSystemService(Context.NOTIFICATION_SERVICE); + + return SDK_INT >= M && !mNotificationManager.isNotificationPolicyAccessGranted(); + } + + /** + * Determine if we have do not disturb permissions. + * + * @param command callback context. Returns with true if the we have + * permissions, false if we do not. + */ + private void hasDoNotDisturbPermissions(CallbackContext command) { + success(command, !needsDoNotDisturbPermissions()); + } + + /** + * Launch an activity to request do not disturb permissions + * + * @param command callback context. Returns with results of + * hasDoNotDisturbPermissions after the activity is closed. + */ + private void requestDoNotDisturbPermissions(CallbackContext command) { + if (needsDoNotDisturbPermissions()) { + this.callbackContext = command; + + PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT); + pluginResult.setKeepCallback(true); // Keep callback + command.sendPluginResult(pluginResult); + + Intent intent = new Intent(android.provider.Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS); + + cordova.startActivityForResult(this, intent, REQUEST_PERMISSIONS_CALL); + return; + } + success(command, true); + } + + /** + * Determine if do not battery optimization permissions have been granted + * + * @return true if we are succcessfully ignoring battery permissions. + */ + private boolean ignoresBatteryOptimizations() { + Context mContext = this.cordova.getActivity().getApplicationContext(); + PowerManager pm = (PowerManager) mContext.getSystemService(POWER_SERVICE); + + return SDK_INT <= M || pm.isIgnoringBatteryOptimizations(mContext.getPackageName()); + } + + /** + * Determine if we have do not disturb permissions. + * + * @param command callback context. Returns with true if the we have + * permissions, false if we do not. + */ + private void isIgnoringBatteryOptimizations(CallbackContext command) { + success(command, ignoresBatteryOptimizations()); + } + + /** + * Launch an activity to request do not disturb permissions + * + * @param command callback context. Returns with results of + * hasDoNotDisturbPermissions after the activity is closed. + */ + private void requestIgnoreBatteryOptimizations(CallbackContext command) { + if (!ignoresBatteryOptimizations()) { + this.callbackContext = command; + + PluginResult pluginResult = new PluginResult(PluginResult.Status.NO_RESULT); + pluginResult.setKeepCallback(true); // Keep callback + command.sendPluginResult(pluginResult); + + String packageName = this.cordova.getContext().getPackageName(); + String action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS; + + // use the generic intent if we don't have access to request ignore permissions + // directly + // User can add "REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" to the manifest, but + // risks having the app banned. + try { + PackageManager packageManager = this.cordova.getContext().getPackageManager(); + PackageInfo pi = packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS); + + for (int i = 0; i < pi.requestedPermissions.length; ++i) { + if (pi.requestedPermissions[i].equals(REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)) { + action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS; + } + } + } catch (PackageManager.NameNotFoundException e) { + // leave action as default if package not found + } + + try { + Intent intent = new Intent(action); + + intent.setData(Uri.parse("package:" + packageName)); + + cordova.startActivityForResult(this, intent, REQUEST_IGNORE_BATTERY_CALL); + } catch (ActivityNotFoundException e) { + // could not find the generic ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS + // and did not have access to launch REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + // Fallback to just figuring out if battery optimizations are removed (probably + // not) + // since we can't ask the user to set it, because we can't launch an activity. + isIgnoringBatteryOptimizations(command); + this.callbackContext = null; + } + + return; + } + success(command, true); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + if (requestCode == REQUEST_PERMISSIONS_CALL && this.callbackContext != null) { + hasDoNotDisturbPermissions(this.callbackContext); + + // clean up callback context. + this.callbackContext = null; + } else if (requestCode == REQUEST_IGNORE_BATTERY_CALL && this.callbackContext != null) { + isIgnoringBatteryOptimizations(this.callbackContext); + + this.callbackContext = null; + } + super.onActivityResult(requestCode, resultCode, data); + } + + /** + * Set launchDetails object. + * + * @param command The callback context used when calling back into JavaScript. + */ + @SuppressLint("DefaultLocale") + private void launch(CallbackContext command) { + if (launchDetails == null) + return; + + JSONObject details = new JSONObject(); + + try { + details.put("id", launchDetails.first); + details.put("action", launchDetails.second); + } catch (JSONException e) { + e.printStackTrace(); + } + + command.success(details); + + launchDetails = null; + } + + /** + * Ask if user has enabled permission for local notifications. + * + * @param command The callback context used when calling back into JavaScript. + */ + private void check(CallbackContext command) { + boolean allowed = getNotMgr().hasPermission(); + success(command, allowed); + } + + /** + * Request permission for local notifications. + * + * @param command The callback context used when calling back into JavaScript. + */ + private void request(CallbackContext command) { + check(command); + } + + /** + * Register action group. + * + * @param args The exec() arguments in JSON form. + * @param command The callback context used when calling back into JavaScript. + */ + private void actions(JSONArray args, CallbackContext command) { + int task = args.optInt(0); + String id = args.optString(1); + JSONArray list = args.optJSONArray(2); + Context context = cordova.getActivity(); + + switch (task) { + case 0: + ActionGroup group = ActionGroup.parse(context, id, list); + ActionGroup.register(group); + command.success(); + break; + case 1: + ActionGroup.unregister(id); + command.success(); + break; + case 2: + boolean found = ActionGroup.isRegistered(id); + success(command, found); + break; + } + } + + /** + * Schedule multiple local notifications. + * + * @param toasts The notifications to schedule. + * @param command The callback context used when calling back into JavaScript. + */ + private void schedule(JSONArray toasts, CallbackContext command) { + Manager mgr = getNotMgr(); + + for (int i = 0; i < toasts.length(); i++) { + JSONObject dict = toasts.optJSONObject(i); + Options options = new Options(dict); + Request request = new Request(options); + Notification toast = mgr.schedule(request, TriggerReceiver.class); + + if (toast != null) { + fireEvent("add", toast); + } + } + + check(command); + } + + /** + * Update multiple local notifications. + * + * @param updates Notification properties including their IDs. + * @param command The callback context used when calling back into JavaScript. + */ + private void update(JSONArray updates, CallbackContext command) { + Manager mgr = getNotMgr(); + + for (int i = 0; i < updates.length(); i++) { + JSONObject update = updates.optJSONObject(i); + int id = update.optInt("id", 0); + Notification toast = mgr.update(id, update, TriggerReceiver.class); + + if (toast == null) + continue; + + fireEvent("update", toast); + } + + check(command); + } + + /** + * Cancel multiple local notifications. + * + * @param ids Set of local notification IDs. + * @param command The callback context used when calling back into JavaScript. + */ + private void cancel(JSONArray ids, CallbackContext command) { + Manager mgr = getNotMgr(); + + for (int i = 0; i < ids.length(); i++) { + int id = ids.optInt(i, 0); + Notification toast = mgr.cancel(id); + + if (toast == null) + continue; + + fireEvent("cancel", toast); + } + + command.success(); + } + + /** + * Cancel all scheduled notifications. + * + * @param command The callback context used when calling back into JavaScript. + */ + private void cancelAll(CallbackContext command) { + getNotMgr().cancelAll(); + fireEvent("cancelall"); + command.success(); + } + + /** + * Clear multiple local notifications without canceling them. + * + * @param ids Set of local notification IDs. + * @param command The callback context used when calling back into JavaScript. + */ + private void clear(JSONArray ids, CallbackContext command) { + Manager mgr = getNotMgr(); + + for (int i = 0; i < ids.length(); i++) { + int id = ids.optInt(i, 0); + Notification toast = mgr.clear(id); + + if (toast == null) + continue; + + fireEvent("clear", toast); + } + + command.success(); + } + + /** + * Clear all triggered notifications without canceling them. + * + * @param command The callback context used when calling back into JavaScript. + */ + private void clearAll(CallbackContext command) { + getNotMgr().clearAll(); + fireEvent("clearall"); + command.success(); + } + + /** + * Get the type of the notification (unknown, scheduled, triggered). + * + * @param args The exec() arguments in JSON form. + * @param command The callback context used when calling back into JavaScript. + */ + private void type(JSONArray args, CallbackContext command) { + int id = args.optInt(0); + Notification toast = getNotMgr().get(id); + + if (toast == null) { + command.success("unknown"); + return; + } + + switch (toast.getType()) { + case SCHEDULED: + command.success("scheduled"); + break; + case TRIGGERED: + command.success("triggered"); + break; + default: + command.success("unknown"); + break; + } + } + + /** + * Set of IDs from all existent notifications. + * + * @param args The exec() arguments in JSON form. + * @param command The callback context used when calling back into JavaScript. + */ + private void ids(JSONArray args, CallbackContext command) { + int type = args.optInt(0); + Manager mgr = getNotMgr(); + List ids; + + switch (type) { + case 0: + ids = mgr.getIds(); + break; + case 1: + ids = mgr.getIdsByType(SCHEDULED); + break; + case 2: + ids = mgr.getIdsByType(TRIGGERED); + break; + default: + ids = new ArrayList(0); + break; + } + + command.success(new JSONArray(ids)); + } + + /** + * Options from local notification. + * + * @param args The exec() arguments in JSON form. + * @param command The callback context used when calling back into JavaScript. + */ + private void notification(JSONArray args, CallbackContext command) { + int id = args.optInt(0); + Options opts = getNotMgr().getOptions(id); + + if (opts != null) { + command.success(opts.getDict()); + } else { + command.success(); + } + } + + /** + * Set of options from local notification. + * + * @param args The exec() arguments in JSON form. + * @param command The callback context used when calling back into JavaScript. + */ + private void notifications(JSONArray args, CallbackContext command) { + int type = args.optInt(0); + JSONArray ids = args.optJSONArray(1); + Manager mgr = getNotMgr(); + List options; + + switch (type) { + case 0: + options = mgr.getOptions(); + break; + case 1: + options = mgr.getOptionsByType(SCHEDULED); + break; + case 2: + options = mgr.getOptionsByType(TRIGGERED); + break; + case 3: + options = mgr.getOptionsById(toList(ids)); + break; + default: + options = new ArrayList(0); + break; + } + + command.success(new JSONArray(options)); + } + + /** + * Call all pending callbacks after the deviceready event has been fired. + */ + private static synchronized void deviceready() { + deviceready = true; + + for (String js : eventQueue) { + sendJavascript(js); + } + + eventQueue.clear(); + } + + /** + * Invoke success callback with a single boolean argument. + * + * @param command The callback context used when calling back into JavaScript. + * @param arg The single argument to pass through. + */ + private void success(CallbackContext command, boolean arg) { + PluginResult result = new PluginResult(PluginResult.Status.OK, arg); + command.sendPluginResult(result); + } + + /** + * Fire given event on JS side. Does inform all event listeners. + * + * @param event The event name. + */ + private void fireEvent(String event) { + fireEvent(event, null, new JSONObject()); + } + + /** + * Fire given event on JS side. Does inform all event listeners. + * + * @param event The event name. + * @param notification Optional notification to pass with. + */ + static void fireEvent(String event, Notification notification) { + fireEvent(event, notification, new JSONObject()); + } + + /** + * Fire given event on JS side. Does inform all event listeners. + * + * @param event The event name. + * @param toast Optional notification to pass with. + * @param data Event object with additional data. + */ + static void fireEvent(String event, Notification toast, JSONObject data) { + String params, js; + + try { + data.put("event", event); + data.put("foreground", isInForeground()); + data.put("queued", !deviceready); + + if (toast != null) { + data.put("notification", toast.getId()); + } + } catch (JSONException e) { + e.printStackTrace(); + } + + if (toast != null) { + params = toast.toString() + "," + data.toString(); + } else { + params = data.toString(); + } + + js = "cordova.plugins.notification.local.fireEvent(" + "\"" + event + "\"," + params + ")"; + + if (launchDetails == null && !deviceready && toast != null) { + launchDetails = new Pair(toast.getId(), event); + } + + sendJavascript(js); + } + + /** + * Use this instead of deprecated sendJavascript + * + * @param js JS code snippet as string. + */ + private static synchronized void sendJavascript(final String js) { + + if (!deviceready || webView == null) { + eventQueue.add(js); + return; + } + + if (!deviceready || webView == null) { + eventQueue.add(js); + return; + } + + final CordovaWebView view = webView.get(); + + ((Activity) (view.getContext())).runOnUiThread(new Runnable() { + public void run() { + view.loadUrl("javascript:" + js); + View engineView = view.getEngine() != null ? view.getEngine().getView() : view.getView(); + + if (!isInForeground()) { + engineView.dispatchWindowVisibilityChanged(View.VISIBLE); + } + } + }); + } + + /** + * If the app is running in foreground. + */ + public static boolean isInForeground() { + + if (!deviceready || webView == null) + return false; + + CordovaWebView view = webView.get(); + + KeyguardManager km = (KeyguardManager) view.getContext().getSystemService(Context.KEYGUARD_SERVICE); + + // noinspection SimplifiableIfStatement + if (km != null && km.isKeyguardLocked()) + return false; + + return view.getView().getWindowVisibility() == View.VISIBLE; + } + + /** + * If the app is running. + */ + static boolean isAppRunning() { + return webView != null; + } + + /** + * Convert JSON array of integers to List. + * + * @param ary Array of integers. + */ + private List toList(JSONArray ary) { + List list = new ArrayList(); + + for (int i = 0; i < ary.length(); i++) { + list.add(ary.optInt(i)); + } + + return list; + } + + /** + * Notification manager instance. + */ + private Manager getNotMgr() { + return Manager.getInstance(cordova.getActivity()); + } + +} + +// codebeat:enable[TOO_MANY_FUNCTIONS] diff --git a/my-plugins/cordova-plugin-local-notification/src/android/RestoreReceiver.java b/my-plugins/cordova-plugin-local-notification/src/android/RestoreReceiver.java new file mode 100644 index 0000000..5c8791e --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/RestoreReceiver.java @@ -0,0 +1,74 @@ +package de.appplant.cordova.plugin.localnotification; + +import android.content.Context; +import android.util.Log; + +import java.util.Date; + +import de.appplant.cordova.plugin.notification.Builder; +import de.appplant.cordova.plugin.notification.Manager; +import de.appplant.cordova.plugin.notification.Notification; +import de.appplant.cordova.plugin.notification.Request; +import de.appplant.cordova.plugin.notification.receiver.AbstractRestoreReceiver; + +import static de.appplant.cordova.plugin.localnotification.LocalNotification.fireEvent; +import static de.appplant.cordova.plugin.localnotification.LocalNotification.isAppRunning; +import static de.appplant.cordova.plugin.localnotification.LocalNotification.isInForeground; + +/** + * This class is triggered upon reboot of the device. It needs to re-register + * the alarms with the AlarmManager since these alarms are lost in case of + * reboot. + */ +public class RestoreReceiver extends AbstractRestoreReceiver { + /** + * Called when a local notification need to be restored. + * + * @param request Set of notification options. + * @param toast Wrapper around the local notification. + */ + @Override + public void onRestore (Request request, Notification toast) { + Date date = request.getTriggerDate(); + boolean after = date != null && date.after(new Date()); + + if (!after && toast.isHighPrio()) { + performNotification(toast); + } else { + // reschedule if we aren't firing here. + // If we do fire, performNotification takes care of + // next schedule. + + Context ctx = toast.getContext(); + Manager mgr = Manager.getInstance(ctx); + + if (after || toast.isRepeating()) { + mgr.schedule(request, TriggerReceiver.class); + } + } + } + + @Override + public void dispatchAppEvent(String key, Notification notification) { + fireEvent(key, notification); + } + + @Override + public boolean checkAppRunning() { + return isAppRunning(); + } + + /** + * Build notification specified by options. + * + * @param builder Notification builder. + */ + @Override + public Notification buildNotification (Builder builder) { + return builder + .setClickActivity(ClickReceiver.class) + .setClearReceiver(ClearReceiver.class) + .build(); + } + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/TriggerReceiver.java b/my-plugins/cordova-plugin-local-notification/src/android/TriggerReceiver.java new file mode 100644 index 0000000..93b9785 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/TriggerReceiver.java @@ -0,0 +1,75 @@ + + +package de.appplant.cordova.plugin.localnotification; + +import android.content.Context; +import android.os.Bundle; +import android.os.PowerManager; + +import java.util.Calendar; + +import de.appplant.cordova.plugin.notification.Builder; +import de.appplant.cordova.plugin.notification.Manager; +import de.appplant.cordova.plugin.notification.Notification; +import de.appplant.cordova.plugin.notification.Options; +import de.appplant.cordova.plugin.notification.Request; +import de.appplant.cordova.plugin.notification.receiver.AbstractTriggerReceiver; +import de.appplant.cordova.plugin.notification.util.LaunchUtils; + +import static android.content.Context.POWER_SERVICE; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.LOLLIPOP; +import static android.os.Build.VERSION_CODES.O; +import static de.appplant.cordova.plugin.localnotification.LocalNotification.fireEvent; +import static de.appplant.cordova.plugin.localnotification.LocalNotification.isAppRunning; +import static de.appplant.cordova.plugin.localnotification.LocalNotification.isInForeground; +import static java.util.Calendar.MINUTE; + +import static android.os.Build.VERSION_CODES.P; + +/** + * The alarm receiver is triggered when a scheduled alarm is fired. This class + * reads the information in the intent and displays this information in the + * Android notification bar. The notification uses the default notification + * sound and it vibrates the phone. + */ +public class TriggerReceiver extends AbstractTriggerReceiver { + + /** + * Called when a local notification was triggered. Does present the local + * notification, re-schedule the alarm if necessary and fire trigger event. + * + * @param notification Wrapper around the local notification. + * @param bundle The bundled extras. + */ + @Override + public void onTrigger(Notification notification, Bundle bundle) { + performNotification(notification); + } + + @Override + public void dispatchAppEvent(String key, Notification notification) { + fireEvent(key, notification); + } + + @Override + public boolean checkAppRunning() { + return isAppRunning(); + } + + /** + * Build notification specified by options. + * + * @param builder Notification builder. + * @param bundle The bundled extras. + */ + @Override + public Notification buildNotification(Builder builder, Bundle bundle) { + return builder + .setClickActivity(ClickReceiver.class) + .setClearReceiver(ClearReceiver.class) + .setExtras(bundle) + .build(); + } + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/build/localnotification.gradle b/my-plugins/cordova-plugin-local-notification/src/android/build/localnotification.gradle new file mode 100644 index 0000000..b8c20fa --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/build/localnotification.gradle @@ -0,0 +1,32 @@ +/* + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apache License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://opensource.org/licenses/Apache-2.0/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + */ + +repositories { + mavenCentral() + jcenter() + maven { + url "https://maven.google.com" + } +} + +if (!project.ext.has('appShortcutBadgerVersion')) { + ext.appShortcutBadgerVersion = '1.1.22' +} + +dependencies { + implementation "me.leolin:ShortcutBadger:${appShortcutBadgerVersion}@aar" +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/Builder.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/Builder.java new file mode 100644 index 0000000..15a0335 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/Builder.java @@ -0,0 +1,484 @@ + + +package de.appplant.cordova.plugin.notification; + +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.net.Uri; +import android.os.Bundle; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationCompat.MessagingStyle.Message; +import androidx.media.app.NotificationCompat.MediaStyle; +import android.support.v4.media.session.MediaSessionCompat; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.graphics.PorterDuffXfermode; +import android.graphics.Rect; +import android.graphics.Paint; +import android.graphics.Canvas; + +import java.util.List; + +import de.appplant.cordova.plugin.notification.action.Action; +import de.appplant.cordova.plugin.notification.util.LaunchUtils; + +import static de.appplant.cordova.plugin.notification.Notification.EXTRA_UPDATE; + +/** + * Builder class for local notifications. Build fully configured local + * notification specified by JSON object passed from JS side. + */ +public final class Builder { + + // Application context passed by constructor + private final Context context; + + // Notification options passed by JS + private final Options options; + + // Receiver to handle the clear event + private Class clearReceiver; + + // Activity to handle the click event + private Class clickActivity; + + // Additional extras to merge into each intent + private Bundle extras; + + /** + * Constructor + * + * @param options Notification options + */ + public Builder(Options options) { + this.context = options.getContext(); + this.options = options; + } + + /** + * Set clear receiver. + * + * @param receiver Broadcast receiver for the clear event. + */ + public Builder setClearReceiver(Class receiver) { + this.clearReceiver = receiver; + return this; + } + + /** + * Set click activity. + * + * @param activity The activity to handler the click event. + */ + public Builder setClickActivity(Class activity) { + this.clickActivity = activity; + return this; + } + + /** + * Set bundle extras. + * + * @param extras The bundled extras to merge into. + */ + public Builder setExtras(Bundle extras) { + this.extras = extras; + return this; + } + + /** + * Creates the notification with all its options passed through JS. + * + * @return The final notification to display. + */ + public Notification build() { + NotificationCompat.Builder builder; + + if (options.isSilent()) { + return new Notification(context, options); + } + + Uri sound = options.getSound(); + Bundle extras = new Bundle(); + + extras.putInt(Notification.EXTRA_ID, options.getId()); + extras.putString(Options.EXTRA_SOUND, sound.toString()); + + builder = findOrCreateBuilder() + .setDefaults(options.getDefaults()) + .setExtras(extras) + .setOnlyAlertOnce(false) + .setChannelId(options.getChannel()) + .setContentTitle(options.getTitle()) + .setContentText(options.getText()) + .setTicker(options.getText()) + .setNumber(options.getNumber()) + .setAutoCancel(options.isAutoClear()) + .setOngoing(options.isSticky()) + .setColor(options.getColor()) + .setVisibility(options.getVisibility()) + .setPriority(options.getPrio()) + .setShowWhen(options.showClock()) + .setUsesChronometer(options.showChronometer()) + .setGroup(options.getGroup()) + .setGroupSummary(options.getGroupSummary()) + .setTimeoutAfter(options.getTimeout()) + .setLights(options.getLedColor(), options.getLedOn(), options.getLedOff()); + + if (!sound.equals(Uri.EMPTY) && !isUpdate()) { + builder.setSound(sound); + } + + // API < 26. Setting sound to null will prevent playing if we have no sound for any reason, + // including a 0 volume. + if (options.isWithoutSound()) { + builder.setSound(null); + } + + if (options.isWithProgressBar()) { + builder.setProgress( + options.getProgressMaxValue(), + options.getProgressValue(), + options.isIndeterminateProgress()); + } + + if (options.hasLargeIcon()) { + builder.setSmallIcon(options.getSmallIcon()); + + Bitmap largeIcon = options.getLargeIcon(); + + if (options.getLargeIconType().equals("circle")) { + largeIcon = getCircleBitmap(largeIcon); + } + + builder.setLargeIcon(largeIcon); + } else { + builder.setSmallIcon(options.getSmallIcon()); + } + + if (options.useFullScreenIntent()) { + applyFullScreenIntent(builder); + } + + applyStyle(builder); + applyActions(builder); + applyDeleteReceiver(builder); + applyContentReceiver(builder); + + return new Notification(context, options, builder); + } + + void applyFullScreenIntent(NotificationCompat.Builder builder) { + String pkgName = context.getPackageName(); + + int notificationId = options.getId(); + Intent intent = context + .getPackageManager() + .getLaunchIntentForPackage(pkgName) + .putExtra("launchNotificationId", notificationId); + + PendingIntent pendingIntent = + LaunchUtils.getActivityPendingIntent(context, intent, notificationId); + builder.setFullScreenIntent(pendingIntent, true); + } + + /** + * Convert a bitmap to a circular bitmap. + * This code has been extracted from the Phonegap Plugin Push plugin: + * https://github.com/phonegap/phonegap-plugin-push + * + * @param bitmap Bitmap to convert. + * @return Circular bitmap. + */ + private Bitmap getCircleBitmap(Bitmap bitmap) { + if (bitmap == null) { + return null; + } + + final Bitmap output = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); + final Canvas canvas = new Canvas(output); + final int color = Color.RED; + final Paint paint = new Paint(); + final Rect rect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); + //final RectF rectF = new RectF(rect); + + paint.setAntiAlias(true); + canvas.drawARGB(0, 0, 0, 0); + paint.setColor(color); + float cx = bitmap.getWidth() / 2.0f; + float cy = bitmap.getHeight() / 2.0f; + float radius = Math.min(cx, cy); + canvas.drawCircle(cx, cy, radius, paint); + + paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_IN)); + canvas.drawBitmap(bitmap, rect, rect, paint); + + bitmap.recycle(); + + return output; + } + + /** + * Find out and set the notification style. + * + * @param builder Local notification builder instance. + */ + private void applyStyle(NotificationCompat.Builder builder) { + Message[] messages = options.getMessages(); + String summary = options.getSummary(); + + if (messages != null) { + applyMessagingStyle(builder, messages); + return; + } + + MediaSessionCompat.Token token = options.getMediaSessionToken(); + + if (token != null) { + applyMediaStyle(builder, token); + return; + } + + List pics = options.getAttachments(); + + if (pics.size() > 0) { + applyBigPictureStyle(builder, pics); + return; + } + + String text = options.getText(); + + if (text != null && text.contains("\n")) { + applyInboxStyle(builder); + return; + } + + if (text == null || summary == null && text.length() < 45) + return; + + applyBigTextStyle(builder); + } + + /** + * Apply inbox style. + * + * @param builder Local notification builder instance. + * @param messages The messages to add to the conversation. + */ + private void applyMessagingStyle(NotificationCompat.Builder builder, + Message[] messages) { + + NotificationCompat.MessagingStyle style; + + style = new NotificationCompat.MessagingStyle("Me") + .setConversationTitle(options.getTitle()); + + for (Message msg : messages) { + style.addMessage(msg); + } + + builder.setStyle(style); + } + + /** + * Apply inbox style. + * + * @param builder Local notification builder instance. + * @param pics The pictures to show. + */ + private void applyBigPictureStyle(NotificationCompat.Builder builder, + List pics) { + + NotificationCompat.BigPictureStyle style; + String summary = options.getSummary(); + String text = options.getText(); + + style = new NotificationCompat.BigPictureStyle(builder) + .setSummaryText(summary == null ? text : summary) + .bigPicture(pics.get(0)); + + builder.setStyle(style); + } + + /** + * Apply inbox style. + * + * @param builder Local notification builder instance. + */ + private void applyInboxStyle(NotificationCompat.Builder builder) { + NotificationCompat.InboxStyle style; + String text = options.getText(); + + style = new NotificationCompat.InboxStyle(builder) + .setSummaryText(options.getSummary()); + + for (String line : text.split("\n")) { + style.addLine(line); + } + + builder.setStyle(style); + } + + /** + * Apply big text style. + * + * @param builder Local notification builder instance. + */ + private void applyBigTextStyle(NotificationCompat.Builder builder) { + NotificationCompat.BigTextStyle style; + + style = new NotificationCompat.BigTextStyle(builder) + .setSummaryText(options.getSummary()) + .bigText(options.getText()); + + builder.setStyle(style); + } + + /** + * Apply media style. + * + * @param builder Local notification builder instance. + * @param token The media session token. + */ + private void applyMediaStyle(NotificationCompat.Builder builder, + MediaSessionCompat.Token token) { + MediaStyle style; + + style = new MediaStyle(builder) + .setMediaSession(token) + .setShowActionsInCompactView(1); + + builder.setStyle(style); + } + + /** + * Set intent to handle the delete event. Will clean up some persisted + * preferences. + * + * @param builder Local notification builder instance. + */ + private void applyDeleteReceiver(NotificationCompat.Builder builder) { + + if (clearReceiver == null) + return; + + int notificationId = options.getId(); + Intent intent = new Intent(context, clearReceiver) + .setAction(options.getIdentifier()) + .putExtra(Notification.EXTRA_ID, notificationId); + + if (extras != null) { + intent.putExtras(extras); + } + + PendingIntent deleteIntent = + LaunchUtils.getBroadcastPendingIntent(context, intent, notificationId); + builder.setDeleteIntent(deleteIntent); + } + + /** + * Set intent to handle the click event. Will bring the app to + * foreground. + * + * @param builder Local notification builder instance. + */ + private void applyContentReceiver(NotificationCompat.Builder builder) { + + if (clickActivity == null) + return; + + Action[] actions = options.getActions(); + if (actions != null && actions.length > 0 ) { + // if actions are defined, the user must click on button actions to launch the app. + // Don't make the notification clickable in this case + return; + } + + int notificationId = options.getId(); + Intent intent = new Intent(context, clickActivity) + .putExtra(Notification.EXTRA_ID, notificationId) + .putExtra(Action.EXTRA_ID, Action.CLICK_ACTION_ID) + .putExtra(Options.EXTRA_LAUNCH, options.isLaunchingApp()) + .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + + if (extras != null) { + intent.putExtras(extras); + } + + PendingIntent contentIntent = + LaunchUtils.getTaskStackPendingIntent(context, intent, notificationId); + builder.setContentIntent(contentIntent); + } + + /** + * Add all actions to the builder if there are any actions. + * + * @param builder Local notification builder instance. + */ + private void applyActions (NotificationCompat.Builder builder) { + Action[] actions = options.getActions(); + NotificationCompat.Action.Builder btn; + + if (actions == null || actions.length == 0) + return; + + for (Action action : actions) { + btn = new NotificationCompat.Action.Builder( + action.getIcon(), action.getTitle(), + getPendingIntentForAction(action)); + + if (action.isWithInput()) { + btn.addRemoteInput(action.getInput()); + } + + builder.addAction(btn.build()); + } + } + + /** + * Returns a new PendingIntent for a notification action, including the + * action's identifier. + * + * @param action Notification action needing the PendingIntent + */ + private PendingIntent getPendingIntentForAction (Action action) { + int notificationId = options.getId(); + Intent intent = new Intent(context, clickActivity) + .putExtra(Notification.EXTRA_ID, notificationId) + .putExtra(Action.EXTRA_ID, action.getId()) + .putExtra(Options.EXTRA_LAUNCH, action.isLaunchingApp()) + .setFlags(Intent.FLAG_ACTIVITY_NO_HISTORY); + + if (extras != null) { + intent.putExtras(extras); + } + + return LaunchUtils.getTaskStackPendingIntent(context, intent, notificationId); + } + + /** + * If the builder shall build an notification or an updated version. + * + * @return true in case of an updated version. + */ + private boolean isUpdate() { + return extras != null + && extras.getBoolean(EXTRA_UPDATE, false); + } + + /** + * Returns a cached builder instance or creates a new one. + */ + private NotificationCompat.Builder findOrCreateBuilder() { + int key = options.getId(); + NotificationCompat.Builder builder = Notification.getCachedBuilder(key); + + if (builder == null) { + builder = new NotificationCompat.Builder(context, options.getChannel()); + } + + return builder; + } + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/Manager.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/Manager.java new file mode 100644 index 0000000..6cce98d --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/Manager.java @@ -0,0 +1,449 @@ + + +// codebeat:disable[TOO_MANY_FUNCTIONS] + +package de.appplant.cordova.plugin.notification; + +import android.app.NotificationChannel; +import android.app.NotificationManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.media.AudioAttributes; +import android.net.Uri; +import android.service.notification.StatusBarNotification; +import androidx.core.app.NotificationManagerCompat; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import de.appplant.cordova.plugin.badge.BadgeImpl; + +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.M; +import static android.os.Build.VERSION_CODES.O; +import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_DEFAULT; +import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_HIGH; +import static androidx.core.app.NotificationManagerCompat.IMPORTANCE_LOW; +import static de.appplant.cordova.plugin.notification.Notification.PREF_KEY_ID; +import static de.appplant.cordova.plugin.notification.Notification.Type.TRIGGERED; + +/** + * Central way to access all or single local notifications set by specific state + * like triggered or scheduled. Offers shortcut ways to schedule, cancel or + * clear local notifications. + */ +public final class Manager { + // The application context + private Context context; + + /** + * Constructor + * + * @param context Application context + */ + private Manager(Context context) { + this.context = context; + } + + /** + * Static method to retrieve class instance. + * + * @param context Application context + */ + public static Manager getInstance(Context context) { + return new Manager(context); + } + + /** + * Check if app has local notification permission. + */ + public boolean hasPermission() { + return getNotCompMgr().areNotificationsEnabled(); + } + + /** + * Schedule local notification specified by request. + * + * @param request Set of notification options. + * @param receiver Receiver to handle the trigger event. + */ + public Notification schedule(Request request, Class receiver) { + Options options = request.getOptions(); + Notification toast = new Notification(context, options); + + toast.schedule(request, receiver); + + return toast; + } + + /** + * Build channel with options + * + * @param soundUri Uri for custom sound (empty to use default) + * @param shouldVibrate whether not vibration should occur during the + * notification + * @param hasSound whether or not sound should play during the notification + * @param channelName the name of the channel (null will pick an appropriate + * default name for the options provided). + * @return channel ID of newly created (or reused) channel + */ + public String buildChannelWithOptions(Uri soundUri, boolean shouldVibrate, boolean hasSound, + CharSequence channelName, String channelId) { + String defaultChannelId, newChannelId; + CharSequence defaultChannelName; + int importance; + + if (hasSound && shouldVibrate) { + defaultChannelId = Options.SOUND_VIBRATE_CHANNEL_ID; + defaultChannelName = Options.SOUND_VIBRATE_CHANNEL_NAME; + importance = IMPORTANCE_HIGH; + shouldVibrate = true; + } else if (hasSound) { + defaultChannelId = Options.SOUND_CHANNEL_ID; + defaultChannelName = Options.SOUND_CHANNEL_NAME; + importance = IMPORTANCE_DEFAULT; + shouldVibrate = false; + } else if (shouldVibrate) { + defaultChannelId = Options.VIBRATE_CHANNEL_ID; + defaultChannelName = Options.VIBRATE_CHANNEL_NAME; + importance = IMPORTANCE_LOW; + shouldVibrate = true; + } else { + defaultChannelId = Options.SILENT_CHANNEL_ID; + defaultChannelName = Options.SILENT_CHANNEL_NAME; + importance = IMPORTANCE_LOW; + shouldVibrate = false; + } + + newChannelId = channelId != null ? channelId : defaultChannelId; + + createChannel(newChannelId, channelName != null ? channelName : defaultChannelName, importance, shouldVibrate, + soundUri); + + return newChannelId; + } + + /** + * Create a channel + */ + public void createChannel(String channelId, CharSequence channelName, int importance, Boolean shouldVibrate, + Uri soundUri) { + NotificationManager mgr = getNotMgr(); + + if (SDK_INT < O) + return; + + NotificationChannel channel = mgr.getNotificationChannel(channelId); + + if (channel != null) + return; + + channel = new NotificationChannel(channelId, channelName, importance); + + channel.enableVibration(shouldVibrate); + + if (!soundUri.equals(Uri.EMPTY)) { + AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build(); + channel.setSound(soundUri, attributes); + } + + mgr.createNotificationChannel(channel); + } + + /** + * Update local notification specified by ID. + * + * @param id The notification ID. + * @param updates JSON object with notification options. + * @param receiver Receiver to handle the trigger event. + */ + public Notification update(int id, JSONObject updates, Class receiver) { + Notification notification = get(id); + + if (notification == null) + return null; + + notification.update(updates, receiver); + + return notification; + } + + /** + * Clear local notification specified by ID. + * + * @param id The notification ID. + */ + public Notification clear(int id) { + Notification toast = get(id); + + if (toast != null) { + toast.clear(); + } + + return toast; + } + + /** + * Clear all local notifications. + */ + public void clearAll() { + List toasts = getByType(TRIGGERED); + + for (Notification toast : toasts) { + toast.clear(); + } + + getNotCompMgr().cancelAll(); + setBadge(0); + } + + /** + * Clear local notification specified by ID. + * + * @param id The notification ID + */ + public Notification cancel(int id) { + Notification toast = get(id); + + if (toast != null) { + toast.cancel(); + } + + return toast; + } + + /** + * Cancel all local notifications. + */ + public void cancelAll() { + List notifications = getAll(); + + for (Notification notification : notifications) { + notification.cancel(); + } + + getNotCompMgr().cancelAll(); + setBadge(0); + } + + /** + * All local notifications IDs. + */ + public List getIds() { + Set keys = getPrefs().getAll().keySet(); + List ids = new ArrayList(); + + for (String key : keys) { + try { + ids.add(Integer.parseInt(key)); + } catch (NumberFormatException e) { + e.printStackTrace(); + } + } + + return ids; + } + + /** + * All local notification IDs for given type. + * + * @param type The notification life cycle type + */ + public List getIdsByType(Notification.Type type) { + + if (type == Notification.Type.ALL) + return getIds(); + + StatusBarNotification[] activeToasts = getActiveNotifications(); + List activeIds = new ArrayList(); + + for (StatusBarNotification toast : activeToasts) { + activeIds.add(toast.getId()); + } + + if (type == TRIGGERED) + return activeIds; + + List ids = getIds(); + ids.removeAll(activeIds); + + return ids; + } + + /** + * List of local notifications with matching ID. + * + * @param ids Set of notification IDs. + */ + private List getByIds(List ids) { + List toasts = new ArrayList(); + + for (int id : ids) { + Notification toast = get(id); + + if (toast != null) { + toasts.add(toast); + } + } + + return toasts; + } + + /** + * List of all local notification. + */ + public List getAll() { + return getByIds(getIds()); + } + + /** + * List of local notifications from given type. + * + * @param type The notification life cycle type + */ + private List getByType(Notification.Type type) { + + if (type == Notification.Type.ALL) + return getAll(); + + List ids = getIdsByType(type); + + return getByIds(ids); + } + + /** + * List of properties from all local notifications. + */ + public List getOptions() { + return getOptionsById(getIds()); + } + + /** + * List of properties from local notifications with matching ID. + * + * @param ids Set of notification IDs + */ + public List getOptionsById(List ids) { + List toasts = new ArrayList(); + + for (int id : ids) { + Options options = getOptions(id); + + if (options != null) { + toasts.add(options.getDict()); + } + } + + return toasts; + } + + /** + * List of properties from all local notifications from given type. + * + * @param type The notification life cycle type + */ + public List getOptionsByType(Notification.Type type) { + ArrayList options = new ArrayList(); + List notifications = getByType(type); + + for (Notification notification : notifications) { + options.add(notification.getOptions().getDict()); + } + + return options; + } + + /** + * Get local notification options. + * + * @param id Notification ID. + * + * @return null if could not found. + */ + public Options getOptions(int id) { + SharedPreferences prefs = getPrefs(); + String toastId = Integer.toString(id); + + if (!prefs.contains(toastId)) + return null; + + try { + String json = prefs.getString(toastId, null); + JSONObject dict = new JSONObject(json); + + return new Options(context, dict); + } catch (JSONException e) { + e.printStackTrace(); + return null; + } + } + + /** + * Get existent local notification. + * + * @param id Notification ID. + * + * @return null if could not found. + */ + public Notification get(int id) { + Options options = getOptions(id); + + if (options == null) + return null; + + return new Notification(context, options); + } + + /** + * Set the badge number of the app icon. + * + * @param badge The badge number. + */ + public void setBadge(int badge) { + if (badge == 0) { + new BadgeImpl(context).clearBadge(); + } else { + new BadgeImpl(context).setBadge(badge); + } + } + + /** + * Get all active status bar notifications. + */ + StatusBarNotification[] getActiveNotifications() { + if (SDK_INT >= M) { + return getNotMgr().getActiveNotifications(); + } else { + return new StatusBarNotification[0]; + } + } + + /** + * Shared private preferences for the application. + */ + private SharedPreferences getPrefs() { + return context.getSharedPreferences(PREF_KEY_ID, Context.MODE_PRIVATE); + } + + /** + * Notification manager for the application. + */ + private NotificationManager getNotMgr() { + return (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + } + + /** + * Notification compat manager for the application. + */ + private NotificationManagerCompat getNotCompMgr() { + return NotificationManagerCompat.from(context); + } + +} + +// codebeat:enable[TOO_MANY_FUNCTIONS] diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/Notification.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/Notification.java new file mode 100644 index 0000000..5b2d10d --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/Notification.java @@ -0,0 +1,469 @@ + + +package de.appplant.cordova.plugin.notification; + +import android.app.AlarmManager; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.service.notification.StatusBarNotification; +import android.util.Pair; +import android.util.Log; +import android.util.SparseArray; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.Set; +import androidx.collection.ArraySet; +import androidx.core.app.NotificationCompat; + +import static android.app.AlarmManager.RTC; +import static android.app.AlarmManager.RTC_WAKEUP; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.M; +import static androidx.core.app.NotificationCompat.PRIORITY_HIGH; +import static androidx.core.app.NotificationCompat.PRIORITY_MAX; +import static androidx.core.app.NotificationCompat.PRIORITY_MIN; + +import de.appplant.cordova.plugin.notification.util.LaunchUtils; + +/** + * Wrapper class around OS notification class. Handles basic operations + * like show, delete, cancel for a single local notification instance. + */ +public final class Notification { + + // Used to differ notifications by their life cycle state + public enum Type { + ALL, SCHEDULED, TRIGGERED + } + + // Extra key for the id + public static final String EXTRA_ID = "NOTIFICATION_ID"; + + // Extra key for the update flag + public static final String EXTRA_UPDATE = "NOTIFICATION_UPDATE"; + + // Key for private preferences + static final String PREF_KEY_ID = "NOTIFICATION_ID"; + + // Key for private preferences + private static final String PREF_KEY_PID = "NOTIFICATION_PID"; + + // Cache for the builder instances + private static SparseArray cache = null; + + // Application context passed by constructor + private final Context context; + + // Notification options passed by JS + private final Options options; + + // Builder with full configuration + private final NotificationCompat.Builder builder; + + /** + * Constructor + * + * @param context Application context. + * @param options Parsed notification options. + * @param builder Pre-configured notification builder. + */ + Notification(Context context, Options options, NotificationCompat.Builder builder) { + this.context = context; + this.options = options; + this.builder = builder; + } + + /** + * Constructor + * + * @param context Application context. + * @param options Parsed notification options. + */ + public Notification(Context context, Options options) { + this.context = context; + this.options = options; + this.builder = null; + } + + /** + * Get application context. + */ + public Context getContext() { + return context; + } + + /** + * Get notification options. + */ + public Options getOptions() { + return options; + } + + /** + * Get notification ID. + */ + public int getId() { + return options.getId(); + } + + /** + * If it's a repeating notification. + */ + public boolean isRepeating() { + return getOptions().getTrigger().has("every"); + } + + /** + * If the notifications priority is high or above. + */ + public boolean isHighPrio() { + return getOptions().getPrio() >= PRIORITY_HIGH; + } + + /** + * Notification type can be one of triggered or scheduled. + */ + public Type getType() { + Manager mgr = Manager.getInstance(context); + StatusBarNotification[] toasts = mgr.getActiveNotifications(); + int id = getId(); + + for (StatusBarNotification toast : toasts) { + if (toast.getId() == id) { + return Type.TRIGGERED; + } + } + + return Type.SCHEDULED; + } + + /** + * Schedule the local notification. + * + * @param request Set of notification options. + * @param receiver Receiver to handle the trigger event. + */ + void schedule(Request request, Class receiver) { + List> intents = new ArrayList>(); + Set ids = new ArraySet(); + AlarmManager mgr = getAlarmMgr(); + + cancelScheduledAlarms(); + + do { + Date date = request.getTriggerDate(); + + Log.d("local-notification", "Next trigger at: " + date); + + if (date == null) + continue; + + Intent intent = new Intent(context, receiver) + .setAction(PREF_KEY_ID + request.getIdentifier()) + .putExtra(Notification.EXTRA_ID, options.getId()) + .putExtra(Request.EXTRA_OCCURRENCE, request.getOccurrence()); + + ids.add(intent.getAction()); + intents.add(new Pair(date, intent)); + } while (request.moveNext()); + + if (intents.isEmpty()) { + unpersist(); + return; + } + + persist(ids); + + if (!options.isInfiniteTrigger()) { + Intent last = intents.get(intents.size() - 1).second; + last.putExtra(Request.EXTRA_LAST, true); + } + + for (Pair pair : intents) { + Date date = pair.first; + long time = date.getTime(); + Intent intent = pair.second; + + if (!date.after(new Date()) && trigger(intent, receiver)) + continue; + + int notificationId = options.getId(); + PendingIntent pi = LaunchUtils.getBroadcastPendingIntent(context, intent, notificationId); + + try { + int currentApiVersion = android.os.Build.VERSION.SDK_INT; + + if (currentApiVersion >= android.os.Build.VERSION_CODES.N) { + // استخدام setInexactRepeating() على أنظمة Android 14 وأحدث + mgr.setInexactRepeating(AlarmManager.RTC_WAKEUP, time, AlarmManager.INTERVAL_DAY, pi); + } else { + // استخدام setExact() على أنظمة Android الأقدم من 14 + mgr.setExact(RTC_WAKEUP, time, pi); + } + } catch (Exception ignore) { + // Samsung devices have a known bug where a 500 alarms limit + // can crash the app + } + } + } + + /** + * Trigger local notification specified by options. + * + * @param intent The intent to broadcast. + * @param cls The broadcast class. + * + * @return false if the receiver could not be invoked. + */ + private boolean trigger(Intent intent, Class cls) { + BroadcastReceiver receiver; + + try { + receiver = (BroadcastReceiver) cls.newInstance(); + } catch (InstantiationException e) { + return false; + } catch (IllegalAccessException e) { + return false; + } + + receiver.onReceive(context, intent); + return true; + } + + /** + * Clear the local notification without canceling repeating alarms. + */ + public void clear() { + getNotMgr().cancel(getId()); + if (isRepeating()) + return; + unpersist(); + } + + /** + * Cancel the local notification. + */ + public void cancel() { + cancelScheduledAlarms(); + unpersist(); + getNotMgr().cancel(getId()); + clearCache(); + } + + /** + * Cancel the scheduled future local notification. + * + * Create an intent that looks similar, to the one that was registered + * using schedule. Making sure the notification id in the action is the + * same. Now we can search for such an intent using the 'getService' + * method and cancel it. + */ + private void cancelScheduledAlarms() { + SharedPreferences prefs = getPrefs(PREF_KEY_PID); + String id = options.getIdentifier(); + int notificationId = options.getId(); + Set actions = prefs.getStringSet(id, null); + + if (actions == null) + return; + + for (String action : actions) { + Intent intent = new Intent(action); + PendingIntent pi = LaunchUtils.getBroadcastPendingIntent(context, intent, notificationId); + if (pi != null) { + getAlarmMgr().cancel(pi); + } + } + } + + /** + * Present the local notification to user. + */ + public void show() { + if (builder == null) + return; + + if (options.showChronometer()) { + cacheBuilder(); + } + + grantPermissionToPlaySoundFromExternal(); + new NotificationVolumeManager(context, options) + .adjustAlarmVolume(); + getNotMgr().notify(getId(), builder.build()); + } + + /** + * Update the notification properties. + * + * @param updates The properties to update. + * @param receiver Receiver to handle the trigger event. + */ + void update(JSONObject updates, Class receiver) { + mergeJSONObjects(updates); + persist(null); + + if (getType() != Type.TRIGGERED) + return; + + Intent intent = new Intent(context, receiver) + .setAction(PREF_KEY_ID + options.getId()) + .putExtra(Notification.EXTRA_ID, options.getId()) + .putExtra(Notification.EXTRA_UPDATE, true); + + trigger(intent, receiver); + } + + /** + * Encode options to JSON. + */ + public String toString() { + JSONObject dict = options.getDict(); + JSONObject json = new JSONObject(); + + try { + json = new JSONObject(dict.toString()); + } catch (JSONException e) { + e.printStackTrace(); + } + + return json.toString(); + } + + /** + * Persist the information of this notification to the Android Shared + * Preferences. This will allow the application to restore the notification + * upon device reboot, app restart, retrieve notifications, aso. + * + * @param ids List of intent actions to persist. + */ + private void persist(Set ids) { + String id = options.getIdentifier(); + SharedPreferences.Editor editor; + + editor = getPrefs(PREF_KEY_ID).edit(); + editor.putString(id, options.toString()); + editor.apply(); + + if (ids == null) + return; + + editor = getPrefs(PREF_KEY_PID).edit(); + editor.putStringSet(id, ids); + editor.apply(); + } + + /** + * Remove the notification from the Android shared Preferences. + */ + private void unpersist() { + String[] keys = { PREF_KEY_ID, PREF_KEY_PID }; + String id = options.getIdentifier(); + SharedPreferences.Editor editor; + + for (String key : keys) { + editor = getPrefs(key).edit(); + editor.remove(id); + editor.apply(); + } + } + + /** + * Since Android 7 the app will crash if an external process has no + * permission to access the referenced sound file. + */ + private void grantPermissionToPlaySoundFromExternal() { + if (builder == null) + return; + + String sound = builder.getExtras().getString(Options.EXTRA_SOUND); + Uri soundUri = Uri.parse(sound); + + context.grantUriPermission( + "com.android.systemui", soundUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + + /** + * Merge two JSON objects. + */ + private void mergeJSONObjects(JSONObject updates) { + JSONObject dict = options.getDict(); + Iterator it = updates.keys(); + + while (it.hasNext()) { + try { + String key = (String) it.next(); + dict.put(key, updates.opt(key)); + } catch (JSONException e) { + e.printStackTrace(); + } + } + } + + /** + * Caches the builder instance so it can be used later. + */ + private void cacheBuilder() { + + if (cache == null) { + cache = new SparseArray(); + } + + cache.put(getId(), builder); + } + + /** + * Find the cached builder instance. + * + * @param key The key under where to look for the builder. + * + * @return null if no builder instance could be found. + */ + static NotificationCompat.Builder getCachedBuilder(int key) { + return (cache != null) ? cache.get(key) : null; + } + + /** + * Caches the builder instance so it can be used later. + */ + private void clearCache() { + if (cache != null) { + cache.delete(getId()); + } + } + + /** + * Shared private preferences for the application. + */ + private SharedPreferences getPrefs(String key) { + return context.getSharedPreferences(key, Context.MODE_PRIVATE); + } + + /** + * Notification manager for the application. + */ + private NotificationManager getNotMgr() { + return (NotificationManager) context + .getSystemService(Context.NOTIFICATION_SERVICE); + } + + /** + * Alarm manager for the application. + */ + private AlarmManager getAlarmMgr() { + return (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + } + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/NotificationVolumeManager.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/NotificationVolumeManager.java new file mode 100644 index 0000000..7fb77c3 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/NotificationVolumeManager.java @@ -0,0 +1,233 @@ +package de.appplant.cordova.plugin.notification; + +import android.annotation.SuppressLint; +import android.app.NotificationManager; +import android.content.Context; +import android.content.SharedPreferences; +import android.media.AudioManager; +import android.util.Log; + +import java.util.Timer; +import java.util.TimerTask; + +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.M; +import static java.lang.Thread.sleep; + +/** + * Class to handle all notification volume changes + */ +public class NotificationVolumeManager { + /** + * Amount of time to sleep while polling to see if all volume writers are closed. + */ + final private int VOLUME_WRITER_POLLING_DURATION = 200; + + /** + * Key for volume writer counter in shared preferences + */ + final private String VOLUME_CONFIG_WRITER_COUNT_KEY = "volumeConfigWriterCount"; + + /** + * Tag for logs + */ + final String TAG = "NotificationVolumeMgr"; + + /** + * Notification manager + */ + private NotificationManager notificationManager; + + /** + * Audio Manager + */ + private AudioManager audioManager; + + /** + * Shared preferences, used to store settings across processes + */ + private SharedPreferences settings; + + /** + * Options for the notification + */ + private Options options; + + /** + * Initialize the NotificationVolumeManager + * @param context Application context + */ + public NotificationVolumeManager (Context context, Options options) { + this.settings = context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE); + this.notificationManager = (NotificationManager)context.getSystemService(Context.NOTIFICATION_SERVICE); + this.audioManager = (AudioManager)context.getSystemService(Context.AUDIO_SERVICE); + this.options = options; + } + + /** + * Ensure that this is the only volume writer. + * Wait until others have closed. + * TODO: Better locking mechanism to ensure concurrency (file lock?) + * @throws InterruptedException Throws an interrupted exception, required by sleep call. + */ + @SuppressLint("ApplySharedPref") + private void ensureOnlyVolumeWriter () throws InterruptedException { + int writerCount = settings.getInt(VOLUME_CONFIG_WRITER_COUNT_KEY, 0) + 1; + settings.edit().putInt(VOLUME_CONFIG_WRITER_COUNT_KEY, writerCount).commit(); + + int resetDelay = options.getResetDelay(); + if (resetDelay == 0) { + resetDelay = Options.DEFAULT_RESET_DELAY; + } + + int resetDelayMs = resetDelay * 1000; + int sleepTotal = 0; + + // Wait until we are the only writer left. + while(writerCount > 1) { + if (sleepTotal > resetDelayMs) { + throw new InterruptedException("Volume writer timeout exceeded reset delay." + + "Something must have gone wrong. Reset volume writer counts to 0 " + + "and reset volume settings to user settings."); + } + + sleep(VOLUME_WRITER_POLLING_DURATION); + sleepTotal += VOLUME_WRITER_POLLING_DURATION; + + writerCount = settings.getInt(VOLUME_CONFIG_WRITER_COUNT_KEY, 0); + } + } + + /** + * Remove one count from active volume writers. Used when writer is finished. + */ + @SuppressLint("ApplySharedPref") + private void decrementVolumeWriter () { + int writerCount = settings.getInt(VOLUME_CONFIG_WRITER_COUNT_KEY, 0) - 1; + settings.edit().putInt(VOLUME_CONFIG_WRITER_COUNT_KEY, Math.max(writerCount, 0)).commit(); + } + + /** + * Reset volume writer counts to 0. To be used in error conditions. + */ + @SuppressLint("ApplySharedPref") + private void resetVolumeWriter () { + settings.edit().putInt(VOLUME_CONFIG_WRITER_COUNT_KEY, 0).commit(); + } + + /** + * Set the volume for our ringer + * @param ringerMode ringer mode enum. Normal ringer or vibration. + * @param volume volume. + */ + private void setVolume (int ringerMode, int volume) { + // After delay, user could have set phone to do not disturb. + // If so and we can't change the ringer, quit so we don't create an error condition + if (canChangeRinger()) { + // Change ringer mode + audioManager.setRingerMode(ringerMode); + + // Change to new Volume + audioManager.setStreamVolume(AudioManager.STREAM_NOTIFICATION, volume, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE); + } + } + + /** + * Set the volume to the last user settings from shared preferences. + */ + private void setVolumeToUserSettings () { + int ringMode = settings.getInt("userRingerMode", -1); + int volume = settings.getInt("userVolume", -1); + + setVolume(ringMode, volume); + } + + /** + * Figure out if we can change the ringer. + * In Android M+, we can't change out of do not disturb if we don't have explicit permission. + * @return whether or not we can change the ringer. + */ + private boolean canChangeRinger() { + return SDK_INT < M || notificationManager.isNotificationPolicyAccessGranted() + || audioManager.getRingerMode() != AudioManager.RINGER_MODE_SILENT; + } + + /** + * Adjusts alarm Volume + * Options object. Contains our volume, reset and vibration settings. + */ + @SuppressLint("ApplySharedPref") + public void adjustAlarmVolume () { + Integer volume = options.getVolume(); + + if (volume.equals(Options.VOLUME_NOT_SET) || !canChangeRinger()) { + return; + } + + try { + ensureOnlyVolumeWriter(); + + boolean vibrate = options.isWithVibration(); + + int delay = options.getResetDelay(); + + if (delay <= 0) { + delay = Options.DEFAULT_RESET_DELAY; + } + + // Count of all alarms currently sounding + Integer count = settings.getInt("alarmCount", 0); + settings.edit().putInt("alarmCount", count + 1).commit(); + + // Get current phone volume + int userVolume = audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION); + + // Get Ringer mode + int userRingerMode = audioManager.getRingerMode(); + + // If this is the first alarm store the users ringer and volume settings + if (count.equals(0)) { + settings.edit().putInt("userVolume", userVolume).apply(); + settings.edit().putInt("userRingerMode", userRingerMode).apply(); + } + + // Calculates a new volume based on the study configure volume percentage and the devices max volume integer + if (volume > 0) { + // Gets devices max volume integer + int maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_NOTIFICATION); + + // Calculates new volume based on devices max volume + double newVolume = Math.ceil(maxVolume * (volume / 100.00)); + + setVolume(AudioManager.RINGER_MODE_NORMAL, (int) newVolume); + } else { + // Volume of 0 + if (vibrate) { + // Change mode to vibrate + setVolume(AudioManager.RINGER_MODE_VIBRATE, 0); + } + } + + // Timer to change users sound back + Timer timer = new Timer(); + timer.schedule(new TimerTask() { + public void run() { + int currentCount = settings.getInt("alarmCount", 0); + currentCount = Math.max(currentCount - 1, 0); + settings.edit().putInt("alarmCount", currentCount).apply(); + + if (currentCount == 0) { + setVolumeToUserSettings(); + } + } + }, delay * 1000); + } catch (InterruptedException e) { + Log.e(TAG, "interrupted waiting for volume set. " + + "Reset to user setting, and set counts to 0: " + e.toString()); + resetVolumeWriter(); + setVolumeToUserSettings(); + } finally { + decrementVolumeWriter(); + } + } +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/Options.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/Options.java new file mode 100644 index 0000000..a9c522e --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/Options.java @@ -0,0 +1,736 @@ + + +// codebeat:disable[TOO_MANY_FUNCTIONS] + +package de.appplant.cordova.plugin.notification; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.net.Uri; +import androidx.core.app.NotificationCompat; +import androidx.core.app.NotificationCompat.MessagingStyle.Message; +import android.support.v4.media.session.MediaSessionCompat; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import de.appplant.cordova.plugin.notification.action.Action; +import de.appplant.cordova.plugin.notification.action.ActionGroup; +import de.appplant.cordova.plugin.notification.util.AssetUtil; + +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.O; +import static androidx.core.app.NotificationCompat.DEFAULT_LIGHTS; +import static androidx.core.app.NotificationCompat.DEFAULT_SOUND; +import static androidx.core.app.NotificationCompat.DEFAULT_VIBRATE; +import static androidx.core.app.NotificationCompat.PRIORITY_MAX; +import static androidx.core.app.NotificationCompat.PRIORITY_MIN; +import static androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC; +import static androidx.core.app.NotificationCompat.VISIBILITY_SECRET; + +/** + * Wrapper around the JSON object passed through JS which contains all possible + * option values. Class provides simple readers and more advanced methods to + * convert independent values into platform specific values. + */ +public final class Options { + // Default Channel ID for SDK < 26 + static final String DEFAULT_CHANNEL_ID = "default-channel-id"; + + // Silent channel + static final String SILENT_CHANNEL_ID = "silent-channel-id"; + static final CharSequence SILENT_CHANNEL_NAME = "Silent Notifications"; + + // Vibrate only channel + static final String VIBRATE_CHANNEL_ID = "vibrate-channel-id"; + static final CharSequence VIBRATE_CHANNEL_NAME = "Low Priority Notifications"; + + // Sound only channel + static final String SOUND_CHANNEL_ID = "sound-channel-id"; + static final CharSequence SOUND_CHANNEL_NAME = "Medium Priority Notifications"; + + // Sound and vibrate channel + static final String SOUND_VIBRATE_CHANNEL_ID = "sound-vibrate-channel-id"; + static final CharSequence SOUND_VIBRATE_CHANNEL_NAME = "High Priority Notifications"; + + // Key name for bundled sound extra + static final String EXTRA_SOUND = "NOTIFICATION_SOUND"; + + // Key name for bundled launch extra + public static final String EXTRA_LAUNCH = "NOTIFICATION_LAUNCH"; + + // Default icon path + private static final String DEFAULT_ICON = "res://icon"; + + public final static Integer DEFAULT_RESET_DELAY = 5; + + public final static Integer VOLUME_NOT_SET = -1; + + // Default wakelock timeout + public final static Integer DEFAULT_WAKE_LOCK_TIMEOUT = 15000; + + // Default icon type + private static final String DEFAULT_ICON_TYPE = "square"; + + // The original JSON object + private final JSONObject options; + + // The application context + private final Context context; + + // Asset util instance + private final AssetUtil assets; + + /** + * When creating without a context, various methods might not work well. + * + * @param options The options dict map. + */ + public Options(JSONObject options) { + this.options = options; + this.context = null; + this.assets = null; + } + + /** + * Constructor + * + * @param context The application context. + * @param options The options dict map. + */ + public Options(Context context, JSONObject options) { + this.context = context; + this.options = options; + this.assets = AssetUtil.getInstance(context); + } + + /** + * Application context. + */ + public Context getContext() { + return context; + } + + /** + * Wrapped JSON object. + */ + public JSONObject getDict() { + return options; + } + + /** + * JSON object as string. + */ + public String toString() { + return options.toString(); + } + + /** + * Gets the ID for the local notification. + * + * @return 0 if the user did not specify. + */ + public Integer getId() { + return options.optInt("id", 0); + } + + /** + * The identifier for the local notification. + * + * @return The notification ID as the string + */ + String getIdentifier() { + return getId().toString(); + } + + /** + * Badge number for the local notification. + */ + public int getBadgeNumber() { + return options.optInt("badge", 0); + } + + /** + * Number for the local notification. + */ + public int getNumber() { + return options.optInt("number", 0); + } + + /** + * ongoing flag for local notifications. + */ + public Boolean isSticky() { + return options.optBoolean("sticky", false); + } + + /** + * autoClear flag for local notifications. + */ + Boolean isAutoClear() { + return options.optBoolean("autoClear", false); + } + + /** + * Gets the raw trigger spec as provided by the user. + */ + public JSONObject getTrigger() { + return options.optJSONObject("trigger"); + } + + /** + * Gets the value of the silent flag. + */ + boolean isSilent() { + return options.optBoolean("silent", false); + } + + /** + * The group for that notification. + */ + String getGroup() { + return options.optString("group", null); + } + + /** + * launch flag for the notification. + */ + boolean isLaunchingApp() { + return options.optBoolean("launch", true); + } + + /** + * flag to auto-launch the application as the notification fires + */ + public boolean isAutoLaunchingApp() { + return options.optBoolean("autoLaunch", true); + } + + /** + * wakeup flag for the notification. + */ + public boolean shallWakeUp() { + return options.optBoolean("wakeup", true); + } + + /** + * Use a fullScreenIntent + */ + public boolean useFullScreenIntent() { return options.optBoolean("fullScreenIntent", true); } + + /** + * Whether or not to trigger a notification in the app. + */ + public boolean triggerInApp() { return options.optBoolean("triggerInApp", false); } + + /** + * Timeout for wakeup (only used if shallWakeUp() is true) + */ + public int getWakeLockTimeout() { + return options.optInt("wakeLockTimeout", DEFAULT_WAKE_LOCK_TIMEOUT); + } + + /** + * Gets the value for the timeout flag. + */ + long getTimeout() { + return options.optLong("timeoutAfter"); + } + + /** + * The channel id of that notification. + */ + String getChannel() { + // If we have a low enough SDK for it not to matter, + // short-circuit. + if (SDK_INT < O) { + return DEFAULT_CHANNEL_ID; + } + + Uri soundUri = getSound(); + boolean hasSound = !isWithoutSound(); + boolean shouldVibrate = isWithVibration(); + CharSequence channelName = options.optString("channelName", null); + String channelId = options.optString("channelId", null); + + channelId = Manager.getInstance(context).buildChannelWithOptions(soundUri, shouldVibrate, hasSound, channelName, + channelId); + + return channelId; + } + + /** + * If the group shall show a summary. + */ + boolean getGroupSummary() { + return options.optBoolean("groupSummary", false); + } + + /** + * Text for the local notification. + */ + public String getText() { + Object text = options.opt("text"); + return text instanceof String ? (String) text : ""; + } + + /** + * Title for the local notification. + */ + public String getTitle() { + String title = options.optString("title", ""); + + if (title.isEmpty()) { + title = context.getApplicationInfo().loadLabel(context.getPackageManager()).toString(); + } + + return title; + } + + /** + * The notification color for LED. + */ + int getLedColor() { + Object cfg = options.opt("led"); + String hex = null; + + if (cfg instanceof String) { + hex = options.optString("led"); + } else if (cfg instanceof JSONArray) { + hex = options.optJSONArray("led").optString(0); + } else if (cfg instanceof JSONObject) { + hex = options.optJSONObject("led").optString("color"); + } + + if (hex == null) + return 0; + + try { + hex = stripHex(hex); + int aRGB = Integer.parseInt(hex, 16); + + return aRGB + 0xFF000000; + } catch (NumberFormatException e) { + e.printStackTrace(); + } + + return 0; + } + + /** + * The notification color for LED. + */ + int getLedOn() { + Object cfg = options.opt("led"); + int defVal = 1000; + + if (cfg instanceof JSONArray) + return options.optJSONArray("led").optInt(1, defVal); + + if (cfg instanceof JSONObject) + return options.optJSONObject("led").optInt("on", defVal); + + return defVal; + } + + /** + * The notification color for LED. + */ + int getLedOff() { + Object cfg = options.opt("led"); + int defVal = 1000; + + if (cfg instanceof JSONArray) + return options.optJSONArray("led").optInt(2, defVal); + + if (cfg instanceof JSONObject) + return options.optJSONObject("led").optInt("off", defVal); + + return defVal; + } + + /** + * The notification background color for the small icon. + * + * @return null, if no color is given. + */ + public int getColor() { + String hex = options.optString("color", null); + + if (hex == null) + return NotificationCompat.COLOR_DEFAULT; + + try { + hex = stripHex(hex); + + if (hex.matches("[^0-9]*")) { + return Color.class.getDeclaredField(hex.toUpperCase()).getInt(null); + } + + int aRGB = Integer.parseInt(hex, 16); + return aRGB + 0xFF000000; + } catch (NumberFormatException e) { + e.printStackTrace(); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + + return NotificationCompat.COLOR_DEFAULT; + } + + /** + * Sound file path for the local notification. + */ + Uri getSound() { + return assets.parse(options.optString("sound", null)); + } + + /** + * Icon resource ID for the local notification. + */ + boolean hasLargeIcon() { + String icon = options.optString("icon", null); + return icon != null; + } + + /** + * Icon bitmap for the local notification. + */ + Bitmap getLargeIcon() { + String icon = options.optString("icon", null); + Uri uri = assets.parse(icon); + Bitmap bmp = null; + + try { + bmp = assets.getIconFromUri(uri); + } catch (Exception e) { + e.printStackTrace(); + } + + return bmp; + } + + /** + * Type of the large icon. + */ + String getLargeIconType() { + return options.optString("iconType", DEFAULT_ICON_TYPE); + } + + /** + * Small icon resource ID for the local notification. + */ + int getSmallIcon() { + String icon = options.optString("smallIcon", DEFAULT_ICON); + int resId = assets.getResId(icon); + + if (resId == 0) { + resId = assets.getResId(DEFAULT_ICON); + } + + if (resId == 0) { + resId = android.R.drawable.ic_popup_reminder; + } + + return resId; + } + + /** + * Get the volume + */ + public Integer getVolume() { + return options.optInt("alarmVolume", VOLUME_NOT_SET); + } + + /** + * Returns the resetDelay until the sound changes revert back to the users + * settings. + * + * @return resetDelay + */ + public Integer getResetDelay() { + return options.optInt("resetDelay", DEFAULT_RESET_DELAY); + } + + /** + * If the phone should vibrate. + */ + public boolean isWithVibration() { + return options.optBoolean("vibrate", true); + } + + /** + * If the phone should play no sound. + */ + public boolean isWithoutSound() { + Object value = options.opt("sound"); + return value == null || value.equals(false) || options.optInt("alarmVolume") == 0; + } + + /** + * If the phone should play the default sound. + */ + public boolean isWithDefaultSound() { + Object value = options.opt("sound"); + return value != null && value.equals(true); + } + + /** + * If the phone should show no LED light. + */ + private boolean isWithoutLights() { + Object value = options.opt("led"); + return value == null || value.equals(false); + } + + /** + * If the phone should show the default LED lights. + */ + private boolean isWithDefaultLights() { + Object value = options.opt("led"); + return value != null && value.equals(true); + } + + /** + * Set the default notification options that will be used. The value should be + * one or more of the following fields combined with bitwise-or: DEFAULT_SOUND, + * DEFAULT_VIBRATE, DEFAULT_LIGHTS. + */ + int getDefaults() { + int defaults = options.optInt("defaults", 0); + + if (isWithVibration()) { + defaults |= DEFAULT_VIBRATE; + } else { + defaults &= DEFAULT_VIBRATE; + } + + if (isWithDefaultSound()) { + defaults |= DEFAULT_SOUND; + } else if (isWithoutSound()) { + defaults &= DEFAULT_SOUND; + } + + if (isWithDefaultLights()) { + defaults |= DEFAULT_LIGHTS; + } else if (isWithoutLights()) { + defaults &= DEFAULT_LIGHTS; + } + + return defaults; + } + + /** + * Gets the visibility for the notification. + * + * @return VISIBILITY_PUBLIC or VISIBILITY_SECRET + */ + int getVisibility() { + if (options.optBoolean("lockscreen", true)) { + return VISIBILITY_PUBLIC; + } else { + return VISIBILITY_SECRET; + } + } + + /** + * Gets the notifications priority. + */ + int getPrio() { + int prio = options.optInt("priority"); + + return Math.min(Math.max(prio, PRIORITY_MIN), PRIORITY_MAX); + } + + /** + * If the notification shall show the when date. + */ + boolean showClock() { + Object clock = options.opt("clock"); + + return (clock instanceof Boolean) ? (Boolean) clock : true; + } + + /** + * If the notification shall show the when date. + */ + boolean showChronometer() { + Object clock = options.opt("clock"); + + return (clock instanceof String) && clock.equals("chronometer"); + } + + /** + * If the notification shall display a progress bar. + */ + boolean isWithProgressBar() { + return options.optJSONObject("progressBar").optBoolean("enabled", false); + } + + /** + * Gets the progress value. + * + * @return 0 by default. + */ + int getProgressValue() { + return options.optJSONObject("progressBar").optInt("value", 0); + } + + /** + * Gets the progress value. + * + * @return 100 by default. + */ + int getProgressMaxValue() { + return options.optJSONObject("progressBar").optInt("maxValue", 100); + } + + /** + * Gets the progress indeterminate value. + * + * @return false by default. + */ + boolean isIndeterminateProgress() { + return options.optJSONObject("progressBar").optBoolean("indeterminate", false); + } + + /** + * If the trigger shall be infinite. + */ + public boolean isInfiniteTrigger() { + JSONObject trigger = options.optJSONObject("trigger"); + + return trigger.has("every") && trigger.optInt("count", -1) < 0; + } + + /** + * The summary for inbox style notifications. + */ + String getSummary() { + return options.optString("summary", null); + } + + /** + * Image attachments for image style notifications. + * + * @return For now it only returns the first item as Android does not support + * multiple attachments like iOS. + */ + List getAttachments() { + JSONArray paths = options.optJSONArray("attachments"); + List pics = new ArrayList(); + + if (paths == null) + return pics; + + for (int i = 0; i < paths.length(); i++) { + Uri uri = assets.parse(paths.optString(i)); + + if (uri == Uri.EMPTY) + continue; + + try { + Bitmap pic = assets.getIconFromUri(uri); + pics.add(pic); + break; + } catch (IOException e) { + e.printStackTrace(); + } + } + + return pics; + } + + /** + * Gets the list of actions to display. + */ + Action[] getActions() { + Object value = options.opt("actions"); + String groupId = null; + JSONArray actions = null; + ActionGroup group = null; + + if (value instanceof String) { + groupId = (String) value; + } else if (value instanceof JSONArray) { + actions = (JSONArray) value; + } + + if (groupId != null) { + group = ActionGroup.lookup(groupId); + } else if (actions != null && actions.length() > 0) { + group = ActionGroup.parse(context, actions); + } + + return (group != null) ? group.getActions() : null; + } + + /** + * Gets the list of messages to display. + * + * @return null if there are no messages. + */ + Message[] getMessages() { + Object text = options.opt("text"); + + if (text == null || text instanceof String) + return null; + + JSONArray list = (JSONArray) text; + + if (list.length() == 0) + return null; + + Message[] messages = new Message[list.length()]; + long now = new Date().getTime(); + + for (int i = 0; i < messages.length; i++) { + JSONObject msg = list.optJSONObject(i); + String message = msg.optString("message"); + long timestamp = msg.optLong("date", now); + String person = msg.optString("person", null); + + messages[i] = new Message(message, timestamp, person); + } + + return messages; + } + + /** + * Gets the token for the specified media session. + * + * @return null if there no session. + */ + MediaSessionCompat.Token getMediaSessionToken() { + String tag = options.optString("mediaSession", null); + + if (tag == null) + return null; + + MediaSessionCompat session = new MediaSessionCompat(context, tag); + + return session.getSessionToken(); + } + + /** + * Strips the hex code #FF00FF => FF00FF + * + * @param hex The hex code to strip. + * + * @return The stripped hex code without a leading # + */ + private String stripHex(String hex) { + return (hex.charAt(0) == '#') ? hex.substring(1) : hex; + } + +} + +// codebeat:enable[TOO_MANY_FUNCTIONS] diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/Request.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/Request.java new file mode 100644 index 0000000..06260bf --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/Request.java @@ -0,0 +1,277 @@ +/* + * Copyright (c) 2014-2015 by appPlant UG. All rights reserved. + * + * @APPPLANT_LICENSE_HEADER_START@ + * + * This file contains Original Code and/or Modifications of Original Code + * as defined in and that are subject to the Apache License + * Version 2.0 (the 'License'). You may not use this file except in + * compliance with the License. Please obtain a copy of the License at + * http://opensource.org/licenses/Apache-2.0/ and read it before using this + * file. + * + * The Original Code and all software distributed under the License are + * distributed on an 'AS IS' basis, WITHOUT WARRANTY OF ANY KIND, EITHER + * EXPRESS OR IMPLIED, AND APPLE HEREBY DISCLAIMS ALL SUCH WARRANTIES, + * INCLUDING WITHOUT LIMITATION, ANY WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE, QUIET ENJOYMENT OR NON-INFRINGEMENT. + * Please see the License for the specific language governing rights and + * limitations under the License. + * + * @APPPLANT_LICENSE_HEADER_END@ + */ + +package de.appplant.cordova.plugin.notification; + +import org.json.JSONObject; + +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import de.appplant.cordova.plugin.notification.trigger.DateTrigger; +import de.appplant.cordova.plugin.notification.trigger.IntervalTrigger; +import de.appplant.cordova.plugin.notification.trigger.MatchTrigger; + +import static de.appplant.cordova.plugin.notification.trigger.IntervalTrigger.Unit; + +/** + * An object you use to specify a notification’s content and the condition + * that triggers its delivery. + */ +public final class Request { + + // Key name for bundled extras + static final String EXTRA_OCCURRENCE = "NOTIFICATION_OCCURRENCE"; + + // Key name for bundled extras + public static final String EXTRA_LAST = "NOTIFICATION_LAST"; + + // The options spec + private final Options options; + + // The right trigger for the options + private final DateTrigger trigger; + + // How often the trigger shall occur + private final int count; + + // The trigger spec + private final JSONObject spec; + + // The current trigger date + private Date triggerDate; + + /** + * Create a request with a base date specified through the passed options. + * + * @param options The options spec. + */ + public Request(Options options) { + this.options = options; + this.spec = options.getTrigger(); + this.count = Math.max(spec.optInt("count"), 1); + this.trigger = buildTrigger(); + this.triggerDate = trigger.getNextTriggerDate(getBaseDate()); + } + + /** + * Create a request with a base date specified via base argument. + * + * @param options The options spec. + * @param base The base date from where to calculate the next trigger. + */ + public Request(Options options, Date base) { + this.options = options; + this.spec = options.getTrigger(); + this.count = Math.max(spec.optInt("count"), 1); + this.trigger = buildTrigger(); + this.triggerDate = trigger.getNextTriggerDate(base); + } + + /** + * Gets the options spec. + */ + public Options getOptions() { + return options; + } + + /** + * The identifier for the request. + * + * @return The notification ID as the string + */ + String getIdentifier() { + return options.getId().toString() + "-" + getOccurrence(); + } + + /** + * The value of the internal occurrence counter. + */ + int getOccurrence() { + return trigger.getOccurrence(); + } + + /** + * If there's one more trigger date to calculate. + */ + private boolean hasNext() { + return triggerDate != null && getOccurrence() <= count; + } + + /** + * Moves the internal occurrence counter by one. + */ + boolean moveNext() { + if (hasNext()) { + triggerDate = getNextTriggerDate(); + } else { + triggerDate = null; + } + + return this.triggerDate != null; + } + + /** + * Gets the current trigger date. + * + * @return null if there's no trigger date. + */ + public Date getTriggerDate() { + Calendar now = Calendar.getInstance(); + + if (triggerDate == null) + return null; + + long time = triggerDate.getTime(); + + if ((now.getTimeInMillis() - time) > 60000) + return null; + + if (time >= spec.optLong("before", time + 1)) + return null; + + return triggerDate; + } + + /** + * Gets the next trigger date based on the current trigger date. + */ + private Date getNextTriggerDate() { + return trigger.getNextTriggerDate(triggerDate); + } + + /** + * Build the trigger specified in options. + */ + private DateTrigger buildTrigger() { + Object every = spec.opt("every"); + + if (every instanceof JSONObject) { + List cmp1 = getMatchingComponents(); + List cmp2 = getSpecialMatchingComponents(); + + return new MatchTrigger(cmp1, cmp2); + } + + Unit unit = getUnit(); + int ticks = getTicks(); + + return new IntervalTrigger(ticks, unit); + } + + /** + * Gets the unit value. + */ + private Unit getUnit() { + Object every = spec.opt("every"); + String unit = "SECOND"; + + if (spec.has("unit")) { + unit = spec.optString("unit", "second"); + } else + if (every instanceof String) { + unit = spec.optString("every", "second"); + } + + return Unit.valueOf(unit.toUpperCase()); + } + + /** + * Gets the tick value. + */ + private int getTicks() { + Object every = spec.opt("every"); + int ticks = 0; + + if (spec.has("at")) { + ticks = 0; + } else + if (spec.has("in")) { + ticks = spec.optInt("in", 0); + } else + if (every instanceof String) { + ticks = 1; + } else + if (!(every instanceof JSONObject)) { + ticks = spec.optInt("every", 0); + } + + return ticks; + } + + /** + * Gets an array of all date parts to construct a datetime instance. + * + * @return [min, hour, day, month, year] + */ + private List getMatchingComponents() { + JSONObject every = spec.optJSONObject("every"); + + return Arrays.asList( + (Integer) every.opt("minute"), + (Integer) every.opt("hour"), + (Integer) every.opt("day"), + (Integer) every.opt("month"), + (Integer) every.opt("year") + ); + } + + /** + * Gets an array of all date parts to construct a datetime instance. + * + * @return [min, hour, day, month, year] + */ + private List getSpecialMatchingComponents() { + JSONObject every = spec.optJSONObject("every"); + + return Arrays.asList( + (Integer) every.opt("weekday"), + (Integer) every.opt("weekdayOrdinal"), + (Integer) every.opt("weekOfMonth"), + (Integer) every.opt("quarter") + ); + } + + /** + * Gets the base date from where to calculate the next trigger date. + */ + private Date getBaseDate() { + if (spec.has("requestBaseDate")) { + return new Date(spec.optLong("requestBaseDate")); + } else + if (spec.has("at")) { + return new Date(spec.optLong("at", 0)); + } else + if (spec.has("firstAt")) { + return new Date(spec.optLong("firstAt", 0)); + } else + if (spec.has("after")) { + return new Date(spec.optLong("after", 0)); + } else { + return new Date(); + } + } + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/action/Action.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/action/Action.java new file mode 100644 index 0000000..62bd596 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/action/Action.java @@ -0,0 +1,118 @@ + + +package de.appplant.cordova.plugin.notification.action; + +import android.content.Context; +import androidx.core.app.RemoteInput; + +import org.json.JSONArray; +import org.json.JSONObject; + +import de.appplant.cordova.plugin.notification.util.AssetUtil; + +/** + * Holds the icon and title components that would be used in a + * NotificationCompat.Action object. Does not include the PendingIntent so + * that it may be generated each time the notification is built. Necessary to + * compensate for missing functionality in the support library. + */ +public final class Action { + + // Key name for bundled extras + public static final String EXTRA_ID = "NOTIFICATION_ACTION_ID"; + + // The id for the click action + public static final String CLICK_ACTION_ID = "click"; + + // The application context + private final Context context; + + // The action spec + private final JSONObject options; + + /** + * Structure to encapsulate a named action that can be shown as part of + * this notification. + * + * @param context The application context. + * @param options The action options. + */ + Action (Context context, JSONObject options) { + this.context = context; + this.options = options; + } + + /** + * Gets the ID for the action. + */ + public String getId() { + return options.optString("id", getTitle()); + } + + /** + * Gets the Title for the action. + */ + public String getTitle() { + return options.optString("title", "unknown"); + } + + /** + * Gets the icon for the action. + */ + public int getIcon() { + AssetUtil assets = AssetUtil.getInstance(context); + String resPath = options.optString("icon"); + int resId = assets.getResId(resPath); + + if (resId == 0) { + resId = android.R.drawable.screen_background_dark; + } + + return resId; + } + + /** + * Gets the value of the launch flag. + */ + public boolean isLaunchingApp() { + return options.optBoolean("launch", false); + } + + /** + * Gets the type for the action. + */ + public boolean isWithInput() { + String type = options.optString("type"); + return type.equals("input"); + } + + /** + * Gets the input config in case of the action is of type input. + */ + public RemoteInput getInput() { + return new RemoteInput.Builder(getId()) + .setLabel(options.optString("emptyText")) + .setAllowFreeFormInput(options.optBoolean("editable", true)) + .setChoices(getChoices()) + .build(); + } + + /** + * List of possible choices for input actions. + */ + private String[] getChoices() { + JSONArray opts = options.optJSONArray("choices"); + + if (opts == null) + return null; + + String[] choices = new String[opts.length()]; + + for (int i = 0; i < choices.length; i++) { + choices[i] = opts.optString(i); + } + + return choices; + } + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/action/ActionGroup.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/action/ActionGroup.java new file mode 100644 index 0000000..6f3214d --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/action/ActionGroup.java @@ -0,0 +1,135 @@ + + +package de.appplant.cordova.plugin.notification.action; + +import android.content.Context; +import android.util.Log; + +import org.json.JSONArray; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.N; + +public final class ActionGroup { + + // Saves all groups for later lookup. + private static final Map groups = new HashMap(); + + // The ID of the action group. + private final String id; + + // List of actions + private final Action[] actions; + + /** + * Lookup the action groups with the specified group id. + * + * @param id The ID of the action group to find. + * + * @return Null if no group was found. + */ + public static ActionGroup lookup(String id) { + return groups.get(id); + } + + /** + * Register the action group for later lookup. + * + * @param group The action group to register. + */ + public static void register (ActionGroup group) { + groups.put(group.getId(), group); + } + + /** + * Unregister the action group. + * + * @param id The id of the action group to remove. + */ + public static void unregister (String id) { + groups.remove(id); + } + + /** + * Check if a action group with that id is registered. + * + * @param id The id of the action group to check for. + */ + public static boolean isRegistered (String id) { + return groups.containsKey(id); + } + + /** + * Creates an action group by parsing the specified action specs. + * + * @param list The list of actions. + * + * @return A new action group. + */ + public static ActionGroup parse (Context context, JSONArray list) { + return parse(context, null, list); + } + + /** + * Creates an action group by parsing the specified action specs. + * + * @param id The id for the action group. + * @param list The list of actions. + * + * @return A new action group. + */ + public static ActionGroup parse (Context context, String id, JSONArray list) { + List actions = new ArrayList(list.length()); + + for (int i = 0; i < list.length(); i++) { + JSONObject opts = list.optJSONObject(i); + String type = opts.optString("type", "button"); + + if (type.equals("input") && SDK_INT < N) { + Log.w("Action", "Type input is not supported"); + continue; + } + + if (!(type.equals("button") || type.equals("input"))) { + Log.w("Action", "Unknown type: " + type); + continue; + } + + actions.add(new Action(context, opts)); + } + + return new ActionGroup(id, actions.toArray(new Action[actions.size()])); + } + + /** + * Creates an action group. + * + * @param id The ID of the group. + * @param actions The list of actions. + */ + private ActionGroup(String id, Action[] actions) { + this.id = id; + this.actions = actions; + } + + /** + * Gets the action group id. + */ + public String getId() { + return id; + } + + /** + * Gets the action list. + */ + public Action[] getActions() { + return actions; + } + +} \ No newline at end of file diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractClearReceiver.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractClearReceiver.java new file mode 100644 index 0000000..fcb0822 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractClearReceiver.java @@ -0,0 +1,49 @@ + + +package de.appplant.cordova.plugin.notification.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import de.appplant.cordova.plugin.notification.Manager; +import de.appplant.cordova.plugin.notification.Notification; + +/** + * Abstract delete receiver for local notifications. Creates the local + * notification and calls the event functions for further proceeding. + */ +abstract public class AbstractClearReceiver extends BroadcastReceiver { + + /** + * Called when the notification was cleared from the notification center. + * + * @param context Application context + * @param intent Received intent with content data + */ + @Override + public void onReceive(Context context, Intent intent) { + Bundle bundle = intent.getExtras(); + + if (bundle == null) + return; + + int toastId = bundle.getInt(Notification.EXTRA_ID); + Notification toast = Manager.getInstance(context).get(toastId); + + if (toast == null) + return; + + onClear(toast, bundle); + } + + /** + * Called when a local notification was cleared from outside of the app. + * + * @param notification Wrapper around the local notification. + * @param bundle The bundled extras. + */ + abstract public void onClear (Notification notification, Bundle bundle); + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractClickReceiver.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractClickReceiver.java new file mode 100644 index 0000000..8b38507 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractClickReceiver.java @@ -0,0 +1,66 @@ +package de.appplant.cordova.plugin.notification.receiver; + +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import de.appplant.cordova.plugin.notification.Manager; +import de.appplant.cordova.plugin.notification.Notification; + +import static de.appplant.cordova.plugin.notification.action.Action.CLICK_ACTION_ID; +import static de.appplant.cordova.plugin.notification.action.Action.EXTRA_ID; + +/** + * Abstract content receiver activity for local notifications. Creates the + * local notification and calls the event functions for further proceeding. + */ +abstract public class AbstractClickReceiver extends NotificationTrampolineActivity { + + public AbstractClickReceiver() { + super(); + } + + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + onHandleIntent(getIntent()); + } + + /** + * Called when local notification was clicked to launch the main intent. + */ + protected void onHandleIntent(Intent intent) { + // Holds a reference to the intent to handle. + + if (intent == null) + return; + + Bundle bundle = intent.getExtras(); + Context context = getApplicationContext(); + + if (bundle == null) + return; + + int toastId = bundle.getInt(Notification.EXTRA_ID); + Notification toast = Manager.getInstance(context).get(toastId); + + if (toast == null) + return; + + onClick(toast, bundle); + } + + /** + * Called when local notification was clicked by the user. + * + * @param notification Wrapper around the local notification. + * @param bundle The bundled extras. + */ + abstract public void onClick (Notification notification, Bundle bundle); + + /** + * The invoked action. + */ + protected String getAction() { + return getIntent().getExtras().getString(EXTRA_ID, CLICK_ACTION_ID); + } +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractNotificationReceiver.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractNotificationReceiver.java new file mode 100644 index 0000000..05a4e1a --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractNotificationReceiver.java @@ -0,0 +1,152 @@ +package de.appplant.cordova.plugin.notification.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.os.PowerManager; +import android.util.Log; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Calendar; + +import de.appplant.cordova.plugin.localnotification.LocalNotification; +import de.appplant.cordova.plugin.notification.Manager; +import de.appplant.cordova.plugin.notification.Notification; +import de.appplant.cordova.plugin.notification.Options; +import de.appplant.cordova.plugin.notification.Request; +import de.appplant.cordova.plugin.notification.util.LaunchUtils; + +import static android.content.Context.POWER_SERVICE; +import static android.os.Build.VERSION.SDK_INT; +import static android.os.Build.VERSION_CODES.O; +import static android.os.Build.VERSION_CODES.P; +import static java.util.Calendar.MINUTE; + +/** + * The base class for any receiver that is trying to display a notification. + */ +abstract public class AbstractNotificationReceiver extends BroadcastReceiver { + private final String TAG = "AbstractNotification"; + + /** + * Perform a notification. All notification logic is here. + * Determines whether to dispatch events, autoLaunch the app, use fullScreenIntents, etc. + * @param notification reference to the notification to be fired + */ + public void performNotification(Notification notification) { + Context context = notification.getContext(); + Options options = notification.getOptions(); + Manager manager = Manager.getInstance(context); + PowerManager pm = (PowerManager) context.getSystemService(POWER_SERVICE); + boolean autoLaunch = options.isAutoLaunchingApp() && SDK_INT <= P && !options.useFullScreenIntent(); + + int badge = options.getBadgeNumber(); + + if (badge > 0) { + manager.setBadge(badge); + } + + if (options.shallWakeUp()) { + wakeUp(notification); + } + + if (autoLaunch) { + LaunchUtils.launchApp(context); + } + + // Show notification if we should (triggerInApp is false) + // or if we can't trigger in the app due to: + // 1. No autoLaunch configured/supported and app is not running. + // 2. Any SDK >= Oreo is asleep (must be triggered here) + boolean didShowNotification = false; + if (!options.triggerInApp() + || (checkAppRunning() && !LocalNotification.isInForeground() ) + || (!checkAppRunning() && !autoLaunch ) + ) { + didShowNotification = true; + notification.show(); + } + + // run trigger function if triggerInApp() is true + // and we did not send a notification. + if (options.triggerInApp() && !didShowNotification) { + // wake up even if we didn't set it to + if (!options.shallWakeUp()) { + wakeUp(notification); + } + + dispatchAppEvent("trigger", notification); + } + + if (!options.isInfiniteTrigger()) + return; + + Calendar cal = Calendar.getInstance(); + cal.add(MINUTE, 1); + + Request req = new Request( + getOptionsWithBaseDate(options, cal.getTimeInMillis()), + cal.getTime() + ); + + manager.schedule(req, this.getClass()); + } + + /** + * Clone options with base date attached to trigger. + * Used so that persisted objects know the last execution time. + * @param baseDateMillis base date represented in milliseconds + * @return new Options object with base time set in requestBaseDate. + */ + private Options getOptionsWithBaseDate(Options options, long baseDateMillis) { + JSONObject optionsDict = options.getDict(); + try { + JSONObject triggerDict = optionsDict.getJSONObject("trigger"); + triggerDict.put("requestBaseDate", baseDateMillis); + optionsDict.remove("trigger"); + optionsDict.put("trigger", triggerDict); + } catch (JSONException e) { + Log.e(TAG, "Unexpected error adding requestBaseDate to JSON structure: " + e.toString()); + } + return new Options(optionsDict); + } + + /** + * Send the application an event using our notification + * @param key key for our event in the app + * @param notification reference to the notification + */ + abstract public void dispatchAppEvent(String key, Notification notification); + + /** + * Check if the application is running. + * Should be developed in local class, which has access to things needed for this. + * @return whether or not app is running + */ + abstract public boolean checkAppRunning(); + + /** + * Wakeup the device. + * + * @param notification The notification used to wakeup the device. + * contains context and timeout. + */ + private void wakeUp(Notification notification) { + Context context = notification.getContext(); + Options options = notification.getOptions(); + String wakeLockTag = context.getApplicationInfo().name + ":LocalNotification"; + PowerManager pm = (PowerManager) context.getSystemService(POWER_SERVICE); + + if (pm == null) + return; + + int level = PowerManager.FULL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP | PowerManager.ON_AFTER_RELEASE; + + PowerManager.WakeLock wakeLock = pm.newWakeLock(level, wakeLockTag); + + wakeLock.setReferenceCounted(false); + wakeLock.acquire(options.getWakeLockTimeout()); + } + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractRestoreReceiver.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractRestoreReceiver.java new file mode 100644 index 0000000..972ef13 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractRestoreReceiver.java @@ -0,0 +1,71 @@ +package de.appplant.cordova.plugin.notification.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.UserManager; + +import org.json.JSONObject; + +import java.util.List; + +import de.appplant.cordova.plugin.notification.Builder; +import de.appplant.cordova.plugin.notification.Manager; +import de.appplant.cordova.plugin.notification.Notification; +import de.appplant.cordova.plugin.notification.Options; +import de.appplant.cordova.plugin.notification.Request; + +import static android.content.Intent.ACTION_BOOT_COMPLETED; +import static android.os.Build.VERSION.SDK_INT; + +/** + * This class is triggered upon reboot of the device. It needs to re-register + * the alarms with the AlarmManager since these alarms are lost in case of + * reboot. + */ +abstract public class AbstractRestoreReceiver extends AbstractNotificationReceiver { + + /** + * Called on device reboot. + * + * @param context Application context + * @param intent Received intent with content data + */ + @Override + public void onReceive (Context context, Intent intent) { + String action = intent.getAction(); + + if (SDK_INT >= 24) { + UserManager um = (UserManager) context.getSystemService(UserManager.class); + if (um == null || um.isUserUnlocked() == false) return; + } + + Manager mgr = Manager.getInstance(context); + List toasts = mgr.getOptions(); + + for (JSONObject data : toasts) { + Options options = new Options(context, data); + Request request = new Request(options); + Builder builder = new Builder(options); + Notification toast = buildNotification(builder); + + onRestore(request, toast); + } + } + + /** + * Called when a local notification need to be restored. + * + * @param request Set of notification options. + * @param toast Wrapper around the local notification. + */ + abstract public void onRestore (Request request, Notification toast); + + /** + * Build notification specified by options. + * + * @param builder Notification builder. + */ + abstract public Notification buildNotification (Builder builder); + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractTriggerReceiver.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractTriggerReceiver.java new file mode 100644 index 0000000..95d8597 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/AbstractTriggerReceiver.java @@ -0,0 +1,66 @@ + + +package de.appplant.cordova.plugin.notification.receiver; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import de.appplant.cordova.plugin.notification.Builder; +import de.appplant.cordova.plugin.notification.Manager; +import de.appplant.cordova.plugin.notification.Notification; +import de.appplant.cordova.plugin.notification.Options; + +/** + * Abstract broadcast receiver for local notifications. Creates the + * notification options and calls the event functions for further proceeding. + */ +abstract public class AbstractTriggerReceiver extends AbstractNotificationReceiver { + + /** + * Called when an alarm was triggered. + * + * @param context Application context + * @param intent Received intent with content data + */ + @Override + public void onReceive(Context context, Intent intent) { + Bundle bundle = intent.getExtras(); + + if (bundle == null) + return; + + int toastId = bundle.getInt(Notification.EXTRA_ID, 0); + Options options = Manager.getInstance(context).getOptions(toastId); + + if (options == null) + return; + + Builder builder = new Builder(options); + Notification toast = buildNotification(builder, bundle); + + if (toast == null) + return; + + onTrigger(toast, bundle); + } + + /** + * Called when a local notification was triggered. + * + * @param notification Wrapper around the local notification. + * @param bundle The bundled extras. + */ + abstract public void onTrigger (Notification notification, Bundle bundle); + + /** + * Build notification specified by options. + * + * @param builder Notification builder. + * @param bundle The bundled extras. + */ + abstract public Notification buildNotification (Builder builder, + Bundle bundle); + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/NotificationTrampolineActivity.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/NotificationTrampolineActivity.java new file mode 100644 index 0000000..e33d7fc --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/receiver/NotificationTrampolineActivity.java @@ -0,0 +1,43 @@ +package de.appplant.cordova.plugin.notification.receiver; + +import androidx.appcompat.app.AppCompatActivity; + +import android.content.Intent; +import android.graphics.Color; +import android.graphics.drawable.ColorDrawable; +import android.os.Bundle; + + +/** + * To satisfy the new android 12 requirement, the broadcast receiver used + * to handle click action on a notification, is replaced with a trampoline activity + * Note: to handle correctly the case where the application is running in background + * while the action is clicked, set + * + * in the config.xml file. + * If you don't add this line, the app is restarted + */ +public class NotificationTrampolineActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String packageName = this.getPackageName(); + Intent launchIntent = this.getPackageManager().getLaunchIntentForPackage(packageName); + String mainActivityClassName = launchIntent.getComponent().getClassName(); + Class mainActivityClass = null; + try { + mainActivityClass = Class.forName(mainActivityClassName); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + return; + } + + Intent intent = new Intent(this, mainActivityClass); + + //pull activity from stack or create instance if it doesn't exist + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + startActivity(intent); + } +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/trigger/DateTrigger.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/trigger/DateTrigger.java new file mode 100644 index 0000000..da6b09b --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/trigger/DateTrigger.java @@ -0,0 +1,51 @@ + + +package de.appplant.cordova.plugin.notification.trigger; + +import java.util.Calendar; +import java.util.Date; + +abstract public class DateTrigger { + + // Default unit is SECOND + public enum Unit { SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR } + + // Internal counter + private int occurrence = 1; + + /** + * Gets the next trigger date. + * + * @param base The date from where to calculate the trigger date. + * + * @return null if there's none next trigger date. + */ + abstract public Date getNextTriggerDate(Date base); + + /** + * The value of the occurrence. + */ + public int getOccurrence() { + return occurrence; + } + + /** + * Increase the occurrence by 1. + */ + void incOccurrence() { + occurrence += 1; + } + + /** + * Gets a calendar instance pointing to the specified date. + * + * @param date The date to point. + */ + Calendar getCal (Date date) { + Calendar cal = Calendar.getInstance(); + cal.setTime(date); + + return cal; + } + +} \ No newline at end of file diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/trigger/IntervalTrigger.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/trigger/IntervalTrigger.java new file mode 100644 index 0000000..d03f965 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/trigger/IntervalTrigger.java @@ -0,0 +1,82 @@ + + +package de.appplant.cordova.plugin.notification.trigger; + +import java.util.Calendar; +import java.util.Date; + +/** + * Trigger class for interval based notification. Trigger by a fixed interval + * from now. + */ +public class IntervalTrigger extends DateTrigger { + + // The number of ticks per interval + private final int ticks; + + // The unit of the ticks + final Unit unit; + + /** + * Interval trigger based from now. + * + * @param ticks The number of ticks per interval. + * @param unit The unit of the ticks. + */ + public IntervalTrigger(int ticks, Unit unit) { + this.ticks = ticks; + this.unit = unit; + } + + /** + * Gets the next trigger date. + * + * @param base The date from where to calculate the trigger date. + * + * @return null if there's none next trigger date. + */ + @Override + public Date getNextTriggerDate(Date base) { + Calendar cal = getCal(base); + + addInterval(cal); + incOccurrence(); + + return cal.getTime(); + } + + /** + * Adds the amount of ticks to the calendar. + * + * @param cal The calendar to manipulate. + */ + void addInterval(Calendar cal) { + switch (unit) { + case SECOND: + cal.add(Calendar.SECOND, ticks); + break; + case MINUTE: + cal.add(Calendar.MINUTE, ticks); + break; + case HOUR: + cal.add(Calendar.HOUR_OF_DAY, ticks); + break; + case DAY: + cal.add(Calendar.DAY_OF_YEAR, ticks); + break; + case WEEK: + cal.add(Calendar.WEEK_OF_YEAR, ticks); + break; + case MONTH: + cal.add(Calendar.MONTH, ticks); + break; + case QUARTER: + cal.add(Calendar.MONTH, ticks * 3); + break; + case YEAR: + cal.add(Calendar.YEAR, ticks); + break; + } + } + +} \ No newline at end of file diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/trigger/MatchTrigger.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/trigger/MatchTrigger.java new file mode 100644 index 0000000..dc00c90 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/trigger/MatchTrigger.java @@ -0,0 +1,341 @@ + + +package de.appplant.cordova.plugin.notification.trigger; + +import java.util.Calendar; +import java.util.Date; +import java.util.List; + +import static de.appplant.cordova.plugin.notification.trigger.DateTrigger.Unit.DAY; +import static de.appplant.cordova.plugin.notification.trigger.DateTrigger.Unit.HOUR; +import static de.appplant.cordova.plugin.notification.trigger.DateTrigger.Unit.MINUTE; +import static de.appplant.cordova.plugin.notification.trigger.DateTrigger.Unit.MONTH; +import static de.appplant.cordova.plugin.notification.trigger.DateTrigger.Unit.WEEK; +import static de.appplant.cordova.plugin.notification.trigger.DateTrigger.Unit.YEAR; +import static java.util.Calendar.DAY_OF_WEEK; +import static java.util.Calendar.WEEK_OF_MONTH; +import static java.util.Calendar.WEEK_OF_YEAR; + +/** + * Trigger for date matching components. + */ +public class MatchTrigger extends IntervalTrigger { + + // Used to determine the interval + private static Unit[] INTERVALS = { null, MINUTE, HOUR, DAY, MONTH, YEAR }; + + // Maps these crap where Sunday is the 1st day of the week + private static int[] WEEKDAYS = { 0, 2, 3, 4, 5, 6, 7, 1 }; + + // Maps these crap where Sunday is the 1st day of the week + private static int[] WEEKDAYS_REV = { 0, 7, 1, 2, 3, 4, 5, 6 }; + + // The date matching components + private final List matchers; + + // The special matching components + private final List specials; + + private static Unit getUnit(List matchers, List specials) { + Unit unit1 = INTERVALS[1 + matchers.indexOf(null)], unit2 = null; + + if (specials.get(0) != null) { + unit2 = WEEK; + } + + if (unit2 == null) + return unit1; + + return (unit1.compareTo(unit2) < 0) ? unit2 : unit1; + } + + /** + * Date matching trigger from now. + * + * @param matchers Describes the date matching parts. + * { day: 15, month: ... } + * @param specials Describes the date matching parts. + * { weekday: 1, weekOfMonth: ... } + */ + public MatchTrigger(List matchers, List specials) { + super(1, getUnit(matchers, specials)); + + if (specials.get(0) != null) { + specials.set(0, WEEKDAYS[specials.get(0)]); + } + + this.matchers = matchers; + this.specials = specials; + } + + /** + * Gets the date from where to start calculating the initial trigger date. + */ + private Calendar getBaseTriggerDate(Date date) { + Calendar cal = getCal(date); + + cal.set(Calendar.SECOND, 0); + + if (matchers.get(0) != null) { + cal.set(Calendar.MINUTE, matchers.get(0)); + } else { + cal.set(Calendar.MINUTE, 0); + } + + if (matchers.get(1) != null) { + cal.set(Calendar.HOUR_OF_DAY, matchers.get(1)); + } else { + cal.set(Calendar.HOUR_OF_DAY, 0); + } + + if (matchers.get(2) != null) { + cal.set(Calendar.DAY_OF_MONTH, matchers.get(2)); + } + + if (matchers.get(3) != null) { + cal.set(Calendar.MONTH, matchers.get(3) - 1); + } + + if (matchers.get(4) != null) { + cal.set(Calendar.YEAR, matchers.get(4)); + } + + return cal; + } + + /** + * Gets the date when to trigger the notification. + * + * @param base The date from where to calculate the trigger date. + * + * @return null if there's none trigger date. + */ + private Date getTriggerDate (Date base) { + Calendar cal = getBaseTriggerDate(base); + Calendar now = getCal(base); + + if (cal.compareTo(now) >= 0) + return applySpecials(cal); + + if (unit == null || cal.get(Calendar.YEAR) < now.get(Calendar.YEAR)) + return null; + + if (cal.get(Calendar.MONTH) < now.get(Calendar.MONTH)) { + switch (unit) { + case MINUTE: + case HOUR: + case DAY: + case WEEK: + if (matchers.get(4) == null) { + addToDate(cal, now, Calendar.YEAR, 1); + break; + } else + return null; + case YEAR: + addToDate(cal, now, Calendar.YEAR, 1); + break; + } + } else + if (cal.get(Calendar.DAY_OF_YEAR) < now.get(Calendar.DAY_OF_YEAR)) { + switch (unit) { + case MINUTE: + case HOUR: + if (matchers.get(3) == null) { + addToDate(cal, now, Calendar.MONTH, 1); + break; + } else + if (matchers.get(4) == null) { + addToDate(cal, now, Calendar.YEAR, 1); + break; + } + else + return null; + case MONTH: + addToDate(cal, now, Calendar.MONTH, 1); + break; + case YEAR: + addToDate(cal, now, Calendar.YEAR, 1); + break; + } + } else + if (cal.get(Calendar.HOUR_OF_DAY) < now.get(Calendar.HOUR_OF_DAY)) { + switch (unit) { + case MINUTE: + if (matchers.get(2) == null) { + addToDate(cal, now, Calendar.DAY_OF_YEAR, 1); + break; + } else + if (matchers.get(3) == null) { + addToDate(cal, now, Calendar.MONTH, 1); + break; + } + else + return null; + case HOUR: + if (cal.get(Calendar.MINUTE) < now.get(Calendar.MINUTE)) { + addToDate(cal, now, Calendar.HOUR_OF_DAY, 1); + } else { + addToDate(cal, now, Calendar.HOUR_OF_DAY, 0); + } + break; + case DAY: + case WEEK: + addToDate(cal, now, Calendar.DAY_OF_YEAR, 1); + break; + case MONTH: + addToDate(cal, now, Calendar.MONTH, 1); + break; + case YEAR: + addToDate(cal, now, Calendar.YEAR, 1); + break; + } + } else + if (cal.get(Calendar.MINUTE) < now.get(Calendar.MINUTE)) { + switch (unit) { + case MINUTE: + addToDate(cal, now, Calendar.MINUTE, 1); + break; + case HOUR: + addToDate(cal, now, Calendar.HOUR_OF_DAY, 1); + break; + case DAY: + case WEEK: + addToDate(cal, now, Calendar.DAY_OF_YEAR, 1); + break; + case MONTH: + addToDate(cal, now, Calendar.MONTH, 1); + break; + case YEAR: + addToDate(cal, now, Calendar.YEAR, 1); + break; + } + } + + return applySpecials(cal); + } + + private Date applySpecials (Calendar cal) { + if (specials.get(2) != null && !setWeekOfMonth(cal)) + return null; + + if (specials.get(0) != null && !setDayOfWeek(cal)) + return null; + + return cal.getTime(); + } + + /** + * Gets the next trigger date. + * + * @param base The date from where to calculate the trigger date. + * + * @return null if there's none next trigger date. + */ + @Override + public Date getNextTriggerDate (Date base) { + Date date = base; + + if (getOccurrence() > 1) { + Calendar cal = getCal(base); + addInterval(cal); + date = cal.getTime(); + } + + incOccurrence(); + + return getTriggerDate(date); + } + + /** + * Sets the field value of now to date and adds by count. + */ + private void addToDate (Calendar cal, Calendar now, int field, int count) { + cal.set(field, now.get(field)); + cal.add(field, count); + } + + /** + * Set the day of the year but ensure that the calendar does point to a + * date in future. + * + * @param cal The calendar to manipulate. + * + * @return true if the operation could be made. + */ + private boolean setDayOfWeek (Calendar cal) { + cal.setFirstDayOfWeek(Calendar.MONDAY); + int day = WEEKDAYS_REV[cal.get(DAY_OF_WEEK)]; + int month = cal.get(Calendar.MONTH); + int year = cal.get(Calendar.YEAR); + int dayToSet = WEEKDAYS_REV[specials.get(0)]; + + if (matchers.get(2) != null) + return false; + + if (day > dayToSet) { + if (specials.get(2) == null) { + cal.add(WEEK_OF_YEAR, 1); + } else + if (matchers.get(3) == null) { + cal.add(Calendar.MONTH, 1); + } else + if (matchers.get(4) == null) { + cal.add(Calendar.YEAR, 1); + } else + return false; + } + + cal.set(Calendar.SECOND, 0); + cal.set(DAY_OF_WEEK, specials.get(0)); + + if (matchers.get(3) != null && cal.get(Calendar.MONTH) != month) + return false; + + //noinspection RedundantIfStatement + if (matchers.get(4) != null && cal.get(Calendar.YEAR) != year) + return false; + + return true; + } + + /** + * Set the week of the month but ensure that the calendar does point to a + * date in future. + * + * @param cal The calendar to manipulate. + * + * @return true if the operation could be made. + */ + private boolean setWeekOfMonth (Calendar cal) { + int week = cal.get(WEEK_OF_MONTH); + int year = cal.get(Calendar.YEAR); + int weekToSet = specials.get(2); + + if (week > weekToSet) { + if (matchers.get(3) == null) { + cal.add(Calendar.MONTH, 1); + } else + if (matchers.get(4) == null) { + cal.add(Calendar.YEAR, 1); + } else + return false; + + if (matchers.get(4) != null && cal.get(Calendar.YEAR) != year) + return false; + } + + int month = cal.get(Calendar.MONTH); + + cal.set(WEEK_OF_MONTH, weekToSet); + + if (cal.get(Calendar.MONTH) != month) { + cal.set(Calendar.DAY_OF_MONTH, 1); + cal.set(Calendar.MONTH, month); + } else + if (matchers.get(2) == null && week != weekToSet) { + cal.set(DAY_OF_WEEK, 2); + } + + return true; + } +} \ No newline at end of file diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/util/AssetProvider.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/util/AssetProvider.java new file mode 100644 index 0000000..569497f --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/util/AssetProvider.java @@ -0,0 +1,26 @@ +/* + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you under the Apache License, Version 2.0 (the + "License"); you may not use this file except in compliance + with the License. You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an + "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, either express or implied. See the License for the + specific language governing permissions and limitations + under the License. + */ + +package de.appplant.cordova.plugin.notification.util; + +import androidx.core.content.FileProvider; + +public class AssetProvider extends FileProvider { + // Nothing to do here +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/util/AssetUtil.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/util/AssetUtil.java new file mode 100644 index 0000000..96c5678 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/util/AssetUtil.java @@ -0,0 +1,352 @@ + + +package de.appplant.cordova.plugin.notification.util; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.os.StrictMode; +import android.util.Log; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.UUID; + +/** + * Util class to map unified asset URIs to native URIs. URIs like file:/// + * map to absolute paths while file:// point relatively to the www folder + * within the asset resources. And res:// means a resource from the native + * res folder. Remote assets are accessible via http:// for example. + */ +public final class AssetUtil { + + // Name of the storage folder + private static final String STORAGE_FOLDER = "/localnotification"; + + // Ref to the context passed through the constructor to access the + // resources and app directory. + private final Context context; + + /** + * Constructor + * + * @param context Application context. + */ + private AssetUtil(Context context) { + this.context = context; + } + + /** + * Static method to retrieve class instance. + * + * @param context Application context. + */ + public static AssetUtil getInstance(Context context) { + return new AssetUtil(context); + } + + /** + * The URI for a path. + * + * @param path The given path. + */ + public Uri parse (String path) { + if (path == null || path.isEmpty()) { + return Uri.EMPTY; + } else if (path.startsWith("res:")) { + return getUriForResourcePath(path); + } else if (path.startsWith("file:///")) { + return getUriFromPath(path); + } else if (path.startsWith("file://")) { + return getUriFromAsset(path); + } else if (path.startsWith("http")){ + return getUriFromRemote(path); + } else if (path.startsWith("content://")){ + return Uri.parse(path); + } + + return Uri.EMPTY; + } + + /** + * URI for a file. + * + * @param path Absolute path like file:///... + * + * @return URI pointing to the given path. + */ + private Uri getUriFromPath(String path) { + String absPath = path.replaceFirst("file://", "") + .replaceFirst("\\?.*$", ""); + File file = new File(absPath); + + if (!file.exists()) { + Log.e("Asset", "File not found: " + file.getAbsolutePath()); + return Uri.EMPTY; + } + + return getUriFromFile(file); + } + + /** + * URI for an asset. + * + * @param path Asset path like file://... + * + * @return URI pointing to the given path. + */ + private Uri getUriFromAsset(String path) { + String resPath = path.replaceFirst("file:/", "www") + .replaceFirst("\\?.*$", ""); + String fileName = resPath.substring(resPath.lastIndexOf('/') + 1); + File file = getTmpFile(fileName); + + if (file == null) + return Uri.EMPTY; + + try { + AssetManager assets = context.getAssets(); + InputStream in = assets.open(resPath); + FileOutputStream out = new FileOutputStream(file); + copyFile(in, out); + } catch (Exception e) { + Log.e("Asset", "File not found: assets/" + resPath); + e.printStackTrace(); + return Uri.EMPTY; + } + + return getUriFromFile(file); + } + + /** + * The URI for a resource. + * + * @param path The given relative path. + * + * @return URI pointing to the given path. + */ + private Uri getUriForResourcePath(String path) { + Resources res = context.getResources(); + String resPath = path.replaceFirst("res://", ""); + int resId = getResId(resPath); + + if (resId == 0) { + Log.e("Asset", "File not found: " + resPath); + return Uri.EMPTY; + } + + return new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(res.getResourcePackageName(resId)) + .appendPath(res.getResourceTypeName(resId)) + .appendPath(res.getResourceEntryName(resId)) + .build(); + } + + /** + * Uri from remote located content. + * + * @param path Remote address. + * + * @return Uri of the downloaded file. + */ + private Uri getUriFromRemote(String path) { + File file = getTmpFile(); + + if (file == null) + return Uri.EMPTY; + + try { + URL url = new URL(path); + HttpURLConnection connection = (HttpURLConnection) url.openConnection(); + + StrictMode.ThreadPolicy policy = + new StrictMode.ThreadPolicy.Builder().permitAll().build(); + + StrictMode.setThreadPolicy(policy); + + connection.setRequestProperty("Connection", "close"); + connection.setConnectTimeout(5000); + connection.connect(); + + InputStream in = connection.getInputStream(); + FileOutputStream out = new FileOutputStream(file); + + copyFile(in, out); + return getUriFromFile(file); + } catch (MalformedURLException e) { + Log.e("Asset", "Incorrect URL"); + e.printStackTrace(); + } catch (FileNotFoundException e) { + Log.e("Asset", "Failed to create new File from HTTP Content"); + e.printStackTrace(); + } catch (IOException e) { + Log.e("Asset", "No Input can be created from http Stream"); + e.printStackTrace(); + } + + return Uri.EMPTY; + } + + /** + * Copy content from input stream into output stream. + * + * @param in The input stream. + * @param out The output stream. + */ + private void copyFile(InputStream in, FileOutputStream out) { + byte[] buffer = new byte[1024]; + int read; + + try { + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + out.flush(); + out.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Resource ID for drawable. + * + * @param resPath Resource path as string. + * + * @return The resource ID or 0 if not found. + */ + public int getResId(String resPath) { + int resId = getResId(context.getResources(), resPath); + + return resId; + } + + /** + * Get resource ID. + * + * @param res The resources where to look for. + * @param resPath The name of the resource. + * + * @return The resource ID or 0 if not found. + */ + private int getResId(Resources res, String resPath) { + String pkgName = getPkgName(res); + String resName = getBaseName(resPath); + int resId; + + resId = res.getIdentifier(resName, "mipmap", pkgName); + + if (resId == 0) { + resId = res.getIdentifier(resName, "drawable", pkgName); + } + + if (resId == 0) { + resId = res.getIdentifier(resName, "raw", pkgName); + } + + return resId; + } + + /** + * Convert URI to Bitmap. + * + * @param uri Internal image URI + */ + public Bitmap getIconFromUri(Uri uri) throws IOException { + InputStream input = context.getContentResolver().openInputStream(uri); + return BitmapFactory.decodeStream(input); + } + + /** + * Extract name of drawable resource from path. + * + * @param resPath Resource path as string. + */ + private String getBaseName (String resPath) { + String drawable = resPath; + + if (drawable.contains("/")) { + drawable = drawable.substring(drawable.lastIndexOf('/') + 1); + } + + if (resPath.contains(".")) { + drawable = drawable.substring(0, drawable.lastIndexOf('.')); + } + + return drawable; + } + + /** + * Returns a file located under the external cache dir of that app. + * + * @return File with a random UUID name. + */ + private File getTmpFile () { + // If random UUID is not be enough see + // https://github.com/LukePulverenti/cordova-plugin-local-notifications/blob/267170db14044cbeff6f4c3c62d9b766b7a1dd62/src/android/notification/AssetUtil.java#L255 + return getTmpFile(UUID.randomUUID().toString()); + } + + /** + * Returns a file located under the external cache dir of that app. + * + * @param name The name of the file. + * + * @return File with the provided name. + */ + private File getTmpFile (String name) { + File dir = context.getExternalCacheDir(); + + if (dir == null) { + dir = context.getCacheDir(); + } + + if (dir == null) { + Log.e("Asset", "Missing cache dir"); + return null; + } + + String storage = dir.toString() + STORAGE_FOLDER; + + //noinspection ResultOfMethodCallIgnored + new File(storage).mkdir(); + + return new File(storage, name); + } + + /** + * Get content URI for the specified file. + * + * @param file The file to get the URI. + * + * @return content://... + */ + private Uri getUriFromFile(File file) { + try { + String authority = context.getPackageName() + ".localnotifications.provider"; + return AssetProvider.getUriForFile(context, authority, file); + } catch (IllegalArgumentException e) { + e.printStackTrace(); + return Uri.EMPTY; + } + } + + /** + * Package name specified by the resource bundle. + */ + private String getPkgName (Resources res) { + return res == Resources.getSystem() ? "android" : context.getPackageName(); + } + +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/notification/util/LaunchUtils.java b/my-plugins/cordova-plugin-local-notification/src/android/notification/util/LaunchUtils.java new file mode 100644 index 0000000..ffd8534 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/notification/util/LaunchUtils.java @@ -0,0 +1,62 @@ +package de.appplant.cordova.plugin.notification.util; + +import android.app.PendingIntent; +import android.app.TaskStackBuilder; +import android.content.Context; +import android.content.Intent; + +import static android.content.Intent.FLAG_ACTIVITY_REORDER_TO_FRONT; +import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; + +import java.util.Random; + +public final class LaunchUtils { + + private static int getIntentFlags() { + int flags = PendingIntent.FLAG_UPDATE_CURRENT; + if (android.os.Build.VERSION.SDK_INT >= 31) { + flags |= PendingIntent.FLAG_MUTABLE; + } + return flags; + } + + public static PendingIntent getBroadcastPendingIntent(Context context, + Intent intent, int notificationId) { + return PendingIntent.getBroadcast(context, notificationId, intent, getIntentFlags()); + } + + public static PendingIntent getActivityPendingIntent(Context context, + Intent intent, int notificationId) { + return PendingIntent.getActivity(context, notificationId, intent, getIntentFlags()); + } + + public static PendingIntent getTaskStackPendingIntent(Context context, + Intent intent, int notificationId) { + TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context); + taskStackBuilder.addNextIntentWithParentStack(intent); + return taskStackBuilder.getPendingIntent(notificationId, getIntentFlags()); + } + + /*** + * Launch main intent from package. + */ + public static void launchApp(Context context) { + String pkgName = context.getPackageName(); + + Intent intent = context + .getPackageManager() + .getLaunchIntentForPackage(pkgName); + + if (intent == null) + return; + + intent.addFlags( + FLAG_ACTIVITY_REORDER_TO_FRONT + | FLAG_ACTIVITY_SINGLE_TOP + | FLAG_ACTIVITY_NEW_TASK + ); + + context.startActivity(intent); + } +} diff --git a/my-plugins/cordova-plugin-local-notification/src/android/xml/localnotification_provider_paths.xml b/my-plugins/cordova-plugin-local-notification/src/android/xml/localnotification_provider_paths.xml new file mode 100644 index 0000000..4ff8931 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/src/android/xml/localnotification_provider_paths.xml @@ -0,0 +1,4 @@ + + + + diff --git a/my-plugins/cordova-plugin-local-notification/www/local-notification.js b/my-plugins/cordova-plugin-local-notification/www/local-notification.js new file mode 100644 index 0000000..7e70c07 --- /dev/null +++ b/my-plugins/cordova-plugin-local-notification/www/local-notification.js @@ -0,0 +1,1096 @@ +var exec = require('cordova/exec'), + channel = require('cordova/channel'); + +// Defaults +exports._defaults = { + actions : [], + alarmVolume : -1, + attachments : [], + autoLaunch : false, + autoClear : true, + badge : null, + channelName : null, + clock : true, + color : null, + data : null, + defaults : 0, + foreground : null, + group : null, + groupSummary : false, + icon : null, + iconType : null, + id : 0, + launch : true, + led : true, + lockscreen : true, + mediaSession : null, + number : 0, + priority : 0, + progressBar : false, + resetDelay : 5, + silent : false, + smallIcon : 'res://icon', + sound : true, + sticky : false, + summary : null, + text : '', + timeoutAfter : false, + title : '', + trigger : { type : 'calendar' }, + vibrate : false, + wakeup : true, + channelId : null, + wakeLockTimeout: null, + fullScreenIntent: false, + triggerInApp: false +}; + +// Event listener +exports._listener = {}; + +/** + * Check permission to show notifications. + * + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.hasPermission = function (callback, scope) { + this._exec('check', null, callback, scope); +}; + +/** + * Request permission to show notifications. + * + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.requestPermission = function (callback, scope) { + this._exec('request', null, callback, scope); +}; + +/** + * Check to see if the user has allowed "Do Not Disturb" permissions for this app. + * This is required to use alarmVolume to take a user out of silent mode. + * + * @param {Function} callback The function to be exec as the callback. + * @param {Object} scope callback function's scope + */ +exports.hasDoNotDisturbPermissions = function (callback, scope) { + this._exec('hasDoNotDisturbPermissions', null, callback, scope); +} + +/** + * Request "Do Not Disturb" permissions for this app. + * The only way to do this is to launch the global do not distrub settings for all apps. + * This permission is required to use alarmVolume to take a user out of silent mode. + * + * @param {Function} callback The function to be exec as the callback. + * @param {Object} scope callback function's scope. + */ +exports.requestDoNotDisturbPermissions = function (callback, scope) { + this._exec('requestDoNotDisturbPermissions', null, callback, scope); +} + +/** + * Check to see if the app is ignoring battery optimizations. This needs + * to be whitelisted by the user. + * + * Callback contains true or false for whether or not we have this permission. + * + * @param {Function} callback The function to be exec as the callback. + * @param {Object} scope callback function's scope + */ +exports.isIgnoringBatteryOptimizations = function (callback, scope) { + this._exec('isIgnoringBatteryOptimizations', null, callback, scope); +} + +/** + * Request permission to ignore battery optimizations. + * The only way to do this is to launch the global battery optimization settings for all apps. + * This permission is required to allow alarm to trigger logic within the app while the app is dead. + * + * Callback is deferred until user returns. + * + * @param {Function} callback The function to be exec as the callback. + * @param {Object} scope callback function's scope + */ +exports.requestIgnoreBatteryOptimizations = function (callback, scope) { + if (device.platform === 'iOS') { + console.warn('[Notifications] requestIgnoreBatteryOptimizations not supported on iOS'); + callback(true); + } + + this._exec('requestIgnoreBatteryOptimizations', null, callback, scope); +} + +/** + * Schedule notifications. + * + * @param [ Array ] notifications The notifications to schedule. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * @param [ Object ] args Optional flags how to schedule. + * + * @return [ Void ] + */ +exports.schedule = function (msgs, callback, scope, args) { + var fn = function (granted) { + var toasts = this._toArray(msgs); + + if (!granted && callback) { + callback.call(scope || this, false); + return; + } + + for (var i = 0, len = toasts.length; i < len; i++) { + var toast = toasts[i]; + this._mergeWithDefaults(toast); + this._convertProperties(toast); + } + + this._exec('schedule', toasts, callback, scope); + }; + + if (args && args.skipPermission) { + fn.call(this, true); + } else { + this.requestPermission(fn, this); + } +}; + +/** + * Schedule notifications. + * + * @param [ Array ] notifications The notifications to schedule. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * @param [ Object ] args Optional flags how to schedule. + * + * @return [ Void ] + */ +exports.update = function (msgs, callback, scope, args) { + var fn = function(granted) { + var toasts = this._toArray(msgs); + + if (!granted && callback) { + callback.call(scope || this, false); + return; + } + + for (var i = 0, len = toasts.length; i < len; i++) { + this._convertProperties(toasts[i]); + } + + this._exec('update', toasts, callback, scope); + }; + + if (args && args.skipPermission) { + fn.call(this, true); + } else { + this.requestPermission(fn, this); + } +}; + +/** + * To set dummyNotifications to get notification for Android 13. + * + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ + exports.setDummyNotifications = function (callback, scope) { + if (device.platform !== 'Android') { + console.warn('[Notifications] setDummyNotifications only supported on Android'); + callback(true); + }else{ + this._exec('dummyNotifications', null, callback, scope); + } +}; + + +/** + * Clear the specified notifications by id. + * + * @param [ Array ] ids The IDs of the notifications. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.clear = function (ids, callback, scope) { + ids = this._toArray(ids); + ids = this._convertIds(ids); + + this._exec('clear', ids, callback, scope); +}; + +/** + * Clear all triggered notifications. + * + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.clearAll = function (callback, scope) { + this._exec('clearAll', null, callback, scope); +}; + +/** + * Clear the specified notifications by id. + * + * @param [ Array ] ids The IDs of the notifications. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.cancel = function (ids, callback, scope) { + ids = this._toArray(ids); + ids = this._convertIds(ids); + + this._exec('cancel', ids, callback, scope); +}; + +/** + * Cancel all scheduled notifications. + * + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.cancelAll = function (callback, scope) { + this._exec('cancelAll', null, callback, scope); +}; + +/** + * Check if a notification is present. + * + * @param [ Int ] id The ID of the notification. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.isPresent = function (id, callback, scope) { + var fn = this._createCallbackFn(callback, scope); + + this.getType(id, function (type) { + fn(type != 'unknown'); + }); +}; + +/** + * Check if a notification is scheduled. + * + * @param [ Int ] id The ID of the notification. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.isScheduled = function (id, callback, scope) { + this.hasType(id, 'scheduled', callback, scope); +}; + +/** + * Check if a notification was triggered. + * + * @param [ Int ] id The ID of the notification. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.isTriggered = function (id, callback, scope) { + this.hasType(id, 'triggered', callback, scope); +}; + +/** + * Check if a notification has a given type. + * + * @param [ Int ] id The ID of the notification. + * @param [ String ] type The type of the notification. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.hasType = function (id, type, callback, scope) { + var fn = this._createCallbackFn(callback, scope); + + this.getType(id, function (type2) { + fn(type == type2); + }); +}; + +/** + * Get the type (triggered, scheduled) for the notification. + * + * @param [ Int ] id The ID of the notification. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.getType = function (id, callback, scope) { + this._exec('type', id, callback, scope); +}; + +/** + * List of all notification ids. + * + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.getIds = function (callback, scope) { + this._exec('ids', 0, callback, scope); +}; + +/** + * List of all scheduled notification IDs. + * + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.getScheduledIds = function (callback, scope) { + this._exec('ids', 1, callback, scope); +}; + +/** + * List of all triggered notification IDs. + * + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.getTriggeredIds = function (callback, scope) { + this._exec('ids', 2, callback, scope); +}; + +/** + * List of local notifications specified by id. + * If called without IDs, all notification will be returned. + * + * @param [ Array ] ids The IDs of the notifications. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.get = function () { + var args = Array.apply(null, arguments); + + if (typeof args[0] == 'function') { + args.unshift([]); + } + + var ids = args[0], + callback = args[1], + scope = args[2]; + + if (!Array.isArray(ids)) { + this._exec('notification', Number(ids), callback, scope); + return; + } + + ids = this._convertIds(ids); + + this._exec('notifications', [3, ids], callback, scope); +}; + +/** + * List for all notifications. + * + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.getAll = function (callback, scope) { + this._exec('notifications', 0, callback, scope); +}; + +/** + * List of all scheduled notifications. + * + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + */ +exports.getScheduled = function (callback, scope) { + this._exec('notifications', 1, callback, scope); +}; + +/** + * List of all triggered notifications. + * + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + */ +exports.getTriggered = function (callback, scope) { + this._exec('notifications', 2, callback, scope); +}; + +/** + * Add an group of actions by id. + * + * @param [ String ] id The Id of the group. + * @param [ Array] actions The action config settings. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.addActions = function (id, actions, callback, scope) { + this._exec('actions', [0, id, actions], callback, scope); +}; + +/** + * Remove an group of actions by id. + * + * @param [ String ] id The Id of the group. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.removeActions = function (id, callback, scope) { + this._exec('actions', [1, id], callback, scope); +}; + +/** + * Check if a group of actions is defined. + * + * @param [ String ] id The Id of the group. + * @param [ Function ] callback The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.hasActions = function (id, callback, scope) { + this._exec('actions', [2, id], callback, scope); +}; + +/** + * The (platform specific) default settings. + * + * @return [ Object ] + */ +exports.getDefaults = function () { + var map = Object.assign({}, this._defaults); + + for (var key in map) { + if (Array.isArray(map[key])) { + map[key] = Array.from(map[key]); + } else + if (Object.prototype.isPrototypeOf(map[key])) { + map[key] = Object.assign({}, map[key]); + } + } + + return map; +}; + +/** + * Overwrite default settings. + * + * @param [ Object ] newDefaults New default values. + * + * @return [ Void ] + */ +exports.setDefaults = function (newDefaults) { + Object.assign(this._defaults, newDefaults); +}; + +/** + * Register callback for given event. + * + * @param [ String ] event The name of the event. + * @param [ Function ] callback The function to be exec as callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Void ] + */ +exports.on = function (event, callback, scope) { + var type = typeof callback; + + if (type !== 'function' && type !== 'string') + return; + + if (!this._listener[event]) { + this._listener[event] = []; + } + + var item = [callback, scope || window]; + + this._listener[event].push(item); +}; + +/** + * Unregister callback for given event. + * + * @param [ String ] event The name of the event. + * @param [ Function ] callback The function to be exec as callback. + * + * @return [ Void ] + */ +exports.un = function (event, callback) { + var listener = this._listener[event]; + + if (!listener) + return; + + for (var i = 0; i < listener.length; i++) { + var fn = listener[i][0]; + + if (fn == callback) { + listener.splice(i, 1); + break; + } + } +}; + +/** + * Fire the event with given arguments. + * + * @param [ String ] event The event's name. + * @param [ *Array] args The callback's arguments. + * + * @return [ Void] + */ +exports.fireEvent = function (event) { + var args = Array.apply(null, arguments).slice(1), + listener = this._listener[event]; + + if (!listener) + return; + + if (args[0] && typeof args[0].data === 'string') { + args[0].data = JSON.parse(args[0].data); + } + + for (var i = 0; i < listener.length; i++) { + var fn = listener[i][0], + scope = listener[i][1]; + + if (typeof fn !== 'function') { + fn = scope[fn]; + } + + fn.apply(scope, args); + } +}; + +/** + * Fire queued events once the device is ready and all listeners are registered. + * + * @return [ Void ] + */ +exports.fireQueuedEvents = function() { + exports._exec('ready'); +}; + +/** + * Merge custom properties with the default values. + * + * @param [ Object ] options Set of custom values. + * + * @retrun [ Object ] + */ +exports._mergeWithDefaults = function (options) { + var values = this.getDefaults(); + + if (values.hasOwnProperty('sticky')) { + options.sticky = this._getValueFor(options, 'sticky', 'ongoing'); + } + + if (options.sticky && options.autoClear !== true) { + options.autoClear = false; + } + + Object.assign(values, options); + + for (var key in values) { + if (values[key] !== null) { + options[key] = values[key]; + } else { + delete options[key]; + } + + if (!this._defaults.hasOwnProperty(key)) { + console.warn('Unknown property: ' + key); + } + } + + options.meta = { + plugin: 'cordova-plugin-local-notification', + version: '0.10.0' + }; + + return options; +}; + +/** + * Convert the passed values to their required type. + * + * @param [ Object ] options Properties to convert for. + * + * @return [ Object ] The converted property list + */ +exports._convertProperties = function (options) { + var parseToInt = function (prop, options) { + if (isNaN(options[prop])) { + console.warn(prop + ' is not a number: ' + options[prop]); + return this._defaults[prop]; + } else { + return Number(options[prop]); + } + }; + + if (options.id) { + options.id = parseToInt('id', options); + } + + if (options.title) { + options.title = options.title.toString(); + } + + if (options.badge) { + options.badge = parseToInt('badge', options); + } + + if (options.defaults) { + options.defaults = parseToInt('defaults', options); + } + + if (options.smallIcon && !options.smallIcon.match(/^res:/)) { + console.warn('Property "smallIcon" must be of kind res://...'); + } + + if (typeof options.timeoutAfter === 'boolean') { + options.timeoutAfter = options.timeoutAfter ? 3600000 : null; + } + + if (options.timeoutAfter) { + options.timeoutAfter = parseToInt('timeoutAfter', options); + } + + options.data = JSON.stringify(options.data); + + this._convertPriority(options); + this._convertTrigger(options); + this._convertActions(options); + this._convertProgressBar(options); + + return options; +}; + +/** + * Convert the passed values for the priority to their required type. + * + * @param [ Map ] options Set of custom values. + * + * @return [ Map ] Interaction object with trigger spec. + */ +exports._convertPriority = function (options) { + var prio = options.priority || options.prio || 0; + + if (typeof prio === 'string') { + prio = { min: -2, low: -1, high: 1, max: 2 }[prio] || 0; + } + + if (options.foreground === true) { + prio = Math.max(prio, 1); + } + + if (options.foreground === false) { + prio = Math.min(prio, 0); + } + + options.priority = prio; + + return options; +}; + +/** + * Convert the passed values to their required type, modifying them + * directly for Android and passing the converted list back for iOS. + * + * @param [ Map ] options Set of custom values. + * + * @return [ Map ] Interaction object with category & actions. + */ +exports._convertActions = function (options) { + var actions = []; + + if (!options.actions || typeof options.actions === 'string') + return options; + + for (var i = 0, len = options.actions.length; i < len; i++) { + var action = options.actions[i]; + + if (!action.id) { + console.warn('Action with title ' + action.title + ' ' + + 'has no id and will not be added.'); + continue; + } + + action.id = action.id.toString(); + + actions.push(action); + } + + options.actions = actions; + + return options; +}; + +/** + * Convert the passed values for the trigger to their required type. + * + * @param [ Map ] options Set of custom values. + * + * @return [ Map ] Interaction object with trigger spec. + */ +exports._convertTrigger = function (options) { + var trigger = options.trigger || {}, + date = this._getValueFor(trigger, 'at', 'firstAt', 'date'); + + var dateToNum = function (date) { + var num = typeof date == 'object' ? date.getTime() : date; + return Math.round(num); + }; + + if (!options.trigger) + return; + + if (!trigger.type) { + trigger.type = trigger.center ? 'location' : 'calendar'; + } + + var isCal = trigger.type == 'calendar'; + + if (isCal && !date) { + date = this._getValueFor(options, 'at', 'firstAt', 'date'); + } + + if (isCal && !trigger.every && options.every) { + trigger.every = options.every; + } + + if (isCal && (trigger.in || trigger.every)) { + date = null; + } + + if (isCal && date) { + trigger.at = dateToNum(date); + } + + if (isCal && trigger.firstAt) { + trigger.firstAt = dateToNum(trigger.firstAt); + } + + if (isCal && trigger.before) { + trigger.before = dateToNum(trigger.before); + } + + if (isCal && trigger.after) { + trigger.after = dateToNum(trigger.after); + } + + if (!trigger.count && device.platform == 'windows') { + trigger.count = trigger.every ? 5 : 1; + } + + if (trigger.count && device.platform == 'iOS') { + console.warn('trigger: { count: } is not supported on iOS.'); + } + + if (!isCal) { + trigger.notifyOnEntry = !!trigger.notifyOnEntry; + trigger.notifyOnExit = trigger.notifyOnExit === true; + trigger.radius = trigger.radius || 5; + trigger.single = !!trigger.single; + } + + if (!isCal || trigger.at) { + delete trigger.every; + } + + delete options.every; + delete options.at; + delete options.firstAt; + delete options.date; + + options.trigger = trigger; + + return options; +}; + +/** + * Convert the passed values for the progressBar to their required type. + * + * @param [ Map ] options Set of custom values. + * + * @return [ Map ] Interaction object with trigger spec. + */ +exports._convertProgressBar = function (options) { + var isAndroid = device.platform == 'Android', + cfg = options.progressBar; + + if (cfg === undefined) + return; + + if (typeof cfg === 'boolean') { + cfg = options.progressBar = { enabled: cfg }; + } + + if (typeof cfg.enabled !== 'boolean') { + cfg.enabled = !!(cfg.value || cfg.maxValue || cfg.indeterminate !== null); + } + + cfg.value = cfg.value || 0; + + if (isAndroid) { + cfg.maxValue = cfg.maxValue || 100; + cfg.indeterminate = !!cfg.indeterminate; + } + + cfg.enabled = !!cfg.enabled; + + if (cfg.enabled && options.clock === true) { + options.clock = 'chronometer'; + } + + return options; +}; + +/** + * Create a callback function to get executed within a specific scope. + * + * @param [ Function ] fn The function to be exec as the callback. + * @param [ Object ] scope The callback function's scope. + * + * @return [ Function ] + */ +exports._createCallbackFn = function (fn, scope) { + + if (typeof fn != 'function') + return; + + return function () { + fn.apply(scope || this, arguments); + }; +}; + +/** + * Convert the IDs to numbers. + * + * @param [ Array ] ids + * + * @return [ Array ] + */ +exports._convertIds = function (ids) { + var convertedIds = []; + + for (var i = 0, len = ids.length; i < len; i++) { + convertedIds.push(Number(ids[i])); + } + + return convertedIds; +}; + +/** + * First found value for the given keys. + * + * @param [ Object ] options Object with key-value properties. + * @param [ *Array ] keys List of keys. + * + * @return [ Object ] + */ +exports._getValueFor = function (options) { + var keys = Array.apply(null, arguments).slice(1); + + for (var i = 0, key = keys[i], len = keys.length; i < len; key = keys[++i]) { + if (options.hasOwnProperty(key)) { + return options[key]; + } + } + + return null; +}; + +/** + * Convert a value to an array. + * + * @param [ Object ] obj Any kind of object. + * + * @return [ Array ] An array with the object as first item. + */ +exports._toArray = function (obj) { + return Array.isArray(obj) ? Array.from(obj) : [obj]; +}; + +/** + * Execute the native counterpart. + * + * @param [ String ] action The name of the action. + * @param [ Array ] args Array of arguments. + * @param [ Function] callback The callback function. + * @param [ Object ] scope The scope for the function. + * + * @return [ Void ] + */ +exports._exec = function (action, args, callback, scope) { + var fn = this._createCallbackFn(callback, scope), + params = []; + + if (Array.isArray(args)) { + params = args; + } else if (args !== null) { + params.push(args); + } + + exec(fn, null, 'LocalNotification', action, params); +}; + +/** + * Set the launch details if the app was launched by clicking on a toast. + * + * @return [ Void ] + */ +exports._setLaunchDetails = function () { + exports._exec('launch', null, function (details) { + if (details) { + exports.launchDetails = details; + } + }); +}; + +// Polyfill for Object.assign +if (typeof Object.assign != 'function') { + Object.assign = function(target) { + 'use strict'; + if (target == null) { + throw new TypeError('Cannot convert undefined or null to object'); + } + + target = Object(target); + for (var index = 1; index < arguments.length; index++) { + var source = arguments[index]; + if (source != null) { + for (var key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + target[key] = source[key]; + } + } + } + } + return target; + }; +} + +// Polyfill for Array.from +// Production steps of ECMA-262, Edition 6, 22.1.2.1 +// Reference: https://people.mozilla.org/~jorendorff/es6-draft.html#sec-array.from +if (!Array.from) { + Array.from = (function () { + var toStr = Object.prototype.toString; + var isCallable = function (fn) { + return typeof fn === 'function' || toStr.call(fn) === '[object Function]'; + }; + var toInteger = function (value) { + var number = Number(value); + if (isNaN(number)) { return 0; } + if (number === 0 || !isFinite(number)) { return number; } + return (number > 0 ? 1 : -1) * Math.floor(Math.abs(number)); + }; + var maxSafeInteger = Math.pow(2, 53) - 1; + var toLength = function (value) { + var len = toInteger(value); + return Math.min(Math.max(len, 0), maxSafeInteger); + }; + + // The length property of the from method is 1. + return function from(arrayLike/*, mapFn, thisArg */) { + // 1. Let C be the this value. + var C = this; + + // 2. Let items be ToObject(arrayLike). + var items = Object(arrayLike); + + // 3. ReturnIfAbrupt(items). + if (arrayLike == null) { + throw new TypeError("Array.from requires an array-like object - not null or undefined"); + } + + // 4. If mapfn is undefined, then let mapping be false. + var mapFn = arguments.length > 1 ? arguments[1] : void undefined; + var T; + if (typeof mapFn !== 'undefined') { + // 5. else + // 5. a If IsCallable(mapfn) is false, throw a TypeError exception. + if (!isCallable(mapFn)) { + throw new TypeError('Array.from: when provided, the second argument must be a function'); + } + + // 5. b. If thisArg was supplied, let T be thisArg; else let T be undefined. + if (arguments.length > 2) { + T = arguments[2]; + } + } + + // 10. Let lenValue be Get(items, "length"). + // 11. Let len be ToLength(lenValue). + var len = toLength(items.length); + + // 13. If IsConstructor(C) is true, then + // 13. a. Let A be the result of calling the [[Construct]] internal method of C with an argument list containing the single item len. + // 14. a. Else, Let A be ArrayCreate(len). + var A = isCallable(C) ? Object(new C(len)) : new Array(len); + + // 16. Let k be 0. + var k = 0; + // 17. Repeat, while k < len… (also steps a - h) + var kValue; + while (k < len) { + kValue = items[k]; + if (mapFn) { + A[k] = typeof T === 'undefined' ? mapFn(kValue, k) : mapFn.call(T, kValue, k); + } else { + A[k] = kValue; + } + k += 1; + } + // 18. Let putStatus be Put(A, "length", len, true). + A.length = len; + // 20. Return A. + return A; + }; + }()); +} + +// Called after 'deviceready' event +channel.deviceready.subscribe(function () { + if (!window.skipLocalNotificationReady) { + exports.fireQueuedEvents(); + } +}); + +// Called before 'deviceready' event +channel.onCordovaReady.subscribe(function () { + channel.onCordovaInfoReady.subscribe(function () { + exports._setLaunchDetails(); + }); +}); diff --git a/my-plugins/cordova-plugin-navigationbar-color/package.json b/my-plugins/cordova-plugin-navigationbar-color/package.json new file mode 100644 index 0000000..766044f --- /dev/null +++ b/my-plugins/cordova-plugin-navigationbar-color/package.json @@ -0,0 +1,34 @@ +{ + "name": "cordova-plugin-navigationbar-color", + "version": "0.1.0", + "description": "Cordova NavigationBar Plugin", + "cordova": { + "id": "cordova-plugin-navigationbar-color", + "platforms": [ + "android" + ] + }, + "keywords": [ + "cordova", + "navigationbar", + "ecosystem:cordova", + "cordova-android" + ], + "scripts": { + "test": "npm run jshint", + "jshint": "node node_modules/jshint/bin/jshint www && node node_modules/jshint/bin/jshint src && node node_modules/jshint/bin/jshint tests" + }, + "engines": { + "cordovaDependencies": { + "0.1.0": { + "cordova": ">=3.0.0" + }, + "3.0.0": { + "cordova": ">100" + } + } + }, + "devDependencies": { + "jshint": "^2.6.0" + } +} diff --git a/my-plugins/cordova-plugin-navigationbar-color/plugin.xml b/my-plugins/cordova-plugin-navigationbar-color/plugin.xml new file mode 100644 index 0000000..159a0fc --- /dev/null +++ b/my-plugins/cordova-plugin-navigationbar-color/plugin.xml @@ -0,0 +1,32 @@ + + + + + NavigationBar + + Cordova NavigationBar Plugin + + cordova,navigationbar + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/my-plugins/cordova-plugin-navigationbar-color/src/android/NavigationBar.java b/my-plugins/cordova-plugin-navigationbar-color/src/android/NavigationBar.java new file mode 100644 index 0000000..b9850d9 --- /dev/null +++ b/my-plugins/cordova-plugin-navigationbar-color/src/android/NavigationBar.java @@ -0,0 +1,193 @@ +package com.viniciusfagundes.cordova.plugin.navigationbar; + + import android.app.Activity; + import android.graphics.Color; + import android.os.Build; + import android.view.View; + import android.view.Window; + import android.view.WindowManager; + + + import androidx.core.view.ViewCompat; + import androidx.core.view.WindowInsetsControllerCompat; + + import org.apache.cordova.CallbackContext; + import org.apache.cordova.CordovaArgs; + import org.apache.cordova.CordovaInterface; + import org.apache.cordova.CordovaPlugin; + import org.apache.cordova.CordovaWebView; + import org.apache.cordova.LOG; + import org.apache.cordova.PluginResult; + import org.json.JSONException; + + public class NavigationBar extends CordovaPlugin { + private static final String TAG = "NavigationBar"; + + /** + * Sets the context of the Command. This can then be used to do things like + * get file paths associated with the Activity. + * + * @param cordova The context of the main Activity. + * @param webView The CordovaWebView Cordova is running in. + */ + @Override + public void initialize(final CordovaInterface cordova, CordovaWebView webView) { + LOG.v(TAG, "NavigationBar: initialization"); + super.initialize(cordova, webView); + + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + // Clear flag FLAG_FORCE_NOT_FULLSCREEN which is set initially + // by the Cordova. + Window window = cordova.getActivity().getWindow(); + window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + + // Read 'NavigationBarBackgroundColor' and 'NavigationBarLight' from config.xml, default is #000000. + setNavigationBarBackgroundColor(preferences.getString("NavigationBarBackgroundColor", "#000000"), preferences.getBoolean("NavigationBarLight", false)); + } + }); + } + + /** + * Executes the request and returns PluginResult. + * + * @param action The action to execute. + * @param args JSONArry of arguments for the plugin. + * @param callbackContext The callback id used when calling back into JavaScript. + * @return True if the action was valid, false otherwise. + */ + @Override + public boolean execute(final String action, final CordovaArgs args, final CallbackContext callbackContext) throws JSONException { + LOG.v(TAG, "Executing action: " + action); + final Activity activity = this.cordova.getActivity(); + final Window window = activity.getWindow(); + + if ("_ready".equals(action)) { + boolean navigationBarVisible = (window.getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0; + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, navigationBarVisible)); + return true; + } + + if ("show".equals(action)) { + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + // SYSTEM_UI_FLAG_FULLSCREEN is available since JellyBean, but we + // use KitKat here to be aligned with "Fullscreen" preference + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + int uiOptions = window.getDecorView().getSystemUiVisibility(); + uiOptions &= ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; + uiOptions &= ~View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + + window.getDecorView().setSystemUiVisibility(uiOptions); + + window.getDecorView().setOnFocusChangeListener(null); + window.getDecorView().setOnSystemUiVisibilityChangeListener(null); + } + + // CB-11197 We still need to update LayoutParams to force navigation bar + // to be hidden when entering e.g. text fields + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + }); + return true; + } + + if ("hide".equals(action)) { + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + // SYSTEM_UI_FLAG_FULLSCREEN is available since JellyBean, but we + // use KitKat here to be aligned with "Fullscreen" preference + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + final int uiOptions = window.getDecorView().getSystemUiVisibility() + | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; + + window.getDecorView().setSystemUiVisibility(uiOptions); + + window.getDecorView().setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + window.getDecorView().setSystemUiVisibility(uiOptions); + } + } + }); + + window.getDecorView().setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() { + @Override + public void onSystemUiVisibilityChange(int visibility) { + window.getDecorView().setSystemUiVisibility(uiOptions); + } + }); + } + + // CB-11197 We still need to update LayoutParams to force navigation bar + // to be hidden when entering e.g. text fields + //window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + } + }); + return true; + } + + if ("backgroundColorByHexString".equals(action)) { + this.cordova.getActivity().runOnUiThread(new Runnable() { + @Override + public void run() { + try { + setNavigationBarBackgroundColor(args.getString(0), args.getBoolean(1)); + } catch (JSONException ignore) { + LOG.e(TAG, "Invalid hexString argument, use f.i. '#777777'"); + } + } + }); + return true; + } + + return false; + } + + private void setNavigationBarBackgroundColor(final String colorPref, Boolean lightNavigationBar) { + + lightNavigationBar = lightNavigationBar != null && lightNavigationBar; + + if (Build.VERSION.SDK_INT >= 21) { + if (colorPref != null && !colorPref.isEmpty()) { + final Window window = cordova.getActivity().getWindow(); + final View decorView = window.getDecorView(); + WindowInsetsControllerCompat wic = ViewCompat.getWindowInsetsController(decorView); + int uiOptions = decorView.getSystemUiVisibility(); + + // 0x80000000 FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS + // 0x00000010 SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR + + uiOptions = uiOptions | 0x80000000; + + if(Build.VERSION.SDK_INT >= 26 && lightNavigationBar) + uiOptions = uiOptions | 0x00000010; + else + uiOptions = uiOptions & ~0x00000010; + + decorView.setSystemUiVisibility(uiOptions); + + if (lightNavigationBar) { + if (wic != null) wic.setAppearanceLightNavigationBars(true); + }else{ + if (wic != null) wic.setAppearanceLightNavigationBars(false); + } + + try { + // Using reflection makes sure any 5.0+ device will work without having to compile with SDK level 21 + window.getClass().getDeclaredMethod("setNavigationBarColor", int.class).invoke(window, Color.parseColor(colorPref)); + } catch (IllegalArgumentException ignore) { + LOG.e(TAG, "Invalid hexString argument, use f.i. '#999999'"); + } catch (Exception ignore) { + // this should not happen, only in case Android removes this method in a version > 21 + LOG.w(TAG, "Method window.setNavigationBarColor not found for SDK level " + Build.VERSION.SDK_INT); + } + } + } + } + } \ No newline at end of file diff --git a/my-plugins/cordova-plugin-navigationbar-color/www/navigationbar.js b/my-plugins/cordova-plugin-navigationbar-color/www/navigationbar.js new file mode 100644 index 0000000..24989b3 --- /dev/null +++ b/my-plugins/cordova-plugin-navigationbar-color/www/navigationbar.js @@ -0,0 +1,68 @@ +var exec = require('cordova/exec'); + +var namedColors = { + "black": "#000000", + "darkGray": "#A9A9A9", + "lightGray": "#D3D3D3", + "white": "#FFFFFF", + "gray": "#808080", + "red": "#FF0000", + "green": "#00FF00", + "blue": "#0000FF", + "cyan": "#00FFFF", + "yellow": "#FFFF00", + "magenta": "#FF00FF", + "orange": "#FFA500", + "purple": "#800080", + "brown": "#A52A2A" +}; + +var NavigationBar = { + + isVisible: true, + + backgroundColorByName: function (colorname, lightNavigationBar) { + return NavigationBar.backgroundColorByHexString(namedColors[colorname], lightNavigationBar); + }, + + backgroundColorByHexString: function (hexString, lightNavigationBar) { + if (hexString.charAt(0) !== "#") { + hexString = "#" + hexString; + } + + if (hexString.length === 4) { + var split = hexString.split(""); + hexString = "#" + split[1] + split[1] + split[2] + split[2] + split[3] + split[3]; + } + + lightNavigationBar = !!lightNavigationBar; + + exec(null, null, "NavigationBar", "backgroundColorByHexString", [hexString, lightNavigationBar]); + }, + + hide: function () { + exec(null, null, "NavigationBar", "hide", []); + NavigationBar.isVisible = false; + }, + + show: function () { + exec(null, null, "NavigationBar", "show", []); + NavigationBar.isVisible = true; + } + +}; + +// prime it. setTimeout so that proxy gets time to init +window.setTimeout(function () { + exec(function (res) { + if (typeof res === 'object') { + if (res.type === 'tap') { + cordova.fireWindowEvent('navigationTap'); + } + } else { + NavigationBar.isVisible = res; + } + }, null, "NavigationBar", "_ready", []); +}, 0); + +module.exports = NavigationBar; \ No newline at end of file diff --git a/my-plugins/cordova-plugin-statusbar/package.json b/my-plugins/cordova-plugin-statusbar/package.json new file mode 100644 index 0000000..27a0117 --- /dev/null +++ b/my-plugins/cordova-plugin-statusbar/package.json @@ -0,0 +1,45 @@ +{ + "name": "cordova-plugin-statusbar", + "version": "4.0.0", + "description": "Cordova StatusBar Plugin", + "types": "./types/index.d.ts", + "cordova": { + "id": "cordova-plugin-statusbar", + "platforms": [ + "android", + "ios" + ] + }, + "repository": "github:apache/cordova-plugin-statusbar", + "bugs": "https://github.com/apache/cordova-plugin-statusbar/issues", + "keywords": [ + "cordova", + "statusbar", + "ecosystem:cordova", + "cordova-android", + "cordova-ios" + ], + "scripts": { + "test": "npm run lint", + "lint": "eslint ." + }, + "engines": { + "cordovaDependencies": { + "0.1.0": { + "cordova": ">=3.0.0" + }, + "4.0.0": { + "cordova-android": ">=10.0.0", + "cordova": ">=3.0.0", + "cordova-ios": ">=6.0.0" + }, + "5.0.0": { + "cordova": ">100" + } + } + }, + "author": "Apache Software Foundation", + "devDependencies": { + "@cordova/eslint-config": "^4.0.0" + } +} diff --git a/my-plugins/cordova-plugin-statusbar/plugin.xml b/my-plugins/cordova-plugin-statusbar/plugin.xml new file mode 100644 index 0000000..1d4703e --- /dev/null +++ b/my-plugins/cordova-plugin-statusbar/plugin.xml @@ -0,0 +1,51 @@ + + + StatusBar + Cordova StatusBar Plugin + cordova,statusbar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/my-plugins/cordova-plugin-statusbar/src/android/StatusBar.java b/my-plugins/cordova-plugin-statusbar/src/android/StatusBar.java new file mode 100644 index 0000000..15394bd --- /dev/null +++ b/my-plugins/cordova-plugin-statusbar/src/android/StatusBar.java @@ -0,0 +1,194 @@ +package org.apache.cordova.statusbar; + +import android.graphics.Color; +import android.os.Build; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.view.WindowCompat; +import androidx.core.view.WindowInsetsControllerCompat; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaArgs; +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaPlugin; +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.LOG; +import org.apache.cordova.PluginResult; +import org.json.JSONException; + +public class StatusBar extends CordovaPlugin { + private static final String TAG = "StatusBar"; + + private static final String ACTION_HIDE = "hide"; + private static final String ACTION_SHOW = "show"; + private static final String ACTION_READY = "_ready"; + private static final String ACTION_BACKGROUND_COLOR_BY_HEX_STRING = "backgroundColorByHexString"; + private static final String ACTION_OVERLAYS_WEB_VIEW = "overlaysWebView"; + private static final String ACTION_STYLE_DEFAULT = "styleDefault"; + private static final String ACTION_STYLE_LIGHT_CONTENT = "styleLightContent"; + + private static final String STYLE_DEFAULT = "default"; + private static final String STYLE_LIGHT_CONTENT = "lightcontent"; + + private AppCompatActivity activity; + private Window window; + + /** + * Sets the context of the Command. This can then be used to do things like + * get file paths associated with the Activity. + * + * @param cordova The context of the main Activity. + * @param webView The CordovaWebView Cordova is running in. + */ + @Override + public void initialize(final CordovaInterface cordova, CordovaWebView webView) { + LOG.v(TAG, "StatusBar: initialization"); + super.initialize(cordova, webView); + + activity = this.cordova.getActivity(); + window = activity.getWindow(); + + activity.runOnUiThread(() -> { + // Clear flag FLAG_FORCE_NOT_FULLSCREEN which is set initially + // by the Cordova. + window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN); + + // Read 'StatusBarOverlaysWebView' from config.xml, default is true. + setStatusBarTransparent(preferences.getBoolean("StatusBarOverlaysWebView", true)); + + // Read 'StatusBarBackgroundColor' from config.xml, default is #000000. + setStatusBarBackgroundColor(preferences.getString("StatusBarBackgroundColor", "#000000")); + + // Read 'StatusBarStyle' from config.xml, default is 'lightcontent'. + setStatusBarStyle( + preferences.getString("StatusBarStyle", STYLE_LIGHT_CONTENT).toLowerCase() + ); + }); + } + + /** + * Executes the request and returns PluginResult. + * + * @param action The action to execute. + * @param args JSONArry of arguments for the plugin. + * @param callbackContext The callback id used when calling back into JavaScript. + * @return True if the action was valid, false otherwise. + */ + @Override + public boolean execute(final String action, final CordovaArgs args, final CallbackContext callbackContext) { + LOG.v(TAG, "Executing action: " + action); + + switch (action) { + case ACTION_READY: + boolean statusBarVisible = (window.getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) == 0; + callbackContext.sendPluginResult(new PluginResult(PluginResult.Status.OK, statusBarVisible)); + return true; + + case ACTION_SHOW: + activity.runOnUiThread(() -> { + int uiOptions = window.getDecorView().getSystemUiVisibility(); + uiOptions &= ~View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN; + uiOptions &= ~View.SYSTEM_UI_FLAG_FULLSCREEN; + + window.getDecorView().setSystemUiVisibility(uiOptions); + + // CB-11197 We still need to update LayoutParams to force status bar + // to be hidden when entering e.g. text fields + window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + }); + return true; + + case ACTION_HIDE: + activity.runOnUiThread(() -> { + int uiOptions = window.getDecorView().getSystemUiVisibility() + | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_FULLSCREEN; + + window.getDecorView().setSystemUiVisibility(uiOptions); + + // CB-11197 We still need to update LayoutParams to force status bar + // to be hidden when entering e.g. text fields + window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); + }); + return true; + + case ACTION_BACKGROUND_COLOR_BY_HEX_STRING: + activity.runOnUiThread(() -> { + try { + setStatusBarBackgroundColor(args.getString(0)); + } catch (JSONException ignore) { + LOG.e(TAG, "Invalid hexString argument, use f.i. '#777777'"); + } + }); + return true; + + case ACTION_OVERLAYS_WEB_VIEW: + activity.runOnUiThread(() -> { + try { + setStatusBarTransparent(args.getBoolean(0)); + } catch (JSONException ignore) { + LOG.e(TAG, "Invalid boolean argument"); + } + }); + return true; + + case ACTION_STYLE_DEFAULT: + activity.runOnUiThread(() -> setStatusBarStyle(STYLE_DEFAULT)); + return true; + + case ACTION_STYLE_LIGHT_CONTENT: + activity.runOnUiThread(() -> setStatusBarStyle(STYLE_LIGHT_CONTENT)); + return true; + + default: + return false; + } + } + + private void setStatusBarBackgroundColor(final String colorPref) { + if (colorPref.isEmpty()) return; + + int color; + try { + color = Color.parseColor(colorPref); + } catch (IllegalArgumentException ignore) { + LOG.e(TAG, "Invalid hexString argument, use f.i. '#999999'"); + return; + } + + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); // SDK 19-30 + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); // SDK 21 + window.setStatusBarColor(color); + } + + private void setStatusBarTransparent(final boolean isTransparent) { + final Window window = cordova.getActivity().getWindow(); + int visibility = isTransparent + ? View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + : View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_VISIBLE; + + window.getDecorView().setSystemUiVisibility(visibility); + + if (isTransparent) { + window.setStatusBarColor(Color.TRANSPARENT); + } + } + + private void setStatusBarStyle(final String style) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !style.isEmpty()) { + View decorView = window.getDecorView(); + WindowInsetsControllerCompat windowInsetsControllerCompat = WindowCompat.getInsetsController(window, decorView); + + if (style.equals(STYLE_DEFAULT)) { + windowInsetsControllerCompat.setAppearanceLightStatusBars(true); + } else if (style.equals(STYLE_LIGHT_CONTENT)) { + windowInsetsControllerCompat.setAppearanceLightStatusBars(false); + } else { + LOG.e(TAG, "Invalid style, must be either 'default' or 'lightcontent'"); + } + } + } +} diff --git a/my-plugins/cordova-plugin-statusbar/types/index.d.ts b/my-plugins/cordova-plugin-statusbar/types/index.d.ts new file mode 100644 index 0000000..73cd33b --- /dev/null +++ b/my-plugins/cordova-plugin-statusbar/types/index.d.ts @@ -0,0 +1,71 @@ +// Type definitions for cordova-plugin-statusbar +// Project: https://github.com/apache/cordova-plugin-statusbar +// Definitions by: Xinkai Chen +// Tim Brust +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped + +/** +* Global object StatusBar. +*/ +interface Window { + StatusBar: StatusBar; + addEventListener(type: "statusTap", listener: (ev: Event) => any, useCapture?: boolean): void; +} + +/** +* The StatusBar object provides some functions to customize the iOS and Android StatusBar. +*/ +interface StatusBar { + /** + * On iOS 7, make the statusbar overlay or not overlay the WebView. + * + * @param isOverlay - On iOS 7, set to false to make the statusbar appear like iOS 6. + * Set the style and background color to suit using the other functions. + */ + overlaysWebView(isOverlay: boolean): void; + + /** + * Use the default statusbar (dark text, for light backgrounds). + */ + styleDefault(): void; + + /** + * Use the lightContent statusbar (light text, for dark backgrounds). + */ + styleLightContent(): void; + + /** + * On iOS 7, when you set StatusBar.statusBarOverlaysWebView to false, + * you can set the background color of the statusbar by color name. + * + * @param color - Supported color names are: + * black, darkGray, lightGray, white, gray, red, green, blue, cyan, yellow, magenta, orange, purple, brown + */ + backgroundColorByName(color: string): void; + + /** + * Sets the background color of the statusbar by a hex string. + * + * @param color - CSS shorthand properties are also supported. + * On iOS 7, when you set StatusBar.statusBarOverlaysWebView to false, you can set the background color of the statusbar by a hex string (#RRGGBB). + * On WP7 and WP8 you can also specify values as #AARRGGBB, where AA is an alpha value + */ + backgroundColorByHexString(color: string): void; + + /** + * Hide the statusbar. + */ + hide(): void; + + /** + * Show the statusbar. + */ + show(): void; + + /** + * Read this property to see if the statusbar is visible or not. + */ + isVisible: boolean; +} + +declare var StatusBar: StatusBar; \ No newline at end of file diff --git a/my-plugins/cordova-plugin-statusbar/www/statusbar.js b/my-plugins/cordova-plugin-statusbar/www/statusbar.js new file mode 100644 index 0000000..a7848ca --- /dev/null +++ b/my-plugins/cordova-plugin-statusbar/www/statusbar.js @@ -0,0 +1,84 @@ +var exec = require('cordova/exec'); + +var namedColors = { + black: '#000000', + darkGray: '#A9A9A9', + lightGray: '#D3D3D3', + white: '#FFFFFF', + gray: '#808080', + red: '#FF0000', + green: '#00FF00', + blue: '#0000FF', + cyan: '#00FFFF', + yellow: '#FFFF00', + magenta: '#FF00FF', + orange: '#FFA500', + purple: '#800080', + brown: '#A52A2A' +}; + +var StatusBar = { + isVisible: true, + + overlaysWebView: function (doOverlay) { + exec(null, null, 'StatusBar', 'overlaysWebView', [doOverlay]); + }, + + styleDefault: function () { + // dark text ( to be used on a light background ) + exec(null, null, 'StatusBar', 'styleDefault', []); + }, + + styleLightContent: function () { + // light text ( to be used on a dark background ) + exec(null, null, 'StatusBar', 'styleLightContent', []); + }, + + backgroundColorByName: function (colorname) { + return StatusBar.backgroundColorByHexString(namedColors[colorname]); + }, + + backgroundColorByHexString: function (hexString) { + if (hexString.charAt(0) !== '#') { + hexString = '#' + hexString; + } + + if (hexString.length === 4) { + var split = hexString.split(''); + hexString = '#' + split[1] + split[1] + split[2] + split[2] + split[3] + split[3]; + } + + exec(null, null, 'StatusBar', 'backgroundColorByHexString', [hexString]); + }, + + hide: function () { + exec(null, null, 'StatusBar', 'hide', []); + StatusBar.isVisible = false; + }, + + show: function () { + exec(null, null, 'StatusBar', 'show', []); + StatusBar.isVisible = true; + } +}; + +// prime it. setTimeout so that proxy gets time to init +window.setTimeout(function () { + exec( + function (res) { + if (typeof res === 'object') { + if (res.type === 'tap') { + cordova.fireWindowEvent('statusTap'); + } + } else { + StatusBar.isVisible = res; + } + }, + null, + 'StatusBar', + '_ready', + [] + ); +}, 0); + +module.exports = StatusBar; diff --git a/package.json b/package.json index 33be4fb..58e0f07 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "com.rn0x.altaqwaa", "displayName": "التقوى", - "version": "1.2.4", + "version": "1.2.5", "description": "تطبيق إسلامي سهل الإستخدام و جامع للكثير من الميزات التي يحتاجها المسلم في يومه", "main": "index.js", "scripts": { @@ -10,8 +10,9 @@ "build": "cordova build android --release", "bundle": "cordova build android --release -- --packageTypjetifiere=bundle", "android": "cordova platform add android@latest", - "clear": "rm -rf ./node_modules && cordova platforms rm android && npm cache clean --force && npm i && cordova platform add android@latest", - "keytool": "node keytool.js" + "clean": "cordova clean && npm cache clean --force", + "keytool": "node keytool.js", + "rm": "rmdir /s /q node_modules platforms plugins && del package-lock.json && npm run android && npm run device" }, "author": { "name": "Ryan Almalki", @@ -24,11 +25,10 @@ "cordova": "^11.1.0", "cordova-clipboard": "^1.3.0", "cordova-plugin-android-permissions": "^1.1.5", + "cordova-plugin-badge-fix": "^0.8.10", "cordova-plugin-device": "^2.1.0", "cordova-plugin-dialogs": "^2.0.2", "cordova-plugin-geolocation": "^4.1.0", - "cordova-plugin-navigationbar-color": "^0.1.0", - "cordova-plugin-statusbar": "^3.0.0", "cordova-plugin-vibration": "^3.1.1" }, "cordova": { @@ -40,6 +40,11 @@ "cordova-plugin-geolocation": { "GPS_REQUIRED": "true" }, + "cordova-plugin-local-notification": { + "ANDROID_SUPPORT_V4_VERSION": "26.+", + "ANDROIDX_VERSION": "1.2.0", + "ANDROIDX_APPCOMPAT_VERSION": "1.3.1" + }, "cordova-plugin-dialogs": {}, "cordova-plugin-downloader": {}, "cordova-plugin-android-permissions": {}, @@ -53,7 +58,10 @@ "devDependencies": { "cordova-android": "^12.0.1", "cordova-plugin-downloader": "github:asadaries/cordova-plugin-downloader", + "cordova-plugin-local-notification": "file:my-plugins/cordova-plugin-local-notification", + "cordova-plugin-navigationbar-color": "file:my-plugins/cordova-plugin-navigationbar-color", "cordova-plugin-network-information": "github:apache/cordova-plugin-network-information", + "cordova-plugin-statusbar": "file:my-plugins/cordova-plugin-statusbar", "phonegap-plugin-mobile-accessibility": "github:phonegap/phonegap-mobile-accessibility" } } \ No newline at end of file diff --git a/resources/ic_stat/drawable-hdpi/ic_stat_onesignal_default.png b/resources/ic_stat/drawable-hdpi/ic_stat_onesignal_default.png new file mode 100644 index 0000000..6588e38 Binary files /dev/null and b/resources/ic_stat/drawable-hdpi/ic_stat_onesignal_default.png differ diff --git a/resources/ic_stat/drawable-mdpi/ic_stat_onesignal_default.png b/resources/ic_stat/drawable-mdpi/ic_stat_onesignal_default.png new file mode 100644 index 0000000..b771dcd Binary files /dev/null and b/resources/ic_stat/drawable-mdpi/ic_stat_onesignal_default.png differ diff --git a/resources/ic_stat/drawable-xhdpi/ic_stat_onesignal_default.png b/resources/ic_stat/drawable-xhdpi/ic_stat_onesignal_default.png new file mode 100644 index 0000000..dd6b7d3 Binary files /dev/null and b/resources/ic_stat/drawable-xhdpi/ic_stat_onesignal_default.png differ diff --git a/resources/ic_stat/drawable-xxhdpi/ic_stat_onesignal_default.png b/resources/ic_stat/drawable-xxhdpi/ic_stat_onesignal_default.png new file mode 100644 index 0000000..0f8295e Binary files /dev/null and b/resources/ic_stat/drawable-xxhdpi/ic_stat_onesignal_default.png differ diff --git a/resources/ic_stat/drawable-xxxhdpi/ic_stat_onesignal_default.png b/resources/ic_stat/drawable-xxxhdpi/ic_stat_onesignal_default.png new file mode 100644 index 0000000..c651a5f Binary files /dev/null and b/resources/ic_stat/drawable-xxxhdpi/ic_stat_onesignal_default.png differ diff --git a/resources/icon-removebg-preview.png b/resources/icon-removebg-preview.png new file mode 100644 index 0000000..e3d3b9d Binary files /dev/null and b/resources/icon-removebg-preview.png differ diff --git a/www/css/ramadanTime.css b/www/css/ramadanTime.css new file mode 100644 index 0000000..954fa8b --- /dev/null +++ b/www/css/ramadanTime.css @@ -0,0 +1,68 @@ +#content { + margin-top: 60px !important; + position: relative; +} + + + +#isRamadan { + display: none; + font-size: 25px; + font-family: var(--Amiri); + color: var(--white); + margin-top: 50px; + position: fixed; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + margin: 0 0 0 0; + white-space: nowrap; +} + + +#NotRamadan { + width: 90%; + display: flex; + justify-content: center; + align-items: center; + align-content: center; + padding: 0px; + list-style: none; + position: fixed; + top: 40%; + left: 50%; + transform: translate(-50%, -50%); + margin-left: auto; + margin-right: auto; +} + +#NotRamadan li { + width: 50%; + background-color: var(--background_div_hover_2); + padding: 10px; + margin: 5px 5px 5px 5px; + border-radius: 10px; + color: var(--white); + box-shadow: rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset; + +} + +#NotRamadan li p{ + margin: 0 0 0 0; +} + +.number_ramadan { + font-size: 50px; + font-family: var(--digital_7); +} +.title_ramadan { + font-size: 18px; + color: var(--background_div); +} + +#title_ramadan { + margin-top: 30px; + color: var(--white); + font-size: 25px; + font-family: var(--Amiri); +} \ No newline at end of file diff --git a/www/data/adhkarMenu.json b/www/data/adhkarMenu.json index d9bf8d4..dc5bf0c 100644 --- a/www/data/adhkarMenu.json +++ b/www/data/adhkarMenu.json @@ -382,7 +382,7 @@ { "id": 16, "title": "أذكار المساء - اللهم عافني في بدني وسمعي وبصري", - "adhkar": "للهُمَّ عَافِنِي فِي بَدَنِي، اللهُمَّ عَافِنِي فِي سَمْعِي، اللهُمَّ عَافِنِي فِي بَصَرِي، لَا إِلَهَ إِلَّا أَنْتَ", + "adhkar": "اللهُمَّ عَافِنِي فِي بَدَنِي، اللهُمَّ عَافِنِي فِي سَمْعِي، اللهُمَّ عَافِنِي فِي بَصَرِي، لَا إِلَهَ إِلَّا أَنْتَ", "description": "عاء لله بحماية البدن والسمع والبصر والاعتماد عليه في حفظهما.", "source": "أخرجه أبو داود، كتاب: الأدب، باب: ما يقول إذا أصبح، برقم (٥٠٩٠)", "repetition": 3 diff --git a/www/img/ramadanTime.png b/www/img/ramadanTime.png new file mode 100644 index 0000000..96c8016 Binary files /dev/null and b/www/img/ramadanTime.png differ diff --git a/www/index.html b/www/index.html index 5455ada..87fa089 100644 --- a/www/index.html +++ b/www/index.html @@ -6,7 +6,7 @@ - + diff --git a/www/js/adhkar.js b/www/js/adhkar.js index 71d9e64..e170434 100644 --- a/www/js/adhkar.js +++ b/www/js/adhkar.js @@ -64,13 +64,16 @@ export default async () => { prayers.addEventListener('click', e => window.location.href = '/pages/adhkar/prayer.html'); tasbih.addEventListener('click', e => window.location.href = '/pages/adhkar/tasbih.html'); - // حدث زر الرجوع للخلف document.addEventListener("backbutton", async function (e) { e.preventDefault(); // منع السلوك الافتراضي لزر الرجوع storage.removeItem('audioPlayingId'); storage.removeItem('icon_audio'); storage.removeItem('AdhanPlaying'); + storage.removeItem('isMediaPlay'); + storage.removeItem('isPaused'); + storage.removeItem('AudioName'); + storage.removeItem('linkAudio'); navigator?.notification?.confirm( 'هل بالفعل تريد الخروج من التطبيق ؟', // message diff --git a/www/js/index.js b/www/js/index.js index cf07a2f..5bbf827 100644 --- a/www/js/index.js +++ b/www/js/index.js @@ -14,21 +14,28 @@ import settings from './settings.js'; import sabha from './sabha.js'; import notification from './notification.js'; import error_handling from './modules/error_handling.js'; +import handleAudio from './modules/handleAudio.js'; +import ramadanTime from './ramadanTime.js'; + // أفحص إذا كانت البيئة تعمل في Cordova const isCordova = !!window.cordova; - document.documentElement.style.setProperty('--animate-duration', '1.5s'); // أضف شرطًا للتحقق مما إذا كان التطبيق يعمل في Cordova أم لا if (isCordova) { - document.addEventListener('deviceready', async (e) => { + document.addEventListener('deviceready', async (event) => { + + event.preventDefault(); try { let permissions = cordova.plugins.permissions; let list = [ permissions.ACCESS_COARSE_LOCATION, - permissions.WRITE_EXTERNAL_STORAGE + permissions.WRITE_EXTERNAL_STORAGE, + permissions.VIBRATE, + permissions.POST_NOTIFICATIONS, + permissions.FOREGROUND_SERVICE ]; permissions.hasPermission(list, (status) => { @@ -43,7 +50,9 @@ if (isCordova) { } catch (error) { error_handling(error); } + await setupApplication(); + }, false); } else { await setupApplication(); @@ -51,6 +60,7 @@ if (isCordova) { async function setupApplication() { + await footer(); await adhkar(); await prayer(); @@ -66,6 +76,7 @@ async function setupApplication() { await settings(); await sabha(); await notification(); + await ramadanTime(); // احصل على جميع عناصر img const imagesAll = document.querySelectorAll('img'); @@ -74,4 +85,7 @@ async function setupApplication() { imagesAll.forEach(img => { img.setAttribute('loading', 'lazy'); }); + + await handleAudio(); // تشغيل الصوت في جميع الصفحات + } \ No newline at end of file diff --git a/www/js/modules/LocalNotification.js b/www/js/modules/LocalNotification.js new file mode 100644 index 0000000..6c65a04 --- /dev/null +++ b/www/js/modules/LocalNotification.js @@ -0,0 +1,152 @@ +/** + * تقوم هذه الدالة بجدولة إشعار محلي باستخدام cordova-plugin-local-notifications. + * + * @param {Object} options - خيارات الإشعار. + * @throws {Error} سيتم إلقاء استثناء إذا فشل جدولة الإشعار. + * + * @example + * // مثال على كيفية استخدام الدالة + * const notificationOptions = { + * id: 1, // إضافة المعرف + * title: 'عنوان الإشعار', + * text: 'محتوى', + * icon: 'file://img/icon.png', + * smallIcon: 'res://drawable-xxxhdpi/ic_stat_onesignal_default.png', + * attachments: ['file://img/images.png'], + * data: { rn0x: 'ryan almalki' }, + * led: '#00FF00', + * sound: "file://audio/sound.mp3", + * badge: 1, + * actions: [{ id: 'accept', title: 'Accept' }, { id: 'reject', title: 'Reject' }], // إضافة الإجراءات + * trigger: { in: 1, unit: 'minute' }, // إضافة المشغل + * }; + * + * scheduleLocalNotification(notificationOptions); + */ + +export function scheduleLocalNotification(options) { + + try { + if (!options || typeof options !== 'object') { + throw new Error('تم تقديم خيارات غير صالحة لجدولة الإشعار.'); + } + + // التحقق من وجود cordova.plugins.notification.local + if (typeof cordova === 'undefined' || !cordova.plugins || !cordova.plugins.notification) { + throw new Error('يجب تشغيل هذا الكود داخل تطبيق Cordova وبعد استعراض الأجهزة.'); + } + + // options.actions = [{ id: 'dummyAction', title: 'close', type: 'button' }]; + + cordova.plugins.notification.local.schedule(options); + } catch (error) { + console.error('حدث خطأ في جدولة الإشعار:', error.message); + throw error; + } +} + + +/** + * تحديث إشعار محلي باستخدام cordova-plugin-local-notifications. + * + * @param {number} notificationId - معرف الإشعار الذي تريد تحديثه. + * @param {Object} options - الخصائص الجديدة التي تريد تحديثها. + * @throws {Error} سيتم إلقاء استثناء إذا فشل تحديث الإشعار. + * + * @example + * +const updatedOptions = { + title: 'عنوان الإشعار المحدث', + text: 'محتوى المحدث', + // وغيرها من الخصائص التي تريد تحديثها +}; + +updateLocalNotification(1, updatedOptions); + + */ +export function updateLocalNotification(notificationId, options) { + try { + if (typeof cordova === 'undefined' || !cordova.plugins || !cordova.plugins.notification) { + throw new Error('يجب تشغيل هذا الكود داخل تطبيق Cordova وبعد استعراض الأجهزة.'); + } + + if (!notificationId || typeof notificationId !== 'number') { + throw new Error('معرف الإشعار غير صالح.'); + } + + if (!options || typeof options !== 'object') { + throw new Error('الخيارات المقدمة غير صالحة لتحديث الإشعار.'); + } + + cordova.plugins.notification.local.update({ + id: notificationId, + ...options, + }); + } catch (error) { + console.error('حدث خطأ في تحديث الإشعار المحلي:', error.message); + throw error; + } +} + +/** + * إلغاء إشعار محلي بواسطة معرف الإشعار. + * + * @param {number} notificationId - معرف الإشعار الذي تريد إلغاؤه. + * @param {Function} [callback=()=>{}] - دالة التابعية التي تُستدعى عند الانتهاء من إلغاء الإشعار. + */ +export function cancelLocalNotification(notificationId, callback = () => { }) { + if (typeof cordova !== 'undefined' && cordova.plugins && cordova.plugins.notification) { + cordova.plugins.notification.local.cancel(notificationId, callback); + } +} + + +/** + * تحقق مما إذا كان إشعارًا محليًا موجودًا باستخدام معرف الإشعار. + * + * @param {number} notificationId - معرف الإشعار الذي تريد التحقق من وجوده. + * @returns {boolean} - القيمة المعادة تحدد ما إذا كان الإشعار موجودًا أم لا. + */ +export function isLocalNotificationExists(notificationId) { + if (typeof cordova !== 'undefined' && cordova.plugins && cordova.plugins.notification) { + return cordova.plugins.notification.local.isPresent(notificationId); + } + return false; +} + +/** + * تسجيل الحدث للاستماع إلى النقر على الإشعارات المحلية والتحقق من الإجراء المطلوب. + * + * @param {Function} callback - دالة التابعية التي تستجيب للحدث. + */ +export function registerLocalNotificationClickEvent(callback) { + try { + if (typeof cordova === 'undefined' || !cordova.plugins || !cordova.plugins.notification) { + throw new Error('يجب تشغيل هذا الكود داخل تطبيق Cordova وبعد استعراض الأجهزة.'); + } + + cordova.plugins.notification.local.on('click', function (notification) { + // التحقق من أن النقر تم على الإجراء المطلوب + if (notification.action === 'closeAudio') { + callback(notification); + } + }); + } catch (error) { + console.error('حدث خطأ في تسجيل الحدث للاستماع إلى النقر على الإشعارات المحلية:', error.message); + throw error; + } +} + + +/** + * تسجيل الحدث للاستماع إلى النقر على الإشعارات المحلية والتحقق من الإجراء المطلوب. + * + * @param {String} actionID - معرف الإجراء او الحدث. + * @param {Function} [callback=()=>{}] - دالة التابعية التي تُستدعى عند الضغط على الإشعار. + */ +export function ClickEvent(actionID, callback) { + if (typeof cordova !== 'undefined' && cordova.plugins && cordova.plugins.notification) { + cordova.plugins.notification.local.on(actionID, callback); + } + return false; +} diff --git a/www/js/modules/adhanModule.js b/www/js/modules/adhanModule.js index ddabe05..cb9c5d6 100644 --- a/www/js/modules/adhanModule.js +++ b/www/js/modules/adhanModule.js @@ -1,9 +1,11 @@ import moment from './moment/moment.js'; +import moment_timezone from './moment/moment-timezone.js'; import moment_hijri from './moment/moment-hijri.js'; import { Coordinates, CalculationMethod, PrayerTimes, Madhab, Shafaq } from './adhan.js'; import momentDurationFormatSetup from './moment/moment-duration-format.js'; import error_handling from './error_handling.js'; momentDurationFormatSetup(moment); +moment_timezone(moment); /** * by rn0x @@ -46,7 +48,8 @@ momentDurationFormatSetup(moment); export default (options) => { try { - let hijri = moment_hijri(moment); + const momentHijri = moment_hijri(moment); + const hijri = momentHijri; let coordinates = new Coordinates(options?.latitude, options?.longitude); let params = CalculationMethod[options?.Calculation]() || CalculationMethod.NorthAmerica(); params.madhab = Madhab[options?.Madhab] || Madhab.Shafi; @@ -55,9 +58,9 @@ export default (options) => { let date = new Date(); let prayerTimes = new PrayerTimes(coordinates, date, params); let nextPrayer = prayerTimes.nextPrayer(); - let timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - let now = moment(); - let end = moment(prayerTimes.timeForPrayer(nextPrayer)); + let timezone = options.timezone; + let now = moment().tz(timezone); + let end = moment(prayerTimes.timeForPrayer(nextPrayer)).tz(timezone); let duration = moment.duration(end.diff(now)); let remaining = duration.format('hh:mm:ss'); remaining = remaining.replace(/[٠١٢٣٤٥٦٧٨٩]/g, (match) => { @@ -69,17 +72,17 @@ export default (options) => { let dayNameArabic = dayNamesArabic[day]; return { - isha: moment(prayerTimes.isha).format('h:mm A'), - maghrib: moment(prayerTimes.maghrib).format('h:mm A'), - asr: moment(prayerTimes.asr).format('h:mm A'), - dhuhr: moment(prayerTimes.dhuhr).format('h:mm A'), - sunrise: moment(prayerTimes.sunrise).format('h:mm A'), - fajr: moment(prayerTimes.fajr).format('h:mm A'), + isha: moment(prayerTimes.isha).tz(timezone).format('h:mm A'), + maghrib: moment(prayerTimes.maghrib).tz(timezone).format('h:mm A'), + asr: moment(prayerTimes.asr).tz(timezone).format('h:mm A'), + dhuhr: moment(prayerTimes.dhuhr).tz(timezone).format('h:mm A'), + sunrise: moment(prayerTimes.sunrise).tz(timezone).format('h:mm A'), + fajr: moment(prayerTimes.fajr).tz(timezone).format('h:mm A'), nextPrayer: nextPrayer, remainingNext: remaining, currentPrayer: prayerTimes.currentPrayer(), timezone: timezone, - data_hijri: hijri().format('iYYYY/iM/iD'), + data_hijri: hijri().tz(timezone).format('iYYYY/iM/iD'), data_Gregorian: now.format('YYYY/M/D'), today: dayNameArabic, hour_minutes: now.format('h:mm'), diff --git a/www/js/modules/getGPS.js b/www/js/modules/getGPS.js index d95ead4..44e366d 100644 --- a/www/js/modules/getGPS.js +++ b/www/js/modules/getGPS.js @@ -1,15 +1,18 @@ /** - * @returns {Promise.<{latitude: Number, longitude: Number, timestamp: Number}>} + * @returns {Promise.<{latitude: Number, longitude: Number, timestamp: Number, timezone: String}>} */ export default () => { return new Promise((resolve, reject) => { navigator.geolocation.getCurrentPosition((position) => { + const timezone = getTimezone(); + resolve({ latitude: position.coords.latitude, longitude: position.coords.longitude, - timestamp: position.timestamp + timestamp: position.timestamp , + timezone : timezone }); }, (error) => { @@ -19,4 +22,21 @@ export default () => { }); }); }); +} + +function getTimezone() { + try { + // إنشاء كائن لتنسيق التاريخ والوقت + const formatter = new Intl.DateTimeFormat(undefined, { + timeZoneName: 'long' + }); + + // الحصول على قيمة timezone + const timezone = formatter.resolvedOptions().timeZone; + + return timezone; + } catch (error) { + console.error('Unable to get timezone:', error); + return null; + } } \ No newline at end of file diff --git a/www/js/modules/getRamadanDate.js b/www/js/modules/getRamadanDate.js new file mode 100644 index 0000000..027b0af --- /dev/null +++ b/www/js/modules/getRamadanDate.js @@ -0,0 +1,69 @@ +import moment from './moment/moment.js'; +import moment_timezone from './moment/moment-timezone.js'; +import moment_hijri from './moment/moment-hijri.js'; +import momentDurationFormatSetup from './moment/moment-duration-format.js'; +momentDurationFormatSetup(moment); + +// تهيئة وحدة التوقيت لاستخدامها في الوقت الحالي وتحديد وقت بداية شهر رمضان +moment_timezone(moment); + +/** + * الحصول على تاريخ بداية شهر رمضان للعام الحالي بالتقويم الميلادي. + * @returns {string} - تاريخ بداية شهر رمضان بالتقويم الميلادي في صيغة "YYYY-MM-DD". + */ +function getRamadanStartDate() { + // الحصول على التاريخ الهجري الحالي + const momentHijri = moment_hijri(moment); + const hijri = momentHijri(); + + // استخدام الرقم 9 لشهر رمضان + const hijriDate = hijri.iMonth(8).startOf('iMonth').format('iYYYY/iM/iD'); + + // تحويل التاريخ الهجري إلى التقويم الميلادي + const ramadanStartGregorian = momentHijri(hijriDate, 'iYYYY/iM/iD').format('YYYY-MM-DD'); + return ramadanStartGregorian; +} + +/** + * حساب الوقت المتبقي حتى بداية شهر رمضان. + * @returns {Object} - كائن يحتوي على الأيام والساعات والدقائق والثواني المتبقية. + */ +export default function remainingTimeUntilRamadan() { + // استرجاع وحدة التوقيت من الذاكرة المؤقتة، وإعدادها لتكون "Asia/Riyadh" إذا لم تكن محددة + const localStorageData = window.localStorage; + const GetTimezone = localStorageData.getItem('timezone_settings'); + const timezone = GetTimezone ? GetTimezone : 'Asia/Riyadh'; + // حساب تاريخ بداية شهر رمضان بالتقويم الميلادي + const ramadanStart = moment.tz(getRamadanStartDate(timezone), timezone); + + // حساب التاريخ الحالي بوحدة التوقيت المحددة + const today = moment.tz(timezone); + + // حساب الوقت المتبقي حتى بداية شهر رمضان بالميلي ثانية + const timeRemaining = ramadanStart - today; + + // تحويل الوقت المتبقي من ميلي ثانية إلى أيام وساعات ودقائق وثواني + const daysRemaining = Math.floor(timeRemaining / (1000 * 60 * 60 * 24)); + const hoursRemaining = Math.floor((timeRemaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60)); + const minutesRemaining = Math.floor((timeRemaining % (1000 * 60 * 60)) / (1000 * 60)); + const secondsRemaining = Math.floor((timeRemaining % (1000 * 60)) / 1000); + + // إذا كان الوقت المتبقي صفراً، فنفترض بأننا في شهر رمضان بالفعل + if (timeRemaining <= 0) { + return { + days: 0, + hours: 0, + minutes: 0, + seconds: 0, + isRamadan: true + }; + } + // إرجاع الوقت المتبقي ككائن + return { + days: daysRemaining, + hours: hoursRemaining, + minutes: minutesRemaining, + seconds: secondsRemaining, + isRamadan: false + }; +} \ No newline at end of file diff --git a/www/js/modules/handleAudio.js b/www/js/modules/handleAudio.js new file mode 100644 index 0000000..fddf6b3 --- /dev/null +++ b/www/js/modules/handleAudio.js @@ -0,0 +1,122 @@ +import { + scheduleLocalNotification, + updateLocalNotification, + cancelLocalNotification, + isLocalNotificationExists, + ClickEvent +} from "./LocalNotification.js"; + +/** + * تشغيل أو إيقاف التشغيل لملف صوتي باستخدام localStorage للحفاظ على حالة التشغيل والاستماع السابق. + * @param {string} linkAudio رابط الملف الصوتي + */ +export default async function handleAudio() { + try { + // جلب حالة التشغيل من localStorage + const isMediaPlay = localStorage.getItem('isMediaPlay') === 'true'; + const isPaused = localStorage.getItem('isPaused') === 'true'; + const AudioName = localStorage.getItem('AudioName'); + const isMediaPlayStorage = (value) => { localStorage.setItem('isMediaPlay', `${value}`) }; + const audioCurrentTimeStorage = (value) => { localStorage.setItem('audioCurrentTime', `${value}`) }; + const isPausedStorage = (value) => { localStorage.setItem('isPaused', `${value}`) }; + const audioCurrentTime = localStorage.getItem('audioCurrentTime'); + const linkAudio = localStorage.getItem('linkAudio'); + const AudioNameStoarge = (value) => { localStorage.setItem('AudioName', `${value}`) }; + + // تحقق مما إذا كان هناك رابط صوتي محدد وإلا فلا داعي للاستمرار + if (!linkAudio || linkAudio === "") { + console.log("لا يوجد رابط صوتي محدد."); + return; + } + + const notificationId = 222; // معرف الإشعار + const actionID = "pause_audio"; // معرف الإجراء + + // إنشاء مرجع للتحكم في تكرار التحديثات الزمنية + let intervalReference; + + // إنشاء عنصر الصوت + const audio = new Audio(linkAudio); + // التحقق من حالة التشغيل وتوقيف التشغيل إذا كان متوقفا وموقفا في localStorage + setInterval(() => { + const isMediaPlay = localStorage.getItem('isMediaPlay') === 'true'; + const isPaused = localStorage.getItem('isPaused') === 'true'; + if (!isMediaPlay && isPaused) { + console.log("التشغيل متوقف."); + audio.pause(); + audioCurrentTimeStorage(0); + clearInterval(intervalReference); + cancelLocalNotification(notificationId, () => { }); + } + }, 1000); + + // إذا كان الملف صوتي قيد التشغيل ولم يتم إيقاف التشغيل + if (isMediaPlay && !isPaused) { + // استئناف التشغيل من نفس الموقع + if (audioCurrentTime) { + audio.currentTime = parseFloat(audioCurrentTime); + } + await audio.play(); + isPausedStorage(false); + intervalReference = setInterval(() => { + console.log("الوقت الحالي للصوت: ", audio.currentTime); + audioCurrentTimeStorage(audio.currentTime); + }, 1000); + + const isNotificationExists = isLocalNotificationExists(notificationId); // تحقق مما إذا كان إشعارًا محليًا موجودًا باستخدام معرف الإشعار. + + if (!isNotificationExists) { + scheduleLocalNotification({ + id: notificationId, + text: `${AudioName}`, + smallIcon: 'res://drawable-xxxhdpi/ic_stat_onesignal_default.png', + badge: 1, + actions: [{ id: actionID, title: 'إيقاف' }], + }); + } + + else { + updateLocalNotification(notificationId, { + id: notificationId, + text: `${AudioName}`, + smallIcon: 'res://drawable-xxxhdpi/ic_stat_onesignal_default.png', + badge: 1, + actions: [{ id: actionID, title: 'إيقاف' }], + }); + } + + + + } else if (!isMediaPlay && !isPaused) { + // إيقاف الصوت إذا كان قيد التشغيل + audio.pause(); + isPausedStorage(true); + audioCurrentTimeStorage(0); + clearInterval(intervalReference); + cancelLocalNotification(notificationId, () => { }); + } + + + ClickEvent(actionID, () => { + audio.pause(); + isPausedStorage(true); + audioCurrentTimeStorage(0); + clearInterval(intervalReference); + cancelLocalNotification(notificationId, () => { }); + }) + + // حفظ حالة التشغيل والاستماع الحالية في localStorage قبل تفريغ الصفحة + window.onbeforeunload = () => { + const isMediaPlay = localStorage.getItem('isMediaPlay') === 'true'; + const isPaused = localStorage.getItem('isPaused') === 'true'; + const audioCurrentTime = localStorage.getItem('audioCurrentTime'); + isMediaPlayStorage(isMediaPlay); + isPausedStorage(isPaused); + if (isMediaPlay && !isPaused) { + audioCurrentTimeStorage(audioCurrentTime); + } + }; + } catch (error) { + console.error("حدث خطأ: ", error); + } +} diff --git a/www/js/modules/localStorage.js b/www/js/modules/localStorage.js new file mode 100644 index 0000000..849ed5e --- /dev/null +++ b/www/js/modules/localStorage.js @@ -0,0 +1,79 @@ +/** + * تقوم هذه الدالة بحفظ البيانات في `localStorage`. + * + * @param {string} key - المفتاح الذي يتم استخدامه لتخزين البيانات. + * @param {any} data - البيانات التي ترغب في حفظها. + * + * @throws {Error} إذا فشلت عملية حفظ البيانات. + */ +function saveData(key, data) { + try { + const serializedData = JSON.stringify(data); + localStorage.setItem(key, serializedData); + } catch (error) { + console.error('حدث خطأ أثناء حفظ البيانات:', error.message); + throw error; + } +} + +/** + * تقوم هذه الدالة باسترجاع البيانات من `localStorage`. + * + * @param {string} key - المفتاح الذي تم استخدامه لتخزين البيانات. + * @returns {any} البيانات المسترجعة. + * + * @throws {Error} إذا فشلت عملية استرجاع البيانات. + */ +function retrieveData(key) { + try { + const serializedData = localStorage.getItem(key); + if (serializedData === null) { + return null; // إذا لم تكن هناك بيانات للاسترجاع + } + + // استخدام typeof لفحص نوع البيانات والتعامل معها بشكل مناسب + const dataType = typeof JSON.parse(serializedData); + + // يتم التحقق من القيم المحتملة بما في ذلك null و undefined + if (dataType === 'object' && JSON.parse(serializedData) === null) { + return null; + } else if (dataType === 'undefined') { + return undefined; + } + + // التعامل مع البيانات حسب نوعها + switch (dataType) { + case 'object': + return JSON.parse(serializedData); + case 'number': + return parseFloat(serializedData); + case 'boolean': + return serializedData === 'true'; + // يمكنك إضافة المزيد من الحالات حسب احتياجاتك + default: + return serializedData; + } + } catch (error) { + console.error('حدث خطأ أثناء استرجاع البيانات:', error.message); + throw error; + } +} + +/** + * تقوم هذه الدالة بحذف البيانات من `localStorage`. + * + * @param {string} key - المفتاح الذي تم استخدامه لتخزين البيانات. + * + * @throws {Error} إذا فشلت عملية حذف البيانات. + */ +function deleteData(key) { + try { + localStorage.removeItem(key); + } catch (error) { + console.error('حدث خطأ أثناء حذف البيانات:', error.message); + throw error; + } +} + + +export { saveData, retrieveData, deleteData } \ No newline at end of file diff --git a/www/js/modules/moment/moment-timezone.js b/www/js/modules/moment/moment-timezone.js new file mode 100644 index 0000000..6958d1e --- /dev/null +++ b/www/js/modules/moment/moment-timezone.js @@ -0,0 +1,1564 @@ +export default function (moment) { + "use strict"; + + // Resolves es6 module loading issue + if (moment.version === undefined && moment.default) { + moment = moment.default; + } + + // Do not load moment-timezone a second time. + // if (moment.tz !== undefined) { + // logError('Moment Timezone ' + moment.tz.version + ' was already loaded ' + (moment.tz.dataVersion ? 'with data from ' : 'without any data') + moment.tz.dataVersion); + // return moment; + // } + + var VERSION = "0.5.44", + zones = {}, + links = {}, + countries = {}, + names = {}, + guesses = {}, + cachedGuess; + + if (!moment || typeof moment.version !== 'string') { + logError('Moment Timezone requires Moment.js. See https://momentjs.com/timezone/docs/#/use-it/browser/'); + } + + var momentVersion = moment.version.split('.'), + major = +momentVersion[0], + minor = +momentVersion[1]; + + // Moment.js version check + if (major < 2 || (major === 2 && minor < 6)) { + logError('Moment Timezone requires Moment.js >= 2.6.0. You are using Moment.js ' + moment.version + '. See momentjs.com'); + } + + /************************************ + Unpacking + ************************************/ + + function charCodeToInt(charCode) { + if (charCode > 96) { + return charCode - 87; + } else if (charCode > 64) { + return charCode - 29; + } + return charCode - 48; + } + + function unpackBase60(string) { + var i = 0, + parts = string.split('.'), + whole = parts[0], + fractional = parts[1] || '', + multiplier = 1, + num, + out = 0, + sign = 1; + + // handle negative numbers + if (string.charCodeAt(0) === 45) { + i = 1; + sign = -1; + } + + // handle digits before the decimal + for (i; i < whole.length; i++) { + num = charCodeToInt(whole.charCodeAt(i)); + out = 60 * out + num; + } + + // handle digits after the decimal + for (i = 0; i < fractional.length; i++) { + multiplier = multiplier / 60; + num = charCodeToInt(fractional.charCodeAt(i)); + out += num * multiplier; + } + + return out * sign; + } + + function arrayToInt (array) { + for (var i = 0; i < array.length; i++) { + array[i] = unpackBase60(array[i]); + } + } + + function intToUntil (array, length) { + for (var i = 0; i < length; i++) { + array[i] = Math.round((array[i - 1] || 0) + (array[i] * 60000)); // minutes to milliseconds + } + + array[length - 1] = Infinity; + } + + function mapIndices (source, indices) { + var out = [], i; + + for (i = 0; i < indices.length; i++) { + out[i] = source[indices[i]]; + } + + return out; + } + + function unpack (string) { + var data = string.split('|'), + offsets = data[2].split(' '), + indices = data[3].split(''), + untils = data[4].split(' '); + + arrayToInt(offsets); + arrayToInt(indices); + arrayToInt(untils); + + intToUntil(untils, indices.length); + + return { + name : data[0], + abbrs : mapIndices(data[1].split(' '), indices), + offsets : mapIndices(offsets, indices), + untils : untils, + population : data[5] | 0 + }; + } + + /************************************ + Zone object + ************************************/ + + function Zone (packedString) { + if (packedString) { + this._set(unpack(packedString)); + } + } + + function closest (num, arr) { + var len = arr.length; + if (num < arr[0]) { + return 0; + } else if (len > 1 && arr[len - 1] === Infinity && num >= arr[len - 2]) { + return len - 1; + } else if (num >= arr[len - 1]) { + return -1; + } + + var mid; + var lo = 0; + var hi = len - 1; + while (hi - lo > 1) { + mid = Math.floor((lo + hi) / 2); + if (arr[mid] <= num) { + lo = mid; + } else { + hi = mid; + } + } + return hi; + } + + Zone.prototype = { + _set : function (unpacked) { + this.name = unpacked.name; + this.abbrs = unpacked.abbrs; + this.untils = unpacked.untils; + this.offsets = unpacked.offsets; + this.population = unpacked.population; + }, + + _index : function (timestamp) { + var target = +timestamp, + untils = this.untils, + i; + + i = closest(target, untils); + if (i >= 0) { + return i; + } + }, + + countries : function () { + var zone_name = this.name; + return Object.keys(countries).filter(function (country_code) { + return countries[country_code].zones.indexOf(zone_name) !== -1; + }); + }, + + parse : function (timestamp) { + var target = +timestamp, + offsets = this.offsets, + untils = this.untils, + max = untils.length - 1, + offset, offsetNext, offsetPrev, i; + + for (i = 0; i < max; i++) { + offset = offsets[i]; + offsetNext = offsets[i + 1]; + offsetPrev = offsets[i ? i - 1 : i]; + + if (offset < offsetNext && tz.moveAmbiguousForward) { + offset = offsetNext; + } else if (offset > offsetPrev && tz.moveInvalidForward) { + offset = offsetPrev; + } + + if (target < untils[i] - (offset * 60000)) { + return offsets[i]; + } + } + + return offsets[max]; + }, + + abbr : function (mom) { + return this.abbrs[this._index(mom)]; + }, + + offset : function (mom) { + logError("zone.offset has been deprecated in favor of zone.utcOffset"); + return this.offsets[this._index(mom)]; + }, + + utcOffset : function (mom) { + return this.offsets[this._index(mom)]; + } + }; + + /************************************ + Country object + ************************************/ + + function Country (country_name, zone_names) { + this.name = country_name; + this.zones = zone_names; + } + + /************************************ + Current Timezone + ************************************/ + + function OffsetAt(at) { + var timeString = at.toTimeString(); + var abbr = timeString.match(/\([a-z ]+\)/i); + if (abbr && abbr[0]) { + // 17:56:31 GMT-0600 (CST) + // 17:56:31 GMT-0600 (Central Standard Time) + abbr = abbr[0].match(/[A-Z]/g); + abbr = abbr ? abbr.join('') : undefined; + } else { + // 17:56:31 CST + // 17:56:31 GMT+0800 (台北標準時間) + abbr = timeString.match(/[A-Z]{3,5}/g); + abbr = abbr ? abbr[0] : undefined; + } + + if (abbr === 'GMT') { + abbr = undefined; + } + + this.at = +at; + this.abbr = abbr; + this.offset = at.getTimezoneOffset(); + } + + function ZoneScore(zone) { + this.zone = zone; + this.offsetScore = 0; + this.abbrScore = 0; + } + + ZoneScore.prototype.scoreOffsetAt = function (offsetAt) { + this.offsetScore += Math.abs(this.zone.utcOffset(offsetAt.at) - offsetAt.offset); + if (this.zone.abbr(offsetAt.at).replace(/[^A-Z]/g, '') !== offsetAt.abbr) { + this.abbrScore++; + } + }; + + function findChange(low, high) { + var mid, diff; + + while ((diff = ((high.at - low.at) / 12e4 | 0) * 6e4)) { + mid = new OffsetAt(new Date(low.at + diff)); + if (mid.offset === low.offset) { + low = mid; + } else { + high = mid; + } + } + + return low; + } + + function userOffsets() { + var startYear = new Date().getFullYear() - 2, + last = new OffsetAt(new Date(startYear, 0, 1)), + lastOffset = last.offset, + offsets = [last], + change, next, nextOffset, i; + + for (i = 1; i < 48; i++) { + nextOffset = new Date(startYear, i, 1).getTimezoneOffset(); + if (nextOffset !== lastOffset) { + // Create OffsetAt here to avoid unnecessary abbr parsing before checking offsets + next = new OffsetAt(new Date(startYear, i, 1)); + change = findChange(last, next); + offsets.push(change); + offsets.push(new OffsetAt(new Date(change.at + 6e4))); + last = next; + lastOffset = nextOffset; + } + } + + for (i = 0; i < 4; i++) { + offsets.push(new OffsetAt(new Date(startYear + i, 0, 1))); + offsets.push(new OffsetAt(new Date(startYear + i, 6, 1))); + } + + return offsets; + } + + function sortZoneScores (a, b) { + if (a.offsetScore !== b.offsetScore) { + return a.offsetScore - b.offsetScore; + } + if (a.abbrScore !== b.abbrScore) { + return a.abbrScore - b.abbrScore; + } + if (a.zone.population !== b.zone.population) { + return b.zone.population - a.zone.population; + } + return b.zone.name.localeCompare(a.zone.name); + } + + function addToGuesses (name, offsets) { + var i, offset; + arrayToInt(offsets); + for (i = 0; i < offsets.length; i++) { + offset = offsets[i]; + guesses[offset] = guesses[offset] || {}; + guesses[offset][name] = true; + } + } + + function guessesForUserOffsets (offsets) { + var offsetsLength = offsets.length, + filteredGuesses = {}, + out = [], + checkedOffsets = {}, + i, j, offset, guessesOffset; + + for (i = 0; i < offsetsLength; i++) { + offset = offsets[i].offset; + if (checkedOffsets.hasOwnProperty(offset)) { + continue; + } + guessesOffset = guesses[offset] || {}; + for (j in guessesOffset) { + if (guessesOffset.hasOwnProperty(j)) { + filteredGuesses[j] = true; + } + } + checkedOffsets[offset] = true; + } + + for (i in filteredGuesses) { + if (filteredGuesses.hasOwnProperty(i)) { + out.push(names[i]); + } + } + + return out; + } + + function rebuildGuess () { + + // use Intl API when available and returning valid time zone + try { + var intlName = Intl.DateTimeFormat().resolvedOptions().timeZone; + if (intlName && intlName.length > 3) { + var name = names[normalizeName(intlName)]; + if (name) { + return name; + } + logError("Moment Timezone found " + intlName + " from the Intl api, but did not have that data loaded."); + } + } catch (e) { + // Intl unavailable, fall back to manual guessing. + } + + var offsets = userOffsets(), + offsetsLength = offsets.length, + guesses = guessesForUserOffsets(offsets), + zoneScores = [], + zoneScore, i, j; + + for (i = 0; i < guesses.length; i++) { + zoneScore = new ZoneScore(getZone(guesses[i]), offsetsLength); + for (j = 0; j < offsetsLength; j++) { + zoneScore.scoreOffsetAt(offsets[j]); + } + zoneScores.push(zoneScore); + } + + zoneScores.sort(sortZoneScores); + + return zoneScores.length > 0 ? zoneScores[0].zone.name : undefined; + } + + function guess (ignoreCache) { + if (!cachedGuess || ignoreCache) { + cachedGuess = rebuildGuess(); + } + return cachedGuess; + } + + /************************************ + Global Methods + ************************************/ + + function normalizeName (name) { + return (name || '').toLowerCase().replace(/\//g, '_'); + } + + function addZone (packed) { + var i, name, split, normalized; + + if (typeof packed === "string") { + packed = [packed]; + } + + for (i = 0; i < packed.length; i++) { + split = packed[i].split('|'); + name = split[0]; + normalized = normalizeName(name); + zones[normalized] = packed[i]; + names[normalized] = name; + addToGuesses(normalized, split[2].split(' ')); + } + } + + function getZone (name, caller) { + + name = normalizeName(name); + + var zone = zones[name]; + var link; + + if (zone instanceof Zone) { + return zone; + } + + if (typeof zone === 'string') { + zone = new Zone(zone); + zones[name] = zone; + return zone; + } + + // Pass getZone to prevent recursion more than 1 level deep + if (links[name] && caller !== getZone && (link = getZone(links[name], getZone))) { + zone = zones[name] = new Zone(); + zone._set(link); + zone.name = names[name]; + return zone; + } + + return null; + } + + function getNames () { + var i, out = []; + + for (i in names) { + if (names.hasOwnProperty(i) && (zones[i] || zones[links[i]]) && names[i]) { + out.push(names[i]); + } + } + + return out.sort(); + } + + function getCountryNames () { + return Object.keys(countries); + } + + function addLink (aliases) { + var i, alias, normal0, normal1; + + if (typeof aliases === "string") { + aliases = [aliases]; + } + + for (i = 0; i < aliases.length; i++) { + alias = aliases[i].split('|'); + + normal0 = normalizeName(alias[0]); + normal1 = normalizeName(alias[1]); + + links[normal0] = normal1; + names[normal0] = alias[0]; + + links[normal1] = normal0; + names[normal1] = alias[1]; + } + } + + function addCountries (data) { + var i, country_code, country_zones, split; + if (!data || !data.length) return; + for (i = 0; i < data.length; i++) { + split = data[i].split('|'); + country_code = split[0].toUpperCase(); + country_zones = split[1].split(' '); + countries[country_code] = new Country( + country_code, + country_zones + ); + } + } + + function getCountry (name) { + name = name.toUpperCase(); + return countries[name] || null; + } + + function zonesForCountry(country, with_offset) { + country = getCountry(country); + + if (!country) return null; + + var zones = country.zones.sort(); + + if (with_offset) { + return zones.map(function (zone_name) { + var zone = getZone(zone_name); + return { + name: zone_name, + offset: zone.utcOffset(new Date()) + }; + }); + } + + return zones; + } + + function loadData (data) { + addZone(data.zones); + addLink(data.links); + addCountries(data.countries); + tz.dataVersion = data.version; + } + + function zoneExists (name) { + if (!zoneExists.didShowError) { + zoneExists.didShowError = true; + logError("moment.tz.zoneExists('" + name + "') has been deprecated in favor of !moment.tz.zone('" + name + "')"); + } + return !!getZone(name); + } + + function needsOffset (m) { + var isUnixTimestamp = (m._f === 'X' || m._f === 'x'); + return !!(m._a && (m._tzm === undefined) && !isUnixTimestamp); + } + + function logError (message) { + if (typeof console !== 'undefined' && typeof console.error === 'function') { + console.error(message); + } + } + + /************************************ + moment.tz namespace + ************************************/ + + function tz (input) { + var args = Array.prototype.slice.call(arguments, 0, -1), + name = arguments[arguments.length - 1], + out = moment.utc.apply(null, args), + zone; + + if (!moment.isMoment(input) && needsOffset(out) && (zone = getZone(name))) { + out.add(zone.parse(out), 'minutes'); + } + + out.tz(name); + + return out; + } + + tz.version = VERSION; + tz.dataVersion = ''; + tz._zones = zones; + tz._links = links; + tz._names = names; + tz._countries = countries; + tz.add = addZone; + tz.link = addLink; + tz.load = loadData; + tz.zone = getZone; + tz.zoneExists = zoneExists; // deprecated in 0.1.0 + tz.guess = guess; + tz.names = getNames; + tz.Zone = Zone; + tz.unpack = unpack; + tz.unpackBase60 = unpackBase60; + tz.needsOffset = needsOffset; + tz.moveInvalidForward = true; + tz.moveAmbiguousForward = false; + tz.countries = getCountryNames; + tz.zonesForCountry = zonesForCountry; + + /************************************ + Interface with Moment.js + ************************************/ + + var fn = moment.fn; + + moment.tz = tz; + + moment.defaultZone = null; + + moment.updateOffset = function (mom, keepTime) { + var zone = moment.defaultZone, + offset; + + if (mom._z === undefined) { + if (zone && needsOffset(mom) && !mom._isUTC && mom.isValid()) { + mom._d = moment.utc(mom._a)._d; + mom.utc().add(zone.parse(mom), 'minutes'); + } + mom._z = zone; + } + if (mom._z) { + offset = mom._z.utcOffset(mom); + if (Math.abs(offset) < 16) { + offset = offset / 60; + } + if (mom.utcOffset !== undefined) { + var z = mom._z; + mom.utcOffset(-offset, keepTime); + mom._z = z; + } else { + mom.zone(offset, keepTime); + } + } + }; + + fn.tz = function (name, keepTime) { + if (name) { + if (typeof name !== 'string') { + throw new Error('Time zone name must be a string, got ' + name + ' [' + typeof name + ']'); + } + this._z = getZone(name); + if (this._z) { + moment.updateOffset(this, keepTime); + } else { + logError("Moment Timezone has no data for " + name + ". See http://momentjs.com/timezone/docs/#/data-loading/."); + } + return this; + } + if (this._z) { return this._z.name; } + }; + + function abbrWrap (old) { + return function () { + if (this._z) { return this._z.abbr(this); } + return old.call(this); + }; + } + + function resetZoneWrap (old) { + return function () { + this._z = null; + return old.apply(this, arguments); + }; + } + + function resetZoneWrap2 (old) { + return function () { + if (arguments.length > 0) this._z = null; + return old.apply(this, arguments); + }; + } + + fn.zoneName = abbrWrap(fn.zoneName); + fn.zoneAbbr = abbrWrap(fn.zoneAbbr); + fn.utc = resetZoneWrap(fn.utc); + fn.local = resetZoneWrap(fn.local); + fn.utcOffset = resetZoneWrap2(fn.utcOffset); + + moment.tz.setDefault = function(name) { + if (major < 2 || (major === 2 && minor < 9)) { + logError('Moment Timezone setDefault() requires Moment.js >= 2.9.0. You are using Moment.js ' + moment.version + '.'); + } + moment.defaultZone = name ? getZone(name) : null; + return moment; + }; + + // Cloning a moment should include the _z property. + var momentProperties = moment.momentProperties; + if (Object.prototype.toString.call(momentProperties) === '[object Array]') { + // moment 2.8.1+ + momentProperties.push('_z'); + momentProperties.push('_a'); + } else if (momentProperties) { + // moment 2.7.0 + momentProperties._z = null; + } + + loadData({ + "version": "2023d", + "zones": [ + "Africa/Abidjan|GMT|0|0||48e5", + "Africa/Nairobi|EAT|-30|0||47e5", + "Africa/Algiers|CET|-10|0||26e5", + "Africa/Lagos|WAT|-10|0||17e6", + "Africa/Khartoum|CAT|-20|0||51e5", + "Africa/Cairo|EET EEST|-20 -30|0101010101010|29NW0 1cL0 1cN0 1fz0 1a10 1fz0 1a10 1fz0 1cN0 1cL0 1cN0 1cL0|15e6", + "Africa/Casablanca|+00 +01|0 -10|010101010101010101010101|1Vq20 jA0 e00 28M0 e00 2600 gM0 2600 e00 2600 gM0 2600 e00 28M0 e00 2600 gM0 2600 e00 28M0 e00 2600 gM0|32e5", + "Europe/Paris|CET CEST|-10 -20|01010101010101010101010|1Vq10 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|11e6", + "Africa/Johannesburg|SAST|-20|0||84e5", + "Africa/Juba|EAT CAT|-30 -20|01|24nx0|", + "Africa/Sao_Tome|GMT WAT|0 -10|010|1UQN0 2q00|", + "Africa/Tripoli|EET|-20|0||11e5", + "America/Adak|HST HDT|a0 90|01010101010101010101010|1VkA0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|326", + "America/Anchorage|AKST AKDT|90 80|01010101010101010101010|1Vkz0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|30e4", + "America/Santo_Domingo|AST|40|0||29e5", + "America/Fortaleza|-03|30|0||34e5", + "America/Asuncion|-03 -04|30 40|01010101010101010101010|1Vq30 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0 19X0 1fB0 19X0 1ip0 17b0 1ip0 17b0 1ip0 19X0 1fB0 19X0 1fB0|28e5", + "America/Panama|EST|50|0||15e5", + "America/Mexico_City|CST CDT|60 50|01010101010|1VsU0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0|20e6", + "America/Managua|CST|60|0||22e5", + "America/Caracas|-04|40|0||29e5", + "America/Lima|-05|50|0||11e6", + "America/Denver|MST MDT|70 60|01010101010101010101010|1Vkx0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|26e5", + "America/Campo_Grande|-03 -04|30 40|0101|1Vc30 1HB0 FX0|77e4", + "America/Chicago|CST CDT|60 50|01010101010101010101010|1Vkw0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|92e5", + "America/Chihuahua|MST MDT CST|70 60 60|01010101012|1VsV0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0|81e4", + "America/Ciudad_Juarez|MST MDT CST|70 60 60|010101010120101010101010|1Vkx0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1wn0 cm0 EP0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|", + "America/Phoenix|MST|70|0||42e5", + "America/Whitehorse|PST PDT MST|80 70 70|0101012|1Vky0 1zb0 Op0 1zb0 Op0 1z90|23e3", + "America/New_York|EST EDT|50 40|01010101010101010101010|1Vkv0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|21e6", + "America/Los_Angeles|PST PDT|80 70|01010101010101010101010|1Vky0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|15e6", + "America/Halifax|AST ADT|40 30|01010101010101010101010|1Vku0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|39e4", + "America/Godthab|-03 -02 -01|30 20 10|0101010101012121212121|1Vq10 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 2so0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|17e3", + "America/Grand_Turk|AST EDT EST|40 40 50|01212121212121212121212|1Vkv0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|37e2", + "America/Havana|CST CDT|50 40|01010101010101010101010|1Vkt0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Oo0 1zc0 Rc0 1zc0 Oo0 1zc0|21e5", + "America/Mazatlan|MST MDT|70 60|01010101010|1VsV0 1nX0 14p0 1lb0 14p0 1lb0 14p0 1nX0 11B0 1nX0|44e4", + "America/Metlakatla|AKST AKDT PST|90 80 80|012010101010101010101010|1Vkz0 1zb0 uM0 jB0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|14e2", + "America/Miquelon|-03 -02|30 20|01010101010101010101010|1Vkt0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|61e2", + "America/Noronha|-02|20|0||30e2", + "America/Ojinaga|MST MDT CST CDT|70 60 60 50|01010101012323232323232|1Vkx0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1wn0 Rc0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|23e3", + "America/Santiago|-03 -04|30 40|01010101010101010101010|1VJD0 Ap0 1zb0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 11B0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0|62e5", + "America/Sao_Paulo|-02 -03|20 30|0101|1Vc20 1HB0 FX0|20e6", + "America/Scoresbysund|-01 +00 -02|10 0 20|0101010101010202020202|1Vq10 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 2pA0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|452", + "America/St_Johns|NST NDT|3u 2u|01010101010101010101010|1Vktu 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Op0 1zb0 Rd0 1zb0 Op0 1zb0|11e4", + "Antarctica/Casey|+11 +08|-b0 -80|010101010101|1Vkh0 1o30 14k0 1kr0 12l0 1o01 14kX 1lf1 14kX 1lf1 13bX|10", + "Asia/Bangkok|+07|-70|0||15e6", + "Asia/Vladivostok|+10|-a0|0||60e4", + "Australia/Sydney|AEDT AEST|-b0 -a0|01010101010101010101010|1VsE0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0|40e5", + "Asia/Tashkent|+05|-50|0||23e5", + "Pacific/Auckland|NZDT NZST|-d0 -c0|01010101010101010101010|1VsC0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00|14e5", + "Europe/Istanbul|+03|-30|0||13e6", + "Antarctica/Troll|+00 +02|0 -20|01010101010101010101010|1Vq10 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|40", + "Antarctica/Vostok|+07 +05|-70 -50|01|2bnv0|25", + "Asia/Dhaka|+06|-60|0||16e6", + "Asia/Amman|EET EEST +03|-20 -30 -30|01010101012|1VrW0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 LA0 1C00|25e5", + "Asia/Kamchatka|+12|-c0|0||18e4", + "Asia/Dubai|+04|-40|0||39e5", + "Asia/Beirut|EET EEST|-20 -30|01010101010101010101010|1VpW0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0|22e5", + "Asia/Kuala_Lumpur|+08|-80|0||71e5", + "Asia/Kolkata|IST|-5u|0||15e6", + "Asia/Chita|+09|-90|0||33e4", + "Asia/Shanghai|CST|-80|0||23e6", + "Asia/Colombo|+0530|-5u|0||22e5", + "Asia/Damascus|EET EEST +03|-20 -30 -30|01010101012|1VrW0 1nX0 11B0 1nX0 11B0 1qL0 WN0 1qL0 WN0 1qL0|26e5", + "Europe/Athens|EET EEST|-20 -30|01010101010101010101010|1Vq10 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|35e5", + "Asia/Gaza|EET EEST|-20 -30|01010101010101010101010|1Vpz0 1qL0 11c0 1on0 11B0 1o00 11A0 1qo0 XA0 1qp0 1cN0 1cL0 17d0 1in0 14p0 1lb0 11B0 1nX0 11B0 1qL0 WN0 1qL0|18e5", + "Asia/Hong_Kong|HKT|-80|0||73e5", + "Asia/Jakarta|WIB|-70|0||31e6", + "Asia/Jayapura|WIT|-90|0||26e4", + "Asia/Jerusalem|IST IDT|-20 -30|01010101010101010101010|1Vpc0 1rz0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0 W10 1rz0 10N0 1oL0 10N0 1oL0 10N0 1oL0 10N0 1rz0 W10 1rz0|81e4", + "Asia/Kabul|+0430|-4u|0||46e5", + "Asia/Karachi|PKT|-50|0||24e6", + "Asia/Kathmandu|+0545|-5J|0||12e5", + "Asia/Sakhalin|+11|-b0|0||58e4", + "Asia/Makassar|WITA|-80|0||15e5", + "Asia/Manila|PST|-80|0||24e6", + "Asia/Pyongyang|KST KST|-8u -90|01|1VGf0|29e5", + "Asia/Qyzylorda|+06 +05|-60 -50|01|1Xei0|73e4", + "Asia/Rangoon|+0630|-6u|0||48e5", + "Asia/Seoul|KST|-90|0||23e6", + "Asia/Tehran|+0330 +0430|-3u -4u|01010101010|1VoIu 1dz0 1cp0 1dz0 1cp0 1dz0 1cN0 1dz0 1cp0 1dz0|14e6", + "Asia/Tokyo|JST|-90|0||38e6", + "Atlantic/Azores|-01 +00|10 0|01010101010101010101010|1Vq10 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|25e4", + "Europe/Lisbon|WET WEST|0 -10|01010101010101010101010|1Vq10 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|27e5", + "Atlantic/Cape_Verde|-01|10|0||50e4", + "Australia/Adelaide|ACDT ACST|-au -9u|01010101010101010101010|1VsEu 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0|11e5", + "Australia/Brisbane|AEST|-a0|0||20e5", + "Australia/Darwin|ACST|-9u|0||12e4", + "Australia/Eucla|+0845|-8J|0||368", + "Australia/Lord_Howe|+11 +1030|-b0 -au|01010101010101010101010|1VsD0 1fAu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1fzu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu 1cLu 1cMu|347", + "Australia/Perth|AWST|-80|0||18e5", + "Pacific/Easter|-05 -06|50 60|01010101010101010101010|1VJD0 Ap0 1zb0 11B0 1nX0 11B0 1nX0 11B0 1nX0 14p0 1lb0 11B0 1qL0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0 1nX0 11B0|30e2", + "Europe/Dublin|GMT IST|0 -10|01010101010101010101010|1Vq10 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|12e5", + "Etc/GMT-1|+01|-10|0||", + "Pacific/Tongatapu|+13|-d0|0||75e3", + "Pacific/Kiritimati|+14|-e0|0||51e2", + "Etc/GMT-2|+02|-20|0||", + "Pacific/Tahiti|-10|a0|0||18e4", + "Pacific/Niue|-11|b0|0||12e2", + "Etc/GMT+12|-12|c0|0||", + "Pacific/Galapagos|-06|60|0||25e3", + "Etc/GMT+7|-07|70|0||", + "Pacific/Pitcairn|-08|80|0||56", + "Pacific/Gambier|-09|90|0||125", + "Etc/UTC|UTC|0|0||", + "Europe/London|GMT BST|0 -10|01010101010101010101010|1Vq10 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|10e6", + "Europe/Chisinau|EET EEST|-20 -30|01010101010101010101010|1Vq00 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|67e4", + "Europe/Moscow|MSK|-30|0||16e6", + "Europe/Volgograd|MSK +04|-30 -40|010|1WQL0 5gn0|10e5", + "Pacific/Honolulu|HST|a0|0||37e4", + "MET|MET MEST|-10 -20|01010101010101010101010|1Vq10 1qM0 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0 WM0 1qM0 11A0 1o00 11A0 1o00 11A0 1o00 11A0 1qM0 WM0 1qM0|", + "Pacific/Chatham|+1345 +1245|-dJ -cJ|01010101010101010101010|1VsC0 1cM0 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1io0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00 1fA0 1a00|600", + "Pacific/Apia|+14 +13|-e0 -d0|01010101|1VsC0 1cM0 1fA0 1a00 1fA0 1a00 1fA0|37e3", + "Pacific/Fiji|+13 +12|-d0 -c0|01010101|1UVO0 1VA0 s00 20o0 pc0 2hc0 bc0|88e4", + "Pacific/Guam|ChST|-a0|0||17e4", + "Pacific/Marquesas|-0930|9u|0||86e2", + "Pacific/Pago_Pago|SST|b0|0||37e2", + "Pacific/Norfolk|+11 +12|-b0 -c0|01010101010101010101|219P0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1fA0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0 1cM0|25e4" + ], + "links": [ + "Africa/Abidjan|Africa/Accra", + "Africa/Abidjan|Africa/Bamako", + "Africa/Abidjan|Africa/Banjul", + "Africa/Abidjan|Africa/Bissau", + "Africa/Abidjan|Africa/Conakry", + "Africa/Abidjan|Africa/Dakar", + "Africa/Abidjan|Africa/Freetown", + "Africa/Abidjan|Africa/Lome", + "Africa/Abidjan|Africa/Monrovia", + "Africa/Abidjan|Africa/Nouakchott", + "Africa/Abidjan|Africa/Ouagadougou", + "Africa/Abidjan|Africa/Timbuktu", + "Africa/Abidjan|America/Danmarkshavn", + "Africa/Abidjan|Atlantic/Reykjavik", + "Africa/Abidjan|Atlantic/St_Helena", + "Africa/Abidjan|Etc/GMT", + "Africa/Abidjan|Etc/GMT+0", + "Africa/Abidjan|Etc/GMT-0", + "Africa/Abidjan|Etc/GMT0", + "Africa/Abidjan|Etc/Greenwich", + "Africa/Abidjan|GMT", + "Africa/Abidjan|GMT+0", + "Africa/Abidjan|GMT-0", + "Africa/Abidjan|GMT0", + "Africa/Abidjan|Greenwich", + "Africa/Abidjan|Iceland", + "Africa/Algiers|Africa/Tunis", + "Africa/Cairo|Egypt", + "Africa/Casablanca|Africa/El_Aaiun", + "Africa/Johannesburg|Africa/Maseru", + "Africa/Johannesburg|Africa/Mbabane", + "Africa/Khartoum|Africa/Blantyre", + "Africa/Khartoum|Africa/Bujumbura", + "Africa/Khartoum|Africa/Gaborone", + "Africa/Khartoum|Africa/Harare", + "Africa/Khartoum|Africa/Kigali", + "Africa/Khartoum|Africa/Lubumbashi", + "Africa/Khartoum|Africa/Lusaka", + "Africa/Khartoum|Africa/Maputo", + "Africa/Khartoum|Africa/Windhoek", + "Africa/Lagos|Africa/Bangui", + "Africa/Lagos|Africa/Brazzaville", + "Africa/Lagos|Africa/Douala", + "Africa/Lagos|Africa/Kinshasa", + "Africa/Lagos|Africa/Libreville", + "Africa/Lagos|Africa/Luanda", + "Africa/Lagos|Africa/Malabo", + "Africa/Lagos|Africa/Ndjamena", + "Africa/Lagos|Africa/Niamey", + "Africa/Lagos|Africa/Porto-Novo", + "Africa/Nairobi|Africa/Addis_Ababa", + "Africa/Nairobi|Africa/Asmara", + "Africa/Nairobi|Africa/Asmera", + "Africa/Nairobi|Africa/Dar_es_Salaam", + "Africa/Nairobi|Africa/Djibouti", + "Africa/Nairobi|Africa/Kampala", + "Africa/Nairobi|Africa/Mogadishu", + "Africa/Nairobi|Indian/Antananarivo", + "Africa/Nairobi|Indian/Comoro", + "Africa/Nairobi|Indian/Mayotte", + "Africa/Tripoli|Europe/Kaliningrad", + "Africa/Tripoli|Libya", + "America/Adak|America/Atka", + "America/Adak|US/Aleutian", + "America/Anchorage|America/Juneau", + "America/Anchorage|America/Nome", + "America/Anchorage|America/Sitka", + "America/Anchorage|America/Yakutat", + "America/Anchorage|US/Alaska", + "America/Campo_Grande|America/Cuiaba", + "America/Caracas|America/Boa_Vista", + "America/Caracas|America/Guyana", + "America/Caracas|America/La_Paz", + "America/Caracas|America/Manaus", + "America/Caracas|America/Porto_Velho", + "America/Caracas|Brazil/West", + "America/Caracas|Etc/GMT+4", + "America/Chicago|America/Indiana/Knox", + "America/Chicago|America/Indiana/Tell_City", + "America/Chicago|America/Knox_IN", + "America/Chicago|America/Matamoros", + "America/Chicago|America/Menominee", + "America/Chicago|America/North_Dakota/Beulah", + "America/Chicago|America/North_Dakota/Center", + "America/Chicago|America/North_Dakota/New_Salem", + "America/Chicago|America/Rainy_River", + "America/Chicago|America/Rankin_Inlet", + "America/Chicago|America/Resolute", + "America/Chicago|America/Winnipeg", + "America/Chicago|CST6CDT", + "America/Chicago|Canada/Central", + "America/Chicago|US/Central", + "America/Chicago|US/Indiana-Starke", + "America/Denver|America/Boise", + "America/Denver|America/Cambridge_Bay", + "America/Denver|America/Edmonton", + "America/Denver|America/Inuvik", + "America/Denver|America/Shiprock", + "America/Denver|America/Yellowknife", + "America/Denver|Canada/Mountain", + "America/Denver|MST7MDT", + "America/Denver|Navajo", + "America/Denver|US/Mountain", + "America/Fortaleza|America/Araguaina", + "America/Fortaleza|America/Argentina/Buenos_Aires", + "America/Fortaleza|America/Argentina/Catamarca", + "America/Fortaleza|America/Argentina/ComodRivadavia", + "America/Fortaleza|America/Argentina/Cordoba", + "America/Fortaleza|America/Argentina/Jujuy", + "America/Fortaleza|America/Argentina/La_Rioja", + "America/Fortaleza|America/Argentina/Mendoza", + "America/Fortaleza|America/Argentina/Rio_Gallegos", + "America/Fortaleza|America/Argentina/Salta", + "America/Fortaleza|America/Argentina/San_Juan", + "America/Fortaleza|America/Argentina/San_Luis", + "America/Fortaleza|America/Argentina/Tucuman", + "America/Fortaleza|America/Argentina/Ushuaia", + "America/Fortaleza|America/Bahia", + "America/Fortaleza|America/Belem", + "America/Fortaleza|America/Buenos_Aires", + "America/Fortaleza|America/Catamarca", + "America/Fortaleza|America/Cayenne", + "America/Fortaleza|America/Cordoba", + "America/Fortaleza|America/Jujuy", + "America/Fortaleza|America/Maceio", + "America/Fortaleza|America/Mendoza", + "America/Fortaleza|America/Montevideo", + "America/Fortaleza|America/Paramaribo", + "America/Fortaleza|America/Punta_Arenas", + "America/Fortaleza|America/Recife", + "America/Fortaleza|America/Rosario", + "America/Fortaleza|America/Santarem", + "America/Fortaleza|Antarctica/Palmer", + "America/Fortaleza|Antarctica/Rothera", + "America/Fortaleza|Atlantic/Stanley", + "America/Fortaleza|Etc/GMT+3", + "America/Godthab|America/Nuuk", + "America/Halifax|America/Glace_Bay", + "America/Halifax|America/Goose_Bay", + "America/Halifax|America/Moncton", + "America/Halifax|America/Thule", + "America/Halifax|Atlantic/Bermuda", + "America/Halifax|Canada/Atlantic", + "America/Havana|Cuba", + "America/Lima|America/Bogota", + "America/Lima|America/Eirunepe", + "America/Lima|America/Guayaquil", + "America/Lima|America/Porto_Acre", + "America/Lima|America/Rio_Branco", + "America/Lima|Brazil/Acre", + "America/Lima|Etc/GMT+5", + "America/Los_Angeles|America/Ensenada", + "America/Los_Angeles|America/Santa_Isabel", + "America/Los_Angeles|America/Tijuana", + "America/Los_Angeles|America/Vancouver", + "America/Los_Angeles|Canada/Pacific", + "America/Los_Angeles|Mexico/BajaNorte", + "America/Los_Angeles|PST8PDT", + "America/Los_Angeles|US/Pacific", + "America/Managua|America/Belize", + "America/Managua|America/Costa_Rica", + "America/Managua|America/El_Salvador", + "America/Managua|America/Guatemala", + "America/Managua|America/Regina", + "America/Managua|America/Swift_Current", + "America/Managua|America/Tegucigalpa", + "America/Managua|Canada/Saskatchewan", + "America/Mazatlan|Mexico/BajaSur", + "America/Mexico_City|America/Bahia_Banderas", + "America/Mexico_City|America/Merida", + "America/Mexico_City|America/Monterrey", + "America/Mexico_City|Mexico/General", + "America/New_York|America/Detroit", + "America/New_York|America/Fort_Wayne", + "America/New_York|America/Indiana/Indianapolis", + "America/New_York|America/Indiana/Marengo", + "America/New_York|America/Indiana/Petersburg", + "America/New_York|America/Indiana/Vevay", + "America/New_York|America/Indiana/Vincennes", + "America/New_York|America/Indiana/Winamac", + "America/New_York|America/Indianapolis", + "America/New_York|America/Iqaluit", + "America/New_York|America/Kentucky/Louisville", + "America/New_York|America/Kentucky/Monticello", + "America/New_York|America/Louisville", + "America/New_York|America/Montreal", + "America/New_York|America/Nassau", + "America/New_York|America/Nipigon", + "America/New_York|America/Pangnirtung", + "America/New_York|America/Port-au-Prince", + "America/New_York|America/Thunder_Bay", + "America/New_York|America/Toronto", + "America/New_York|Canada/Eastern", + "America/New_York|EST5EDT", + "America/New_York|US/East-Indiana", + "America/New_York|US/Eastern", + "America/New_York|US/Michigan", + "America/Noronha|Atlantic/South_Georgia", + "America/Noronha|Brazil/DeNoronha", + "America/Noronha|Etc/GMT+2", + "America/Panama|America/Atikokan", + "America/Panama|America/Cancun", + "America/Panama|America/Cayman", + "America/Panama|America/Coral_Harbour", + "America/Panama|America/Jamaica", + "America/Panama|EST", + "America/Panama|Jamaica", + "America/Phoenix|America/Creston", + "America/Phoenix|America/Dawson_Creek", + "America/Phoenix|America/Fort_Nelson", + "America/Phoenix|America/Hermosillo", + "America/Phoenix|MST", + "America/Phoenix|US/Arizona", + "America/Santiago|Chile/Continental", + "America/Santo_Domingo|America/Anguilla", + "America/Santo_Domingo|America/Antigua", + "America/Santo_Domingo|America/Aruba", + "America/Santo_Domingo|America/Barbados", + "America/Santo_Domingo|America/Blanc-Sablon", + "America/Santo_Domingo|America/Curacao", + "America/Santo_Domingo|America/Dominica", + "America/Santo_Domingo|America/Grenada", + "America/Santo_Domingo|America/Guadeloupe", + "America/Santo_Domingo|America/Kralendijk", + "America/Santo_Domingo|America/Lower_Princes", + "America/Santo_Domingo|America/Marigot", + "America/Santo_Domingo|America/Martinique", + "America/Santo_Domingo|America/Montserrat", + "America/Santo_Domingo|America/Port_of_Spain", + "America/Santo_Domingo|America/Puerto_Rico", + "America/Santo_Domingo|America/St_Barthelemy", + "America/Santo_Domingo|America/St_Kitts", + "America/Santo_Domingo|America/St_Lucia", + "America/Santo_Domingo|America/St_Thomas", + "America/Santo_Domingo|America/St_Vincent", + "America/Santo_Domingo|America/Tortola", + "America/Santo_Domingo|America/Virgin", + "America/Sao_Paulo|Brazil/East", + "America/St_Johns|Canada/Newfoundland", + "America/Whitehorse|America/Dawson", + "America/Whitehorse|Canada/Yukon", + "Asia/Bangkok|Antarctica/Davis", + "Asia/Bangkok|Asia/Barnaul", + "Asia/Bangkok|Asia/Ho_Chi_Minh", + "Asia/Bangkok|Asia/Hovd", + "Asia/Bangkok|Asia/Krasnoyarsk", + "Asia/Bangkok|Asia/Novokuznetsk", + "Asia/Bangkok|Asia/Novosibirsk", + "Asia/Bangkok|Asia/Phnom_Penh", + "Asia/Bangkok|Asia/Saigon", + "Asia/Bangkok|Asia/Tomsk", + "Asia/Bangkok|Asia/Vientiane", + "Asia/Bangkok|Etc/GMT-7", + "Asia/Bangkok|Indian/Christmas", + "Asia/Chita|Asia/Dili", + "Asia/Chita|Asia/Khandyga", + "Asia/Chita|Asia/Yakutsk", + "Asia/Chita|Etc/GMT-9", + "Asia/Chita|Pacific/Palau", + "Asia/Dhaka|Asia/Almaty", + "Asia/Dhaka|Asia/Bishkek", + "Asia/Dhaka|Asia/Dacca", + "Asia/Dhaka|Asia/Kashgar", + "Asia/Dhaka|Asia/Omsk", + "Asia/Dhaka|Asia/Qostanay", + "Asia/Dhaka|Asia/Thimbu", + "Asia/Dhaka|Asia/Thimphu", + "Asia/Dhaka|Asia/Urumqi", + "Asia/Dhaka|Etc/GMT-6", + "Asia/Dhaka|Indian/Chagos", + "Asia/Dubai|Asia/Baku", + "Asia/Dubai|Asia/Muscat", + "Asia/Dubai|Asia/Tbilisi", + "Asia/Dubai|Asia/Yerevan", + "Asia/Dubai|Etc/GMT-4", + "Asia/Dubai|Europe/Astrakhan", + "Asia/Dubai|Europe/Samara", + "Asia/Dubai|Europe/Saratov", + "Asia/Dubai|Europe/Ulyanovsk", + "Asia/Dubai|Indian/Mahe", + "Asia/Dubai|Indian/Mauritius", + "Asia/Dubai|Indian/Reunion", + "Asia/Gaza|Asia/Hebron", + "Asia/Hong_Kong|Hongkong", + "Asia/Jakarta|Asia/Pontianak", + "Asia/Jerusalem|Asia/Tel_Aviv", + "Asia/Jerusalem|Israel", + "Asia/Kamchatka|Asia/Anadyr", + "Asia/Kamchatka|Etc/GMT-12", + "Asia/Kamchatka|Kwajalein", + "Asia/Kamchatka|Pacific/Funafuti", + "Asia/Kamchatka|Pacific/Kwajalein", + "Asia/Kamchatka|Pacific/Majuro", + "Asia/Kamchatka|Pacific/Nauru", + "Asia/Kamchatka|Pacific/Tarawa", + "Asia/Kamchatka|Pacific/Wake", + "Asia/Kamchatka|Pacific/Wallis", + "Asia/Kathmandu|Asia/Katmandu", + "Asia/Kolkata|Asia/Calcutta", + "Asia/Kuala_Lumpur|Asia/Brunei", + "Asia/Kuala_Lumpur|Asia/Choibalsan", + "Asia/Kuala_Lumpur|Asia/Irkutsk", + "Asia/Kuala_Lumpur|Asia/Kuching", + "Asia/Kuala_Lumpur|Asia/Singapore", + "Asia/Kuala_Lumpur|Asia/Ulaanbaatar", + "Asia/Kuala_Lumpur|Asia/Ulan_Bator", + "Asia/Kuala_Lumpur|Etc/GMT-8", + "Asia/Kuala_Lumpur|Singapore", + "Asia/Makassar|Asia/Ujung_Pandang", + "Asia/Rangoon|Asia/Yangon", + "Asia/Rangoon|Indian/Cocos", + "Asia/Sakhalin|Asia/Magadan", + "Asia/Sakhalin|Asia/Srednekolymsk", + "Asia/Sakhalin|Etc/GMT-11", + "Asia/Sakhalin|Pacific/Bougainville", + "Asia/Sakhalin|Pacific/Efate", + "Asia/Sakhalin|Pacific/Guadalcanal", + "Asia/Sakhalin|Pacific/Kosrae", + "Asia/Sakhalin|Pacific/Noumea", + "Asia/Sakhalin|Pacific/Pohnpei", + "Asia/Sakhalin|Pacific/Ponape", + "Asia/Seoul|ROK", + "Asia/Shanghai|Asia/Chongqing", + "Asia/Shanghai|Asia/Chungking", + "Asia/Shanghai|Asia/Harbin", + "Asia/Shanghai|Asia/Macao", + "Asia/Shanghai|Asia/Macau", + "Asia/Shanghai|Asia/Taipei", + "Asia/Shanghai|PRC", + "Asia/Shanghai|ROC", + "Asia/Tashkent|Antarctica/Mawson", + "Asia/Tashkent|Asia/Aqtau", + "Asia/Tashkent|Asia/Aqtobe", + "Asia/Tashkent|Asia/Ashgabat", + "Asia/Tashkent|Asia/Ashkhabad", + "Asia/Tashkent|Asia/Atyrau", + "Asia/Tashkent|Asia/Dushanbe", + "Asia/Tashkent|Asia/Oral", + "Asia/Tashkent|Asia/Samarkand", + "Asia/Tashkent|Asia/Yekaterinburg", + "Asia/Tashkent|Etc/GMT-5", + "Asia/Tashkent|Indian/Kerguelen", + "Asia/Tashkent|Indian/Maldives", + "Asia/Tehran|Iran", + "Asia/Tokyo|Japan", + "Asia/Vladivostok|Antarctica/DumontDUrville", + "Asia/Vladivostok|Asia/Ust-Nera", + "Asia/Vladivostok|Etc/GMT-10", + "Asia/Vladivostok|Pacific/Chuuk", + "Asia/Vladivostok|Pacific/Port_Moresby", + "Asia/Vladivostok|Pacific/Truk", + "Asia/Vladivostok|Pacific/Yap", + "Atlantic/Cape_Verde|Etc/GMT+1", + "Australia/Adelaide|Australia/Broken_Hill", + "Australia/Adelaide|Australia/South", + "Australia/Adelaide|Australia/Yancowinna", + "Australia/Brisbane|Australia/Lindeman", + "Australia/Brisbane|Australia/Queensland", + "Australia/Darwin|Australia/North", + "Australia/Lord_Howe|Australia/LHI", + "Australia/Perth|Australia/West", + "Australia/Sydney|Antarctica/Macquarie", + "Australia/Sydney|Australia/ACT", + "Australia/Sydney|Australia/Canberra", + "Australia/Sydney|Australia/Currie", + "Australia/Sydney|Australia/Hobart", + "Australia/Sydney|Australia/Melbourne", + "Australia/Sydney|Australia/NSW", + "Australia/Sydney|Australia/Tasmania", + "Australia/Sydney|Australia/Victoria", + "Etc/UTC|Etc/UCT", + "Etc/UTC|Etc/Universal", + "Etc/UTC|Etc/Zulu", + "Etc/UTC|UCT", + "Etc/UTC|UTC", + "Etc/UTC|Universal", + "Etc/UTC|Zulu", + "Europe/Athens|Asia/Famagusta", + "Europe/Athens|Asia/Nicosia", + "Europe/Athens|EET", + "Europe/Athens|Europe/Bucharest", + "Europe/Athens|Europe/Helsinki", + "Europe/Athens|Europe/Kiev", + "Europe/Athens|Europe/Kyiv", + "Europe/Athens|Europe/Mariehamn", + "Europe/Athens|Europe/Nicosia", + "Europe/Athens|Europe/Riga", + "Europe/Athens|Europe/Sofia", + "Europe/Athens|Europe/Tallinn", + "Europe/Athens|Europe/Uzhgorod", + "Europe/Athens|Europe/Vilnius", + "Europe/Athens|Europe/Zaporozhye", + "Europe/Chisinau|Europe/Tiraspol", + "Europe/Dublin|Eire", + "Europe/Istanbul|Antarctica/Syowa", + "Europe/Istanbul|Asia/Aden", + "Europe/Istanbul|Asia/Baghdad", + "Europe/Istanbul|Asia/Bahrain", + "Europe/Istanbul|Asia/Istanbul", + "Europe/Istanbul|Asia/Kuwait", + "Europe/Istanbul|Asia/Qatar", + "Europe/Istanbul|Asia/Riyadh", + "Europe/Istanbul|Etc/GMT-3", + "Europe/Istanbul|Europe/Minsk", + "Europe/Istanbul|Turkey", + "Europe/Lisbon|Atlantic/Canary", + "Europe/Lisbon|Atlantic/Faeroe", + "Europe/Lisbon|Atlantic/Faroe", + "Europe/Lisbon|Atlantic/Madeira", + "Europe/Lisbon|Portugal", + "Europe/Lisbon|WET", + "Europe/London|Europe/Belfast", + "Europe/London|Europe/Guernsey", + "Europe/London|Europe/Isle_of_Man", + "Europe/London|Europe/Jersey", + "Europe/London|GB", + "Europe/London|GB-Eire", + "Europe/Moscow|Europe/Kirov", + "Europe/Moscow|Europe/Simferopol", + "Europe/Moscow|W-SU", + "Europe/Paris|Africa/Ceuta", + "Europe/Paris|Arctic/Longyearbyen", + "Europe/Paris|Atlantic/Jan_Mayen", + "Europe/Paris|CET", + "Europe/Paris|Europe/Amsterdam", + "Europe/Paris|Europe/Andorra", + "Europe/Paris|Europe/Belgrade", + "Europe/Paris|Europe/Berlin", + "Europe/Paris|Europe/Bratislava", + "Europe/Paris|Europe/Brussels", + "Europe/Paris|Europe/Budapest", + "Europe/Paris|Europe/Busingen", + "Europe/Paris|Europe/Copenhagen", + "Europe/Paris|Europe/Gibraltar", + "Europe/Paris|Europe/Ljubljana", + "Europe/Paris|Europe/Luxembourg", + "Europe/Paris|Europe/Madrid", + "Europe/Paris|Europe/Malta", + "Europe/Paris|Europe/Monaco", + "Europe/Paris|Europe/Oslo", + "Europe/Paris|Europe/Podgorica", + "Europe/Paris|Europe/Prague", + "Europe/Paris|Europe/Rome", + "Europe/Paris|Europe/San_Marino", + "Europe/Paris|Europe/Sarajevo", + "Europe/Paris|Europe/Skopje", + "Europe/Paris|Europe/Stockholm", + "Europe/Paris|Europe/Tirane", + "Europe/Paris|Europe/Vaduz", + "Europe/Paris|Europe/Vatican", + "Europe/Paris|Europe/Vienna", + "Europe/Paris|Europe/Warsaw", + "Europe/Paris|Europe/Zagreb", + "Europe/Paris|Europe/Zurich", + "Europe/Paris|Poland", + "Pacific/Auckland|Antarctica/McMurdo", + "Pacific/Auckland|Antarctica/South_Pole", + "Pacific/Auckland|NZ", + "Pacific/Chatham|NZ-CHAT", + "Pacific/Easter|Chile/EasterIsland", + "Pacific/Galapagos|Etc/GMT+6", + "Pacific/Gambier|Etc/GMT+9", + "Pacific/Guam|Pacific/Saipan", + "Pacific/Honolulu|HST", + "Pacific/Honolulu|Pacific/Johnston", + "Pacific/Honolulu|US/Hawaii", + "Pacific/Kiritimati|Etc/GMT-14", + "Pacific/Niue|Etc/GMT+11", + "Pacific/Pago_Pago|Pacific/Midway", + "Pacific/Pago_Pago|Pacific/Samoa", + "Pacific/Pago_Pago|US/Samoa", + "Pacific/Pitcairn|Etc/GMT+8", + "Pacific/Tahiti|Etc/GMT+10", + "Pacific/Tahiti|Pacific/Rarotonga", + "Pacific/Tongatapu|Etc/GMT-13", + "Pacific/Tongatapu|Pacific/Enderbury", + "Pacific/Tongatapu|Pacific/Fakaofo", + "Pacific/Tongatapu|Pacific/Kanton" + ], + "countries": [ + "AD|Europe/Andorra", + "AE|Asia/Dubai", + "AF|Asia/Kabul", + "AG|America/Puerto_Rico America/Antigua", + "AI|America/Puerto_Rico America/Anguilla", + "AL|Europe/Tirane", + "AM|Asia/Yerevan", + "AO|Africa/Lagos Africa/Luanda", + "AQ|Antarctica/Casey Antarctica/Davis Antarctica/Mawson Antarctica/Palmer Antarctica/Rothera Antarctica/Troll Antarctica/Vostok Pacific/Auckland Pacific/Port_Moresby Asia/Riyadh Antarctica/McMurdo Antarctica/DumontDUrville Antarctica/Syowa", + "AR|America/Argentina/Buenos_Aires America/Argentina/Cordoba America/Argentina/Salta America/Argentina/Jujuy America/Argentina/Tucuman America/Argentina/Catamarca America/Argentina/La_Rioja America/Argentina/San_Juan America/Argentina/Mendoza America/Argentina/San_Luis America/Argentina/Rio_Gallegos America/Argentina/Ushuaia", + "AS|Pacific/Pago_Pago", + "AT|Europe/Vienna", + "AU|Australia/Lord_Howe Antarctica/Macquarie Australia/Hobart Australia/Melbourne Australia/Sydney Australia/Broken_Hill Australia/Brisbane Australia/Lindeman Australia/Adelaide Australia/Darwin Australia/Perth Australia/Eucla", + "AW|America/Puerto_Rico America/Aruba", + "AX|Europe/Helsinki Europe/Mariehamn", + "AZ|Asia/Baku", + "BA|Europe/Belgrade Europe/Sarajevo", + "BB|America/Barbados", + "BD|Asia/Dhaka", + "BE|Europe/Brussels", + "BF|Africa/Abidjan Africa/Ouagadougou", + "BG|Europe/Sofia", + "BH|Asia/Qatar Asia/Bahrain", + "BI|Africa/Maputo Africa/Bujumbura", + "BJ|Africa/Lagos Africa/Porto-Novo", + "BL|America/Puerto_Rico America/St_Barthelemy", + "BM|Atlantic/Bermuda", + "BN|Asia/Kuching Asia/Brunei", + "BO|America/La_Paz", + "BQ|America/Puerto_Rico America/Kralendijk", + "BR|America/Noronha America/Belem America/Fortaleza America/Recife America/Araguaina America/Maceio America/Bahia America/Sao_Paulo America/Campo_Grande America/Cuiaba America/Santarem America/Porto_Velho America/Boa_Vista America/Manaus America/Eirunepe America/Rio_Branco", + "BS|America/Toronto America/Nassau", + "BT|Asia/Thimphu", + "BW|Africa/Maputo Africa/Gaborone", + "BY|Europe/Minsk", + "BZ|America/Belize", + "CA|America/St_Johns America/Halifax America/Glace_Bay America/Moncton America/Goose_Bay America/Toronto America/Iqaluit America/Winnipeg America/Resolute America/Rankin_Inlet America/Regina America/Swift_Current America/Edmonton America/Cambridge_Bay America/Inuvik America/Dawson_Creek America/Fort_Nelson America/Whitehorse America/Dawson America/Vancouver America/Panama America/Puerto_Rico America/Phoenix America/Blanc-Sablon America/Atikokan America/Creston", + "CC|Asia/Yangon Indian/Cocos", + "CD|Africa/Maputo Africa/Lagos Africa/Kinshasa Africa/Lubumbashi", + "CF|Africa/Lagos Africa/Bangui", + "CG|Africa/Lagos Africa/Brazzaville", + "CH|Europe/Zurich", + "CI|Africa/Abidjan", + "CK|Pacific/Rarotonga", + "CL|America/Santiago America/Punta_Arenas Pacific/Easter", + "CM|Africa/Lagos Africa/Douala", + "CN|Asia/Shanghai Asia/Urumqi", + "CO|America/Bogota", + "CR|America/Costa_Rica", + "CU|America/Havana", + "CV|Atlantic/Cape_Verde", + "CW|America/Puerto_Rico America/Curacao", + "CX|Asia/Bangkok Indian/Christmas", + "CY|Asia/Nicosia Asia/Famagusta", + "CZ|Europe/Prague", + "DE|Europe/Zurich Europe/Berlin Europe/Busingen", + "DJ|Africa/Nairobi Africa/Djibouti", + "DK|Europe/Berlin Europe/Copenhagen", + "DM|America/Puerto_Rico America/Dominica", + "DO|America/Santo_Domingo", + "DZ|Africa/Algiers", + "EC|America/Guayaquil Pacific/Galapagos", + "EE|Europe/Tallinn", + "EG|Africa/Cairo", + "EH|Africa/El_Aaiun", + "ER|Africa/Nairobi Africa/Asmara", + "ES|Europe/Madrid Africa/Ceuta Atlantic/Canary", + "ET|Africa/Nairobi Africa/Addis_Ababa", + "FI|Europe/Helsinki", + "FJ|Pacific/Fiji", + "FK|Atlantic/Stanley", + "FM|Pacific/Kosrae Pacific/Port_Moresby Pacific/Guadalcanal Pacific/Chuuk Pacific/Pohnpei", + "FO|Atlantic/Faroe", + "FR|Europe/Paris", + "GA|Africa/Lagos Africa/Libreville", + "GB|Europe/London", + "GD|America/Puerto_Rico America/Grenada", + "GE|Asia/Tbilisi", + "GF|America/Cayenne", + "GG|Europe/London Europe/Guernsey", + "GH|Africa/Abidjan Africa/Accra", + "GI|Europe/Gibraltar", + "GL|America/Nuuk America/Danmarkshavn America/Scoresbysund America/Thule", + "GM|Africa/Abidjan Africa/Banjul", + "GN|Africa/Abidjan Africa/Conakry", + "GP|America/Puerto_Rico America/Guadeloupe", + "GQ|Africa/Lagos Africa/Malabo", + "GR|Europe/Athens", + "GS|Atlantic/South_Georgia", + "GT|America/Guatemala", + "GU|Pacific/Guam", + "GW|Africa/Bissau", + "GY|America/Guyana", + "HK|Asia/Hong_Kong", + "HN|America/Tegucigalpa", + "HR|Europe/Belgrade Europe/Zagreb", + "HT|America/Port-au-Prince", + "HU|Europe/Budapest", + "ID|Asia/Jakarta Asia/Pontianak Asia/Makassar Asia/Jayapura", + "IE|Europe/Dublin", + "IL|Asia/Jerusalem", + "IM|Europe/London Europe/Isle_of_Man", + "IN|Asia/Kolkata", + "IO|Indian/Chagos", + "IQ|Asia/Baghdad", + "IR|Asia/Tehran", + "IS|Africa/Abidjan Atlantic/Reykjavik", + "IT|Europe/Rome", + "JE|Europe/London Europe/Jersey", + "JM|America/Jamaica", + "JO|Asia/Amman", + "JP|Asia/Tokyo", + "KE|Africa/Nairobi", + "KG|Asia/Bishkek", + "KH|Asia/Bangkok Asia/Phnom_Penh", + "KI|Pacific/Tarawa Pacific/Kanton Pacific/Kiritimati", + "KM|Africa/Nairobi Indian/Comoro", + "KN|America/Puerto_Rico America/St_Kitts", + "KP|Asia/Pyongyang", + "KR|Asia/Seoul", + "KW|Asia/Riyadh Asia/Kuwait", + "KY|America/Panama America/Cayman", + "KZ|Asia/Almaty Asia/Qyzylorda Asia/Qostanay Asia/Aqtobe Asia/Aqtau Asia/Atyrau Asia/Oral", + "LA|Asia/Bangkok Asia/Vientiane", + "LB|Asia/Beirut", + "LC|America/Puerto_Rico America/St_Lucia", + "LI|Europe/Zurich Europe/Vaduz", + "LK|Asia/Colombo", + "LR|Africa/Monrovia", + "LS|Africa/Johannesburg Africa/Maseru", + "LT|Europe/Vilnius", + "LU|Europe/Brussels Europe/Luxembourg", + "LV|Europe/Riga", + "LY|Africa/Tripoli", + "MA|Africa/Casablanca", + "MC|Europe/Paris Europe/Monaco", + "MD|Europe/Chisinau", + "ME|Europe/Belgrade Europe/Podgorica", + "MF|America/Puerto_Rico America/Marigot", + "MG|Africa/Nairobi Indian/Antananarivo", + "MH|Pacific/Tarawa Pacific/Kwajalein Pacific/Majuro", + "MK|Europe/Belgrade Europe/Skopje", + "ML|Africa/Abidjan Africa/Bamako", + "MM|Asia/Yangon", + "MN|Asia/Ulaanbaatar Asia/Hovd Asia/Choibalsan", + "MO|Asia/Macau", + "MP|Pacific/Guam Pacific/Saipan", + "MQ|America/Martinique", + "MR|Africa/Abidjan Africa/Nouakchott", + "MS|America/Puerto_Rico America/Montserrat", + "MT|Europe/Malta", + "MU|Indian/Mauritius", + "MV|Indian/Maldives", + "MW|Africa/Maputo Africa/Blantyre", + "MX|America/Mexico_City America/Cancun America/Merida America/Monterrey America/Matamoros America/Chihuahua America/Ciudad_Juarez America/Ojinaga America/Mazatlan America/Bahia_Banderas America/Hermosillo America/Tijuana", + "MY|Asia/Kuching Asia/Singapore Asia/Kuala_Lumpur", + "MZ|Africa/Maputo", + "NA|Africa/Windhoek", + "NC|Pacific/Noumea", + "NE|Africa/Lagos Africa/Niamey", + "NF|Pacific/Norfolk", + "NG|Africa/Lagos", + "NI|America/Managua", + "NL|Europe/Brussels Europe/Amsterdam", + "NO|Europe/Berlin Europe/Oslo", + "NP|Asia/Kathmandu", + "NR|Pacific/Nauru", + "NU|Pacific/Niue", + "NZ|Pacific/Auckland Pacific/Chatham", + "OM|Asia/Dubai Asia/Muscat", + "PA|America/Panama", + "PE|America/Lima", + "PF|Pacific/Tahiti Pacific/Marquesas Pacific/Gambier", + "PG|Pacific/Port_Moresby Pacific/Bougainville", + "PH|Asia/Manila", + "PK|Asia/Karachi", + "PL|Europe/Warsaw", + "PM|America/Miquelon", + "PN|Pacific/Pitcairn", + "PR|America/Puerto_Rico", + "PS|Asia/Gaza Asia/Hebron", + "PT|Europe/Lisbon Atlantic/Madeira Atlantic/Azores", + "PW|Pacific/Palau", + "PY|America/Asuncion", + "QA|Asia/Qatar", + "RE|Asia/Dubai Indian/Reunion", + "RO|Europe/Bucharest", + "RS|Europe/Belgrade", + "RU|Europe/Kaliningrad Europe/Moscow Europe/Simferopol Europe/Kirov Europe/Volgograd Europe/Astrakhan Europe/Saratov Europe/Ulyanovsk Europe/Samara Asia/Yekaterinburg Asia/Omsk Asia/Novosibirsk Asia/Barnaul Asia/Tomsk Asia/Novokuznetsk Asia/Krasnoyarsk Asia/Irkutsk Asia/Chita Asia/Yakutsk Asia/Khandyga Asia/Vladivostok Asia/Ust-Nera Asia/Magadan Asia/Sakhalin Asia/Srednekolymsk Asia/Kamchatka Asia/Anadyr", + "RW|Africa/Maputo Africa/Kigali", + "SA|Asia/Riyadh", + "SB|Pacific/Guadalcanal", + "SC|Asia/Dubai Indian/Mahe", + "SD|Africa/Khartoum", + "SE|Europe/Berlin Europe/Stockholm", + "SG|Asia/Singapore", + "SH|Africa/Abidjan Atlantic/St_Helena", + "SI|Europe/Belgrade Europe/Ljubljana", + "SJ|Europe/Berlin Arctic/Longyearbyen", + "SK|Europe/Prague Europe/Bratislava", + "SL|Africa/Abidjan Africa/Freetown", + "SM|Europe/Rome Europe/San_Marino", + "SN|Africa/Abidjan Africa/Dakar", + "SO|Africa/Nairobi Africa/Mogadishu", + "SR|America/Paramaribo", + "SS|Africa/Juba", + "ST|Africa/Sao_Tome", + "SV|America/El_Salvador", + "SX|America/Puerto_Rico America/Lower_Princes", + "SY|Asia/Damascus", + "SZ|Africa/Johannesburg Africa/Mbabane", + "TC|America/Grand_Turk", + "TD|Africa/Ndjamena", + "TF|Asia/Dubai Indian/Maldives Indian/Kerguelen", + "TG|Africa/Abidjan Africa/Lome", + "TH|Asia/Bangkok", + "TJ|Asia/Dushanbe", + "TK|Pacific/Fakaofo", + "TL|Asia/Dili", + "TM|Asia/Ashgabat", + "TN|Africa/Tunis", + "TO|Pacific/Tongatapu", + "TR|Europe/Istanbul", + "TT|America/Puerto_Rico America/Port_of_Spain", + "TV|Pacific/Tarawa Pacific/Funafuti", + "TW|Asia/Taipei", + "TZ|Africa/Nairobi Africa/Dar_es_Salaam", + "UA|Europe/Simferopol Europe/Kyiv", + "UG|Africa/Nairobi Africa/Kampala", + "UM|Pacific/Pago_Pago Pacific/Tarawa Pacific/Midway Pacific/Wake", + "US|America/New_York America/Detroit America/Kentucky/Louisville America/Kentucky/Monticello America/Indiana/Indianapolis America/Indiana/Vincennes America/Indiana/Winamac America/Indiana/Marengo America/Indiana/Petersburg America/Indiana/Vevay America/Chicago America/Indiana/Tell_City America/Indiana/Knox America/Menominee America/North_Dakota/Center America/North_Dakota/New_Salem America/North_Dakota/Beulah America/Denver America/Boise America/Phoenix America/Los_Angeles America/Anchorage America/Juneau America/Sitka America/Metlakatla America/Yakutat America/Nome America/Adak Pacific/Honolulu", + "UY|America/Montevideo", + "UZ|Asia/Samarkand Asia/Tashkent", + "VA|Europe/Rome Europe/Vatican", + "VC|America/Puerto_Rico America/St_Vincent", + "VE|America/Caracas", + "VG|America/Puerto_Rico America/Tortola", + "VI|America/Puerto_Rico America/St_Thomas", + "VN|Asia/Bangkok Asia/Ho_Chi_Minh", + "VU|Pacific/Efate", + "WF|Pacific/Tarawa Pacific/Wallis", + "WS|Pacific/Apia", + "YE|Asia/Riyadh Asia/Aden", + "YT|Africa/Nairobi Indian/Mayotte", + "ZA|Africa/Johannesburg", + "ZM|Africa/Maputo Africa/Lusaka", + "ZW|Africa/Maputo Africa/Harare" + ] + }); + + + return moment; +} diff --git a/www/js/more.js b/www/js/more.js index 4fd0e3c..3f2d647 100644 --- a/www/js/more.js +++ b/www/js/more.js @@ -16,6 +16,7 @@ export default async () => { let more_info = document.getElementById("more_info"); let more_sabha = document.getElementById("more_sabha"); let more_questions = document.getElementById("more_questions"); + let more_ramadanTime = document.getElementById("more_ramadanTime"); more_questions.addEventListener("click", e => { @@ -67,6 +68,11 @@ export default async () => { window.location.href = "/pages/sabha.html" }); + + more_ramadanTime.addEventListener("click", e => { + + window.location.href = "/pages/ramadanTime.html" + }); } catch (error) { diff --git a/www/js/notification.js b/www/js/notification.js index 7cbbffd..45fda74 100644 --- a/www/js/notification.js +++ b/www/js/notification.js @@ -1,7 +1,16 @@ import adhanModule from './modules/adhanModule.js'; -import error_handling from './modules/error_handling.js'; +import errorHandling from './modules/error_handling.js'; import getGPS from './modules/getGPS.js'; +import { + scheduleLocalNotification, + updateLocalNotification, + cancelLocalNotification, + isLocalNotificationExists, + ClickEvent +} from "./modules/LocalNotification.js"; import moment from './modules/moment/moment.js'; +import moment_timezone from './modules/moment/moment-timezone.js'; +moment_timezone(moment); export default async () => { @@ -9,106 +18,99 @@ export default async () => { // LocalStorage - let storage = window.localStorage; - let Calculation = storage.getItem('Calculation'); - let Madhab = storage.getItem('Madhab'); - let Shafaq = storage.getItem('Shafaq'); - let Setfajr = storage.getItem('fajr'); - let Setdhuhr = storage.getItem('dhuhr'); - let Setasr = storage.getItem('asr'); - let Setmaghrib = storage.getItem('maghrib'); - let Setisha = storage.getItem('isha'); - let Getlatitude = storage.getItem('latitude_settings'); - let Getlongitude = storage.getItem('longitude_settings'); - let notification = storage.getItem('notification'); - - if (Getlongitude === null || Getlatitude === null) { + let localStorageData = window.localStorage; + let calculationMethod = localStorageData.getItem('Calculation_settings'); + let madhab = localStorageData.getItem('madhab_settings'); + let shafaqMethod = localStorageData.getItem('Shafaq_settings'); + let fajrAngle = localStorageData.getItem('fajr_settings'); + let sunriseAngle = localStorageData.getItem('sunrise_settings'); + let dhuhrAngle = localStorageData.getItem('dhuhr_settings'); + let asrAngle = localStorageData.getItem('asr_settings'); + let maghribAngle = localStorageData.getItem('maghrib_settings'); + let ishaAngle = localStorageData.getItem('isha_settings'); + let latitude = localStorageData.getItem('latitude_settings'); + let longitude = localStorageData.getItem('longitude_settings'); + let timezone = localStorageData.getItem('timezone_settings'); + let notificationEnabled = localStorageData.getItem('notifications_adhan'); - let GPS = await getGPS(); - Getlatitude = GPS.latitude; - Getlongitude = GPS.longitude; - storage.setItem("latitude_settings", Getlatitude); - storage.setItem("longitude_settings", Getlongitude); - - } - - while (notification ? bool(notification) : true) { - - - let timenow = moment().format('h:mm A'); - let adhan = adhanModule({ - Calculation: Calculation ? Calculation : "UmmAlQura", - latitude: Number(Getlatitude), - longitude: Number(Getlongitude), - Madhab: Madhab ? Madhab : "Shafi", - Shafaq: Shafaq ? Shafaq : "General", - fajr: Setfajr ? Number(Setfajr) : 0, - dhuhr: Setdhuhr ? Number(Setdhuhr) : 0, - asr: Setasr ? Number(Setasr) : 0, - maghrib: Setmaghrib ? Number(Setmaghrib) : 0, - isha: Setisha ? Number(Setisha) : 0, - }); - // let slah = adhan.nextPrayer === "fajr" ? "الفجر" : adhan.nextPrayer === "dhuhr" ? "الظهر" : adhan.nextPrayer === "asr" ? "العصر" : adhan.nextPrayer === "maghrib" ? "المغرب" : adhan.nextPrayer === "isha" ? "العشاء" : "لايوجد"; - let fileAdhan = adhan.nextPrayer === "fajr" ? "/mp3/002.mp3" : "/mp3/001.mp3" - - switch (timenow) { - case adhan?.fajr: - - await notification_adhan("الفجر", fileAdhan, storage); - - break; - - case adhan?.dhuhr: - await notification_adhan("الظهر", fileAdhan, storage); + if (longitude === null || latitude === null || timezone === null) { - break; - - case adhan?.asr: - - await notification_adhan("العصر", fileAdhan, storage); - - break; - - case adhan?.maghrib: - - await notification_adhan("المغرب", fileAdhan, storage); - - break; - - case adhan?.isha: - - await notification_adhan("العشاء", fileAdhan, storage); + let GPS = await getGPS(); + latitude = GPS.latitude; + longitude = GPS.longitude; + timezone = GPS.timezone; + localStorageData.setItem("latitude_settings", latitude); + localStorageData.setItem("longitude_settings", longitude); + localStorageData.setItem("timezone_settings", timezone); - break; + } - default: - break; + setInterval(async () => { + if (notificationEnabled ? bool(notificationEnabled) : true) { + const timeNow = moment().tz(timezone).format('h:mm A'); + let prayerTimes = adhanModule({ + Calculation: calculationMethod ? calculationMethod : "UmmAlQura", + latitude: Number(latitude), + longitude: Number(longitude), + timezone: timezone, + madhab: madhab ? madhab : "Shafi", + Shafaq: shafaqMethod ? shafaqMethod : "General", + fajr: fajrAngle ? Number(fajrAngle) : 0, + sunrise: sunriseAngle ? Number(sunriseAngle) : 0, + dhuhr: dhuhrAngle ? Number(dhuhrAngle) : 0, + asr: asrAngle ? Number(asrAngle) : 0, + maghrib: maghribAngle ? Number(maghribAngle) : 0, + isha: ishaAngle ? Number(ishaAngle) : 0, + }); + + const fileAdhan = prayerTimes.nextPrayer === "fajr" ? "/mp3/002.mp3" : "/mp3/001.mp3" + + switch (timeNow) { + case prayerTimes?.fajr: + await notificationAdhan("الفجر", fileAdhan, localStorageData, prayerTimes?.fajr); + await iiqamaTime("الفجر", prayerTimes?.fajr, 25); + break; + case prayerTimes?.dhuhr: + await notificationAdhan("الظهر", fileAdhan, localStorageData, prayerTimes?.dhuhr); + await iiqamaTime("الظهر", prayerTimes?.dhuhr, 20); + break; + case prayerTimes?.asr: + await notificationAdhan("العصر", fileAdhan, localStorageData, prayerTimes?.asr); + await iiqamaTime("العصر", prayerTimes?.asr, 20); + break; + case prayerTimes?.maghrib: + await notificationAdhan("المغرب", fileAdhan, localStorageData, prayerTimes?.maghrib); + await iiqamaTime("المغرب", prayerTimes?.maghrib, 10); + break; + case prayerTimes?.isha: + await notificationAdhan("العشاء", fileAdhan, localStorageData, prayerTimes?.isha); + await iiqamaTime("العشاء", prayerTimes?.isha, 20); + break; + default: + break; + } } - - // sleep - await new Promise(r => setTimeout(r, 6000)); - - }; + }, 6000); } catch (error) { - error_handling(error); + errorHandling(error); } } -function bool(v) { - return v === "false" || v === "null" || v === "NaN" || v === "undefined" || v === "0" ? false : !!v; +function bool(value) { + return value === "false" || value === "null" || value === "NaN" || value === "undefined" || value === "0" ? false : !!value; } -async function notification_adhan(name, fileAdhan, storage) { +async function notificationAdhan(name, fileAdhan, localStorageData, time) { - let AdhanPlaying = storage.getItem('AdhanPlaying'); - AdhanPlaying === null ? storage.setItem('AdhanPlaying', "false") : false; + let adhanPlaying = localStorageData.getItem('AdhanPlaying'); + adhanPlaying === null ? localStorageData.setItem('AdhanPlaying', "false") : false; - if (!bool(AdhanPlaying)) { + if (!bool(adhanPlaying)) { let audioAdhan = new Audio(fileAdhan); audioAdhan.id = 'audioAdhan'; @@ -116,29 +118,30 @@ async function notification_adhan(name, fileAdhan, storage) { audioAdhan.preload = 'none'; audioAdhan.autoplay = false; - storage.setItem('AdhanPlaying', "true"); + localStorageData.setItem('AdhanPlaying', "true"); await audioAdhan.play(); audioAdhan.addEventListener('ended', () => { audioAdhan.pause(); audioAdhan.currentTime = 0; - storage.setItem('AdhanPlaying', "false"); + cancelLocalNotification(5); + localStorageData.setItem('AdhanPlaying', "false"); }); - navigator?.notification?.confirm( - `حان الآن وقت صلاة ${name}`, - (e) => { - if (e === 1) { - audioAdhan.pause(); - audioAdhan.currentTime = 0; - } else if (e === 2) { // 2 corresponds to the index of the 'خروج' option - // Do nothing on exit for now - } - }, - 'تنبيه بوقت الصلاة', - ['إيقاف الأذان', 'خروج'] - ); + scheduleLocalNotification({ + id: 5, + title: `تنبيه بدخول وقت الصلاة 🔔`, + text: `حان الآن وقت صلاة ${name} ⏰ ${time}`, + smallIcon: 'res://drawable-xxxhdpi/ic_stat_onesignal_default.png', + badge: 1, + actions: [{ id: 'closeAudio', title: 'إيقاف' }], + }); + + ClickEvent("closeAudio", () => { + audioAdhan.pause(); + audioAdhan.currentTime = 0; + }) audioAdhan.addEventListener("pause", async () => { if (audioAdhan.currentTime !== 0) { @@ -152,11 +155,39 @@ async function notification_adhan(name, fileAdhan, storage) { setTimeout(() => { - if (bool(AdhanPlaying)) { - storage.setItem('AdhanPlaying', "false"); + if (bool(adhanPlaying)) { + localStorageData.setItem('AdhanPlaying', "false"); } }, 70000); } +} + + + +async function iiqamaTime(name, time, minute) { + try { + const minuteInMillis = 60 * 1000; + const millis = minute * minuteInMillis; + await new Promise(r => setTimeout(r, millis)); + const audio = new Audio("/mp3/iiqama.mp3"); + await audio.play(); + + scheduleLocalNotification({ + id: 123, + title: `حَانَ الانْ وَقْتُ الإِقَامَةِ 🔔`, + text: `حَانَ الانْ وَقْتُ الإِقَامَةِ لصلاة ${name} - ${time} ⏰`, + smallIcon: 'res://drawable-xxxhdpi/ic_stat_onesignal_default.png', + badge: 1, + }); + + audio.addEventListener('ended', () => { + audioAdhan.pause(); + audioAdhan.currentTime = 0; + cancelLocalNotification(123); + }); + } catch (error) { + console.error(error); + } } \ No newline at end of file diff --git a/www/js/prayer.js b/www/js/prayer.js index dc915d3..911232e 100644 --- a/www/js/prayer.js +++ b/www/js/prayer.js @@ -1,6 +1,6 @@ import adhanModule from './modules/adhanModule.js'; import getGPS from './modules/getGPS.js'; -import error_handling from './modules/error_handling.js'; +import errorHandling from './modules/error_handling.js'; /** * الدالة الرئيسية لتحديث وعرض أوقات الصلاة @@ -15,68 +15,99 @@ export default async () => { if (window.location.pathname === prayerPagePath) { try { - const statusPERM = await checkPermissionStatus(); + const loadingElement = document.getElementById('loading'); + const permissionStatus = await checkPermissionStatus(); const prayerTimeContainer = document.getElementById('prayer_time'); - const alertElm = document.getElementById('alert'); - const storage = window.localStorage; - - const { - Calculation = "UmmAlQura", - Madhab = "Shafi", - Shafaq = "General", - fajr: Setfajr = 0, - sunrise: Setsunrise = 0, - dhuhr: Setdhuhr = 0, - asr: Setasr = 0, - maghrib: Setmaghrib = 0, - isha: Setisha = 0, - latitude_settings: Getlatitude, - longitude_settings: Getlongitude - } = storage; - - if (statusPERM || (Getlongitude && Getlatitude)) { - prayerTimeContainer.style.display = "block"; - - if (!Getlongitude || !Getlatitude) { - const { latitude, longitude } = await getGPS(); - storage.setItem("latitude_settings", latitude); - storage.setItem("longitude_settings", longitude); + const alertElement = document.getElementById('alert'); + const localStorageData = window.localStorage; + + let { + latitude_settings: storedLatitude, + longitude_settings: storedLongitude, + timezone_settings: storedTimezone + } = localStorageData; + + if (permissionStatus || (storedLongitude && storedLatitude && storedTimezone)) { + loadingElement.style.display = "block"; + + if (!storedLongitude || !storedLatitude || !storedTimezone) { + const { latitude, longitude, timezone } = await getGPS(); + localStorageData.setItem("latitude_settings", latitude); + localStorageData.setItem("longitude_settings", longitude); + localStorageData.setItem("timezone_settings", timezone); } - setInterval(() => { + setInterval(async () => { + let { + Calculation_settings: calculationMethod = "UmmAlQura", + madhab_settings: madhab = "Shafi", + Shafaq_settings: shafaqMethod = "General", + fajr_settings: storedFajrAngle = 0, + sunrise_settings: storedSunriseAngle = 0, + dhuhr_settings: storedDhuhrAngle = 0, + asr_settings: storedAsrAngle = 0, + maghrib_settings: storedMaghribAngle = 0, + isha_settings: storedIshaAngle = 0, + latitude_settings: storedLatitude, + longitude_settings: storedLongitude, + timezone_settings: storedTimezone + } = localStorageData; + const prayerTimes = adhanModule({ - Calculation, - latitude: Number(Getlatitude), - longitude: Number(Getlongitude), - Madhab, - Shafaq, - fajr: Number(Setfajr), - sunrise: Number(Setsunrise), - dhuhr: Number(Setdhuhr), - asr: Number(Setasr), - maghrib: Number(Setmaghrib), - isha: Number(Setisha), + Calculation: calculationMethod, + latitude: Number(storedLatitude), + longitude: Number(storedLongitude), + timezone: storedTimezone, + madhab, + Shafaq: shafaqMethod, + fajr: Number(storedFajrAngle), + sunrise: Number(storedSunriseAngle), + dhuhr: Number(storedDhuhrAngle), + asr: Number(storedAsrAngle), + maghrib: Number(storedMaghribAngle), + isha: Number(storedIshaAngle), }); - updateUI(prayerTimes); - }, 1000); - setInterval(() => { - const { latitude_settings, longitude_settings } = storage; - if (statusPERM || (longitude_settings && latitude_settings)) { + if (permissionStatus || (storedLongitude && storedLatitude && storedTimezone)) { + if (!storedLongitude || !storedLatitude || !storedTimezone) { + const { latitude, longitude, timezone } = await getGPS(); + localStorageData.setItem("latitude_settings", latitude); + localStorageData.setItem("longitude_settings", longitude); + localStorageData.setItem("timezone_settings", timezone); + } prayerTimeContainer.style.display = "block"; - alertElm.style.display = "none"; + alertElement.style.display = "none"; + loadingElement.style.display = "none"; + + const { + timezone, + fajr, + sunrise, + dhuhr, + asr, + maghrib, + isha, + } = prayerTimes; + + if (fajr && sunrise && dhuhr && asr && maghrib && isha && timezone) { + updateUI(prayerTimes); + } + } else { + loadingElement.style.display = "none"; prayerTimeContainer.style.display = "none"; - alertElm.style.display = "block"; + alertElement.style.display = "block"; } - }, 2000); + + }, 1000); } else { // Handle the case when permission is not granted prayerTimeContainer.style.display = "none"; - alertElm.style.display = "block"; + alertElement.style.display = "block"; } + } catch (error) { - error_handling(error); + errorHandling(error); } } }; @@ -90,12 +121,19 @@ export default async () => { */ async function checkPermissionStatus() { - return new Promise((resolve) => { - const permissions = cordova.plugins.permissions; - permissions.hasPermission(permissions.ACCESS_COARSE_LOCATION, (status) => { - resolve(status.hasPermission); + + if (typeof cordova === 'undefined' || !cordova.plugins || !cordova.plugins.permissions) { + return true; + } + + else { + return new Promise((resolve) => { + const permissions = cordova.plugins.permissions; + permissions.hasPermission(permissions.ACCESS_COARSE_LOCATION, async (status) => { + resolve(status.hasPermission); + }); }); - }); + } } /** @@ -139,22 +177,26 @@ function updateUI(prayerTimes) { document.getElementById('time_isha').innerText = isha; const prayerList = ['fajr', 'sunrise', 'dhuhr', 'asr', 'maghrib', 'isha']; - for (const prayer of prayerList) { - const li = document.getElementById(`${prayer}_li`); - const span = document.getElementById(prayer); - if (prayer === nextPrayer) { - document.getElementById('remaining_name').innerText = getPrayerName(prayer); - document.getElementById('remaining').style.display = 'block'; - document.getElementById('remaining_time').style.display = 'block'; - li.style.background = 'var(--background_div_hover)'; - li.style.boxShadow = 'rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset'; - span.style.color = 'var(--white-div)'; - } else { - li.style.background = null; - li.style.boxShadow = null; - span.style.color = null; - } + const foundPrayer = prayerList.find(item => item === nextPrayer); + + const liElements = document.getElementById(`${foundPrayer}_li`); + const span = document.getElementById(foundPrayer); + if (foundPrayer) { + document.getElementById('remaining_name').innerText = getPrayerName(foundPrayer); + document.getElementById('remaining').style.display = 'block'; + document.getElementById('remaining_time').style.display = 'block'; + liElements.style.background = 'var(--background_div_hover)'; + liElements.style.boxShadow = 'rgba(0, 0, 0, 0.4) 0px 2px 4px, rgba(0, 0, 0, 0.3) 0px 7px 13px -3px, rgba(0, 0, 0, 0.2) 0px -3px 0px inset'; + span.style.color = 'var(--white-div)'; + } else { + document.getElementById('remaining_name').innerText = getPrayerName(foundPrayer); + document.getElementById('remaining').style.display = 'none'; + document.getElementById('remaining_time').style.display = 'none'; + liElements.style.background = null; + liElements.style.boxShadow = null; + span.style.color = null; } + } /** diff --git a/www/js/quran.js b/www/js/quran.js index ce151d9..1965459 100644 --- a/www/js/quran.js +++ b/www/js/quran.js @@ -153,34 +153,62 @@ function handleReaderClick(item, mp3quran) { let audio = new Audio(iterator?.link); let audioId = `quran_reader_audio_id_${iterator?.id}`; audio.preload = 'none'; - audio.autoplay = false; + audio.autoplay = false; audio.id = audioId; + const isMediaPlayStorage = (value) => { localStorage.setItem('isMediaPlay', `${value}`) }; + const isPausedStorage = (value) => { localStorage.setItem('isPaused', `${value}`) }; + const AudioNameStoarge = (value) => { localStorage.setItem('AudioName', `${value}`) }; + const linkAudioStoarge = (value) => { localStorage.setItem('linkAudio', `${value}`) }; // إضافة حدث انتهاء التشغيل audio.addEventListener("ended", () => { console.log("تم الإنتهاء من الصوت وإيقافه"); isAudioPlaying = false; playButton.src = "/img/play.png"; + isMediaPlayStorage(false); + isPausedStorage(true); }); // حدث زر التشغيل والإيقاف الخاص بالصوت - playButton.addEventListener("click", () => { + playButton.addEventListener("click", async () => { + + linkAudioStoarge(iterator?.link); if (!isAudioPlaying) { + playButton.src = "/img/loading.svg"; + isMediaPlayStorage(false); + isPausedStorage(true); + await new Promise(r => setTimeout(r, 2000)); playAudio(audio, playButton); + AudioNameStoarge(`${item?.name} - ${iterator?.name}`); currentAudio = audio; currentPlayButton = playButton; + isMediaPlayStorage(true); + isPausedStorage(false); } else { if (currentAudio && currentAudio !== audio) { + playButton.src = "/img/loading.svg"; + isMediaPlayStorage(false); + isPausedStorage(true); + await new Promise(r => setTimeout(r, 2000)); stopAudio(currentAudio, currentPlayButton); playAudio(audio, playButton); + AudioNameStoarge(`${item?.name} - ${iterator?.name}`); currentAudio = audio; currentPlayButton = playButton; + isMediaPlayStorage(true); + isPausedStorage(false); } else { stopAudio(audio, playButton); + isMediaPlayStorage(false); + isPausedStorage(true); } } + + setInterval(() => { + localStorage.setItem('audioCurrentTime', audio.currentTime); + }, 1000); }); // إضافة حدث النقر لتحميل الملف الصوتي @@ -201,8 +229,6 @@ function handleReaderClick(item, mp3quran) { * @param {HTMLElement} playButton - زر التشغيل الذي يتم النقر عليه. */ function playAudio(audio, playButton) { - - playButton.src = "/img/loading.svg"; audio.play().then(() => { console.log("تم تشغيل الصوت"); isAudioPlaying = true; diff --git a/www/js/radio.js b/www/js/radio.js index 320f472..d7ad724 100644 --- a/www/js/radio.js +++ b/www/js/radio.js @@ -61,33 +61,42 @@ export default async () => { radio_quran_play.id = `radio_quran_play_id_${item?.id}`; radio_quran_play.src = "/img/play.png"; + const isMediaPlayStorage = (value) => { localStorage.setItem('isMediaPlay', `${value}`) }; + const isPausedStorage = (value) => { localStorage.setItem('isPaused', `${value}`) }; + const AudioNameStoarge = (value) => { localStorage.setItem('AudioName', `${value}`) }; + const linkAudioStoarge = (value) => { localStorage.setItem('linkAudio', `${value}`) }; + + radio_quran_play.addEventListener("click", async () => { try { + + linkAudioStoarge(item?.link); + if (currentAudio && currentAudio !== audio) { currentAudio.pause(); setPlayIcon(currentIcon); + isMediaPlayStorage(false); + isPausedStorage(true); } if (audio.paused) { if (audio.buffered.length === 0) { radio_quran_play.src = "/img/loading.svg"; } + radio_quran_play.src = "/img/loading.svg"; + isMediaPlayStorage(false); + isPausedStorage(true); + await new Promise(r => setTimeout(r, 2000)); await audio.play(); + AudioNameStoarge(`${item?.name}`); + isMediaPlayStorage(true); + isPausedStorage(false); setStopIcon(radio_quran_play); - - // Start background audio - if (typeof cordova !== 'undefined' && cordova.plugins && cordova.plugins.backgroundaudio) { - cordova.plugins.backgroundaudio.unmute(); // Unmute to allow background playback - cordova.plugins.backgroundaudio.play(); - } } else { audio.pause(); setPlayIcon(radio_quran_play); - - // Stop background audio - if (typeof cordova !== 'undefined' && cordova.plugins && cordova.plugins.backgroundaudio) { - cordova.plugins.backgroundaudio.stop(); - } + isMediaPlayStorage(false); + isPausedStorage(true); } currentAudio = audio; diff --git a/www/js/ramadanTime.js b/www/js/ramadanTime.js new file mode 100644 index 0000000..42befae --- /dev/null +++ b/www/js/ramadanTime.js @@ -0,0 +1,73 @@ +import error_handling from "./modules/error_handling.js"; +import remainingTimeUntilRamadan from './modules/getRamadanDate.js'; + +export default async () => { + + try { + if (window.location.pathname === "/pages/ramadanTime.html") { + + const loadingElement = document.getElementById('loading'); + loadingElement.style.display = "block"; + const back = document.getElementById('back'); + back.addEventListener("click", e => { + window.location.href = "/more.html"; + }); + + + const title_ramadan = document.getElementById('title_ramadan'); + const isRamadan = document.getElementById('isRamadan'); + const NotRamadan = document.getElementById('NotRamadan'); + const seconds_r = document.getElementById('seconds_r'); + const minutes_r = document.getElementById('minutes_r'); + const hours_r = document.getElementById('hours_r'); + const days_r = document.getElementById('days_r'); + + const timeUntilRamadan = remainingTimeUntilRamadan(); + + if (timeUntilRamadan.isRamadan) { + NotRamadan.style.display = "none"; + title_ramadan.style.display = "none"; + isRamadan.style.display = "block"; + } + + else { + isRamadan.style.display = "none"; + title_ramadan.style.display = "block"; + NotRamadan.style.display = "flex"; + seconds_r.innerText = timeUntilRamadan.seconds; + minutes_r.innerText = timeUntilRamadan.minutes; + hours_r.innerText = timeUntilRamadan.hours; + days_r.innerText = timeUntilRamadan.days; + } + + + + setInterval(() => { + const timeUntilRamadan = remainingTimeUntilRamadan(); + + if (timeUntilRamadan.isRamadan) { + NotRamadan.style.display = "none"; + title_ramadan.style.display = "none"; + isRamadan.style.display = "block"; + } + + else { + isRamadan.style.display = "none"; + title_ramadan.style.display = "block"; + NotRamadan.style.display = "flex"; + seconds_r.innerText = timeUntilRamadan.seconds; + minutes_r.innerText = timeUntilRamadan.minutes; + hours_r.innerText = timeUntilRamadan.hours; + days_r.innerText = timeUntilRamadan.days; + } + // console.log(`الوقت المتبقي حتى دخول شهر رمضان: ${timeUntilRamadan.days} يوم ${timeUntilRamadan.hours} ساعة ${timeUntilRamadan.minutes} دقيقة ${timeUntilRamadan.seconds} ثانية.`); + }, 1000); + + + await new Promise(r => setTimeout(r, 2000)); + loadingElement.style.display = "none"; + } + } catch (error) { + error_handling(error); + } +} \ No newline at end of file diff --git a/www/js/settings.js b/www/js/settings.js index 4a223be..b992a85 100644 --- a/www/js/settings.js +++ b/www/js/settings.js @@ -18,6 +18,7 @@ const DEFAULT_VALUES = { notifications_adhan: true, longitude_settings: null, latitude_settings: null, + timezone_settings: null, fajr_settings: 0, sunrise_settings: 0, dhuhr_settings: 0, @@ -61,6 +62,12 @@ const setDefaultValues = () => { if (longitudeValue && latitudeValue) { element.value = key === 'longitude_settings' ? longitudeValue : latitudeValue; } + } else if (key === 'timezone_settings') { + const timezoneValue = storage.getItem('timezone_settings'); + + if (timezoneValue) { + element.value = timezoneValue; + } } else { element.value = storedValue !== null && storedValue !== undefined ? Number(storedValue) : value; } @@ -77,11 +84,12 @@ const handleRefreshLocation = async () => { const statusPERM = await permissionStatus(); if (statusPERM) { - const { latitude, longitude } = await getGPS(); + const { latitude, longitude, timezone } = await getGPS(); const storage = window.localStorage; storage.setItem('latitude_settings', latitude); storage.setItem('longitude_settings', longitude); + storage.setItem('timezone_settings', timezone); const alertEl = getElementById('alert'); if (alertEl) { @@ -107,6 +115,7 @@ const handleRefreshLocation = async () => { }, 3000); } } + } catch (error) { error_handling(error); } @@ -191,8 +200,10 @@ export default async () => { if (saveSettings) { saveSettings.addEventListener('click', handleSaveSettings); } + } catch (error) { error_handling(error); } } + }; \ No newline at end of file diff --git a/www/more.html b/www/more.html index 49c8982..6fee74c 100644 --- a/www/more.html +++ b/www/more.html @@ -6,7 +6,7 @@ - + @@ -84,6 +84,16 @@

+
  • + + + +

    + المتبقي على رمضان +

    + +
  • +
  • diff --git a/www/mp3/iiqama.mp3 b/www/mp3/iiqama.mp3 new file mode 100644 index 0000000..1936cdb Binary files /dev/null and b/www/mp3/iiqama.mp3 differ diff --git a/www/pages/adhkar/evening.html b/www/pages/adhkar/evening.html index 78d395a..b9dc8b0 100644 --- a/www/pages/adhkar/evening.html +++ b/www/pages/adhkar/evening.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/adhkar/food.html b/www/pages/adhkar/food.html index afd68b8..c6a3475 100644 --- a/www/pages/adhkar/food.html +++ b/www/pages/adhkar/food.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/adhkar/morning.html b/www/pages/adhkar/morning.html index a60c474..6bbff52 100644 --- a/www/pages/adhkar/morning.html +++ b/www/pages/adhkar/morning.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/adhkar/prayer.html b/www/pages/adhkar/prayer.html index 24043ca..8e42bbc 100644 --- a/www/pages/adhkar/prayer.html +++ b/www/pages/adhkar/prayer.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/adhkar/sleeping.html b/www/pages/adhkar/sleeping.html index 205b520..91f61a0 100644 --- a/www/pages/adhkar/sleeping.html +++ b/www/pages/adhkar/sleeping.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/adhkar/tasbih.html b/www/pages/adhkar/tasbih.html index 8196a56..ecc49d7 100644 --- a/www/pages/adhkar/tasbih.html +++ b/www/pages/adhkar/tasbih.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/albitaqat.html b/www/pages/albitaqat.html index 7339304..2123f55 100644 --- a/www/pages/albitaqat.html +++ b/www/pages/albitaqat.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/allah.html b/www/pages/allah.html index 5e830d9..0d9e187 100644 --- a/www/pages/allah.html +++ b/www/pages/allah.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/hisnmuslim.html b/www/pages/hisnmuslim.html index 3b0a034..eed9b6c 100644 --- a/www/pages/hisnmuslim.html +++ b/www/pages/hisnmuslim.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/images.html b/www/pages/images.html index 2891074..1c213aa 100644 --- a/www/pages/images.html +++ b/www/pages/images.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/info.html b/www/pages/info.html index 743c3d8..c20c9bb 100644 --- a/www/pages/info.html +++ b/www/pages/info.html @@ -6,7 +6,7 @@ - + @@ -57,7 +57,7 @@ version

    الإصدار: - v1.2.4 + v1.2.5

  • diff --git a/www/pages/questions.html b/www/pages/questions.html index 1f25a96..888f93e 100644 --- a/www/pages/questions.html +++ b/www/pages/questions.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/questions_page_2.html b/www/pages/questions_page_2.html index 0505ee1..d6fc926 100644 --- a/www/pages/questions_page_2.html +++ b/www/pages/questions_page_2.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/radio.html b/www/pages/radio.html index 59edbb6..ceb2558 100644 --- a/www/pages/radio.html +++ b/www/pages/radio.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/ramadanTime.html b/www/pages/ramadanTime.html new file mode 100644 index 0000000..8758f2f --- /dev/null +++ b/www/pages/ramadanTime.html @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + التقوى + + + + +
    + + + +

    + المتبقي على رمضان +

    + +
    + +
    + + loading + + + +

    + الوقت المتبقي لشهر رمضان الكريم +

    + +
    + 🌙 نحن الآن في شهر رمضان 🌙 +
    + +
      + +
    • +

      + +

      + +

      + ثانية +

      +
    • + +
    • +

      + +

      + +

      + دقيقة +

      +
    • + +
    • +

      + +

      + +

      + ساعة +

      +
    • + +
    • +

      + +

      + +

      + يوم +

      +
    • + +
    + + + +
    + + +
    + +
    + + + + + + \ No newline at end of file diff --git a/www/pages/sabha.html b/www/pages/sabha.html index fbfb1fd..92758b8 100644 --- a/www/pages/sabha.html +++ b/www/pages/sabha.html @@ -6,7 +6,7 @@ - + diff --git a/www/pages/settings.html b/www/pages/settings.html index 4eb1a61..7bb94cd 100644 --- a/www/pages/settings.html +++ b/www/pages/settings.html @@ -6,7 +6,7 @@ - + @@ -136,6 +136,18 @@

    +
  • +
    +

    + المنطقة الزمنية +

    +
    + +
    + +
    +
  • +
  • diff --git a/www/pages/tfs.html b/www/pages/tfs.html index 70e940b..32ad75e 100644 --- a/www/pages/tfs.html +++ b/www/pages/tfs.html @@ -6,7 +6,7 @@ - + diff --git a/www/prayer.html b/www/prayer.html index 588d584..bc83468 100644 --- a/www/prayer.html +++ b/www/prayer.html @@ -6,7 +6,7 @@ - + @@ -27,6 +27,8 @@

    + loading +
    diff --git a/www/quran.html b/www/quran.html index 21638db..8e5b420 100644 --- a/www/quran.html +++ b/www/quran.html @@ -5,7 +5,7 @@ - +