5

I have an application that starts with an Activity to load stuff before the main Activity is shown. Starting the application normally does MyApplication --> MyLoadingActivity --> MyMainActivity. In MyMainActivity there is a ViewPager with RecyclerViews and other stuff. The state in MyMainActivity is properly saved and restored when navigation to and from other Activities in the application, but when starting MyMainActivity from a Notification all the state is cleared since Android restarts that Activity.

The Notifications are created from a Service checking for updates from a remote server. Once updates are found, the new data are stored in the SQLite database and a Notification about this is created. When I click this Notification, MyMainActicity is started, but the state is lost.

Following the official guide, here is how I create the Notification inside the Service:

NotificationCompat.Builder builder = new NotificationCompat.Builder(context)
    .setSmallIcon(R.drawable.foo)
    .setAutoCancel(true)
    .setContentTitle("title")
    .setContentText("text")
    .setStyle(new NotificationCompat.BigTextStyle().bigText("A long text will go here if we are on Lollipop or above."));

Intent notifyIntent = new Intent(context, MyMainActivity.class);
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);

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

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

I have tried combining the Intent.FLAG_*** in various ways, but I have not been able to make it work properly. I cannot figure this out, hence this question.

What I want to achieve when opening the Notification is:

  1. If the application is not running, start the application normally and go to MyMainActivity.
  2. If the application is running and MyMainActivity is active, just bring that to the front with the existing state and run a few lines of code.
  3. If the application is running and another Activity is running, go back to MyMainActivity, preferably using the existing stack. Most other activities are children of MyMainActivity.

What happens using the current code:

  1. Using Intent.FLAG_ACTIVITY_NEW_TASK creates a new instance of MyMainActivity which has empty state, even though its description states

When using this flag, if a task is already running for the activity you are now starting, then a new activity will not be started; instead, the current task will simply be brought to the front of the screen with the state it was last in.

  1. Also using Intent.FLAG_ACTIVITY_CLEAR_TASK calls onDestroy() on the existing MyMainActivity after the new MyMainActivity is created. That does not help much.

What other things I have tried:

A. Setting the Intent to start as the Application normally does, as described here, does not work.

B. Various combinations of flags has not been able to give the desired behavior.

C. Various other answers here on Stackoverflow.

For reference, here is my manifest file:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.my.app"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="11"
        android:targetSdkVersion="22" />

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/my_app_name"
        android:theme="@style/MyTheme"
        android:name=".MyApplication" >
        <activity
            android:name=".activities.MyLoadingActivity"
            android:icon="@drawable/ic_launcher"
            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=".activities.MyMainActivity"
            android:icon="@drawable/ic_launcher"
            android:label="@string/my_app_name"
            android:screenOrientation="portrait" >
        </activity>
        <activity
            android:name=".activities.AnotherActivity"
            android:label="@string/aa_title"
            android:icon="@drawable/ic_launcher"
            android:parentActivityName=".activities.MyMainActivity"
            android:screenOrientation="portrait">
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value=".activities.MyMainActivity" >
            </meta-data>
        </activity>

        <!-- More children activities of MyMainActivity here -->

        <receiver android:name=".services.ScheduleUpdateReciever">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <action android:name="com.my.app.action.ScheduleUpdateReceiver"/>
            </intent-filter>
        </receiver>
        <receiver android:name=".services.StartUpdateReceiver" />

        <service android:enabled="true" android:name=".services.UpdateService" />

    </application>

</manifest>

Update: saving/restoring

Here is how MyMainActivity is saved and restored. I realize that this might need some cleaning. Maybe it is related to the problems. Other parts of the class is omitted for brevity. As noted above, all of the saving and restoring works fine when navigating around inside the app.

private ShoppingList mShoppingList;
private FilteredRecipes mFilteredRecipes;
private MainFragment mMainFragment;

@Override
protected void onCreate(Bundle savedInstanceState)
{
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main_activity_layout);

    // Start the Broadcast receiver that creates notifications if new data is found.
    startService(new Intent(this, ScheduleUpdateReciever.class));
    sendBroadcast(new Intent(SCHEDULE_UPDATE_RECEIVER_BROADCAST));

    // Setup Toolbar, NavigationView, DrawerLayout etc. here. No state restored for these widgets.

    if (mShoppingList == null)
    {
        mShoppingList = new ShoppingList(this);
    }

    if (mFilteredRecipes == null)
    {
        mFilteredRecipes = new FilteredRecipes(this);
    }

    if (savedInstanceState == null)
    {
        mMainFragment = new MainFragment();
    }
    else
    {
        restoreState(savedInstanceState);
    }

    getSupportFragmentManager().beginTransaction()
            .replace(R.id.content_frame, mMainFragment, FragmentConstants.MAIN_FRAGMENT_TAG)
            .commit();


}

