How to update Notification with RemoteViews?

2019-06-23 00:55发布

问题:

I'm creating a notification with RemoteViews from a custom Service, which is running with notification in a foreground mode (that is, service will remain active as long as notification is visible to user). Notification is set as Ongoing so user cannot swipe it off.

I'd like to change for example bitmap shown in ImageView, contained within remote view's layout or change text value in a TextView. Layout in remote view is set with XML layout file.

My problem is that once notification is created and visible to user, if I call any of RemoteViews's functions like setImageViewResource() to change Bitmap shown in an ImageView, the change is not visible, unless I do call setImageViewResource() I call afterwards:

NotificationManager.notify( id, notification );

or

Service.startForeground(id,notification);

This doesn't sound right to me though. I can't believe that to update RemoteViews UI in a notification that is already created, I have to re-initialize notification. If I have Button control in a notification, it updates itself on touch and release. So there's gotta be a way to do this properly, but I don't know how.

Here is my code which creates notification inside my Service instance:

this.notiRemoteViews = new MyRemoteViews(this,this.getApplicationContext().getPackageName(),R.layout.activity_noti1);

Notification.Builder notibuilder = new Notification.Builder(this.getApplicationContext());
notibuilder.setContentTitle("Test");
notibuilder.setContentText("test");
notibuilder.setSmallIcon(R.drawable.icon2);
notibuilder.setOngoing(true);

this.manager = (NotificationManager)this.getSystemService(Context.NOTIFICATION_SERVICE);
this.noti = notibuilder.build();
this.noti.contentView = this.notiRemoteViews;
this.noti.bigContentView = this.notiRemoteViews;
this.startForeground(NOTIFICATION_ID, this.noti);

And function that 'forces' UI changes to notification:

public void updateNotiUI(){
    this.startForeground(NOTIFICATION_ID, this.noti);
}

Within MyRemoteViews class, when required, I do this to make changes to UI:

this.setImageViewResource(R.id.iconOFF, R.drawable.icon_off2);
this.ptMyService.updateNotiUI();

Can anyone tell me what is the correct way of updating UI components of a RemoteViews in the notification?

回答1:

Here's a detail example for you to update the notification using RemoteViews:

private static final int NOTIF_ID = 1234;
private NotificationCompat.Builder mBuilder;
private NotificationManager mNotificationManager;
private RemoteViews mRemoteViews;
private Notification mNotification;
...

// call this method to setup notification for the first time
private void setUpNotification(){

    mNotificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);

    // we need to build a basic notification first, then update it
    Intent intentNotif = new Intent(this, MainActivity.class);
    intentNotif.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
    PendingIntent pendIntent = PendingIntent.getActivity(this, 0, intentNotif, PendingIntent.FLAG_UPDATE_CURRENT);

    // notification's layout
    mRemoteViews = new RemoteViews(getPackageName(), R.layout.custom_notification_small);
    // notification's icon
    mRemoteViews.setImageViewResource(R.id.notif_icon, R.drawable.ic_launcher);
    // notification's title
    mRemoteViews.setTextViewText(R.id.notif_title, getResources().getString(R.string.app_name));
    // notification's content
    mRemoteViews.setTextViewText(R.id.notif_content, getResources().getString(R.string.content_text));

    mBuilder = new NotificationCompat.Builder(this);

    CharSequence ticker = getResources().getString(R.string.ticker_text);
    int apiVersion = Build.VERSION.SDK_INT;

    if (apiVersion < VERSION_CODES.HONEYCOMB) {
        mNotification = new Notification(R.drawable.ic_launcher, ticker, System.currentTimeMillis());
        mNotification.contentView = mRemoteViews;
        mNotification.contentIntent = pendIntent;

        mNotification.flags |= Notification.FLAG_NO_CLEAR; //Do not clear the notification
        mNotification.defaults |= Notification.DEFAULT_LIGHTS;

        // starting service with notification in foreground mode
        startForeground(NOTIF_ID, mNotification);

    }else if (apiVersion >= VERSION_CODES.HONEYCOMB) {
        mBuilder.setSmallIcon(R.drawable.ic_launcher)
                .setAutoCancel(false)
                .setOngoing(true)
                .setContentIntent(pendIntent)
                .setContent(mRemoteViews)
                .setTicker(ticker);

        // starting service with notification in foreground mode
        startForeground(NOTIF_ID, mBuilder.build());
    }
}

