Android: How to RESUME Fragment class from Notification. NOT create new instance

5.7k views Asked by At

I want to start a CountDownTimer in a Fragment class, and when the timer hits, say 3 minutes, the fragment class (though the app is in the background) will create a Notification. When the user pushes the notification, the app holding this fragment with the CountDownTimer, should resume to its "state" as if it was re-opened from the home-screen. So when the user hits the "back-button" the user should be taken to the the previous Activity in the app, and not back to the home screen.

My Fragment_C.java class, which is inside an Activity_C.java class:

package example.app;

import java.text.SimpleDateFormat;
import java.util.TimeZone;

import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Bundle;
import android.os.CountDownTimer;
import android.support.v4.app.Fragment;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.telephony.SmsManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.TextView;

public class Fragment_C extends Fragment {

    static int amount;
    static String phone;
    static String message;

    long mMilliseconds;
    SimpleDateFormat mSimpleDateFormat = new SimpleDateFormat("HH:mm:ss");
    TextView mTextView;

    CountDownTimer mCountDownTimer = new CountDownTimer(amount * 60000, 1000) {
        @Override
        public void onFinish() {
            mTextView.setText(mSimpleDateFormat.format(0));
                        // Do something cool
        }

        public void onTick(long millisUntilFinished) {
                        // Show the countdown in a TextView:
            mTextView.setText(mSimpleDateFormat.format(millisUntilFinished));

                        // Get the time in string format, to use for if-statements:
            String millisInString = mSimpleDateFormat.format(millisUntilFinished);

            if (millisInString.equals("00:03:00")) {
                generateNotification();
                Log.i("There is exactly 3 minutes left", "YO!");
            }
            if (millisInString.equals("00:02:00")) {
                generateNotification();
                Log.i("There is exactly 2 minutes left", "YO!");
            }
            if (millisInString.equals("00:01:00")) {
                generateNotification();
                Log.i("There is exactly 1 minute left", "YO!");
            }

        }
    };

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        // Get the view for this fragment, to retrieve views from it.
        View view = inflater.inflate(R.layout.fragment_c, container, false);

        mSimpleDateFormat.setTimeZone(TimeZone.getTimeZone("UTC"));
        mTextView = (TextView) view.findViewById(R.id.timerview);

        mCountDownTimer.start();

        // Get the start_alarm_button object from the fragment_c.xml:
        Button button = (Button) view
                .findViewById(R.id.fragment_c_cancel_alarm_button);
        button.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                // If this button is pushed, return to Activity_B:
                mCountDownTimer.cancel();
                                // Basically push the "back-button"
                getActivity().finish();
            }
        });

        return view;
    }

    @Override
    public void onDestroy(){
        super.onDestroy();
                // Stop the CountDownTimer if the Activity gets destroyed:
        if(mCountDownTimer != null){
            mCountDownTimer.cancel();
        }
    }

    public static void setTimer(int min) {
        amount = min;
    }

    public static void setMessage(String msg) {
        message = msg;
    }

    public static void setNumber(String num) {
        phone = num;
    }

    public void generateNotification() {

                // This basically taken from developer.android.com
        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
                getActivity()).setSmallIcon(R.drawable.ic_launcher)
                .setContentTitle("Rescue Me ALARM")
                .setContentText("Press here to cancel the SOS SMS");

        // Make the notification play the default notification sound:
        Uri alarmSound = RingtoneManager
                .getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        mBuilder.setSound(alarmSound);

        // Creates an explicit intent for an Activity in your app
        Intent resultIntent = new Intent(getActivity(), Activity_C.class);

        // The stack builder object will contain an artificial back stack for the started  
                // Activity.
        // This ensures that navigating backward from the Activity leads out of
        // your application to the Home screen.
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(getActivity());

        // Adds the back stack for the Intent (but not the Intent itself)
        stackBuilder.addParentStack(Activity_C.class);

        // Adds the Intent that starts the Activity to the top of the stack
        stackBuilder.addNextIntent(resultIntent);

        PendingIntent resultPendingIntent = stackBuilder.getPendingIntent(0,
                PendingIntent.FLAG_UPDATE_CURRENT);

        mBuilder.setContentIntent(resultPendingIntent);
        NotificationManager mNotificationManager = (NotificationManager) getActivity()
                .getSystemService(Context.NOTIFICATION_SERVICE);

        // mId allows you to update the notification later on.
        mNotificationManager.notify(0, mBuilder.build());
    }
}

Here's the fragment_c.xml file:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >

    <Button
        android:id="@+id/fragment_c_cancel_alarm_button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@string/fragment_c_cancel_alarm_button" />

</LinearLayout>

Here's the Activity_C.java class:

package example.app;

import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import android.view.Menu;