@Override
protected void onSaveInstanceState(Bundle outState)
{
    super.onSaveInstanceState(outState);

    // These calls adds stuff directly to the Bundle instance, be it basic variables, lists or Parcelable classes.
    mShoppingList.onSaveInstanceState(outState);
    mFilteredRecipes.onSaveInstanceState(outState);
    getSupportFragmentManager().putFragment(outState, SAVE_RESTORE_MAIN_FRAGMENT, mMainFragment);
}


private void restoreState(Bundle savedInstanceState)
{
    // These calls restores stuff directly from the Bundle instance, be it basic variables, lists or Parcelable classes.
    mShoppingList.onRestoreInstanceState(savedInstanceState);
    mFilteredRecipes.onRestoreInstanceState(savedInstanceState);
    mMainFragment = (MainFragment) getSupportFragmentManager().getFragment(savedInstanceState, SAVE_RESTORE_MAIN_FRAGMENT);
}
Community
  • 1
  • 1
Krøllebølle
  • 2,878
  • 6
  • 54
  • 79
  • How are you restoring/saving the state of MyMainActicity when moving from/to another activity? Are you using intents, sharedPreferences, values from the database? If so, are you restoring MyMainActicity in onCreate, onStart or onResume? – Nikiole91 Jan 28 '16 at 17:52
  • Have you tried using the flags: FLAG_ACTIVITY_SINGLE_TOP and FLAG_ACTIVITY_CLEAR_TOP like: intent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP); – Nikiole91 Jan 28 '16 at 18:00
  • See update above for information on the saving and restoring of state. There are no Intents, SharedPreferences or database calls involved in this process. I save state in `onSaveInstanceState()` and resume state in `onCreate()`. Neither `onResume()` nor `onStart()` are implemented. – Krøllebølle Jan 28 '16 at 18:19
  • I edited my answer, if you need any concrete examples of how to save your state with sharedPreferences, let me know :) – Nikiole91 Jan 28 '16 at 18:52

1 Answers1

2

From the android documentation: http://developer.android.com/reference/android/content/Intent.html

About FLAG_ACTIVITY_CLEAR_TOP:

If set, and the activity being launched is already running in the current task, then instead of launching a new instance of that activity, all of the other activities on top of it will be closed and this Intent will be delivered to the (now on top) old activity as a new Intent.

About FLAG_ACTIVITY_SINGLE_TOP:

If set, the activity will not be launched if it is already running at the top of the history stack.

It sound pretty much of what you are attempting to achieve. Try this:

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

EDIT:

You are using savedInstanceState to save the data from your activity, I don't recommend to use this approach because it isn't reliable, from the documentation (http://developer.android.com/reference/android/app/Activity.html#onSaveInstanceState%28android.os.Bundle%29) you can infer that onSaveInstanceState() could not be called at all:

Do not confuse this method with activity lifecycle callbacks such as onPause(), which is always called when an activity is being placed in the background or on its way to destruction, or onStop() which is called before destruction. One example of when onPause() and onStop() is called and not this method is when a user navigates back from activity B to activity A: there is no need to call onSaveInstanceState(Bundle) on B because that particular instance will never be restored, so the system avoids calling it. An example when onPause() is called and not onSaveInstanceState(Bundle) is when activity B is launched in front of activity A: the system may avoid calling onSaveInstanceState(Bundle) on activity A if it isn't killed during the lifetime of B since the state of the user interface of A will stay intact.

You should use a more persistent way to store your key/value information, such as sharedPreferences or a database and retrieve those values in your onCreate method. There is more information about it here: http://developer.android.com/training/basics/data-storage/index.html

You can also check my answer here: https://stackoverflow.com/a/35023304/5837758 for an easy way to use sharedPreferences.

Community
  • 1
  • 1
Nikiole91
  • 344
  • 2
  • 10
  • Thanks, the flags you mention seems promising, I don't know how I skipped them while looking through the flags. I have also overridden `onNewIntent()` to catch intent extras to be able to perform some actions when the Acticity is started from a Notification and its eems to work ok. I will do some more testing to make sure everything works as expected. And yes, I can see myself making changes to saving/restoring state in the near future (I kinda knew that). – Krøllebølle Jan 28 '16 at 18:55
  • Yes, the flags works as expected. Thank you for looking into this. I do not believe I need any more persistent saving than using `onSaveInstanceState()` though. In my case, if the user kills the app it would be expected to lose the state. In this case I will save the data I need to a file (I have implemented some automatic saving now and then which might be utilized in that case). If the app is destroyed by Android while in the background, `onSaveInstanceState()` will also do in my case. – Krøllebølle Jan 31 '16 at 16:02