Universal Music Player 源码分析 (三)-- 其他类分析

文章集合:
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<String, Bitmap[]>(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);
                }
            }
        }
    }

这段代码告诉我两件事情,

  1. 一个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的实现和原理

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,761评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,953评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,998评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,248评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,130评论 4 356
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,145评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,550评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,236评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,510评论 1 291
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,601评论 2 310
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,376评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,247评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,613评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,911评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,191评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,532评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,739评论 2 335

推荐阅读更多精彩内容