// use this method to update the Notification's UI
private void updateNotification(){

    int api = Build.VERSION.SDK_INT;
    // update the icon
    mRemoteViews.setImageViewResource(R.id.notif_icon, R.drawable.icon_off2);
    // update the title
    mRemoteViews.setTextViewText(R.id.notif_title, getResources().getString(R.string.new_title));
    // update the content
    mRemoteViews.setTextViewText(R.id.notif_content, getResources().getString(R.string.new_content_text));

    // update the notification
    if (api < VERSION_CODES.HONEYCOMB) {
        mNotificationManager.notify(NOTIF_ID, mNotification);
    }else if (api >= VERSION_CODES.HONEYCOMB) {
        mNotificationManager.notify(NOTIF_ID, mBuilder.build());
    }
}

Layout for the Notification, i.e. res/layout/custom_notification_small.xml:

<!-- We have to set the height to 64dp, this is the rule of the small notification -->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="64dp"
    android:orientation="horizontal"
    android:id="@+id/notif_small"
    android:background="@drawable/notification_background">

    <ImageView
        android:id="@+id/notif_icon"
        android:contentDescription="@string/notif_small_desc"
        android:layout_width="47dp"
        android:layout_height="wrap_content"
        android:layout_centerVertical="true"
        android:layout_alignParentLeft="true"
        android:src="@drawable/ic_launcher"
        android:layout_marginLeft="7dp"
        android:layout_marginRight="9dp"/>

    <TextView
        android:id="@+id/notif_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/notif_icon"
        android:singleLine="true"
        android:paddingTop="8dp"
        android:textSize="17sp"
        android:textStyle="bold"
        android:textColor="#000000"
        android:text="@string/app_name"/>

    <TextView
        android:id="@+id/notif_content"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_toRightOf="@id/notif_icon"
        android:paddingBottom="9dp"
        android:layout_alignParentBottom="true"
        android:singleLine="true"
        android:textSize="13sp"
        android:textColor="#575757"
        android:text="Content" />
</RelativeLayout>

Hope this example helps you a lot!

NOTE: You can't update the custom NotificationCompat on pre-Honeycomb, so I added an alternative way to update it on pre-Honeycomb, i.e. checking the API level first and use the deprecated Notification instead.



回答2:

WARNING!

The only correct way to update notification is to recreate RemoteViews before each NotificationManager#notify. Why? There's a memory leak leading to TransactionTooLargeException, as it has been reported in those questions:

  • TransactionTooLargeException on Samsung S7
  • TransactionTooLargeException while notify notification
  • Android notification manager TransactionTooLargeException
  • and in my case

Each call on RemoteViews such as setViewVisibility(...) and so on adds corresponding action to queue of actions are to be applied. After notify the remote view is inflated and actions are actually applied. But the queue is not cleared!

Take a look at screenshot taken during debugging this case.

There I'm updating audio player notification with data coming from ViewModel. Application is stopped on line #81 and you can see the RemoteViews instance that has array of actions with size 51! But I only switched audio track twice and pressed pause! Of course I had to observe application crash with TransactionTooLargeException after a while.

Shallow research confirmed there's no public API to directly or indirectly clear actions queue, so the only way to update notification view is to hold its state separately and recreate RemoteViews instance passed to Notification.Builder, anyway this doesn't overload UI thread a lot



回答3:

You would have to call NotificationManager.notify(id, notification) to let Notification System know that you want to update the notification view. Here's the docs link http://developer.android.com/training/notify-user/managing.html.

Have a method which returns Notification object.

private Notification getNotification(NotificationCompat.Builder mBuilder) {
    RemoteViews mRemoteViews = new RemoteViews(getPackageName(), R.layout.notification_layout);
    // Update your RemoteViews
    mBuilder.setContent(mRemoteView);
    Notification mNotification = mBuilder.build();
    // set mNotification.bigContentView if you want to
    return mNotification;

}

private void refreshNotification() {
    mNotificationManager.notify(getNotification(mNotificationBuilder),
                        NOTIFICATION_ID);
    // mNotificationBuilder is initialized already
}

Also, note that bigContentView and RemoteViews are not completely redrawn. If some of the elements of bigContentView has visibility set to GONE, and if you want to show it the next time, you have to explicitly set visibility to VISIBLE.



回答4:

Don't store Notification object, but the Notification.Builder object. Produce new notification every time before pushing it to

NotificationManager.notify( id, notification );