phonegap / phonegap-plugin-push

Register and receive push notifications
MIT License
1.94k stars 1.91k forks source link

Detect If App Was Launched Via Push #501

Open 84pennies opened 8 years ago

84pennies commented 8 years ago

Is there a parameter or flag somewhere that can let me know if the application was coldbooted via a push notification?

Currently, the on Notification handler fires concurrent to some other init code I have running at app start and it would be helpful to know if the app launched via a push notification before starting into my other code.

Thanks,

Matt

macdonst commented 8 years ago

@84pennies look at data.additionalData.coldstart. It should be false if your app is already started and true if the app is not already started.

I have to document this in #483

fredgalvao commented 8 years ago

He's referring to something outside the scope of the events triggered. Some global js var, or maybe a method on the api like this (which I agree would be usefull):

w.on('deviceReady', () => {
  PushNotification.wasColdstartedFromPlugin((answer) => {
    console.log("Was phonegap-plugin-push responisible for the last app coldstart? ", answer);
  });
})

I know the runtime is almost identical, but the limitation with how events work is tha we can't guarantee that it'll trigger sometime, as it will only trigger when that answer is true. This suggestion would be a nice way to enable handling push notifications before the app is completely loaded to the user.

84pennies commented 8 years ago

@fredgalvao Understood correctly what I was asking about. The behavior I am trying to solidify is when cold booting the application from a push notification, the most efficient possible path would be to know ahead of time during the boot that the app was launched via push, make the call to 'register', and then wait for the .on('notification') handler to handle the background push and then do something accordingly. However, since the app boot/init is not aware of how the app was launched or why, the .on('notification') handler ends up running at some point concurrent to another block of init code that has to be able to respond to the possibility of a push notification without warning.

Even at the least, it would be helpful to know when the registration occurs as a callback param that a notification is pending processing. Or maybe some type of global var or param that indicates the app was launched via the user interacting with a push notification.

Thanks!

84pennies commented 8 years ago

So, adding this block to AppDelegate.m (the file at the root level in xcode, not the appDelegate+notification file in the pushplugin directory) will let you know if the app was launched via push on iOS. Note, there is likely a whole bunch of code already in this method.

- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    //adding this variable to track if app was launched via a push notif
    BOOL launchedViaPush = false;

    //check if app was launched via push
    UILocalNotification *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
    if (notification) {
        NSLog(@"app recieved notification from remote%@",notification);
        launchedViaPush = true;
    } else {
        NSLog(@"app did not recieve notification");
    }
}

My solution to getting this data into the cordova side of things is to add a query string parameter to the launch url (kind of hacky, yes) like so (later on in the same block):

//appending a querystring parameter to tell cordova if it was launched via push
if (launchedViaPush) {
    self.viewController.startPage = @"index.html?pushLaunch=y";
}
else {
    self.viewController.startPage = @"index.html?pushLaunch=n";
}

This works just fine, but I have yet to find an equivalent Android solution (still working on it). The solution for Android is below. I am sure you could use a similar code block to interface it with the push plugin. I'd try to write it, but I am not yet that familiar with the cordova exec/plugin interface stuff.

fredgalvao commented 8 years ago

Be warned though, that piece of code will only work if the page to be loaded is exactly index.html. An ideal handling of that would be to query cordova settings to get the actual file name to be loaded, which may be different than index.html. Regardless, I still think that approach is suboptimal, as it would be lost as soon as a page change occured, so we really need that value to come from the native side directly (through an API method, as I suggested) so that it would survive any changes to the web side of things and still be available through the API.

84pennies commented 8 years ago

@fredgalvao Totally agree. Just sort of hacking my way through a solution. And my page is never changing as it is a single page app.

84pennies commented 8 years ago

For anyone wanting to see how to detect launch via push in the Android side of things, here's what you can do. First, find this block in PushHandlerActivity.java (in the plugin source files)

/**
 * Forces the main activity to re-launch if it's unloaded.
 */
private void forceMainActivityReload() {
    PackageManager pm = getPackageManager();
    Intent launchIntent = pm.getLaunchIntentForPackage(getApplicationContext().getPackageName());

    //THIS ADDS A KEY TO THE INTENT TO INDICATE THAT IT WAS LAUNCHED VIA PUSH!
    String pushLaunch = "Y";
    launchIntent.putExtra("PUSH_LAUNCH", pushLaunch);

    startActivity(launchIntent);
}

With the launchIntent, use the method putExtra(). This method allows you to add an extra parameter to the Intent that is being passed to the app. In this case, I am simply adding a parameter to tell the onCreate() method in our main cordova app java that the launchIntent was invoked by interacting with a push notification.

