Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Properly start Activity from Notification regardless of app state

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>
like image 607
Krøllebølle Avatar asked Feb 21 '16 17:02

Krøllebølle


People also ask

How do I start an activity from notifications?

Build and issue the notification: Create an Intent that starts the Activity . Set the Activity to start in a new, empty task by calling setFlags() with the flags FLAG_ACTIVITY_NEW_TASK and FLAG_ACTIVITY_CLEAR_TASK . Create a PendingIntent by calling getActivity() .

How do I start an app from another activity?

To allow other apps to start your activity in this way, you need to add an <intent-filter> element in your manifest file for the corresponding <activity> element.


1 Answers

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.

like image 197
Omid Heshmatinia Avatar answered Oct 07 '22 11:10

Omid Heshmatinia