0

I am writing an app that can be launched from another app by receiving an intent with ACTION_VIEW or ACTION_EDIT. For example, it can be opened by viewing an email attachment. The trouble is that when you click on the home button and click again on the launch icon of the email app you were using, my activity is killed and any user edits that had been made are lost. What I want to happen is that when the user clicks the home button, my activity is re-parented so that it resumes when the user clicks on the launch icon of my app. I've tried setting android:allowTaskReparenting="true" in manifest.xml but this doesn't work. Sometimes it doesn't have any effect at all, and sometimes the activity is moved to my launch icon, yet still gets killed when you click again on the email app icon. The documentation on allowTaskReparenting is really vague. It says the property means:

“Whether or not the activity can move from the task that started it to the task it has an affinity for.”

What does the word can mean here? What I want is a guarantee that the activity does move (and stays there). Is there any way to achieve this?

Thanks in advance to anyone who can help.

EDIT

In response to comments below, I have put together a baby version demonstrating the problems I am encountering. When you start EditFileActivity by clicking on a file in another app (e.g, an attachment to an email) you can then edit the file. But clicking on the home icon and then clicking again on the email app icon causes the changes you have made to the file to be lost. I want the android system to only forget about an instance of EditFileActivity if the user explicitly clicks back and then says "yes" or "no". Ideally I want all instances of EditFileActivity to stack up on my app's launch icon. I could implement something similar to this by using singleTask or singleInstance and writing some kind of activity showing all open files in tabs, but it would be much easier if I could get the android system itself to help me. Any ideas?

Here is a complete project demonstrating the problem.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
         package="com.example.Example"
         android:versionCode="1"
         android:versionName="1.0">
   <uses-sdk
       android:minSdkVersion="11"
       android:targetSdkVersion="11"/>
   <application
       android:label="Example"
       android:icon="@drawable/ic_launcher">
       <activity
           android:name=".LaunchActivity"
           android:label="LaunchActivity"
           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=".EditFileActivity"
           android:label="EditFileActivity"
           android:screenOrientation="portrait">
           <!-- This is just an example. I wouldn't use this intent filter in a real app! -->
           <intent-filter>
               <action android:name="android.intent.action.VIEW"/>
               <action android:name="android.intent.action.EDIT"/>
               <category android:name="android.intent.category.DEFAULT"/>
               <category android:name="android.intent.category.BROWSABLE"/>
               <data android:scheme="file"/>
               <data android:scheme="content"/>
               <data android:mimeType="*/*"/>
               <data android:host="*"/>
           </intent-filter>
       </activity>
   </application>
