Keep Activity state when starting from Notification Intent - android

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:
If the application is not running, start the application normally and go to MyMainActivity.
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.
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:
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.
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);
}

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.

Related

Backstack not cleared when notification clicked

I'm using Firebase Cloud Messenger (FCM) to push notifications to my app.
The notifications are received when the app is in the background so onMessageReceived is not triggered as the notification doesn't have a payload (data). Everything is fine with that but it means I can't create my own notification as everything is automatically handled by System tray.
When I click the notification I expect the entire backstack to be cleared and the app to restart from scratch. Basically I want the opposite of this post.
This is supposed to be the default behaviour.
However, when I click on the notification, if the app was already opened, the app restarts from the launcher but on top of the existing backstack.
For instance if you have:
HomeScreen -> Page1
when the notification is clicked, you now have in the stack:
HomeScreen -> Page1 -> HomeScreen
when it's supposed to only be:
HomeScreen
My launcher is an Activity only displayed when the app starts so I don't want it to be kept in the backstack. I turns out this this why I get this issue. So basically if the Launcher Activity calls finish() on itself and/or has noHistory="true" set in the Manifest, the backstack is not cleared when the notification is clicked.
How can I solve this issue?
I found a solution. The idea is to create a new LauncherActivity in charge of launching the existing one and clearing the backstack in the process.
There are probably other ways to do that but I wanted to keep the original Launcher with noHistory="true" as otherwise I have issue with the transition animation with the next Activity if I implemented the below solution directly to it.
The new Launcher is called StartActivity
In the Manifest:
<activity
android:name=".StartActivity"
android:screenOrientation="portrait"
android:theme="#style/AppTheme">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
The Activity:
public class StartActivity extends AppCompatActivity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(this, LauncherActivity.class);
// Add the flags to clear the stack
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NEW_TASK);
// Start the intent with a defined transition animation. The animation is not
// required but it make the transition seamless.
startActivity(intent, getFadeInOutAnimation(this));
// Necessary for the app not to crash. Basically just a FrameLayout
setContentView(R.layout.activity_start);
}
public static Bundle getFadeInOutAnimation(Context context) {
// Allows us to display a fading animation as transition between the activities.
// The animation can be whatever you want
return ActivityOptions.makeCustomAnimation(context,
R.anim.fade_in, R.anim.fade_out).toBundle();
}
}

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:
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.
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.
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.
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).
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:
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.
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?
This currently works, since the Notification just closes the child Activity, which is ok.
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>
Such a complicated Question :D
Here is how you should treat this problem :
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.
in the intent service,Broadcast something.
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
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.
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.
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
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.
App is in the background: same behavior.
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
Running in the front: The Intent is caught in onNewIntent and everything is golden. Do what we want.
App is in the background: same behavior.
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.

How to receive Intent when USB Accessory is attached if application is already running

I was able to read up here about how to launch my app when a USB accessory is attached, and that much all works fine: I have an IntentFilter built into my manifest which will launch the appropriate activity each time the specified accessory is attached. Here is how that looks:
<activity
android:name=".MainActivity"
android:label="#string/app_name"
android:exported="true">
<intent-filter>
<action android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED" />
</intent-filter>
<meta-data
android:name="android.hardware.usb.action.USB_ACCESSORY_ATTACHED"
android:resource="#xml/accessory_filter" />
</activity>
However, I am having issues if my Activity is already running when the accessory is attached. Within MainActivity, I look for the USB_ACCESSORY_ATTACHED Intent in two places: onCreate, and onNewIntent, like so:
protected void onCreate(Bundle savedInstanceState) {
//Check for some other actions first
if (UsbManager.ACTION_USB_ACCESSORY_ATTACHED.equals(intent.getAction()))
usbAttached(intent);
}
protected void onNewIntent(Intent intent) {
if (UsbManager.ACTION_USB_ACCESSORY_ATTACHED.equals(intent.getAction()))
usbAttached(intent);
}
However, neither onCreate nor onNewIntent is being called when the accessory is plugged in if MainActivity is already running. In that case, I need to close my app before plugging in the accessory, which would be a hassle for users. What would be the appropriate way to receive the Intent from my USB accessory if my activity is already running? Would I need to implement a separate listener within the activity itself?
If you look at the documentation of onNewIntent() it reads:
protected void onNewIntent (Intent intent)
Added in API level 1
This is called for activities that set launchMode to "singleTop" in their package, or if a client used the FLAG_ACTIVITY_SINGLE_TOP flag when calling startActivity(Intent). In either case, when the activity is re-launched while at the top of the activity stack instead of a new instance of the activity being started, onNewIntent() will be called on the existing instance with the Intent that was used to re-launch it.
...
So you need to set the launchMode of your Activity to singleTop in your manifest, otherwise you will receive no calls to onNewIntent(). The result should look something like this:
<activity
android:name=".MainActivity"
android:label="#string/app_name"
android:launchMode="singleTop"
android:exported="true">
...
</activity>
If you think about it that behavior actually makes a lot of sense. Next time look at the documentation before asking here. Don't assume how stuff works if you don't know.
Write a BroadcastReceiver and register it in one your activity lifecycle methods via Context.registerReceiver(...). Don't forget to unregister it in the corresponding teardown lifecycle method.