public class Activity_C extends FragmentActivity {

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

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.activity__c, menu);
        return true;
    }

}

Here's the activity_c.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent"
    android:baselineAligned="false">

    <fragment android:name="app.me.rescue.rescuemeapp.Fragment_C"
            android:id="@+id/fragment_c" 
            android:layout_weight="1"
            android:layout_width="0dp" 
            android:layout_height="match_parent" />

 </LinearLayout>

Here's the AndroidManifest.xml file:

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

    <uses-sdk
        android:minSdkVersion="10"
        android:targetSdkVersion="19" />

    <uses-permission android:name="android.permission.SEND_SMS" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="app.me.rescue.rescuemeapp.Activity_A"
            android:label="@string/app_name"
            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="app.me.rescue.rescuemeapp.Activity_B"
            android:label="@string/title_activity_activity__b"
            android:screenOrientation="portrait">
        </activity>
        <activity
            android:name="app.me.rescue.rescuemeapp.Activity_C"
            android:label="@string/title_activity_activity__c"
            android:screenOrientation="portrait"
            android:launchMode="singleTask">

        </activity>
    </application>

</manifest>

I have tried some other solutions like resuming an activity from a notification or Android: How to resume an App from a Notification?, but with no luck, also because no one seem to use Fragments anywhere :) I need to though.

Any help is VERY welcome :) I'm a noob, so if you could make your answers easy to understand, it would be much appreciated :)

//////////////// ////////////////

EDIT 2:

I'm embarrassed by this, but I got it to work, but I don't really know how or why. I tried implementing different solutions, but in the end the part missing were this example's alteration of PendingIntent.getActivity()'s second argument: android pending intent notification problem (See the second answer by U-Ramos)

the changes I made are solely in the AndroidManifest.xml file and the "generateNotification()"-method in the Fragment_C.java class:

Fragment.java:

public void generateNotification() {
        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(
                getActivity()).setSmallIcon(R.drawable.ic_launcher)
                .setContentTitle("Rescue Me ALARM")
                .setContentText("Press here to cancel the SOS SMS");

        // Make the notification play the default notification sound:
        Uri alarmSound = RingtoneManager
                .getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        mBuilder.setSound(alarmSound);
        mBuilder.setOngoing(true);

        // Creates an explicit intent for an Activity in your app
        Intent resultIntent = new Intent(getActivity(), Activity_C.class);

        // This somehow makes sure, there is only 1 CountDownTimer going if the notification is pressed:
        resultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);

        // The stack builder object will contain an artificial back stack for the started Activity.
        // This ensures that navigating backward from the Activity leads out of
        // your application to the Home screen.
        TaskStackBuilder stackBuilder = TaskStackBuilder.create(getActivity());

        // Adds the back stack for the Intent (but not the Intent itself)
        stackBuilder.addParentStack(Activity_C.class);

        // Adds the Intent that starts the Activity to the top of the stack
        stackBuilder.addNextIntent(resultIntent);

        // Make this unique ID to make sure there is not generated just a brand new intent with new extra values:
        int requestID = (int) System.currentTimeMillis();

        // Pass the unique ID to the resultPendingIntent:
        PendingIntent resultPendingIntent = PendingIntent.getActivity(getActivity(), requestID, resultIntent, 0);

        mBuilder.setContentIntent(resultPendingIntent);
        NotificationManager mNotificationManager = (NotificationManager) getActivity()
                .getSystemService(Context.NOTIFICATION_SERVICE);

        // mId allows you to update the notification later on.
        mNotificationManager.notify(0, mBuilder.build());
    }

AndroidManifest.xml:

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

    <uses-sdk
        android:minSdkVersion="10"
        android:targetSdkVersion="19" />


    <uses-permission android:name="android.permission.SEND_SMS" />
    <uses-permission android:name="android.permission.READ_CONTACTS" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="app.me.rescue.rescuemeapp.Activity_A"
            android:label="@string/app_name"
            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="app.me.rescue.rescuemeapp.Activity_B"
            android:label="@string/title_activity_activity__b"
            android:screenOrientation="portrait">
        </activity>
        <activity
            android:name="app.me.rescue.rescuemeapp.Activity_C"
            android:parentActivityName="app.me.rescue.rescuemeapp.Activity_B"
            android:label="@string/title_activity_activity__c"
            android:screenOrientation="portrait">
            <meta-data
                android:name="android.support.PARENT_ACTIVITY"
                android:value="app.me.rescue.rescuemeapp.Activity_B" />            
        </activity>
    </application>

</manifest>

If anyone wants to leave a comment or a reason for why this does the trick, you are most welcome to do so :)

Happy Coding!

//////////////// ////////////////

EDIT 3:

Also! if you want the notification to go away, when the user is pressing it, simply put in the following code beneath the instantiation of the mBuider object:

