katzer / cordova-plugin-local-notifications

Cordova Local-Notification Plugin
Apache License 2.0
2.56k stars 1.75k forks source link

Notifications only works if being triggered at the current time, not a time in future! #1862

Open hnguyen48206 opened 4 years ago

hnguyen48206 commented 4 years ago

WARNING: IF YOU IGNORE THIS TEMPLATE, WE'LL IGNORE YOUR ISSUE. YOU MUST FILL THIS IN!

Last year, I have used this plugin for an Ionic v3 application (which I no longer have the access to the source code so no idea which version it was) and it worked just great. However, now when I tried to used it in another new one, it doesn't operate as it should. In short, I cannot make local notification comes at a specific time in the future, only at the current moment, there is no error log shown when calling the plugin.

Any help would be much appreciated.

Your Environment

Plugin: cordova-plugin-local-notification 0.9.0-beta.2 "LocalNotification"

Ionic: Ionic CLI : 5.2.5 (C:\Users\hoangN\AppData\Roaming\npm\node_modules\ionic) Ionic Framework : ionic-angular 3.9.2 @ionic/app-scripts : 3.1.8

Cordova: Cordova CLI : 8.1.2 (cordova-lib@8.1.1) Cordova Platforms : android 8.1.0 Cordova Plugins : cordova-plugin-ionic-keyboard 2.0.5, cordova-plugin-ionic-webview 1.1.1, (and 13 other plugins)

Utility: cordova-res : 0.8.0 (update available: 0.8.1) native-run : 0.2.9

System: NodeJS : v10.16.3 (C:\Program Files\nodejs\node.exe) npm : 6.9.0 OS : Windows 10

Expected Behavior

Notifications should come at the time specified in the trigger param.

Actual Behavior

Notifications only come at current moment, no future triggering.

Steps to Reproduce

This is the simplified piece of code that I'm using in order to demonstarte this issue, I've created 2 button to fire notifications, one is for current moment, one is for 2 minutes later from that.

 setLocalNotiNow() {
    let promise = new Promise(() => {
      let currdate = moment().format('YYYY-MM-DD HH:mm:ss');  
      let currentMoment= moment(currdate,'YYYY-MM-DD HH:mm:ss' )
      console.log(currentMoment.toDate())
      this.localNotifications.schedule({
        text: 'Xin vui lòng đến đúng lịch để chúng tôi có thể phục vụ bạn tốt hơn.',
        trigger: { at: currentMoment.toDate() },
        led: 'FF0000',
        sound: null,
        priority:1,
        foreground:true
      });

    });
    return promise;
  }
  setLocalNotiLater() {
    let promise = new Promise(() => {
      let currdate = moment().format('YYYY-MM-DD HH:mm:ss');  
      let currentMoment= moment(currdate,'YYYY-MM-DD HH:mm:ss' )
      let time = moment.duration("00:02:00");
      currentMoment.add(time)
      console.log(currentMoment.toDate())
      this.localNotifications.schedule({
        text: 'Xin vui lòng đến đúng lịch để chúng tôi có thể phục vụ bạn tốt hơn.',
        trigger: { at: currentMoment.toDate() },
        led: 'FF0000',
        sound: null,
        priority:1,
        foreground:true
      });

    });
    return promise;
  }

Context

What were you trying to do?

Debug logs

Include iOS / Android logs

m0dch3n commented 4 years ago

@katzer I'm facing the same problem...

m0dch3n commented 4 years ago

btw I tested now on IOS, where it's working, so seems to be related to Android...

I tested on Android 9

hnguyen48206 commented 4 years ago

It's definitely an android problem. However, when I try to log the registered notifications to the console, I can clearly see them which means the scheduling was successful (the trigger time is correct too) except it doesn't come when the time is right. Below is my current configuration:

