How to decide what Activity
a Notification
should launch if the target might depend on the configuration (screen size, orientation etc); as is often the case when one uses Fragment
s?
Let's consider the NewsReader sample that demonstrates how to use Fragment
s to produce an app that plays well with multiple screen sizes and orientations. This app is structured as follows:
HeadlinesFragment
.ArticleFragment
.NewsReaderActivity
). In dual pane mode, this activity contains both the fragments. In single-pane mode, it only contains the HeadlinesFragment
.ArticleActivity
. This activity is only used in single pane mode; and it contains the ArticleFragment
.Now, suppose I were to enhance this app to add a background Service
that listens for news updates and notifies the user via status bar notifications whenever there are new news items. A reasonable requirements listing might read like so:
Note that these requirements translate to different target activities depending on current configuration. In particular,
NewsReaderActivity
.NewsReaderActivity
.ArticleActivity
.What would be an elegant way to achieve (2) and (3) above? I think one can safely rule out the possibility of the Service
probing for the current configuration to decide what activity to target with the PendingIntent
.
One solution I thought of was to skip (2) and always do (3) - i.e., always launch ArticleActivity
if there's only one news update. This snippet from ArticleActivity looked promising:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
//...
// If we are in two-pane layout mode, this activity is no longer necessary
if (getResources().getBoolean(R.bool.has_two_panes)) {
finish();
return;
}
//...
//...
}
This code ensures that if one is viewing the ArticleActivity
, but switches to a configuration where it is no longer required (for example from portrait to landscape); then the activity simple closes.
However, this won't work in our case since the intent will have the FLAG_ACTIVITY_NEW_TASK
flag set; we would have created a new task, and there is no "previous" activity on the stack. So, calling finish()
would just clear the entire stack.
So, how does one decide what activity to launch from a notification, if the activity to launch depends on screen configuration?
This is a really good question!
I think one can safely rule out the possibility of the Service probing for the current configuration to decide what activity to target with the PendingIntent.
I'd agree that it would be preferable to keep UI decisions at the UI layer, but having the service make the decision would certainly be an expedient choice. You might use a static method on a UI layer class to keep the decision code technically outside of the service (e.g., a static createArticlePendingIntent()
method on NewsReaderActivity
that the service uses to build its Notification
).
So, how does one decide what activity to launch from a notification, if the activity to launch depends on screen configuration?
Use a getActivity()
PendingIntent
for NewsReaderActivity
in your Notification
, with enough extras that NewsReaderActivity
knows that it is in this "show the article" scenario. Before it calls setContentView()
, have it determine if ArticleActivity
is the right answer. If so, NewsReaderActivity
calls startActivity()
to launch ArticleActivity
, then calls finish()
to get rid of itself (or not, if you want BACK from the article to go to NewsReaderActivity
).
Or, use a getActivity()
PendingIntent
for ICanHazArticleActivity
in your Notification
. ICanHazArticleActivity
has Theme.NoDisplay
, so it will not have a UI. It makes the decision of whether to launch NewsReaderActivity
or ArticleActivity
, calls startActivity()
on the right answer, and then calls finish()
. The advantage over the previous solution is that there is no brief flash of NewsReaderActivity
if the end destination is ArticleActivity
.
Or, use the createArticlePendingIntent()
option I mentioned in the first paragraph of my answer.
There may be other options as well, but those are what come to mind.
When I'm using a dual pane/single pane approach in my apps then I'm only using one activity. In this case it means get rid of ArticleActivity. Here's how you could proceed instead:
First off you control the appearance of the fragments by using XML layouts for the different configurations, e.g:
single pane (res/layout):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >
<FrameLayout
android:id="@+id/mainFragment"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>
dual pane (res/layout-xlarge-land):
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >
<FrameLayout
android:id="@+id/mainFragment"
android:layout_weight="2"
android:layout_width="0dp"
android:layout_height="fill_parent" />
<FrameLayout
android:id="@+id/detailsFragment"
android:layout_weight="1"
android:layout_width="0dp"
android:layout_height="fill_parent" />
</LinearLayout>
In NewsReaderActivity you only need to check which fragments are existing in the layout:
boolean isMainFragment = (findViewById(R.id.mainFragment) != null);
if (isMainFragment) {
mainFragment = new ListFragment();
}
boolean isDetailFragment = (findViewById(R.id.detailsFragment) != null);
if (isDetailFragment) {
detailFragment = new DetailsFragment();
}
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
if (isMainFragment ) {
transaction.add(R.id.mainFragment, mainFragment);
}
if (isDetailFragment ) {
transaction.add(R.id.detailFragment, detaislFragment);
}
transaction.commit();
Then when you are in single pane mode (detailsFragment not existing) and want to show the details screen then you simply launch the same activity again but include a parameter in the intent to tell which content is needed:
void onViewDetails() {
Intent i = new Intent(this, NewsReaderActivity.class);
i.putExtra("showDetails", true);
startActivity(i);
}
In onCreate() of your activity you choose the fragment depending on this parameter:
boolean isDetailFragment = (findViewById(R.id.detailsFragment) != null);
if (!isDetailFragment) {
boolean showDetails = getIntent().getBooleanExtra("showDetails", false);
if (showDetails) {
mainFragment = new DetailsFragment();
}
else {
mainFragment = new ListFragment();
}
}
Now when you finally launch the activity from the notification then you just don't care about the current screen orientation anymore!
Just include the "showDetails" flag in your intent and set it as appropriate, just like it's done in onViewDetails()
. Your activity will then show both fragments when you are in dual pane mode (you can still do some special behavior if "showDetails" is true), or otherwise when you are in a configuration that requires single pain mode, then the fragment you specified in the "showDetails" flag is shown.
I hope this helps and gives you a good understanding about this approach.
The no-UI Activity approach mentioned in the accepted answer is what I decided on. I tried another option, but it did not work out. What I tried was this:
Service
, build an Intent
stack with the Intent
for NewsReaderActivity
at the bottom of the stack and that for ArticleActivity
at the top.PendingIntent.getActivities()
and pass in the stack as created in Step1 to obtain PendingIntent
representing the full stack.In ArticleActivity
, we have the following code:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//...
//...
// If we are in two-pane layout mode, this activity is no longer necessary
if (getResources().getBoolean(R.bool.has_two_panes)) {
finish();
return;
}
//...
//...
}
So, you first target ArticleActivity
- which does a sort of self-introspection to decide if it is useful in the current configuration. If not, it simply moves out of the way with finish()
. Since the NewsReaderActivity
is already present "before" ArticleActivity
in the back-stack, this takes you to NewsReaderActivity
.
This seemed the perfect solution - except for one point which I had overlooked: PendingIntent.getActivities()
only works with API 11 or above. There is a rough equivalent in the support library: TaskStackBuilder
. One can go on adding Intents to the stack with addNextIntent()
, and finally call getPendingIntent()
to achieve something similar to PendingIntent.getActivities()
(I assume).
However, on pre-HC devices, this will result in only the top-most Activity
from the stack being started in a new task (emphasis mine):
On devices running Android 3.0 or newer, calls to the startActivities() method or sending the PendingIntent generated by getPendingIntent(int, int) will construct the synthetic back stack as prescribed. On devices running older versions of the platform, these same calls will invoke the topmost activity in the supplied stack, ignoring the rest of the synthetic stack and allowing the back key to navigate back to the previous task.
So on pre-HC devices, pressing back from ArticleActivity will still take you back to the task that was running prior to ours. This is not what we want.
I will probably share my project sometime soon. I was also concerned about starting fresh tasks even when I was doing intra-app notifications (for example, a "new news article" notification while I am reading an article). I'll hopefully post that as a separate question.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With