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?
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.
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
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
.
Don't store Notification
object, but the Notification.Builder
object. Produce new notification every time before pushing it to
NotificationManager.notify( id, notification );