Fragments and Notifications: Target different Acti

2019-03-18 09:23发布

问题:

Question:

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 Fragments?


Details:

Let's consider the NewsReader sample that demonstrates how to use Fragments to produce an app that plays well with multiple screen sizes and orientations. This app is structured as follows:

  • A HeadlinesFragment.
  • An ArticleFragment.
  • A "main" activity (NewsReaderActivity). In dual pane mode, this activity contains both the fragments. In single-pane mode, it only contains the HeadlinesFragment.
  • An 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:

  1. If there are multiple news updates, clicking on the notification should always take the user to the headlines list.
  2. If there's only one update, clicking on the notification should open up the brand new news article.

Note that these requirements translate to different target activities depending on current configuration. In particular,

  1. Requirement (1) in either mode = NewsReaderActivity.
  2. Requirement (2) in dual-pane mode = NewsReaderActivity.
  3. Requirement (2) in single-pane mode = 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?

回答1:

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.



回答2:

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.



回答3:

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:

  1. In the Service, build an Intent stack with the Intent for NewsReaderActivity at the bottom of the stack and that for ArticleActivity at the top.
  2. Use PendingIntent.getActivities() and pass in the stack as created in Step1 to obtain PendingIntent representing the full stack.
  3. 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.