(*) I also have upgraded the plugin to the lastest 0.9.0 beta3

   this.localNotifications.schedule({
          text: 'Bạn có cuộc hẹn vào ngày mai (id: ' + bookingID + ') lúc: ' + timeFrom + '. Xin vui lòng đến đúng lịch để chúng tôi có thể phục vụ bạn tốt hơn.',
          trigger: { at: triggerTimeA },
          foreground: true,
          id: Math.floor(Date.now() / 1000 + 2020),
          channel:'PushPluginChannel'
        });
Tawpie commented 4 years ago

I don't think I can help much since I don't have any issues at all on Android or iOS (Android7-10 and iOS10-13) but I'd check a couple of things:

First, are you certain absolutely that triggerTimeA is a javascript date object, and that it is in the same timezone as the device's system time? I have found unpleasant surprises due to JS's untyped nature so I use trigger: { at: new Date( triggerTimeA ) } to ensure double redundant fingers crossed safety overkill. The time penalty is minor.

Second, be aware that "older" Android would fire the notification sometime close to the desired time but not necessarily exactly when you told it to fire. The variance could be up to several minutes and there didn't seem to be any reason for it other than perhaps how Android was waking up to service the notification queue. I don't know if newer versions of Android changed anything, but I would give it several minutes to fire—it might be late.

iOS has always fired on time but does not fire local notifications "in the past" so the device's system clock must pass through the notification time in order for the notification to fire. Because of that I don't schedule any notifications that would fire within a minute of when I started the scheduling operation. This gives the OS a chance to get the notifications processed, something that is fast on iOS and Android but is dreadfully slow on Windows Phone. Remember, the plugin is handing off the actual scheduling to the phone, so you have to allow the phone some time to do its magic too (although you mentioned that you got confirmation via the .on event, so your notifications should be in the phone's system)

m0dch3n commented 4 years ago

@Tawpie which plugin version are you using?

0.9.0-beta3?

Can you also try now + 1 second? I tried also with now + 400 ms an now + 600 ms, which still works... But 1, 5 or 15 seconds does not work... I'll try with now with 5 minutes...

Btw I tested this plugin a 1 month ago, where it still worked on Android with 5 seconds on my playground...

Now I wanted to implement it on my production app, and it now longer works... Unfortunately I deleted the playground, so I can't try or look, what the difference is... The device and emulator are the same... And this last month, there was no new npm package released... This makes it so strange...

Tawpie commented 4 years ago

I actually use a private fork of beta3 that includes fixes for 1664 and 1541. Scheduling new Date(Date.now().getTime + 1000) does work for me on Oreo (Pixel 1)—I think the notification is late by a few seconds but it does fire. I don't use foreground (don't like the UX interrupted) so the notification dings the phone and appears in the notification tray. You might try without foreground?

Besides me not using Ionic (I use just Cordova), the only difference in the way I schedule is I put the notifications into an array even if there is just one notification to schedule. I found early on that scheduling multiple notifications one at a time could step on each other unless you allowed a long time between scheduling activities (or wait for .on confirmation) to allow the phone to do its thing before you start scheduling another. The extra logic wasn't worth it, arrays work every time.

I doubt you'll have an ah-ha moment but this code does work on 9 (my 10 device is with my testers so I'm stuck with 9!!)

/**
 * an_scheduleSingleLocalNotification
 *
 * queue up a system local notification to fire at a specific time. when dAtDate is blank
 * or bogus we'll go for 10 seconds from now.
 *
 * @param sActions {string} // the 'type' of action, input or button or...
 * @param sTitle {string} // title string for the notification
 * @param sText {string} // body text for the notification
 * @param dAtDate {object} // date object containing the fire time
 * @param bWithoutCancelling {boolean} // when true will NOT cancel existing notifications first
 */
