Properly start Activity from Notification regardless of app state
Asked Answered
A

3

12

I have an app with a splash screen Activity, followed by a main Activity. The splash screen loads stuff (database, etc.) before starting the main Activity. From this main Activity the user can navigate to multiple other child Activities and back. Some of the child Activities are started using startActivityForResult(), others just startActivity().

The Activity hierarchy are as depicted below.

|                    Child A (startActivityForResult)
|                   /
|--> Splash --> Main -- Child B (startActivityForResult)
|      ^            \
|      |             Child C (startActivity)
|       \
|        This Activity is currently skipped if a Notification is started
|        while the app is not running or in the background.

I need to achieve the following behavior when clicking a Notification:

  1. The state in the Activity must be maintained, since the user has selected some recipes to create a shopping list. If a new Activity is started, I believe the state will be lost.
  2. If the app is in the Main Activity, bring that to the front and let me know in code that I arrived from a Notification.
  3. If the app is in a child Activity started with startActivityForResult(), I need to add data to an Intent before going back to the Main Activity so that it can catch the result properly.
  4. If the app is in a child Activity started with startActivity() I just need to go back since there is nothing else to do (this currently works).
  5. If the app is not in the background, nor the foreground (i.e. it is not running) I must start the Main Activity and also know that I arrived from a Notification, so that I can set up things that are not set up yet, since the Splash Activity is skipped in this case in my current setup.

I have tried lots of various suggestions here on SO and elsewhere, but I have not been able to successfully get the behavior described above. I have also tried reading the documentation without becoming a lot wiser, just a little. My current situation for the cases above when clicking my Notification is:

  1. I arrive in the Main Activity in onNewIntent(). I do not arrive here if the app is not running (or in the background). This seems to be expected and desired behavior.
  2. I am not able to catch that I am coming from a Notification in any child Activities, thus I am not able to properly call setResult() in those Activities. How should I do this?
  3. This currently works, since the Notification just closes the child Activity, which is ok.
  4. I am able to get the Notification Intent in onCreate() by using getIntent() and Intent.getBooleanExtra() with a boolean set in the Notification. I should thus be able to make it work, but I am not sure that this is the best way. What is the preferred way of doing this?

Current code

Creating Notification:

The Notification is created when an HTTP request inside a Service returns some data.

NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
        .setSmallIcon(getNotificationIcon())
        .setAutoCancel(true)
        .setColor(ContextCompat.getColor(context, R.color.my_brown))
        .setContentTitle(getNotificationTitle(newRecipeNames))
        .setContentText(getContentText(newRecipeNames))
        .setStyle(new NotificationCompat.BigTextStyle().bigText("foo"));

Intent notifyIntent = new Intent(context, MainActivity.class);
notifyIntent.setAction(Intent.ACTION_MAIN);
notifyIntent.addCategory(Intent.CATEGORY_LAUNCHER);

notifyIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);

/* Add a thing to let MainActivity know that we came from a Notification. */
notifyIntent.putExtra("intent_bool", true);