Next, in the main app java file (platform>android>src>[your main app java file directory]), you can do the following to detect if the Intent has that extra parameter or not and therefore detect if the app was launched via push. Note: There may be a better place to do this, but with my limited knowledge of java/android, this is where I got it to work.

import android.os.Bundle; //may need to add this to work with bundles
import org.apache.cordova.*;
import android.content.Intent; //need to add this 

public class CordovaApp extends CordovaActivity
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        super.init();           
        final Intent intent = getIntent();  //this gets the intent
        boolean PushLaunch = false;  //var to store if this is a push launch
        Bundle extras = intent.getExtras();  //get the extra params from the intent

        //I chose to iterate here mostly for debugging purposes, but you can also use .getString() to retreive a key value from a bundle
        if (extras != null) {
            for (String key : extras.keySet()) {        
                String value = extras.get(key).toString();      
                if (key.equals("PUSH_LAUNCH") && value.equals("Y")) {
                    PushLaunch = true;
                }
            }               
        }

        //here I am adding a querystring parameter to the launch url.  not the most ideal way to pass data to the webview, but this is where you could do something more elegant...
        if (PushLaunch) {
            launchUrl += "?pushLaunch=y";       
        }
        else {
            launchUrl += "?pushLaunch=n";           
        }
        loadUrl(launchUrl);
    }
}

So, at this point, I have figured out how to discover if the app was coldbooted from a push notification, but my solution for getting that data to the Cordova side is definitely lacking. If anyone else wants to use this as a starting point for a proper solution, be my guest!

macdonst commented 8 years ago

@84pennies why don't you send a PR?

vlaraort commented 8 years ago

@84pennies I am trying to implement your solution, as my app also opens in the www/index.html of cordova, but I am unable to make it work

I am using

- (BOOL)application:(UIApplication*)application didFinishLaunchingWithOptions:(NSDictionary*)launchOptions
{
    //adding this variable to track if app was launched via a push notif
    BOOL launchedViaPush = false;

    //check if app was launched via push
    UILocalNotification *notification = [launchOptions objectForKey:UIApplicationLaunchOptionsRemoteNotificationKey];
    if (notification) {
        NSLog(@"APP START recieved notification from remote%@",notification);
        launchedViaPush = true;
    } else {
        NSLog(@"APP START did not recieve notification");
    }
    //appending a querystring parameter to tell cordova if it was launched via push
    if (launchedViaPush) {
        self.viewController.startPage = @"index.html?pushLaunch=y";
    }
    else {
        self.viewController.startPage = @"index.html";
    }
    return YES;

}

Into the AppDelegate+notification.m, the log works fine, but my app stays in a black screen. I am really bad at objective, so maybe is a simple error. Can you help me?

Thanks in advance

84pennies commented 8 years ago

@vlaraort If you read my previous comment, ignore it. I may not have been totally clear, but I added the code to the Root AppDelegate.m through xcode. I didn't make the change to the AppDelegate+notification.m.

It would certainly make sense for it to live somewhere inside the src for ios on this plugin, but it is beyond me to figure out how to do that.

84pennies commented 8 years ago

@macdonst To be honest, I have never done a pull request. I did some research and it seems simple enough (been in an svn world for a while now). However, some of my changes are outside the scope of the plugin source -- specifically, I made changes to the master AppDelegate.m and the CordovaApp.java files to glean the launch intent/action. So, I am not sure if submitting a PR would work since it relies on edits to those files outside the plugin.

I would certainly have no issue with someone using any of my code if it's helpful though!

I also think that my solution needs some tweaks, i.e. querying the start page rather than hard coding it like I did.

Would you suggest I still try and submit anything?

zabojad commented 8 years ago

@84pennies have you PR your work ? This is definitively something we need to merge into this plugin...

84pennies commented 8 years ago

@zabojad See my reply to @macdonst above... Basically, I had to alter files outside the scope of this plugin in order for me to get this solution working. I am sure there's a way to avoid doing that, but my experience in this arena is limited. Feel free to use my code if it helps, but I am hoping others can implement a solution that doesn't require editing files outside the plugin.

csga5000 commented 8 years ago

I believe this has been changed

It's now notification.additionalData.foreground. False if the app was opened from the notification

84pennies commented 8 years ago

@csga5000 I was looking for a more general way to know how if the app was booted cold by interacting with a push notification. It is more of a launch intent question. That flag you noted works great if you want to know at the point of handling the push notification, but that happens too late in the launch/init process to be helpful for my use case.

csga5000 commented 8 years ago

