文章集合:
Universal Music Player 源码解析(一)--MediaSession框架
Univeral Music Player 源码解析 -- 让人头疼的playback
Universal Music Player 源码解析(二)--MusicService 和 MediaController
Universal Music Player 源码分析 (三)-- 其他类分析
这篇文章的内容有:
- UMP的一些基本的其他类 如工具类的分析 和图片缓存类
- UMP 的 NotificationManager类和保活措施
BitmapHelper
BitmapHelper中封装了一些对于位图的裁剪处理处理和对图片的获取:
public static Bitmap scaleBitmap(int scaleFactor, InputStream is) {
// Get the dimensions of the bitmap
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
// Decode the image file into a Bitmap sized to fill the View
//没有必要将位图加载,只是将位图的宽和长获取一下 避免OOM
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
return BitmapFactory.decodeStream(is, null, bmOptions);
}
@SuppressWarnings("SameParameterValue")
public static Bitmap fetchAndRescaleBitmap(String uri, int width, int height)
throws IOException {
URL url = new URL(uri);
BufferedInputStream is = null;
try {
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
is = new BufferedInputStream(urlConnection.getInputStream());
is.mark(MAX_READ_LIMIT_PER_IMG);
int scaleFactor = findScaleFactor(width, height, is);
LogHelper.d(TAG, "Scaling bitmap ", uri, " by factor ", scaleFactor, " to support ",
width, "x", height, "requested dimension");
is.reset();
return scaleBitmap(scaleFactor, is);
} finally {
if (is != null) {
is.close();
}
}
}
然后在通过网络请求拿到经过裁剪了的bitmap
AlbumArtCache
这个类使用了LruCache作为缓存处理,key是url value 是bitmap,
声明了一个最大的缓存大小
private static final int MAX_ALBUM_ART_CACHE_SIZE = 12*1024*1024;
看一下构造函数
private AlbumArtCache() {
// Holds no more than MAX_ALBUM_ART_CACHE_SIZE bytes, bounded by maxmemory/4 and
// Integer.MAX_VALUE:
//自定义单位
int maxSize = Math.min(MAX_ALBUM_ART_CACHE_SIZE,
(int) (Math.min(Integer.MAX_VALUE, Runtime.getRuntime().maxMemory()/4)));
mCache = new LruCache(maxSize) {
@Override
protected int sizeOf(String key, Bitmap[] value) {
//返回的单位是用户自定义的
return value[BIG_BITMAP_INDEX].getByteCount()
+ value[ICON_BITMAP_INDEX].getByteCount();
}
};
}
值得一提的是,文档中说明的sizeof返回的大小是用户自定义的,也就是是说不需要严格限定大小是字节还是别的什么的.
MediaNotificationManager
MediaNotificationManager 继承了 BroadcastReceiver 一来是为了让用户可以在通知栏操作播放,另一个是为了让MusicService作为一个前台服务,在low memory的时候不会被干掉,我们看一下他的构造函数:
public MediaNotificationManager(MusicService service) throws RemoteException {
mService = service;
updateSessionToken();
mNotificationColor = ResourceHelper.getThemeColor(mService, R.attr.colorPrimary,
Color.DKGRAY);
mNotificationManager = (NotificationManager) mService.getSystemService(Context.NOTIFICATION_SERVICE);
String pkg = mService.getPackageName();
mPauseIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
new Intent(ACTION_PAUSE).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
mPlayIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
new Intent(ACTION_PLAY).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
mPreviousIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
new Intent(ACTION_PREV).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
mNextIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
new Intent(ACTION_NEXT).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
mStopIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
new Intent(ACTION_STOP).setPackage(pkg), PendingIntent.FLAG_CANCEL_CURRENT);
mStopCastIntent = PendingIntent.getBroadcast(mService, REQUEST_CODE,
new Intent(ACTION_STOP_CASTING).setPackage(pkg),
PendingIntent.FLAG_CANCEL_CURRENT);
// Cancel all notifications to handle the case where the Service was killed and
// restarted by the system.
mNotificationManager.cancelAll();
}
传入的参数是一个MusicService ,同样的,通过传入的遮盖MusicService的参数很容易得到Session.Token
请看updateSessionToken函数:
private void updateSessionToken() throws RemoteException {
MediaSessionCompat.Token freshToken = mService.getSessionToken();
if (mSessionToken == null && freshToken != null ||
mSessionToken != null && !mSessionToken.equals(freshToken)) {
if (mController != null) {
mController.unregisterCallback(mCb);
}
mSessionToken = freshToken;
if (mSessionToken != null) {
mController = new MediaControllerCompat(mService, mSessionToken);
mTransportControls = mController.getTransportControls();
if (mStarted) {
mController.registerCallback(mCb);
}
}
}
}
这段代码告诉我两件事情,
- 一个MediaController最好和一个callback绑定,虽然没有找到文档中相关证据,但是这样肯定有一定的道理;
2.需要时用最新的sessionToken,如果是旧的sessionToken,一定需要更新
我们来看一下刚才构造函数中的pendingIntent有干了什么:
首先是 playIntent 和 pauseIntent
private int addActions(final NotificationCompat.Builder notificationBuilder) {
....
// Play or pause button, depending on the current state.
final String label;
final int icon;
final PendingIntent intent;
if (mPlaybackState.getState() == PlaybackStateCompat.STATE_PLAYING) {
label = mService.getString(R.string.label_pause);
icon = R.drawable.uamp_ic_pause_white_24dp;
intent = mPauseIntent;
} else {
label = mService.getString(R.string.label_play);
icon = R.drawable.uamp_ic_play_arrow_white_24dp;
intent = mPlayIntent;
}
notificationBuilder.addAction(new NotificationCompat.Action(icon, label, intent));
// If skip to next action is enabled
if ((mPlaybackState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0) {
notificationBuilder.addAction(R.drawable.ic_skip_next_white_24dp,
mService.getString(R.string.label_next), mNextIntent);
}
return playPauseButtonPosition;
}
一个Notification.Action 就是这个封装了label intent 还有icon 作为提示的一部分,比如播放action 就是一个播放的按钮,暂停action就是一个暂停的按钮
addActions()最终在createNotifications()中被调用:
private Notification createNotification() {
LogHelper.d(TAG, "updateNotificationMetadata. mMetadata=" + mMetadata);
if (mMetadata == null || mPlaybackState == null) {
return null;
}
MediaDescriptionCompat description = mMetadata.getDescription();
String fetchArtUrl = null;
Bitmap art = null;
if (description.getIconUri() != null) {
// This sample assumes the iconUri will be a valid URL formatted String, but
// it can actually be any valid Android Uri formatted String.
// async fetch the album art icon
String artUrl = description.getIconUri().toString();
art = AlbumArtCache.getInstance().getBigImage(artUrl);
if (art == null) {
fetchArtUrl = artUrl;
// use a placeholder art while the remote art is being downloaded
art = BitmapFactory.decodeResource(mService.getResources(),
R.drawable.ic_default_art);
}
}
// Notification channels are only supported on Android O+.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createNotificationChannel();
}
final NotificationCompat.Builder notificationBuilder =
new NotificationCompat.Builder(mService, CHANNEL_ID);
final int playPauseButtonPosition = addActions(notificationBuilder);
notificationBuilder
//使用了 MediaStyle的特殊的notification
.setStyle(new MediaStyle()
// show only play/pause in compact view
.setShowActionsInCompactView(playPauseButtonPosition)
.setShowCancelButton(true)
.setCancelButtonIntent(mStopIntent)
.setMediaSession(mSessionToken))
.setDeleteIntent(mStopIntent)
.setColor(mNotificationColor)
.setSmallIcon(R.drawable.ic_notification)
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setOnlyAlertOnce(true)
.setContentIntent(createContentIntent(description))
.setContentTitle(description.getTitle())
.setContentText(description.getSubtitle())
.setLargeIcon(art);
if (mController != null && mController.getExtras() != null) {
String castName = mController.getExtras().getString(MusicService.EXTRA_CONNECTED_CAST);
if (castName != null) {
String castInfo = mService.getResources()
.getString(R.string.casting_to_device, castName);
notificationBuilder.setSubText(castInfo);
notificationBuilder.addAction(R.drawable.ic_close_black_24dp,
mService.getString(R.string.stop_casting), mStopCastIntent);
}
}
setNotificationPlaybackState(notificationBuilder);
if (fetchArtUrl != null) {
fetchBitmapFromURLAsync(fetchArtUrl, notificationBuilder);
}
return notificationBuilder.build();
}
这段代码就是对有 mediaController获取的mediaMetaData进行解析,然后对这个notification设置了特殊的style,在addActions的时候,因为最多会有三个按钮,依次是previous play/pause next 所以需要获取到他们的index,然后在
new MediaStyle()
// show only play/pause in compact view
.setShowActionsInCompactView(index)
指定哪个加进去的action可以显示 但是源代码中明确注明了只是加入了play/pause 但是只要稍微改一下:
new MediaStyle()
.setShowActionsInCompactView(0,1,2)
就可以看到三种按钮
再说一下playbackState是如何"告知"notification的:
还记得之前的这个函数嘛:
mController.registerCallback(mCb);
通过在回调函数中
@Override
public void onPlaybackStateChanged(@NonNull PlaybackStateCompat state) {
mPlaybackState = state;
LogHelper.d(TAG, "Received new playback state", state);
if (state.getState() == PlaybackStateCompat.STATE_STOPPED ||
state.getState() == PlaybackStateCompat.STATE_NONE) {
stopNotification();
} else {
Notification notification = createNotification();
if (notification != null) {
mNotificationManager.notify(NOTIFICATION_ID, notification);
}
}
}
同样的对metaData改变的回调也在里面写好了:
@Override
public void onMetadataChanged(MediaMetadataCompat metadata) {
mMetadata = metadata;
LogHelper.d(TAG, "Received new metadata ", metadata);
Notification notification = createNotification();
if (notification != null) {
mNotificationManager.notify(NOTIFICATION_ID, notification);
}
}
都是换汤不换药的notify()进行替换
links
Notifications: Styling
Java 和 Android中的LRU的实现和原理