PendingIntent notifyPendingIntent = PendingIntent.getActivity(context, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(notifyPendingIntent);

NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(111, builder.build());

MainActivity.java:

@Override
protected void onCreate(Bundle savedInstanceState)
{
    Intent intent = getIntent();
    if (intent.getBooleanExtra("intent_bool", false))
    {
        // We arrive here if the app was not running, as described in point 4 above.
    }

    ...
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data)
{
    switch (requestCode)
    {
        case CHILD_A:
            // Intent data is null here when starting from Notification. We will thus crash and burn if using it. Normally data has values when closing CHILD_A properly.
            // This is bullet point 2 above.
            break;

        case CHILD_B:
            // Same as CHILD_A
            break;
    }

    ...
}

@Override
protected void onNewIntent(Intent intent)
{
    super.onNewIntent(intent);
    boolean arrivedFromNotification = intent.getBooleanExtra("intent_bool", false);
    // arrivedFromNotification is true, but onNewIntent is only called if the app is already running.
    // This is bullet point 1 above.
    // Do stuff with Intent.
    ... 
}

Inside a child Activity started with startActivityForResult():

@Override
protected void onNewIntent(Intent intent)
{
    // This point is never reached when opening a Notification while in the child Activity.
    super.onNewIntent(intent);
}

@Override
public void onBackPressed()
{
    // This point is never reached when opening a Notification while in the child Activity.

    Intent resultIntent = getResultIntent();
    setResult(Activity.RESULT_OK, resultIntent);

    // NOTE! super.onBackPressed() *must* be called after setResult().
    super.onBackPressed();
    this.finish();
}

private Intent getResultIntent()
{
    int recipeCount = getRecipeCount();
    Recipe recipe   = getRecipe();

    Intent recipeIntent = new Intent();
    recipeIntent.putExtra(INTENT_RECIPE_COUNT, recipeCount);
    recipeIntent.putExtra(INTENT_RECIPE, recipe);

    return recipeIntent;
}

AndroidManifest.xml:

<application
    android:allowBackup="true"
    android:icon="@mipmap/my_launcher_icon"
    android:label="@string/my_app_name"
    android:theme="@style/MyTheme"
    android:name="com.mycompany.myapp.MyApplication" >

    <activity
        android:name="com.mycompany.myapp.activities.SplashActivity"
        android:screenOrientation="portrait" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

    <activity
        android:name="com.mycompany.myapp.activities.MainActivity"
        android:label="@string/my_app_name"
        android:screenOrientation="portrait"
        android:windowSoftInputMode="adjustPan" >
    </activity>

    <activity
        android:name="com.mycompany.myapp.activities.ChildActivityA"
        android:label="@string/foo"
        android:parentActivityName="com.mycompany.myapp.activities.MainActivity"
        android:screenOrientation="portrait"
        android:windowSoftInputMode="adjustPan" >
        <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value="com.mycompany.myapp.activities.MainActivity" >
        </meta-data>
    </activity>

    <activity
        android:name="com.mycompany.myapp.activities.ChildActivityB"
        android:label="@string/foo"
        android:parentActivityName="com.mycompany.myapp.activities.MainActivity"
        android:screenOrientation="portrait" >
        <meta-data
            android:name="android.support.PARENT_ACTIVITY"
            android:value="com.mycompany.myapp.activities.MainActivity" >
        </meta-data>
    </activity>

    ...
</manifest>
Adrienneadrift answered 21/2, 2016 at 17:20 Comment(10)
You have a complicated problem here. I doubt you'll get a comprehensive solution on SO! That said, one thing that might help you formulate a solution is the fact that a notification can also trigger a broadcast (not just an activity). You could take advantage of this to use a BroadcastReceiver to make a decision about what exactly to do with the click before any activities are invoked. I would very much not depend on an activity-based intent to be able to do what you want.Hypsometry
Thanks for the tip, I'll look into the broadcasting part of Notifications. If it works the way it seems I might be able to work something out.Isma
But a (Push-)Notification comes from a BroadcastReceiver. There is no need to start another BroadcastReceiver from your Notification.Tormoria
And if you store your activity state in shared preferences you can access it before you create the Notification. For example store all needed data (shopping list, last open activity, etc) in onPause().Tormoria
I suppose I can play around with storing some state in SharedPreferences to make life easier for myself. Saving last open Activity in SharedPreferences like that might actually make some of the logic a lot simpler. Is there a way to intercept the Intent from a Notification in a child Activity when it is closed due to using the FLAG_ACTIVITY_CLEAR_TOP flag?Isma
@Adrienneadrift Why don't you just write same Receiver with same Action to catch in each of your activity obviously register in onResume and unregister in onPause,Now send a Broadcast with that Unique action for Notification click, whichever activity would be available to catch, will catch it and according code would have been written in it that will handle it simply. I think this will work for sure, I don't see any problem. Try this and tell me what happens.Thaw
Have a read of the OP's solution on this post https://mcmap.net/q/82467/-how-to-determine-if-one-of-my-activities-is-in-the-foreground-duplicate/1256219 Detecting that the broadcast failed and therefore starting the splash activity is probably how best to handle this.Saxton
if you app started with background service and notification in show, then you have to skip splash screen from start activity when notification is started.???Ellisellison
Have you solved your problem.??Ellisellison
Have you solved your problem.??Ellisellison
H
5

Such a complicated Question :D Here is how you should treat this problem :

  1. Use an IntentService in your notification instead of Intent notifyIntent = new Intent(context, MainActivity.class);

by now, whenever user click on the notification, an intentservice would be called.

  1. in the intent service,Broadcast something.

  2. in OnResume of all your desired activity register the broadcast listener (for the broadcast you create in 2nd phase) and in OnPause unregister it

by now whenever you are in any activity and the user click on notification, you would be informed without any problem and without any recreation of activity

  1. in your Application class define a public Boolean. lets called it APP_IS_RUNNING=false; in your MainActivity, in OnPause make it false and in OnResume make it true;

By doing this you can understand your app is running or not or is in background.

NOTE : if you want to handle more states, like isInBackground,Running,Destroyed,etc... you can use an enum or whatever you like

You want to do different things when the app is running, am i right ? so in the intent service which you declared in 1st phase check the parameter you define in your Application Class. (i mean APP_IS_RUNNING in our example) if it was true use broadcast and otherwise call an intent which open your desired Activity.

Herzel answered 18/4, 2016 at 6:44 Comment(4)
This worked well all the way to starting an Acticity (i.e. not a broadcast) from the IntentService when the app is not running in the foreground. As far as I understand we must provide the Intent.FLAG_ACTIVITY_NEW_TASK flag to launch an Activity in this way, thus the current stack and the state will be lost. Any suggestions to work around this?Isma
sorryyyyy. you are right. do this trick now. in your intent service use both these flags Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT then make another activity which have no UI and close itself as it open ! and in your intent call that. by this you will return to the last activity in stack :D @AdrienneadriftHerzel
Ok, so from there, why not always saving your last state / activity / data / and all what you need to come back to your previous state in a SystemPreferences ? Doing this would allow you to get the latest data saved that you need to reconstruct your views once a new visit from notification is detected...Holo
@JBA: Yes, you are right, and doing this is actually simplifying other things in my app as well. It is also very useful if the user closes the app and I want to save the state of something for later.Isma
R
0

You are going on a wrong way buddy. onActivityResult is not the solution. Just A simple Answer to this would be to use Broadcast Receiver

Declare an action In your manifest file:

<receiver android:name="com.myapp.receiver.AudioPlayerBroadcastReceiver" >
            <intent-filter>
                <action android:name="com.myapp.receiver.ACTION_PLAY" />
<!-- add as many actions as you want here -->
</intent-filter>
        </receiver>

Create Broadcast receiver's class:

public class AudioPlayerBroadcastReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        String action = intent.getAction();
        if(action.equalsIgnoreCase("com.myapp.receiver.ACTION_PLAY")){

  Myactivity.doSomething(); //access static method of your activity
// do whatever you want to do for this specific action
//do things when the button is clicked inside notification.
         }
    }
}