Well if you received a notification and opened the app, then it'll be true and you can redirect. But if you find a better option feel free to post it

84pennies commented 8 years ago

I did post it a few months back :) On Thu, Sep 29, 2016 at 5:26 PM Chase Anderson notifications@github.com wrote:

Well if you received a notification and opened the app, then it'll be true and you can redirect. But if you find a better option feel free to post it

— You are receiving this because you were mentioned.

Reply to this email directly, view it on GitHub https://github.com/phonegap/phonegap-plugin-push/issues/501#issuecomment-250628915, or mute the thread https://github.com/notifications/unsubscribe-auth/ACmI4O_sdOwS1bApCY3toS5oBrXOcHm_ks5qvFcggaJpZM4HEgsL .

fredgalvao commented 8 years ago

@macdonst I really want to have a callback to the init function with the queued pushNotification and some additional info to solve this issue forever. Do you think it would go against something important?

Basically, what I want is to have every coldstart of the app noticeable by inspecting the object returned by the init event/callback.

interface InitInfo {
    wasLaunchedByNotification: boolean;
    notificationThatLaunchedTheApp: NotificationEventResponse;
}

That way we can solve two issues at the same time:

As it is right now, having to "wait indefinitely until a pushNotification event maybe happens" is a really bad practice, and unfortunately we have no other option.

Maybe something like this:

const push = PushNotification.init({
    android: {},
    browser: {},
    ios: {},
    windows: {}
}, (initInfo: InitInfo) => {
    if(initInfo.wasLaunchedByNotification) {
        app.registerNotificationAsProcessed(initInfo.notificationThatLaunchedTheApp);
        $state.go('pushNotificationPage', {notification: initInfo.notificationThatLaunchedTheApp});
    } else {
        $state.go('home');
    }
});

or this

const push = PushNotification.init({
    android: {},
    browser: {},
    ios: {},
    windows: {}
});

push.on('initialization', (initInfo: InitInfo) => {
    if(initInfo.wasLaunchedByNotification) {
        app.registerNotificationAsProcessed(initInfo.notificationThatLaunchedTheApp);
        $state.go('pushNotificationPage', {notification: initInfo.notificationThatLaunchedTheApp});
    } else {
        $state.go('home');
    }
});

This is in a way an improved version of what coldstart aims to provide, but with a better time of execution. We could get rid of coldstart if we did what I'm suggesting, IMO. Of course this would be something for a major version bump, and we're lucky to already have one incoming (2.0 😉 )

filipsuk commented 7 years ago

Any updates on this? It would be nice to know if this feature can be expected in the future. Right now when user opens the app from notification the default screen is shown, then loading and then the right screen from notification payload. It would be great to skip the first screen.

fredgalvao commented 7 years ago

I also am waiting for this, or at least some more discussion so a PR might maybe who knows sometimes be born :wink:

zwacky commented 7 years ago

@84pennies this is exactly what I'm trying to do, sadly I don't succeed with. I pretty much have the same source like you have in your example from the 15th Jan 2016, but I can't get the extras back. Extra is always null.

Any hints? Would be very much interested to put a working PR up when everything is working. I see this as a huge benefit.