Android - Wrong activity sometimes starts

I have an Android App with a number of activities. The wrong activity is being started sometimes.
Normally, an Application subclass starts, then start activity (StartAct... android:name="android.intent.action.MAIN", android:name="android.intent.category.LAUNCHER")
does some work and then launches InitializeActivity. This does some work and then fires off my main display activity (MainAct). The first two activities do some essential initialization including setting a static "isInitialized" flag just before the intent is launched for the MainAct.
Activities are launched with startActivity() using a specific intent (...activity.class specified), and call finish() after startActivity().
However, here is what sometimes happen, and I don't know why...
In short, the app is killed and when the icon is pressed to start it, it jumps straight to the third (MainAct) activity. This causes the app to detect an error (isInitialized flag is false) and stop:
Launch the app normally with the Icon:
...Application subclass starts, also fires up some worker threads
...StartActivity runs, then fires InitializeActivity and finishes
...InitializeActivity runs, then sets isInitialized and starts MainAct and finishes
...MainAct starts, runs okay
...Home button is hit and Angry Birds is run
...MainAct logs onPause, then onStop.
...Worker threads owned by Application subclass continue to periodically do stuff and log.
After 25 minutes, the entire application is suddenly killed. This observation is based on thhe end of logging activity,
Time goes by
Home button hit
Launcher ICON is pressed for the app
Application subclass onCreate is called and returns
*MainAct.onCreate is called! (no StartAct, no InitializeActivity)*
What am I missing?
Note: the initialize flag was added because of this issue. It is set in the only place in the code that starts the main activity, and checked only in onCreate in the main activity.
[per request]
Manifest file (slightly redacted). Note that the service in here is not currently used.
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="xxx.yyy.zzz"
android:versionCode="1" android:versionName="1.0.1">
<application
android:icon="#drawable/icon_nondistr"
android:label="#string/app_name"
android:name=".app.MainApp"
android:debuggable="true">
<activity
android:label="#string/app_name"
android:name=".app.StartAct" android:theme="#android:style/Theme.NoTitleBar">
<intent-filter>
<action
android:name="android.intent.action.MAIN" />
<category
android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:label="Html"
android:name=".app.HtmlDisplayAct"/>
<activity
android:label="Init"
android:configChanges="orientation"
android:name=".app.InitializeActivity" android:theme="#android:style/Theme.NoTitleBar"/>
<activity
android:label="MyPrefs"
android:name=".app.PrefsAct" />
<activity
android:label="#string/app_name"
android:theme="#android:style/Theme.NoTitleBar"
android:name=".app.MainAct">
</activity>
<service
android:name=".app.svcs.DataGetterService" />
</application>
<uses-sdk android:minSdkVersion="4"/>
<uses-permission
android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission
android:name="com.android.vending.CHECK_LICENSE" />
<uses-feature
android:name="android.hardware.location.network"
android:required="false" />
</manifest>
The fact that the application is killed because of low memory should be transparent to the users. This is why when the application is killed, Android remembers what was the last activity running in this application, and creates directly this activity when the user returns to the application.
Perhaps you could do something in the onCreate() method of your Application (or of your MainAct) to ensure that everything is properly initialized.
By the way, unless you really need to, you shouldn’t have worker threads doing some work when the user is not using your application. Depending on what you do this could drain the battery quickly, or make the user think that it could drain the battery quickly (which is worse, because the user will uninstall your app!)
You could also make the application finish every activity when the user is quitting the application,
This is really a case of "adding insult to injury" - first Android kills my awesome app, and then when the user restarts my app Android tries to be "helpful" by launching the wrong activity, causing my app to crash. Sigh.
Here is my very kludgy workaround to counteract Android's helpfulness. My app requires that StartActivity must be the first activity, so for all other activities I add one line to the onCreate() method. For example:
public class HelpActivity extends AppCompatActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (StaticMethods.switchToStartActivityIfNecessary(this)) return; // <- this is the magic line
...
}
...
}
To make this work I've added a switch variable to my Application class:
public class OutBackClientApplication extends Application {
...
// Switch to indicate if StartActivity has been started.
// Values: -1 = StartActity has never been started or this is first invocation.
// 0 = normal situation, StartActivity has been run at least once.
private int _startActivityStatus = -1;
public int getStartActivityStatus() { return _startActivityStatus; }
public void setStartActivityStatus(int startActivityStatus) {
_startActivityStatus = startActivityStatus;
}
...
}
And I have a class called StaticMethods, which includes the following method:
public class StaticMethods {
/**
* Method to test for the problematic situation where Android has previously killed this app, and
* then when the user restarts the app Android tries to be helpful by restarting the activity
* that was in the foreground when it killed the app, instead of starting the activity specified
* in the manifest as the launch activity. See here:
* http://stackoverflow.com/questions/6673271/android-wrong-activity-sometimes-starts
*
* The following line should be added to the onCreate() method of every Activity, except for
* StartActivity, of course:
*
* if (StaticMethods.switchToStartActivityIfNecessary(this)) return;
*/
public static boolean switchToStartActivityIfNecessary(Activity currentActivity) {
OutBackClientApplication outBackClientApplication =
(OutBackClientApplication) currentActivity.getApplication();
if (outBackClientApplication.getStartActivityStatus() == -1) {
currentActivity.startActivity(new Intent(currentActivity, StartActivity.class));
currentActivity.finish();
return true;
}
return false;
}
}
Finally, when StartActivity starts it needs to reset the switch:
public class StartActivity extends AppCompatActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((OutBackClientApplication) getApplication()).setStartActivityStatus(0);
...
}
...
}
All that, just to counteract Android's "helpfulness" ...