mBuilder.setAutoCancel(true);

So that's that !

5

There are 5 answers

0
NooberMan On BEST ANSWER

I made it work, but not completely sure why.

See the changes made in EDIT 1, in the question for details.

Does anyone know why it works now? :)

1
Caye On

In my opinion the first thing you should always do is overwrite onSaveInstanceState and onRestoreInstanceState in your activity so you can save the state of the activity once is stopped and set it back once is resumed again.

Having done that you can do several things, the bad one is to save the activity that triggered this one from the incoming intent and override onBackPressed to go back to the previous activity.

The better and easier one is just add resultIntent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); as stated here

1
need1milliondollars On

When you use addParentStack(Activity_C.class); according to google docs it

adds the activity parent chain as specified by manifest elements to the task stack builder.

So if you want to use this approach you should properly declare parent activity for Activity_C.

0
Sferus On

I know why it works in EDIT 2 and not in first post.

For the actual root cause I'm still working to find the wonderland solution.

  • because in my case I need also to stat an activity (not the main app activity but a child activity in app) from system notification - when I do that with code in EDIT2 the launch is ok - but when I press back to return to main app activity - surprise!!! - main is destroyed and recreated, instead of just being resumed if it was prev opened (like child was opened over main app activity - using sys notif) - I relay need to resume main app activity NOT re-create :)

Manifest for my child activity launched by notif (with "obfuscations" for names):

<activity
android:name="com.bla.XActivity"
android:configChanges="orientation|screenSize|touchscreen|keyboard|keyboardHidden"
android:logo="@drawable/ic_bla"
android:parentActivityName="com.bla.MainActivity" >
<meta-data
  android:name="android.support.PARENT_ACTIVITY"
  android:value="com.bla.MainActivity" />
</activity>

Main activity manifest:

<activity
android:name="com.bla.MainActivity"
android:configChanges="orientation|screenSize|touchscreen|keyboard|keyboardHidden"
android:label="@string/app_name"
android:logo="@drawable/ic_bla" >
</activity>

Now let's try to explain what I think is the root causes:

1) In my case --- main activity is recreated (not resumed) simply because it is instantiated by this intent used in notification builder when I return from child to main activity:

Intent resultIntent = new Intent(this, XActivity.class); 
// "this" is a background service context - could be any valid context

AND because main app activity will be created with this intent in the new artificial TaskStask created for child XActivity (I suppose) - that's maybe the only visible/existing/ontop task stack and main does not have an instance in this stack yet - it's like the old main activity stack went out for lunch.

TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
// Adds the back stack for the Intent (but not the Intent itself)
stackBuilder.addParentStack(XActivity.class); 

OK - this is what I think for main activity RESTART ISSUE - please post if you have any ideas on solving this.

2) For why EDIT 2 is working and the first post is not - it's simple... - in your example there is a minor but huge difference in manifest file - the ROOT cause of all these issues:

* When you try to launch an activity from sys notification it will not start if you have in manifest the launchMode changed for any of the opened activities - by notif or added to it's parent activity (main in my case):

WRONG CODE - for some reason : - because we have android:launchMode="singleTask"> - it does not matter (from my tests) which value is used other than default - it will not work (notif won't launch activity) using this:

<activity
            android:name="app.me.rescue.rescuemeapp.Activity_C"
            android:label="@string/title_activity_activity__c"
            android:screenOrientation="portrait"
            android:launchMode="singleTask">

</activity>

In EDIT 2 post these is NO launchMode specified (default is used) and WORKS

OBS: Same issues if you try to set launch mode via intent with (maybe other flag combinations too)

intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

* Now the key questions are:

*Why this is not working - why "singleInstance" or "singleTask" are not good for launching activities from system notification?

*Why only default works?

P.S.

IN MY CASE I thought that if I want to launch an activity from system notif I can do it in any way I want, any task I want to create and as single as I want it to be :D etc.

Also I thought that may Main app stack is preserved in overall system back stack --- and when the activity opened by notif is closed (back button) (it was over main app activity) --- it will also close it's artificial stack and bring/switch to main app activity stack + normal app resume ->> BUT NO - it re-creates the main activity.

If someone with much more knowledge on this can explain these "whys" will be much appreciated, it's a root cause for our problems anyway... :)

I'll try to find out the reasons/solution, if I do - I'll post it.

One last OBS: When I was experimenting with launchModes for notif activity I was forced to restart the device in order to see the default mode working again, because main app reinstall did not kill residual activities from the experiments.

Happy New Year!

0
george50450 On

I think that the following line in the manifest did the trick. android:launchMode="singleTask"

"In this launch mode a new task will always be created and a new instance will be pushed to the task as the root one. If an instance of activity exists on the separate task, a new instance will not be created and Android system routes the intent information through onNewIntent() method."