(i'm using the latest plugin version 1.9.2)

iwtkachenko commented 7 years ago

Perfectly work the following combination to cover all cases. Cordova itself has event 'resume' and 'pause'. One can listen to them to change the state of a flag foreground/background state of application (let't name it isPaused).

While the app is in the foreground, the notification observer is still called when a notification arrives. So isPaused === true, notificationadditionalData.coldstart === false, notification.additionalData.foreground === false We do not process this case. While you click on the app icon on the desktop. You don't get the notification observer notified. When you click on the message while you are on the desktop, you will get the following combination of state in the notification observer: isPaused === false, notificationadditionalData.coldstart === false, notification.additionalData.foreground === true

If you run the app clicking on the message, you will get the following combination of state flags: isPaused === false, notificationadditionalData.coldstart === true, notification.additionalData.foreground === false

When the app is run and you get the message. You get the following state: isPaused === false, notificationadditionalData.coldstart === false, notification.additionalData.foreground === true

When you click on the system message while you are in the app you get something like this again: isPaused === false, notificationadditionalData.coldstart === false, notification.additionalData.foreground === false

Working for me as a charm.

document.addEventListener 'pause', ()=>
  console.log 'GO TO BACKGROUND'
  @_isPaused = true

document.addEventListener 'resume', ()=>
  console.log 'GO TO FOREGROUND'
  @_isPaused = false

# ....
# Different code
# .....

notify : (notification)->
  if @_isPaused
    return

  if notification.additionalData?.coldstart || !notification.additionalData?.foreground
    console.log 'cold'
    (@_castMessageType notification).open @_state
    return
zwacky commented 7 years ago

@vashigor the discussion is about a slight different problem here. you need notification.additionalData, which you only get back when you have initialized PushNotification.init.

The idea is to know if the app has any push notifications queued before you call PushNotification.init and wait for the onNotification callback. This way you can skip the default routing of the app and save a lot of time and the whole deep linking experience will be much more fluid.

P.s. I still couldn't get it to work with my native android skills, because getting the Intent won't give me the extras I put by the NotificationHandler beforehand. Any hints? If this all works, I'd be very happy to create a working PR or put it in a separate plugin… I think it'd be a very good improvement for apps that work with deeplinks a lot.

benag commented 7 years ago

It seems that push.init is called before push.notification, is there any solution for this? is this plugin production ready?

zwacky commented 7 years ago

@benag this is going a bit off-topic: yes, this plugin is very production ready.

I still couldn't find a way to getExtras() of the right intent back.

iwtkachenko commented 7 years ago

@zwacky , I see the point now. Looks like I'm not so advanced with solving plugin's tricks yet. Probably the configuration of the initialization can be optionally offload to native part of the plugin installation, the same way that GCM id is specified. But I'll report results of my experiments with moving of notification processing before routing if I do some.

benag commented 7 years ago

If it is guaranteed that the notification is called before any other code, then i can work around, if not I cant see how to use the plugin. It seems to be the case but is it certain on all devices?

zwacky commented 7 years ago

@benag I don't think you're in the right issue for your problem. You seem to have a question with using the plugin iself. Please open a new issue for that.

dmarcs commented 7 years ago

This would be a really useful feature to add if someone can figure out how to implement it. My code looks something like this:

function1(); push.on('notification') { function2(); }

I want function2() to get called if the app is opened by clicking on the push notification. I want function1() to get called if the app is opened not by clicking on the push notification.

Right now function1() and function2() get called if I open the app by clicking on a push notification. I only want function2() to called called if I open the app by clicking on a push notification. Right now function1() gets called each time I open the app (I only want it called if I open the app not by clicking on the push notification).

Ideally this plugin would have a global variable called didOpenByClickingPushNotification. That way I could solve my problem by adding an if statement as follows:

if(!didOpenByClickingPushNotification) { function1(); } push.on('notification') { function2(); }

The problem right now is that even though on('notification') does fire shortly after opening the app by clicking the push notification, my app doesn't know when it will get called.

Are there any plans to add this to the next version of this plugin?

dmarcs commented 7 years ago

In AppDelegate+notification.m I'd like to add sendPluginResult to the createNotificationChecker function, but I don't have access to self.commandDelegate in AppDelegate+notification.m. [self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId];

- (void)createNotificationChecker:(NSNotification *)notification
{

    NSLog(@"createNotificationChecker");
    if (notification)
    {
        NSDictionary *launchOptions = [notification userInfo];
        if (launchOptions) {
            NSLog(@"coldstart");
            self.launchNotification = [launchOptions objectForKey: @"UIApplicationLaunchOptionsRemoteNotificationKey"];
            self.coldstart = [NSNumber numberWithBool:YES];
        } else {
            NSLog(@"not coldstart");
            self.coldstart = [NSNumber numberWithBool:NO];
        }
    }
}

I also tried adding a load function to PushPlugin.m which calls the finishLaunching function like this:

- (void)load
{
[[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(finishLaunching:)
        name:UIApplicationDidFinishLaunchingNotification
        object :nil];
}

- (void)finishLaunching:(NSNotification *)notification
{
}

But finishLaunching doesn't get called unless I make both class methods:

+(void)load
{
[[NSNotificationCenter defaultCenter] addObserver:self
        selector:@selector(finishLaunching:)
        name:UIApplicationDidFinishLaunchingNotification
        object :nil];
}

+ (void)finishLaunching:(NSNotification *)notification
{
}

The problem is that if they're class methods with +, then I can't use

[self.commandDelegate sendPluginResult:pluginResult callbackId:self.callbackId];
hnguyen48206 commented 6 years ago

I'm facing the same matter too (using the plugin for an ionic project) and came across this topic :D I think this's definitely worth a PR. For now, I'm using a very ugly setTimeout to postpone the initial navigation to the root page in order to give the onNotification event enough time to figure out whether the app has been started from cold-state, if the answer is yes then I'll do st else with another page as the new root page. 1000 mili seconds is all it needs.

konnectappdev commented 5 years ago

Would be very nice to have this enhancement fixed! +1