Improve performance of notifications

This commit is contained in:
Synced Synapse 2015-02-15 22:42:20 +00:00
parent 4c18a6563c
commit d10df30080
3 changed files with 114 additions and 35 deletions

View File

@ -23,6 +23,8 @@ import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.IBinder;
import android.support.v4.app.NotificationCompat;
@ -30,13 +32,17 @@ import android.support.v4.app.TaskStackBuilder;
import android.view.View;
import android.widget.RemoteViews;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import com.syncedsynapse.kore2.R;
import com.syncedsynapse.kore2.host.HostConnectionObserver;
import com.syncedsynapse.kore2.host.HostManager;
import com.syncedsynapse.kore2.jsonrpc.type.ListType;
import com.syncedsynapse.kore2.jsonrpc.type.PlayerType;
import com.syncedsynapse.kore2.ui.RemoteActivity;
import com.syncedsynapse.kore2.utils.CharacterDrawable;
import com.syncedsynapse.kore2.utils.LogUtils;
import com.syncedsynapse.kore2.utils.UIUtils;
import com.syncedsynapse.kore2.utils.Utils;
/**
@ -162,11 +168,14 @@ public class NotificationService extends Service
stopSelf();
}
// Picasso target that will be used to load images
private static Target picassoTarget = null;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
private void buildNotification(PlayerType.GetActivePlayersReturnType getActivePlayerResult,
PlayerType.PropertyValue getPropertiesResult,
ListType.ItemsAll getItemResult) {
String title, underTitle, poster;
final String title, underTitle, poster;
int smallIcon, playPauseIcon, rewindIcon, ffIcon;
boolean isVideo = ((getItemResult.type.equals(ListType.ItemsAll.TYPE_MOVIE)) ||
@ -229,7 +238,7 @@ public class NotificationService extends Service
}
// Setup the collpased and expanded notifications
RemoteViews collapsedRV = new RemoteViews(this.getPackageName(), R.layout.notification_colapsed);
final RemoteViews collapsedRV = new RemoteViews(this.getPackageName(), R.layout.notification_colapsed);
collapsedRV.setImageViewResource(R.id.rewind, rewindIcon);
collapsedRV.setOnClickPendingIntent(R.id.rewind, rewindPendingItent);
collapsedRV.setImageViewResource(R.id.play, playPauseIcon);
@ -239,7 +248,7 @@ public class NotificationService extends Service
collapsedRV.setTextViewText(R.id.title, title);
collapsedRV.setTextViewText(R.id.text2, underTitle);
RemoteViews expandedRV = new RemoteViews(this.getPackageName(), R.layout.notification_expanded);
final RemoteViews expandedRV = new RemoteViews(this.getPackageName(), R.layout.notification_expanded);
expandedRV.setImageViewResource(R.id.rewind, rewindIcon);
expandedRV.setOnClickPendingIntent(R.id.rewind, rewindPendingItent);
expandedRV.setImageViewResource(R.id.play, playPauseIcon);
@ -248,7 +257,7 @@ public class NotificationService extends Service
expandedRV.setOnClickPendingIntent(R.id.fast_forward, ffPendingItent);
expandedRV.setTextViewText(R.id.title, title);
expandedRV.setTextViewText(R.id.text2, underTitle);
int expandedIconResId;
final int expandedIconResId;
if (isVideo) {
expandedIconResId = R.id.icon_slim;
expandedRV.setViewVisibility(R.id.icon_slim, View.VISIBLE);
@ -261,7 +270,7 @@ public class NotificationService extends Service
// Build the notification
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
Notification notification = builder
final Notification notification = builder
.setSmallIcon(smallIcon)
.setShowWhen(false)
.setOngoing(true)
@ -271,30 +280,66 @@ public class NotificationService extends Service
.setContent(collapsedRV)
.build();
// Load images. Use the same dimensions as the remote to hit the cache both times
// This is a convoluted way of loading the image and showing the
// notification, but it's what works with Picasso and is efficient.
// Here's what's going on:
//
// 1. The image is loaded asynchronously into a Target, and only after
// it is loaded is the notification shown. Using targets is a lot more
// efficient than letting Picasso load it directly into the
// notification imageview, which causes a lot of flickering
//
// 2. The target needs to be static, because Picasso only keeps a weak
// reference to it, so we need to keed a strong reference and reset it
// to null when we're done. We also need to check if it is not null in
// case a previous request hasn't finished yet.
//
// 3. We can only show the notification after the bitmap is loaded into
// the target, so it is done in the callback
//
// 4. We specifically resize the image to the same dimensions used in
// the remote, so that Picasso reuses it in the remote and here from the cache
Resources resources = this.getResources();
int posterWidth = resources.getDimensionPixelOffset(R.dimen.now_playing_poster_width);
int posterHeight = isVideo?
final int posterWidth = resources.getDimensionPixelOffset(R.dimen.now_playing_poster_width);
final int posterHeight = isVideo?
resources.getDimensionPixelOffset(R.dimen.now_playing_poster_height):
posterWidth;
if (picassoTarget == null ) {
picassoTarget = new Target() {
@Override
public void onBitmapLoaded(Bitmap bitmap, Picasso.LoadedFrom from) {
showNotification(bitmap);
}
HostManager hostManager = HostManager.getInstance(this);
hostManager.getPicasso()
.load(hostManager.getHostInfo().getImageUrl(poster))
.resize(posterWidth, posterHeight)
.into(collapsedRV, R.id.icon, NOTIFICATION_ID, notification);
@Override
public void onBitmapFailed(Drawable errorDrawable) {
CharacterDrawable avatarDrawable = UIUtils.getCharacterAvatar(NotificationService.this, title);
showNotification(Utils.drawableToBitmap(avatarDrawable, posterWidth, posterHeight));
}
if (Utils.isJellybeanOrLater()) {
notification.bigContentView = expandedRV;
@Override
public void onPrepareLoad(Drawable placeHolderDrawable) { }
private void showNotification(Bitmap bitmap) {
collapsedRV.setImageViewBitmap(R.id.icon, bitmap);
if (Utils.isJellybeanOrLater()) {
notification.bigContentView = expandedRV;
expandedRV.setImageViewBitmap(expandedIconResId, bitmap);
}
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, notification);
picassoTarget = null;
}
};
// Load the image
HostManager hostManager = HostManager.getInstance(this);
hostManager.getPicasso()
.load(hostManager.getHostInfo().getImageUrl(poster))
.resize(posterWidth, posterHeight)
.into(expandedRV, expandedIconResId, NOTIFICATION_ID, notification);
.into(picassoTarget);
}
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(NOTIFICATION_ID, notification);
}
private PendingIntent buildActionPendingIntent(int playerId, String action) {

View File

@ -129,22 +129,8 @@ public class UIUtils {
String imageUrl, String stringAvatar,
ImageView imageView,
int imageWidth, int imageHeight) {
// Load character avatar
if (characterAvatarColors == null) {
characterAvatarColors = context.getResources()
.obtainTypedArray(R.array.character_avatar_colors);
}
char charAvatar = TextUtils.isEmpty(stringAvatar) ?
' ' : stringAvatar.charAt(0);
avatarColorsIdx = TextUtils.isEmpty(stringAvatar) ? 0 :
Math.max(Character.getNumericValue(stringAvatar.charAt(0)) +
Character.getNumericValue(stringAvatar.charAt(stringAvatar.length() - 1)) +
stringAvatar.length(), 0) % characterAvatarColors.length();
int color = characterAvatarColors.getColor(avatarColorsIdx, 0xff000000);
CharacterDrawable avatarDrawable = new CharacterDrawable(charAvatar, color);
// avatarColorsIdx = randomGenerator.nextInt(characterAvatarColors.length());
CharacterDrawable avatarDrawable = getCharacterAvatar(context, stringAvatar);
if (TextUtils.isEmpty(imageUrl)) {
imageView.setImageDrawable(avatarDrawable);
return;
@ -166,6 +152,31 @@ public class UIUtils {
}
}
/**
* Returns a CharacterDrawable that is suitable to use as an avatar
* @param context Context
* @param str String to use to create the avatar
* @return Character avatar to use in a image view
*/
public static CharacterDrawable getCharacterAvatar(Context context, String str) {
// Load character avatar
if (characterAvatarColors == null) {
characterAvatarColors = context
.getResources()
.obtainTypedArray(R.array.character_avatar_colors);
}
char charAvatar = TextUtils.isEmpty(str) ?
' ' : str.charAt(0);
avatarColorsIdx = TextUtils.isEmpty(str) ? 0 :
Math.max(Character.getNumericValue(str.charAt(0)) +
Character.getNumericValue(str.charAt(str.length() - 1)) +
str.length(), 0) % characterAvatarColors.length();
int color = characterAvatarColors.getColor(avatarColorsIdx, 0xff000000);
// avatarColorsIdx = randomGenerator.nextInt(characterAvatarColors.length());
return new CharacterDrawable(charAvatar, color);
}
/**
* Sets play/pause button icon on a ImageView, based on speed
* @param context Activity

View File

@ -17,6 +17,10 @@ package com.syncedsynapse.kore2.utils;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Build;
import android.text.TextUtils;
@ -131,4 +135,23 @@ public class Utils {
context.startActivity(intent);
}
}
/**
* Converts a drawable to a bitmap
* @param drawable Drawable to convert
* @return Bitmap
*/
public static Bitmap drawableToBitmap (Drawable drawable, int width, int height) {
if (drawable instanceof BitmapDrawable) {
return ((BitmapDrawable)drawable).getBitmap();
}
Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight());
drawable.draw(canvas);
return bitmap;
}
}