export function an_scheduleSingleLocalNotification(
  sActions,
  sTitle,
  sText,
  dAtDate,
  bWithoutCancelling
) {
  if (an_isLocalNotificationPluginValid()) {
    if (typeof bWithoutCancelling === "undefined") {
      bWithoutCancelling = true;
    }

    // compute a fire date and time
    let dFireAt = new Date(dAtDate);
    if (
      typeof dFireAt === "undefined" ||
      dFireAt.toString() === "Invalid Date"
    ) {
      dFireAt = dateAdd(new Date(), 10, "secs");
    }

    // build the reminder, start with a basic no-action reminder
    let oKatzerLNPluginReminder = {
      id: new Date().getTime(),
      trigger: { at: new Date(dFireAt) }, // MUST be a date object
      text:
        sText ||
        "This is a test message scheduled to fire at " + dFireAt.toString(),
      title: sTitle || "TESTING!",
      data:
        '{"rcls":"' +
        applicationStateStore.getState().activeModule +
        '","nid":"Test"}'
    };

    // add actions as indicated
    switch (sActions) {
      case SPGaD.ksLocalNotificationCategoryID_buttons_yes_no:
        oKatzerLNPluginReminder.actions = SPGaD.ksLocalNotificationCategoryID_buttons_yes_no;

        // install the listeners
        cordova.plugins.notification.local.un(
          SPGaD.ksLocalNotificationActionID_buttons_yes_no_ButtonID_yes,
          handleLNResponse_yes
        );
        cordova.plugins.notification.local.on(
          SPGaD.ksLocalNotificationActionID_buttons_yes_no_ButtonID_yes,
          handleLNResponse_yes
        );

        cordova.plugins.notification.local.un(
          SPGaD.ksLocalNotificationActionID_buttons_yes_no_ButtonID_no,
          handleLNResponse_no
        );
        cordova.plugins.notification.local.on(
          SPGaD.ksLocalNotificationActionID_buttons_yes_no_ButtonID_no,
          handleLNResponse_no
        );
        break;
      case SPGaD.ksLocalNotificationActionID_input:
        oKatzerLNPluginReminder.actions = SPGaD.ksLocalNotificationCategoryID_input;

        cordova.plugins.notification.local.un(
          SPGaD.ksLocalNotificationActionID_input,
          handleLNResponse_input
        );
        cordova.plugins.notification.local.on(
          SPGaD.ksLocalNotificationActionID_input,
          handleLNResponse_input
        );
        break;
      default:
        break;
    }

    // android can show the smallIcon in the actionbar
    if (gsRunningOnPlatform .toLowerCase() === kPlatformandroid ) {
      oKatzerLNPluginReminder.smallIcon = "res://" + SPGaD.gsANAndroidPushIcon;
    }

    let aMultiRemindersForKatzerLNPlugin = [oKatzerLNPluginReminder];

    if (aMultiRemindersForKatzerLNPlugin.length) {
      pRunPromisesSerially(getTestKatzerActionGroupArray()).then(
        () => {
          if (!bWithoutCancelling) {
            cordova.plugins.notification.local.cancelAll(
              function scheduleAllRemindersViaKatzer() {
                console.log(
                  "ANSLMH.an_sSLNFR - cancellation completed. Elapsed: " +
                  (new Date().getTime() - new Date().getTime()) / 1000 +
                  " secs"
                );
                console.log(
                  " ANSLMH.an_sSLNFR - scheduling >" +
                  aMultiRemindersForKatzerLNPlugin.length +
                  "< local reminders"
                );
                setLSInt(SPGaD.ksLocalMessagesActuallyScheduled, 0); // reset the count
                SPGaD.goScheduledLocalMessages = {}; // purge our copy

                cordova.plugins.notification.local.schedule(
                  aMultiRemindersForKatzerLNPlugin
                );
              }
            );
          } else {
            console.log(
              " ANSLMH.an_sSLNFR - scheduling >" +
              aMultiRemindersForKatzerLNPlugin.length +
              "< local reminders without cancelling"
            );

            cordova.plugins.notification.local.schedule(
              aMultiRemindersForKatzerLNPlugin
            );
          }
        },
        () => {
          console.error(
            "ANSLMH.an_sSLNFR - Can't schedule local notifications, the actions group promise was rejected"
          );
        }
      );
    } else {
      console.error(
        "ANSLMH.an_sSLNFR - Can't schedule local notifications, the local notification plugin is invalid"
      );
    }
  }
}
hnguyen48206 commented 4 years ago