In your setNotification() Method

Notification notification = new Notification.Builder(this).
                    setWhen(System.currentTimeMillis())
                    .setSmallIcon(R.drawable.no_art).build();
RemoteView remoteview = new RemoteViews(getPackageName(), R.layout.my_notification);

notification.contentView = remoteview;

Intent playIntent = new Intent("com.myapp.receiver.ACTION_PLAY");
        PendingIntent playSwitch = PendingIntent.getBroadcast(this, 100, playIntent, 0);
        remoteview.setOnClickPendingIntent(R.id.play_button_my_notification, playSwitch); 
//this handle view click for the specific action for this specific ID used in broadcast receiver

Now when user will click on the button in Notification and broacast receiver will catch that event and perform the action.

Routh answered 23/4, 2016 at 19:57 Comment(0)
A
0

Here is what I ended up doing. It is a working solution and every situation of app state, child Activity, etc. is tested. Further comments are highly appreciated.

Creating the Notification

The Notification is still created as in the original question. I tried using an IntentService with a broadcast as suggested by @Smartiz. This works fine while the app is running; the registered child Activities receives the broadcast and we can do what we like from that point on, like taking care of the state. The problem, however, is when the app is not running in the foreground. Then we must use the flag Intent.FLAG_ACTIVITY_NEW_TASK in the Intent to broadcast from the IntentService (Android requires this), thus we will create a new stack and things starts to get messy. This can probably be worked around, but I think it easier to save the state using SharedPreferences or similar things as others pointed out. This is also a more useful way to store persistent state.