Android: bug in launchMode="singleTask"? -> activity stack not preserved

My main activity A has as set android:launchMode="singleTask" in the manifest. Now, whenever I start another activity from there, e.g. B and press the HOME BUTTON on the phone to return to the home screen and then again go back to my app, either via pressing the app's button or pressing the HOME BUTTONlong to show my most recent apps it doesn't preserve my activity stack and returns straight to A instead of the expected activity B.
Here the two behaviors:
Expected: A > B > HOME > B
Actual: A > B > HOME > A (bad!)
Is there a setting I'm missing or is this a bug? If the latter, is there a workaround for this until the bug is fixed?
FYI: This question has already been discussed here. However, it doesn't seem that there is any real solution to this, yet.
This is not a bug. When an existing singleTask activity is launched, all other activities above it in the stack will be destroyed.
When you press HOME and launch the activity again, ActivityManger calls an intent
{act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER]flag=FLAG_ACTIVITY_NEW_TASK|FLAG_ACTIVITY_RESET_IF_NEEDED cmp=A}
So the result is A > B > HOME > A.
It's different when A's launchMode is "Standard". The task which contains A will come to the foreground and keep the state the same as before.
You can create a "Standard" activity eg. C as the launcher and startActivity(A) in the onCreate method of C
OR
Just remove the launchMode="singleTask" and set FLAG_ACTIVITY_CLEAR_TOP|FLAG_ACTIVITY_SINGLE_TOP flag whenever call an intent to A
From http://developer.android.com/guide/topics/manifest/activity-element.html on singleTask
The system creates the activity at the root of a new task and routes the intent to it. However, if an instance of the activity already exists, the system routes the intent to existing instance through a call to its onNewIntent() method, rather than creating a new one.
This means when the action.MAIN and category.LAUNCHER flags targets your application from the Launcher, the system would rather route the intent to the existing ActivityA as opposed to creating a new task and setting a new ActivityA as the root. It would rather tear down all activities above existing task ActivityA lives in, and invoke it's onNewIntent().
If you want to capture both the behavior of singleTop and singleTask, create a separate "delegate" activity named SingleTaskActivity with the singleTask launchMode which simply invokes the singleTop activity in its onCreate() and then finishes itself. The singleTop activity would still have the MAIN/LAUNCHER intent-filters to continue acting as the application's main Launcher activity, but when other activities desire calling this singleTop activity it must instead invoke the SingleTaskActivity as to preserve the singleTask behavior. The intent being passed to the singleTask activity should also be carried over to the singleTop Activity, so something like the following has worked for me since I wanted to have both singleTask and singleTop launch modes.
<activity android:name=".activities.SingleTaskActivity"
android:launchMode="singleTask"
android:noHistory="true"/>
public class SingleTaskActivity extends Activity{
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = getIntent();
intent.setClass(this, SingleTop.class);
startActivity(intent);
}
}
And your singleTop activity would continue having its singleTop launch mode.
<activity
android:name=".activities.SingleTopActivity"
android:launchMode="singleTop"
android:noHistory="true"/>
Good luck.
Stefan, you ever find an answer to this? I put together a testcase for this and am seeing the same (perplexing) behavior...I'll paste the code below in case anyone comes along and sees something obvious:
AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example" >
<uses-sdk android:minSdkVersion="3"/>
<application android:icon="#drawable/icon" android:label="testSingleTask">
<activity android:name=".ActivityA"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name=".ActivityB"/>
</application>
</manifest>
ActivityA.java:
public class ActivityA extends Activity implements View.OnClickListener
{
#Override
public void onCreate( Bundle savedInstanceState )
{
super.onCreate( savedInstanceState );
setContentView( R.layout.main );
View button = findViewById( R.id.tacos );
button.setOnClickListener( this );
}
public void onClick( View view )
{
//Intent i = new Intent( this, ActivityB.class );
Intent i = new Intent();
i.setComponent( new ComponentName( this, ActivityB.class ) );
startActivity( i );
}
}
ActivityB.java:
public class ActivityB extends Activity
{
#Override
public void onCreate( Bundle savedInstanceState )
{
super.onCreate( savedInstanceState );
setContentView( R.layout.layout_b );
}
}
I tried changing minSdkVersion to no avail. This just seems to be a bug, at least according to the documentation, which states the following:
As noted above, there's never more than one instance of a "singleTask" or "singleInstance" activity, so that instance is expected to handle all new intents. A "singleInstance" activity is always at the top of the stack (since it is the only activity in the task), so it is always in position to handle the intent. However, a "singleTask" activity may or may not have other activities above it in the stack. If it does, it is not in position to handle the intent, and the intent is dropped. (Even though the intent is dropped, its arrival would have caused the task to come to the foreground, where it would remain.)
I think this is the behaviour you want:
singleTask resets the stack on home press for some retarded reason that I don't understand.
The solution is instead to not use singleTask and use standard or singleTop for launcher activity instead (I've only tried with singleTop to date though).
Because apps have an affinity for each other, launching an activity like this:
Intent launchIntent = context.getPackageManager().getLaunchIntentForPackage(packageName);
if(launchIntent!=null) {
launchIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED);
}
will cause your activty stack to reappear as it was, without it starting a new activity upon the old one (which was my main problem before). The flags are the important ones:
FLAG_ACTIVITY_NEW_TASK Added in API level 1
If set, this activity will become the start of a new task on this
history stack. A task (from the activity that started it to the next
task activity) defines an atomic group of activities that the user can
move to. Tasks can be moved to the foreground and background; all of
the activities inside of a particular task always remain in the same
order. See Tasks and Back Stack for more information about tasks.
This flag is generally used by activities that want to present a
"launcher" style behavior: they give the user a list of separate
things that can be done, which otherwise run completely independently
of the activity launching them.
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. See FLAG_ACTIVITY_MULTIPLE_TASK
for a flag to disable this behavior.
This flag can not be used when the caller is requesting a result from
the activity being launched.
And:
FLAG_ACTIVITY_RESET_TASK_IF_NEEDED Added in API level 1
If set, and this activity is either being started in a new task or
bringing to the top an existing task, then it will be launched as the
front door of the task. This will result in the application of any
affinities needed to have that task in the proper state (either moving
activities to or from it), or simply resetting that task to its
initial state if needed.
Without them the launched activity will just be pushed ontop of the old stack or some other undesirable behaviour (in this case of course)
I believe the problem with not receiving the latest Intent can be solved like this (out of my head):
#Override
public void onActivityReenter (int resultCode, Intent data) {
onNewIntent(data);
}
Try it out!
I've found this issue happens only if the launcher activity's launch mode is set to singleTask or singleInstance.
So, I've created a new launcher activity whose launch mode is standard or singleTop. And made this launcher activity to call my old main activity whose launch mode is single task.
LauncherActivity (standard/no history) -> MainActivity (singleTask).
Set splash screen to launcher activity. And killed launcher activity right after I call the main activity.
public LauncherActivity extends Activity {
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Intent intent = new Intent(this, HomeActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
startActivity(intent);
finish();
}
}
<activity
android:name=".LauncherActivity"
android:noHistory="true"
android:theme="#style/Theme.LauncherScreen">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Launcher screen theme should be set for the case that app is restarting after the process is killed. -->
<activity
android:name=".MainActivity"
android:launchMode="singleTask"
android:theme="#style/Theme.LauncherScreen"/>
Pros: Can keep the MainActivity's launch mode as singleTask to make sure there always is no more than one MainActivity.
If both A and B belong to the same Application, try removing
android:launchMode="singleTask"
from your Activities and test because I think the default behavior is what you described as expected.
Whenever you press the home button to go back to your home screen the activity stack kills some of the previously launched and running apps.
To verify this fact try to launch an app from the notification panel after going from A to B in your app and come back using the back button ..........you will find your app in the same state as you left it.
When using launch mode as singleTop make sure to call finish() (on current activity say A) when starting the next activity (using startActivity(Intent) method say B). This way the current activity gets destroyed.
A -> B -> Pause the app and click on launcher Icon, Starts A
In oncreate method of A, you need to have a check,
if(!TaskRoot()) {
finish();
return;
}
This way when launching app we are checking for root task and previously root task is B but not A. So this check destroys the activity A and takes us to activity B which is currently top of the stack.
Hope it works for you!.
This is how I finally solved this weird behavior. In AndroidManifest, this is what I added:
Application & Root activity
android:launchMode="singleTop"
android:alwaysRetainTaskState="true"
android:taskAffinity="<name of package>"
Child Activity
android:parentActivityName=".<name of parent activity>"
android:taskAffinity="<name of package>"
Add below in android manifest activity, it will add new task to top of the view destroying earlier tasks.
android:launchMode="singleTop" as below
<activity
android:name=".MainActivity"
android:label="#string/app_name"
android:launchMode="singleTop"
android:theme="#style/AppTheme.NoActionBar">
</activity>
In child activity or in B activity
#Override
public void onBackPressed() {
Intent intent = new Intent(getApplicationContext(), Parent.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP|Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);
finish();
}
<activity android:name=".MainActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
//Try to use launchMode="singleTop" in your main activity to maintain single instance of your application. Go to manifest and change.

Categories

Resources