I don't think I can help much since I don't have any issues at all on Android or iOS (Android7-10 and iOS10-13) but I'd check a couple of things:

First, are you certain absolutely that triggerTimeA is a javascript date object, and that it is in the same timezone as the device's system time? I have found unpleasant surprises due to JS's untyped nature so I use trigger: { at: new Date( triggerTimeA ) } to ensure double redundant fingers crossed safety overkill. The time penalty is minor.

Second, be aware that "older" Android would fire the notification sometime close to the desired time but not necessarily exactly when you told it to fire. The variance could be up to several minutes and there didn't seem to be any reason for it other than perhaps how Android was waking up to service the notification queue. I don't know if newer versions of Android changed anything, but I would give it several minutes to fire—it might be late.

iOS has always fired on time but does not fire local notifications "in the past" so the device's system clock must pass through the notification time in order for the notification to fire. Because of that I don't schedule any notifications that would fire within a minute of when I started the scheduling operation. This gives the OS a chance to get the notifications processed, something that is fast on iOS and Android but is dreadfully slow on Windows Phone. Remember, the plugin is handing off the actual scheduling to the phone, so you have to allow the phone some time to do its magic too (although you mentioned that you got confirmation via the .on event, so your notifications should be in the phone's system)

Thanks so much for your your suggestion. Yeah, I am quite certain that the object that has been passed to the trigger object was indeed a Date object and when I log it out to the console, the timezone seems right to where I'm living so I don't know if there is any further conversion or checkup that 's needed. :((

Wed Jan 01 2020 18:30:00 GMT+0700 (Indochina Time) And as being mentioned above, when I logged out the result of the scheduling using this method: localNotifications.getAll() , I do see the notification with the correct trigger time

trigger:
at: 1577878200000
type: "calendar"

Now, I'm trying to remove foreground properties as well as putting notification in an array. Fingers crossed.

mushu8 commented 4 years ago

@hnguyen48206 i'm trying to figure this out too. I've found that the smallIcon property could be needed for the notification to be displayed. I'll try adding an icon this evening and give feed back when done

mushu8 commented 4 years ago

found this : https://developer.android.com/training/notify-user/build-notification#builder :

A small icon, set by setSmallIcon(). This is the only user-visible content that's required.

mushu8 commented 4 years ago

i cannot find anywhere what res:// means. Does anyone has information about this protocol ? smallIcon can only reference an image from ionic the resources/ folder (ionic 4) ? is this supposed to start like 'android/icon/something.jpg' ?

mushu8 commented 4 years ago

by the way, by opening the cordova generated project in Android Studio and adding some breakpoints in TriggerReceiver.onReceived(notif, bundle) andNotification.show(), i could see that the app on the phone in background actually wake up and breakpoints are hit. following in the ts code for firing a notif :


this.notificationPlugin.schedule([{
      id: 1,
      title: 'A title',
      text: 'Single ILocalNotification',
      data: { smth: 'great' },
      smallIcon: 'res://android/icon/',
      trigger: { in: 5, unit: ELocalNotificationTriggerUnit.SECOND }
    }]);
mushu8 commented 4 years ago

i'm receiving the notification when adding smallIcon: 'res://favicon'

hnguyen48206 commented 4 years ago

@Tawpie hahaha, I did have an ah-ha moment following your instructions, confirm that having no foreground attribute and putting notification in array does the magic. Thanks sir.

mushu8 commented 4 years ago

looks like there is a default value res:// for this parameter but i didn't test it cf: www/local-notification-util.js :

// Default values
exports._defaults = {
    actionGroupId : null,
    actions       : [],
    attachments   : [],
    autoClear     : true,
    badge         : null,
    channel       : null,
    color         : null,
    data          : null,
    defaults      : 0,
    foreground    : false,
    group         : null,
    groupSummary  : false,
    icon          : null,
    id            : 0,
    launch        : true,
    led           : true,
    lockscreen    : true,
    mediaSession  : null,
    number        : 0,
    priority      : 0,
    progressBar   : false,
    showWhen      : true,
    silent        : false,
    smallIcon     : 'res://icon',
    sound         : true,
    sticky        : false,
    summary       : null,
    text          : '',
    title         : '',
    trigger       : { type : 'calendar' },
    vibrate       : false,
    wakeup        : true
};
m0dch3n commented 4 years ago

I finally found some time to investigate into this, and it seems that my fix is solving this issue. Here is an explanation why it's not working.

https://github.com/katzer/cordova-plugin-local-notifications/blob/caff55ec758fdf298029ae98aff7f6a8a097feac/src/android/notification/Notification.java#L217-L243

The first line decides if the Notification Intent is triggered immediately or if its a PendingIntent added to the AlarmManager.

The AlarmManager then did not wake up, and fired the notification, because the following Receivers were not added to the AndroidManifest.xml

https://github.com/katzer/cordova-plugin-local-notifications/blob/caff55ec758fdf298029ae98aff7f6a8a097feac/plugin.xml#L103-L135

I fixed this in my fork:

https://github.com/katzer/cordova-plugin-local-notifications/compare/master...m0dch3n:master

So the solution which worked for me is:

cordova plugin remove cordova-plugin-local-notification
cordova plugin add https://github.com/m0dch3n/cordova-plugin-local-notifications.git

It seems this plugin is also no longer maintained, and JFYI, I will neither maintain my fork, unless I find specific problems to my apps.

BTW: I think it has nothing todo with the smallIcon, or array solution, but you can correct me.. I suppose it only worked during your debug session, because you stopped long enough in your Breakpoint, so that

!date.after(new Date())

became true...

m0dch3n commented 4 years ago

BTW I changed some other things, mainly that ids don't need to be integers... As I don't know all the side effects of this, I don't suggest to use my repository, but to only merge the mentioned commit into your repository

TheNotorius0 commented 4 years ago

I have had the same issue. Actually it worked for me few weeks ago and then it stopped working randomly. The notifications worked only when triggered instantly.

But I've found a solution! At least for me. The code below was missing on my AndroidManifest.xml. I have no idea why, maybe another plugin messed it up (probably firebasex with his push notifications?)

Anyway just add it to your manifest and it will start working again.

briansg commented 4 years ago

I have had the same issue. Actually it worked for me few weeks ago and then it stopped working randomly. The notifications worked only when triggered instantly.

But I've found a solution! At least for me. The code below was missing on my AndroidManifest.xml. I have no idea why, maybe another plugin messed it up (probably firebasex with his push notifications?)

Anyway just add it to your manifest and it will start working again.

<activity android:exported="false" android:launchMode="singleInstance" android:name="de.appplant.cordova.plugin.localnotification.ClickReceiver" android:theme="@android:style/Theme.Translucent"/>
<provider android:authorities="${applicationId}.provider" android:exported="false" android:grantUriPermissions="true" android:name="de.appplant.cordova.plugin.notification.util.AssetProvider">
  <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/localnotification_provider_paths"/>
</provider>
<receiver android:exported="false" android:name="de.appplant.cordova.plugin.localnotification.TriggerReceiver"/>
<receiver android:exported="false" android:name="de.appplant.cordova.plugin.localnotification.ClearReceiver"/>

☝️☝️☝️

@TheNotorius0 is right, make sure your AndroidManifest.xml has the needed activity, provider and receivers. That fixed it for me.

m0dch3n commented 4 years ago

@TheNotorius0

that's excatly what my fork has fixed!

Don't do this manually, because your AndroidManifest will be rewritten, each time, you add or remove plugins... As this plugin is no longer maintained, I suggest creating your own fork, an pull some commits from other forks, to fix some common issues.

To fix this future triggered issue, you only need to PR or commit this into your fork

https://github.com/m0dch3n/cordova-plugin-local-notifications/commit/d80b5732571cb6271dca1c2fafb877b759a7b34d