Thus the Notification is simply created as before:

NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
        .setSmallIcon(getNotificationIcon())
        .setAutoCancel(true)
        .setColor(ContextCompat.getColor(context, R.color.my_brown))
        .setContentTitle(getNotificationTitle(newRecipeNames))
        .setContentText(getContentText(newRecipeNames))
        .setStyle(new NotificationCompat.BigTextStyle().bigText("foo"));

Intent notifyIntent = new Intent(context, MainActivity.class);
notifyIntent.setAction(Intent.ACTION_MAIN);
notifyIntent.addCategory(Intent.CATEGORY_LAUNCHER);

notifyIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);

/* Add a thing to let MainActivity know that we came from a Notification.
Here we can add other data we desire as well. */
notifyIntent.putExtra("intent_bool", true);

PendingIntent notifyPendingIntent = PendingIntent.getActivity(context, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT);
builder.setContentIntent(notifyPendingIntent);

NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(111, builder.build());

Saving state

In the child Activities that need to save state I simply save the I need to SharedPreferences in onPause(). Thus that state can be reused wherever needed at a later point. This is also a highly useful way of storing state in a more general way. I had not though of it since I thought the SharedPreferences were reserved for preferences, but it can be used for anything. I wish I had realized this sooner.

Opening the Notification

Now, when opening a Notification the following things occur, depending on the state of the app and which child Activity is open/paused. Remember that the flags used are Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP.

A. Child Activity

  1. Running in the front: The child Activity is closed, applicable state is saved using SharedPreferences in onPause and can be fetched in onCreate or wherever in the main Activity.
  2. App is in the background: same behavior.
  3. App is in the background, but killed by the OS (tested using adb shell: There is no stack at this point, thus MainActivity is opened. The app is in a dirty state, however, so I revert that intent back to the splash screen with the incoming data and back to the main Activity. The state is again saved in onPause in the child Activity when the user closed it and it can be fetched in the main Activity.

B. Main Activity

  1. Running in the front: The Intent is caught in onNewIntent and everything is golden. Do what we want.
  2. App is in the background: same behavior.
  3. App is in the background, but killed by the OS (tested using adb shell: The app is in a dirty state, so we revert the Intent to the splash screen/loading screen and back to the main Activity.

C. App is not running at all

This is really the same as if Android killed the app in the background to free resources. Just open the main Activity, revert to the splash screen for loading and back to the main Activity.

D. Splash Activity

It is not very likely that a user can be in the splash Activity/loading Activity while a Notification is pressed, but it is possible in theory. If a user does this the StrictMode complains about having 2 main Activities when closing the app, but I am not certain that it is entirely correct. Anyway, this is highly hypothetical, so I am not going to spend much time on it at this point.

I do not think this is a perfect solution since it requires a little bit of coding here and little bit of coding there and reverting Intents back and forth if the app is in a dirty state, but it works. Comments are highly appreciated.

Adrienneadrift answered 24/4, 2016 at 10:20 Comment(1)
Great, my question now is how to replicate this with firebase notificationsCharkha

© 2022 - 2024 — McMap. All rights reserved.