</manifest>` 

LaunchActivity:

public class LaunchActivity extends Activity {

   @Override
   public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       TextView textView = new TextView(this);
       textView.setText("This is the activity you see when you click on the application's launch icon. It does absolutely nothing.");
       textView.setTextSize(18);
       setContentView(textView);
   }
}

EditFileActivity:

public class EditFileActivity extends Activity {

   // This String represents the contents of the file.
   // In a "real" app the String would be initialised by reading the data from the Intent that started the activity.
   // However, for the purposes of this example, the initial value is "Default".
   private String fileContents = "Default";
   private boolean editsMade = false;
   private TextView textView;

   @Override
   public void onCreate(Bundle savedInstanceState) {
       super.onCreate(savedInstanceState);
       textView = new TextView(this);
       textView.setText(fileContents);
       textView.setTextSize(18);
       textView.setPadding(10, 10, 10, 10);
       setContentView(textView);
       textView.setOnClickListener(new View.OnClickListener() {
           @Override
           public void onClick(View v) {
               makeEdits();
           }
       });     
   }

   @Override
   public void onBackPressed() {
       if (editsMade) {
           savePrompt();
       } else {
           finish();
       }
   }

   private void savePrompt() {
       DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
           @Override
           public void onClick(DialogInterface dialog, int which) {
               if (which == Dialog.BUTTON_POSITIVE) {
                   // Here is where I would save the edited file.
                   Toast.makeText(EditFileActivity.this, "File saved", Toast.LENGTH_LONG).show();
               }
               finish();
           }
       };
       new AlertDialog.Builder(this)
               .setTitle("Close File")
               .setMessage("Do you want to save the changes you made?")
               .setPositiveButton("Yes", listener)
               .setNegativeButton("No", listener)
               .show();
   }

   private void makeEdits() {
       final EditText editText = new EditText(this);
       editText.setText(fileContents);
       new AlertDialog.Builder(this)
               .setTitle("Edit File")
               .setView(editText)
               .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                   @Override
                   public void onClick(DialogInterface dialog, int whichButton) {
                       Editable editable = editText.getText();
                       assert editable != null;
                       String newContents = editable.toString();
                       if (!fileContents.equals(newContents)) {
                           editsMade = true;
                           fileContents = newContents;
                           textView.setText(fileContents);
                       }
                   }
               })
               .setNegativeButton("Cancel", null)
               .show();
   }
}

UPDATE 10/12/2014

The problems encountered were due to the use of the Intent flag FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET. Fortunately, Google have deprecated this flag, as of API Level 21.

Paul Boddington
  • 37,127
  • 10
  • 65
  • 116
  • Have you tried using singleInstance? – Cookster Aug 29 '14 at 03:50
  • I've tried everything. Nothing connected to tasks seems to reliably work. I don't think I want singleInstance anyway. I want user to be able to have several instances open at the same time that get stacked up on my app's launch icon. – Paul Boddington Aug 29 '14 at 03:53
  • 1
    I believe the problem is, a task doesn't exist with the affinity. Therefore the activity can't be re-parented. You could test this, by opening your app, then pressing the home button (task now exists in background), then press email app and do your action_view/edit. – Cookster Aug 29 '14 at 04:07
  • No, I've tried it both with my app already open and not already open. It's not budging. It's definitely possible to do it even if the task doesn't exist anyway. If I open the Yahoo mail app, follow a link in an email and complete the task with Chrome, the Chrome activity is currently in the Yahoo task. But when I then click on the home button, the Chrome activity moves to the Chrome icon, even if Chrome wasn't initially showing up in my list of recent tasks. How do you do that?! – Paul Boddington Aug 29 '14 at 04:36
  • Chrome uses `singleTask` flag. When it is launched from mail app, it will be launched in it's own task, however back key is handled correctly so that user can go back to mail app, as though chrome is working in email app task. Did you debug why your activity is killed on clicking the email app? I feel something is wrong at this place. – Manish Mulimani Aug 29 '14 at 15:38
  • When I use singleTask I start to get somewhere, my activity starts in a new task on my icon, which is good. However, I'm not sure haw to handle the back key correctly so you return to the email app - as you don't know the sender of the intent, surely all you can do is call finish()? As for why my activity is killed on clicking the email app, I'm not even sure that's a bug - it happens with all apps. For example if I click on a photo in an email and open it in Gallery, you find that you get the email app - not Gallery - when you click on the email icon a second time. – Paul Boddington Aug 29 '14 at 16:09
  • You might need to use setResult() prior to calling finish to alert the email app. But I believe Manish is correct and singleTask is the way to go. – Cookster Aug 29 '14 at 17:27
  • If you use `singleTask`, you will not be able to explicitly manage going back to the sender of the Intent. Hence `singleTask` and `singleInstance` are not recommended for general use, as it will result in interaction model which user is not aware of. Still I'm not able to understand, why your activity is killed when email app is launched again, as your activity should have remained with the email app task for ever, until killed. If possible please share snippets of manifest file, it might help. – Manish Mulimani Aug 30 '14 at 03:29

2 Answers2

1

Issue:

The trouble is that when you click on the home button and click again on the launch icon of the email app you were using, my activity is killed and any user edits that had been made are lost.

This happens because the email application had set FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET intent flag while launching your activity. When this flag is set, next time when the task is brought to the foreground, your activity will be finished, so that user returns to the previous activity.

From docs:

This is useful for cases where you have a logical break in your application. For example, an e-mail application may have a command to view an attachment, which launches an image view activity to display it. This activity should be part of the e-mail application's task, since it is a part of the task the user is involved in. However, if the user leaves that task, and later selects the e-mail app from home, we may like them to return to the conversation they were viewing, not the picture attachment, since that is confusing. By setting this flag when launching the image viewer, that viewer and any activities it starts will be removed the next time the user returns to mail.

Solution: Use singleTask launchMode for your activity. The email app will not kill your activity, as the activity belongs to different task now.

If the activity instance is already in the task and an attempt is made to launch the activity again, then a new instance is not created. Instead onNewIntent is called. Here you can prompt the user to save the previous edit if any, before presenting new content.

Manish Mulimani
  • 17,535
  • 2
  • 41
  • 60
  • Thank you for clarifying this. I'm convinced you're right and that FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET is the cause of the problem. However, the documentation for allowTaskReparenting seems to imply that I might be able to get the activity to switch tasks without using singleTask, which brings me back to my original question... – Paul Boddington Sep 03 '14 at 08:22
  • @pbabcdefp With the shared code, I set `allowTaskReparenting` to true, launched the activity from Gmail app by clicking on the attachment, pressed the home button. I went to Launcher app and clicked the LaunchActivity icon, the task move did happen. Ensure you don't use `singleTask` and `singleInstance` launch modes with `allowTaskReparenting`. – Manish Mulimani Sep 03 '14 at 11:31
  • I'm not getting it to work, and - worse - I'm getting different results on different devices. On my Samsung Galaxy Ace 2, allowTaskReparenting works once. You can click on the launch icon once and you find that EditFileActivity has moved. But click on the icon a second time and it's gone. On my Motorola Moto G, allowTaskReparenting doesn't seem to have any effect at all. I'm going to cut my losses and give up on this one. I'll go down the singleTask route, but it's a lot of extra work. – Paul Boddington Sep 03 '14 at 23:21
  • Hmm.. somehow on my Moto G, it works as expected. If it does not work on some devices, then it certainly makes sense to go ahead with `singleTask`. – Manish Mulimani Sep 04 '14 at 02:38
  • BTW, even if `allowTaskReparenting` works, when user clicks on say gmail app, your activity will be destroyed. Hence `singleTask` is the only way to go. – Manish Mulimani Sep 04 '14 at 11:06
0

As discussed above, the system's default behavior preserves the state of an activity when it is stopped. This way, when users navigate back to a previous activity, its user interface appears the way they left it. However, you can—and should—proactively retain the state of your activities using callback methods, in case the activity is destroyed and must be recreated.

When the system stops one of your activities (such as when a new activity starts or the task moves to the background), the system might destroy that activity completely if it needs to recover system memory. When this happens, information about the activity state is lost. If this happens, the system still knows that the activity has a place in the back stack, but when the activity is brought to the top of the stack the system must recreate it (rather than resume it). In order to avoid losing the user's work, you should proactively retain it by implementing the onSaveInstanceState() callback methods in your activity.

Source

yiati
  • 995
  • 1
  • 12
  • 27
  • There's nothing in that about how to get an activity to move from one tasks to another. It doesn't even mention tasks or allowTaskReparenting. – Paul Boddington Aug 29 '14 at 04:43
  • Have you tried [android:alwaysRetainTaskState](http://developer.android.com/guide/topics/manifest/activity-element.html#always)? – yiati Aug 29 '14 at 05:35
  • The documentation on alwaysRestainTaskState says "This attribute is meaningful only for the root activity of a task; it's ignored for all other activities." My activity is not the root of a task. There's another question on SO about alwaysRetainTaskState, where even the accepted answer ends with the words "finally I just gave up and yanked this code out."! Essentially I think the whole android task system is full of bugs and nobody seems to know how it works. I'm hoping I get proved wrong. – Paul Boddington Aug 29 '14 at 05:41