LocalMedia 是 AndroidCar 中自带的本地音乐播放器,它可以识别出系统中的音乐,并进行播放。本质上属于一个功能比较完善的Demo,官方的目的可能是为了演示如何使用 MediaSession 框架写一个音乐播放器。关于MediaSession框架之前已经介绍过了,本篇就简单解析一下这个Demo。
源码包主要由三部分构成:
com.android.car.media.localmediaplayer 该app是一个Service,主要作用是检索出本地的音乐多媒体,并封装成元数据。
com.android.car.media 主要用于展示HMI和用户交互,源码量非常庞大。
com.android.car.media.common
lib下的基类库,抽成基类主要是为了方便其他应用引用,比如launcher,因为桌面有播放小组件。
其实还有还有一个进程android.car.media ,官方给出的注释是这么介绍它的: CarMediaService 管理汽车应用程序当前活动的媒体源。 这与 MediaSessionManager 的活动会话不同,因为汽车中只能有一个活动源,通过浏览和播放。在汽车中,活动媒体源不一定有活动的 MediaSession,例如 如果它只是被浏览。 但是,该来源仍被视为活动来源,并且应该是任何与媒体相关的 UI(媒体中心、主屏幕等)中显示的来源。
这里就不介绍CarMediaService ,在源码中被分类在com.android.car目录下,已经不属于应用的范畴,本质上属于Framework。
我们先来看看com.android.car.media.localmediaplayer 是如何实现。
源码位于:/packages/apps/Car/LocalMediaPlayer
应用的源码分析讨论都是一样的,先从AndroidManifest开始。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 <manifest xmlns:android ="http://schemas.android.com/apk/res/android" package ="com.android.car.media.localmediaplayer" android:sharedUserId ="com.android.car.media" > <uses-permission android:name ="android.permission.READ_EXTERNAL_STORAGE" /> <application android:theme ="@style/LocalMediaPlayerAppTheme" > <service android:name =".LocalMediaBrowserService" android:exported ="true" android:label ="@string/app_name" > <intent-filter > <action android:name ="android.media.browse.MediaBrowserService" /> </intent-filter > </service > <activity android:name =".PermissionsActivity" android:theme ="@android:style/Theme.Translucent.NoTitleBar.Fullscreen" > <meta-data android:name ="distractionOptimized" android:value ="true" /> </activity > </application > </manifest >
可以看出Service的结构很简单,LocalMediaBrowserService是MediaSession的容器,PermissionsActivity则是负责权限检查和申请。
LocalMediaBrowserService继承自MediaBrowserService,作为一个容器,主要就是用来初始化其它组件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 @Override public void onCreate () { super .onCreate(); mDataModel = new DataModel(this ); addRootItems(); mSession = new MediaSession(this , MEDIA_SESSION_TAG); setSessionToken(mSession.getSessionToken()); mPlayer = new Player(this , mSession, mDataModel); mSession.setCallback(mPlayer); mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS | MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS); mPlayer.maybeRestoreState(); IntentFilter filter = new IntentFilter(); filter.addAction(ACTION_PLAY); filter.addAction(ACTION_PAUSE); filter.addAction(ACTION_NEXT); filter.addAction(ACTION_PREV); registerReceiver(mNotificationReceiver, filter); }
创建 DataModel 用来检索设备本地的多媒体数据。其内部主要封装的都是如何在设备上查询各种多媒体数据。
初始化 RootItem
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 List<MediaBrowser.MediaItem> mRootItems = new ArrayList<>(); private void addRootItems () { MediaDescription folders = new MediaDescription.Builder() .setMediaId(FOLDERS_ID) .setTitle(getString(R.string.folders_title)) .setIconUri(Utils.getUriForResource(this , R.drawable.ic_folder)) .build(); mRootItems.add(new MediaBrowser.MediaItem(folders, MediaBrowser.MediaItem.FLAG_BROWSABLE)); MediaDescription albums = new MediaDescription.Builder() .setMediaId(ALBUMS_ID) .setTitle(getString(R.string.albums_title)) .setIconUri(Utils.getUriForResource(this , R.drawable.ic_album)) .build(); mRootItems.add(new MediaBrowser.MediaItem(albums, MediaBrowser.MediaItem.FLAG_BROWSABLE)); MediaDescription artists = new MediaDescription.Builder() .setMediaId(ARTISTS_ID) .setTitle(getString(R.string.artists_title)) .setIconUri(Utils.getUriForResource(this , R.drawable.ic_artist)) .build(); mRootItems.add(new MediaBrowser.MediaItem(artists, MediaBrowser.MediaItem.FLAG_BROWSABLE)); MediaDescription genres = new MediaDescription.Builder() .setMediaId(GENRES_ID) .setTitle(getString(R.string.genres_title)) .setIconUri(Utils.getUriForResource(this , R.drawable.ic_genre)) .build(); mRootItems.add(new MediaBrowser.MediaItem(genres, MediaBrowser.MediaItem.FLAG_BROWSABLE)); }
RootItems是在HMI查询ROOT_ID时返回的一个列表,列表中包含四个默认的MediaItem,而且Flag都是***FLAG_BROWSABLE***表示MediaItem是可浏览的(文件夹)。四个MediaItem对应HMI上显示的四个大类。
mRootItems 会在onLoadChildren()方法中传给HMI端。HMI端需要调用MediaBrowser .subscribe才能触发onLoadChildren()。
1 2 3 4 5 6 7 8 9 10 11 12 13 @Override public void onLoadChildren (String parentId, Result<List<MediaBrowser.MediaItem>> result) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onLoadChildren parentId=" + parentId); } switch (parentId) { case ROOT_ID: result.sendResult(mRootItems); mLastCategory = parentId; break ; } }
创建 Player 创建本地播放器。内部主要基于MediaPlayer实现。
添加广播监听 用来响应Notification中的动作。支持4个动作。
检索/封装Audio - DataModel DataModel主要用于检索设备本地的多媒体数据,提供了以下四种从ContentProvider检索方式。
文件夹(Folder)检索 1 2 3 4 5 6 7 8 9 private static final Uri[] ALL_AUDIO_URI = new Uri[] { MediaStore.Audio.Media.INTERNAL_CONTENT_URI, MediaStore.Audio.Media.EXTERNAL_CONTENT_URI }; public void onQueryByFolder (String parentId, Result<List<MediaItem>> result) { FilesystemListTask query = new FilesystemListTask(result, ALL_AUDIO_URI, mResolver); queryInBackground(result, query); }
专辑(Album)检索 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private static final Uri[] ALBUMS_URI = new Uri[] { MediaStore.Audio.Albums.INTERNAL_CONTENT_URI, MediaStore.Audio.Albums.EXTERNAL_CONTENT_URI }; public void onQueryByAlbum (String parentId, Result<List<MediaItem>> result) { QueryTask query = new QueryTask.Builder() .setResolver(mResolver) .setResult(result) .setUri(ALBUMS_URI) .setKeyColumn(AudioColumns.ALBUM_KEY) .setTitleColumn(AudioColumns.ALBUM) .setFlags(MediaItem.FLAG_BROWSABLE) .build(); queryInBackground(result, query); }
艺术家(Artist)检索 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private static final Uri[] ARTISTS_URI = new Uri[] { MediaStore.Audio.Artists.INTERNAL_CONTENT_URI, MediaStore.Audio.Artists.EXTERNAL_CONTENT_URI }; public void onQueryByArtist (String parentId, Result<List<MediaItem>> result) { QueryTask query = new QueryTask.Builder() .setResolver(mResolver) .setResult(result) .setUri(ARTISTS_URI) .setKeyColumn(AudioColumns.ARTIST_KEY) .setTitleColumn(AudioColumns.ARTIST) .setFlags(MediaItem.FLAG_BROWSABLE) .build(); queryInBackground(result, query); }
流派(Genre)检索 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 private static final Uri[] GENRES_URI = new Uri[] { MediaStore.Audio.Genres.INTERNAL_CONTENT_URI, MediaStore.Audio.Genres.EXTERNAL_CONTENT_URI }; public void onQueryByGenre (String parentId, Result<List<MediaItem>> result) { QueryTask query = new QueryTask.Builder() .setResolver(mResolver) .setResult(result) .setUri(GENRES_URI) .setKeyColumn(MediaStore.Audio.Genres._ID) .setTitleColumn(MediaStore.Audio.Genres.NAME) .setFlags(MediaItem.FLAG_BROWSABLE) .build(); queryInBackground(result, query); }
模糊检索 该方法主要就是检索出设备中所有的Audio数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 private static final String QUERY_BY_KEY_WHERE_CLAUSE = AudioColumns.ALBUM_KEY + "= ? or " + AudioColumns.ARTIST_KEY + " = ? or " + AudioColumns.TITLE_KEY + " = ? or " + AudioColumns.DATA + " like ?" ; public void onQueryByKey (String lastCategory, String parentId, Result<List<MediaItem>> result) { mQueue.clear(); QueryTask.Builder query = new QueryTask.Builder() .setResolver(mResolver) .setResult(result); if (LocalMediaBrowserService.GENRES_ID.equals(lastCategory)) { try { long id = Long.parseLong(parentId); query.setUri(new Uri[] { MediaStore.Audio.Genres.Members.getContentUri(EXTERNAL, id), MediaStore.Audio.Genres.Members.getContentUri(INTERNAL, id) }); } catch (NumberFormatException e) { Log.e(TAG, "Incorrect key type: " + parentId + ", sending empty result" ); result.sendResult(new ArrayList<MediaItem>()); return ; } } else { query.setUri(ALL_AUDIO_URI) .setWhereClause(QUERY_BY_KEY_WHERE_CLAUSE) .setWhereArgs(new String[] { parentId, parentId, parentId, parentId }); } query.setKeyColumn(AudioColumns.TITLE_KEY) .setTitleColumn(AudioColumns.TITLE) .setSubtitleColumn(AudioColumns.ALBUM) .setFlags(MediaItem.FLAG_PLAYABLE) .setQueue(mQueue); queryInBackground(result, query.build()); }
QueryTask 由于ContentProvider#query是一个耗时方法,所以需要放在子线程中执行,于是就有了QueryTask 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 @Override protected Void doInBackground (Void... voids) { List<MediaItem> results = new ArrayList<>(); long idx = 0 ; Cursor cursor = null ; for (Uri uri : mUris) { try { cursor = mResolver.query(uri, mColumns, mWhereClause, mWhereArgs, null ); if (cursor != null ) { int keyColumn = cursor.getColumnIndex(mKeyColumn); int titleColumn = cursor.getColumnIndex(mTitleColumn); int pathColumn = cursor.getColumnIndex(AudioColumns.DATA); int subtitleColumn = -1 ; if (mSubtitleColumn != null ) { subtitleColumn = cursor.getColumnIndex(mSubtitleColumn); } while (cursor.moveToNext()) { Bundle path = new Bundle(); if (pathColumn != -1 ) { path.putString(PATH_KEY, cursor.getString(pathColumn)); } MediaDescription.Builder builder = new MediaDescription.Builder() .setMediaId(cursor.getString(keyColumn)) .setTitle(cursor.getString(titleColumn)) .setExtras(path); if (subtitleColumn != -1 ) { builder.setSubtitle(cursor.getString(subtitleColumn)); } MediaDescription description = builder.build(); results.add(new MediaItem(description, mFlags)); if (mQueue != null ) { mQueue.add(new QueueItem(description, idx)); } idx++; } } } catch (SQLiteException e) { Log.i(TAG, "Failed to execute query " + e); } finally { if (cursor != null ) { cursor.close(); } } } mResult.sendResult(results); return null ; }
QueryTask 从名字上就能猜出来,是一个AsyncTask,而且实际只用到了doInBackground(),在后台执行完查询结果后,执行mResult.sendResult(results),结果就会从Service传递给HMI。QueryTask封装了多个可配置参数,还用到一个简单建造者模式,不过我们自己改写的话,用比AsyncTask 更轻量的一些的线程池 或协程 即可。
FilesystemListTask FilesystemListTask 与 QueryTask 一样都是 AsyncTask,FilesystemListTask 主要用于文件夹检索 ,为了便于理解代码,没有和QueryTask封装在一起。如果强行写在一起,看起来非常奇怪并且过度参数化,有可能变得更加冗长。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Override protected Void doInBackground (Void... voids) { Set<String> paths = new HashSet<String>(); Cursor cursor = null ; for (Uri uri : mUris) { try { cursor = mResolver.query(uri, COLUMNS, null , null , null ); if (cursor != null ) { int pathColumn = cursor.getColumnIndex(AudioColumns.DATA); while (cursor.moveToNext()) { String fullPath = cursor.getString(pathColumn); int fileNameStart = fullPath.lastIndexOf(File.separator); if (fileNameStart < 0 ) { continue ; } String dirPath = fullPath.substring(0 , fileNameStart); paths.add(dirPath); } } } catch (SQLiteException e) { Log.e(TAG, "Failed to execute query " + e); } finally { if (cursor != null ) { cursor.close(); } } } List<MediaItem> results = new ArrayList<>(); for (String path : paths) { int dirNameStart = path.lastIndexOf(File.separator) + 1 ; String dirName = path.substring(dirNameStart, path.length()); MediaDescription description = new MediaDescription.Builder() .setMediaId(path + "%" ) .setTitle(dirName) .setSubtitle(path) .build(); results.add(new MediaItem(description, MediaItem.FLAG_BROWSABLE)); } mResult.sendResult(results); return null ; }
媒体播放器-Player Player继承MediaSession.Callback,所以需要处理HMI端调用MediaController.transportControls.xxx的对应方法。在内部逻辑主要就是记录播放状态和处理音频焦点。
初始化媒体播放器 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 public Player (Context context, MediaSession session, DataModel dataModel) { mContext = context; mDataModel = dataModel; mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); mSession = session; mSharedPrefs = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE); mShuffle = new CustomAction.Builder(SHUFFLE, context.getString(R.string.shuffle), R.drawable.shuffle).build(); mMediaPlayer = new MediaPlayer(); mMediaPlayer.reset(); mMediaPlayer.setOnCompletionListener(mOnCompletionListener); mErrorState = new PlaybackState.Builder() .setState(PlaybackState.STATE_ERROR, 0 , 0 ) .setErrorMessage(context.getString(R.string.playback_error)) .build(); mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); Notification.Action prevAction = makeNotificationAction( LocalMediaBrowserService.ACTION_PREV, R.drawable.ic_prev, R.string.prev); Notification.Action nextAction = makeNotificationAction( LocalMediaBrowserService.ACTION_NEXT, R.drawable.ic_next, R.string.next); Notification.Action playAction = makeNotificationAction( LocalMediaBrowserService.ACTION_PLAY, R.drawable.ic_play, R.string.play); Notification.Action pauseAction = makeNotificationAction( LocalMediaBrowserService.ACTION_PAUSE, R.drawable.ic_pause, R.string.pause); mPlayingNotificationBuilder = new Notification.Builder(context) .setVisibility(Notification.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_sd_storage_black) .addAction(prevAction) .addAction(pauseAction) .addAction(nextAction); mPausedNotificationBuilder = new Notification.Builder(context) .setVisibility(Notification.VISIBILITY_PUBLIC) .setSmallIcon(R.drawable.ic_sd_storage_black) .addAction(prevAction) .addAction(playAction) .addAction(nextAction); } private Notification.Action makeNotificationAction (String action, int iconId, int stringId) { PendingIntent intent = PendingIntent.getBroadcast(mContext, REQUEST_CODE, new Intent(action), PendingIntent.FLAG_UPDATE_CURRENT); Notification.Action notificationAction = new Notification.Action.Builder(iconId, mContext.getString(stringId), intent) .build(); return notificationAction; }
1 2 3 4 5 6 7 8 9 private OnCompletionListener mOnCompletionListener = new OnCompletionListener() { @Override public void onCompletion (MediaPlayer mediaPlayer) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onCompletion()" ); } safeAdvance(); } };
处理音频焦点 关于如何管理音频焦点可参考官方文档
Android Developer
这里我们看下LocalMedia中的处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private boolean requestAudioFocus (Runnable onSuccess) { int result = mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN); if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { onSuccess.run(); return true ; } Log.e(TAG, "Failed to acquire audio focus" ); return false ; } private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() { @Override public void onAudioFocusChange (int focus) { switch (focus) { case AudioManager.AUDIOFOCUS_GAIN: resumePlayback(); break ; case AudioManager.AUDIOFOCUS_LOSS: case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT: case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK: pausePlayback(); break ; default : Log.e(TAG, "Unhandled audio focus type: " + focus); } } };
播放指定的媒体 在HMI端调用 MediaController.transportControls.playFromMediaId()时触发。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 @Override public void onPlayFromMediaId (String mediaId, Bundle extras) { super .onPlayFromMediaId(mediaId, extras); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onPlayFromMediaId mediaId" + mediaId + " extras=" + extras); } requestAudioFocus(() -> startPlayback(mediaId)); } private void startPlayback (String key) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "startPlayback()" ); } List<QueueItem> queue = mDataModel.getQueue(); int idx = 0 ; int foundIdx = -1 ; for (QueueItem item : queue) { if (item.getDescription().getMediaId().equals(key)) { foundIdx = idx; break ; } idx++; } if (foundIdx == -1 ) { mSession.setPlaybackState(mErrorState); return ; } mQueue = new ArrayList<>(queue); mCurrentQueueIdx = foundIdx; QueueItem current = mQueue.get(mCurrentQueueIdx); String path = current.getDescription().getExtras().getString(DataModel.PATH_KEY); MediaMetadata metadata = mDataModel.getMetadata(current.getDescription().getMediaId()); updateSessionQueueState(); try { play(path, metadata); } catch (IOException e) { Log.e(TAG, "Playback failed." , e); mSession.setPlaybackState(mErrorState); } } private void play (String path, MediaMetadata metadata) throws IOException { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "play path=" + path + " metadata=" + metadata); } mMediaPlayer.reset(); mMediaPlayer.setDataSource(path); mMediaPlayer.prepare(); if (metadata != null ) { mSession.setMetadata(metadata); } boolean wasGrantedAudio = requestAudioFocus(() -> { mMediaPlayer.start(); updatePlaybackStatePlaying(); }); if (!wasGrantedAudio) { pausePlayback(); } }
getMetadata()是DataModel 中的方法,主要就是将从ContentProvider中查询到的原始数据,封装成元数据。下面的代码,演示了如何封装。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 public MediaMetadata getMetadata (String key) { Cursor cursor = null ; MediaMetadata.Builder metadata = new MediaMetadata.Builder(); try { for (Uri uri : ALL_AUDIO_URI) { cursor = mResolver.query(uri, null , AudioColumns.TITLE_KEY + " = ?" , new String[]{ key }, null ); if (cursor != null ) { int title = cursor.getColumnIndex(AudioColumns.TITLE); int artist = cursor.getColumnIndex(AudioColumns.ARTIST); int album = cursor.getColumnIndex(AudioColumns.ALBUM); int albumId = cursor.getColumnIndex(AudioColumns.ALBUM_ID); int duration = cursor.getColumnIndex(AudioColumns.DURATION); while (cursor.moveToNext()) { metadata.putString(MediaMetadata.METADATA_KEY_TITLE, cursor.getString(title)); metadata.putString(MediaMetadata.METADATA_KEY_ARTIST, cursor.getString(artist)); metadata.putString(MediaMetadata.METADATA_KEY_ALBUM, cursor.getString(album)); metadata.putLong(MediaMetadata.METADATA_KEY_DURATION, cursor.getLong(duration)); String albumArt = null ; Uri albumArtUri = ContentUris.withAppendedId(ART_BASE_URI, cursor.getLong(albumId)); try { InputStream dummy = mResolver.openInputStream(albumArtUri); albumArt = albumArtUri.toString(); dummy.close(); } catch (IOException e) { } metadata.putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArt); break ; } } } } finally { if (cursor != null ) { cursor.close(); } } return metadata.build(); }
恢复播放 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @Override public void onPlay () { super .onPlay(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onPlay" ); } if (!Utils.hasRequiredPermissions(mContext)) { setMissingPermissionError(); } else { requestAudioFocus(() -> resumePlayback()); } } private void setMissingPermissionError () { Intent prefsIntent = new Intent(); prefsIntent.setClass(mContext, PermissionsActivity.class); prefsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0 , prefsIntent, 0 ); Bundle extras = new Bundle(); extras.putString(Utils.ERROR_RESOLUTION_ACTION_LABEL, mContext.getString(R.string.permission_error_resolve)); extras.putParcelable(Utils.ERROR_RESOLUTION_ACTION_INTENT, pendingIntent); PlaybackState state = new PlaybackState.Builder() .setState(PlaybackState.STATE_ERROR, 0 , 0 ) .setErrorMessage(mContext.getString(R.string.permission_error)) .setExtras(extras) .build(); mSession.setPlaybackState(state); } private void resumePlayback () { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "resumePlayback()" ); } updatePlaybackStatePlaying(); if (!mMediaPlayer.isPlaying()) { mMediaPlayer.start(); } }
播放时还要同步更新播放状态,并通过MediaSession将状态告知HMI端。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private void updatePlaybackStatePlaying () { if (!mSession.isActive()) { mSession.setActive(true ); } CustomAction action = new CustomAction .Builder("android.car.media.localmediaplayer.shuffle" , mContext.getString(R.string.shuffle), R.drawable.shuffle) .build(); PlaybackState state = new PlaybackState.Builder() .setState(PlaybackState.STATE_PLAYING, mMediaPlayer.getCurrentPosition(), PLAYBACK_SPEED) .setActions(PLAYING_ACTIONS) .addCustomAction(action) .setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId()) .build(); mSession.setPlaybackState(state); postMediaNotification(mPlayingNotificationBuilder); }
暂停 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 @Override public void onPause () { super .onPause(); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onPause" ); } pausePlayback(); mAudioManager.abandonAudioFocus(mAudioFocusListener); } private void pausePlayback () { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "pausePlayback()" ); } long currentPosition = 0 ; if (mMediaPlayer.isPlaying()) { currentPosition = mMediaPlayer.getCurrentPosition(); mMediaPlayer.pause(); } PlaybackState state = new PlaybackState.Builder() .setState(PlaybackState.STATE_PAUSED, currentPosition, PLAYBACK_SPEED_STOPPED) .setActions(PAUSED_ACTIONS) .addCustomAction(mShuffle) .setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId()) .build(); mSession.setPlaybackState(state); postMediaNotification(mPausedNotificationBuilder); }
终止播放 在Service被销毁时需要终止播放,并销毁播放器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public void destroy () { stopPlayback(); mNotificationManager.cancelAll(); mAudioManager.abandonAudioFocus(mAudioFocusListener); mMediaPlayer.release(); } private void stopPlayback () { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "stopPlayback()" ); } if (mMediaPlayer.isPlaying()) { mMediaPlayer.stop(); } PlaybackState state = new PlaybackState.Builder() .setState(PlaybackState.STATE_STOPPED, PLAYBACK_POSITION_STOPPED, PLAYBACK_SPEED_STOPPED) .setActions(STOPPED_ACTIONS) .build(); mSession.setPlaybackState(state); }
切换下一曲 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 @Override public void onSkipToNext () { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onSkipToNext()" ); } safeAdvance(); } private void safeAdvance () { try { advance(); } catch (IOException e) { Log.e(TAG, "Failed to advance." , e); mSession.setPlaybackState(mErrorState); } } private void advance () throws IOException { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "advance()" ); } if (mQueue != null && !mQueue.isEmpty()) { mCurrentQueueIdx = (mCurrentQueueIdx + 1 ) % mQueue.size(); playCurrentQueueIndex(); } else { stopPlayback(); } }
切换上一曲 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 @Override public void onSkipToPrevious () { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "onSkipToPrevious()" ); } safeRetreat(); } private void safeRetreat () { try { retreat(); } catch (IOException e) { Log.e(TAG, "Failed to advance." , e); mSession.setPlaybackState(mErrorState); } } private void retreat () throws IOException { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "retreat()" ); } if (mQueue != null ) { mCurrentQueueIdx--; if (mCurrentQueueIdx < 0 ) { mCurrentQueueIdx = mQueue.size() - 1 ; } playCurrentQueueIndex(); } else { stopPlayback(); } }
播放指定的媒体 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Override public void onSkipToQueueItem (long id) { try { mCurrentQueueIdx = (int ) id; playCurrentQueueIndex(); } catch (IOException e) { Log.e(TAG, "Failed to play." , e); mSession.setPlaybackState(mErrorState); } } private void playCurrentQueueIndex () throws IOException { MediaDescription next = mQueue.get(mCurrentQueueIdx).getDescription(); String path = next.getExtras().getString(DataModel.PATH_KEY); MediaMetadata metadata = mDataModel.getMetadata(next.getMediaId()); play(path, metadata); }
随机播放 随机播放 在MediaSession.Callback中并没有定义,所以需要使用MediaSession.Callback中提供的onCustomAction 进行拓展。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 @Override public void onCustomAction (String action, Bundle extras) { switch (action) { case SHUFFLE: shuffle(); break ; default : Log.e(TAG, "Unhandled custom action: " + action); } } private void shuffle () { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Shuffling" ); } if (mQueue != null && mQueue.size() > 2 ) { QueueItem current = mQueue.remove(mCurrentQueueIdx); Collections.shuffle(mQueue); mQueue.add(0 , current); for (int i = 0 ; i < mQueue.size(); i++) { mQueue.set(i, new QueueItem(mQueue.get(i).getDescription(), i)); } mCurrentQueueIdx = 0 ; updateSessionQueueState(); } } private void updateSessionQueueState () { mSession.setQueueTitle(mContext.getString(R.string.playlist)); mSession.setQueue(mQueue); }
该部分为HMI部分,LocalMedia的源码中HMI部分的量尤其的大,而且包含了很多动画、公共控件,所以HMI的源码分析只介绍播放界面,其它部分暂时不做介绍。
源码位置:/packages/apps/Car/Media
播放界面源码结构 播放界面就是一个Fragment,而且也是应用开发中很常见的Fragment+ViewModel+Repository 架构,但是它并没有完全遵守MVVM架构的设计规范,倒不是因为它没有使用DataBinding,而是因为Fragment的实现中直接调用了Repository的方法,这不符合MVVM架构的设计思想。
MediaSourceViewModel通过CarMediaManager来监听当前系统中媒体源,并使用MediaBrowserConnector来连接到MediaBrowserService。
CarMediaManager是Framework层封装的API,主要的通信对象是CarMediaService,关于CarAndroid中Framework层各个Service的实现,我们等车载应用都说完后再来一一解析。这里我们暂时不需要理解,因为在实际的车载应用开发中,CarMediaService往往都会被裁剪掉。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 private void updateModelState (MediaSource newMediaSource) { MediaSource oldMediaSource = mPrimaryMediaSource.getValue(); if (Objects.equals(oldMediaSource, newMediaSource)) { return ; } mPrimaryMediaSource.setValue(newMediaSource); if (newMediaSource != null ) { mBrowserConnector.connectTo(newMediaSource); } } private final MediaBrowserConnector mBrowserConnector;private final MediaBrowserConnector.Callback mBrowserCallback = new MediaBrowserConnector.Callback() { @Override public void onBrowserConnectionChanged (@NonNull BrowsingState state) { mBrowsingState.setValue(state); } };
MediaBrowserConnector的连接状态会通过callback返回给MediaSourceViewModel。MediaSourceViewModel则将其封装在LiveData中,供其它有需要的模块监听MediaBrowserService 的连接状态。
MediaBrowserConnector的逻辑从名字上就能看出来。主要就是创建MediaBrowserCompat并连接到MediaBrowserService ,并把连接过程、连接状态以及MediaBrowser的实例封装在BrowsingState中暴露给MediaSourceViewModel完成闭环。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public void connectTo (@Nullable MediaSource mediaSource) { if (mBrowser != null && mBrowser.isConnected()) { if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Disconnecting: " + getSourcePackage() + " mBrowser: " + idHash(mBrowser)); } sendNewState(ConnectionStatus.DISCONNECTING); mBrowser.disconnect(); } mMediaSource = mediaSource; if (mMediaSource != null ) { mBrowser = createMediaBrowser(mMediaSource, new BrowserConnectionCallback()); if (Log.isLoggable(TAG, Log.DEBUG)) { Log.d(TAG, "Connecting to: " + getSourcePackage() + " mBrowser: " + idHash(mBrowser)); } try { sendNewState(ConnectionStatus.CONNECTING); mBrowser.connect(); } catch (IllegalStateException ex) { Log.e(TAG, "Connection exception: " + ex); sendNewState(ConnectionStatus.SUSPENDED); } } else { mBrowser = null ; } } @NonNull protected MediaBrowserCompat createMediaBrowser (@NonNull MediaSource mediaSource, @NonNull MediaBrowserCompat.ConnectionCallback callback) { Bundle rootHints = new Bundle(); rootHints.putInt(MediaConstants.EXTRA_MEDIA_ART_SIZE_HINT_PIXELS, mMaxBitmapSizePx); ComponentName browseService = mediaSource.getBrowseServiceComponentName(); return new MediaBrowserCompat(mContext, browseService, callback, rootHints); }
MediaItemRepository对外提供媒体项目搜索和子查询功能。
MediaItemRepository使用了单例模式,在创建过程中会从同样基于单例模式的MediaSourceViewModel中获取到LiveData<BrowsingState>。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private static MediaItemsRepository[] sInstances = new MediaItemsRepository[2 ];public static MediaItemsRepository get (@NonNull Application application, int mode) { if (sInstances[mode] == null ) { sInstances[mode] = new MediaItemsRepository( MediaSourceViewModel.get(application, mode).getBrowsingState() ); } return sInstances[mode]; } @VisibleForTesting public MediaItemsRepository (LiveData<BrowsingState> browsingState) { browsingState.observeForever(this ::onMediaBrowsingStateChanged); }
通过观察LiveData,根据不同的连接状态,处理不同的逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private void onMediaBrowsingStateChanged (BrowsingState newBrowsingState) { mBrowsingState = newBrowsingState; if (mBrowsingState == null ) { Log.e(TAG, "Null browsing state (no media source!)" ); return ; } mBrowsingStateLiveData.setValue(mBrowsingState); switch (mBrowsingState.mConnectionStatus) { case CONNECTING: mRootMediaItems.setLoading(); break ; case CONNECTED: String rootId = mBrowsingState.mBrowser.getRoot(); getCache().mRootId = rootId; getMediaChildren(rootId); break ; case DISCONNECTING: unsubscribeNodes(); clearSearchResults(); clearNodes(); break ; case REJECTED: case SUSPENDED: onBrowseData(getCache().mRootId, null ); clearSearchResults(); clearNodes(); } }
如果连接成功,默认检索根节点,并更新本地数据。
基于节点检索 1 2 3 4 5 6 7 8 9 10 11 12 13 14 public MediaItemsLiveData getMediaChildren (String nodeId) { PerMediaSourceCache cache = getCache(); MediaChildren items = cache.mChildrenByNodeId.get(nodeId); if (items == null ) { items = new MediaChildren(nodeId); cache.mChildrenByNodeId.put(nodeId, items); } mBrowsingState.mBrowser.unsubscribe(nodeId); mBrowsingState.mBrowser.subscribe(nodeId, mBrowseCallback); return items.mLiveData; }
在SubscriptionCallback中更新本地缓存数据,同时也更新对外暴露的MediaItemsLiveData。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 private final SubscriptionCallback mBrowseCallback = new SubscriptionCallback() { @Override public void onChildrenLoaded (@NonNull String parentId, @NonNull List<MediaBrowserCompat.MediaItem> children) { onBrowseData(parentId, children.stream() .filter(Objects::nonNull) .map(MediaItemMetadata::new ) .collect(Collectors.toList())); } @Override public void onChildrenLoaded (@NonNull String parentId, @NonNull List<MediaBrowserCompat.MediaItem> children, @NonNull Bundle options) { onChildrenLoaded(parentId, children); } @Override public void onError (@NonNull String parentId) { onBrowseData(parentId, null ); } @Override public void onError (@NonNull String parentId, @NonNull Bundle options) { onError(parentId); } }; private void onBrowseData (@NonNull String parentId, @Nullable List<MediaItemMetadata> list) { PerMediaSourceCache cache = getCache(); MediaChildren children = cache.mChildrenByNodeId.get(parentId); if (children == null ) { if (Log.isLoggable(TAG, Log.WARN)) { Log.w(TAG, "Browse parent not in the cache: " + parentId); } return ; } List<MediaItemMetadata> old = children.mPreviousValue; children.mPreviousValue = list; children.mLiveData.onDataLoaded(old, list); if (Objects.equals(parentId, cache.mRootId)) { mRootMediaItems.onDataLoaded(old, list); } }
基于关键字检索 关键字检索通过search()方法实现。使用时先调用getSearchMediaItems()拿到一个LiveData并持续观察,再调用setSearchQuery()。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 public void setSearchQuery (String query) { mSearchQuery = query; if (TextUtils.isEmpty(mSearchQuery)) { clearSearchResults(); } else { mSearchMediaItems.setLoading(); mBrowsingState.mBrowser.search(mSearchQuery, null , mSearchCallback); } } private final SearchCallback mSearchCallback = new SearchCallback() { @Override public void onSearchResult (@NonNull String query, Bundle extras, @NonNull List<MediaBrowserCompat.MediaItem> items) { super .onSearchResult(query, extras, items); if (Objects.equals(mSearchQuery, query)) { onSearchData(items.stream() .filter(Objects::nonNull) .map(MediaItemMetadata::new ) .collect(toList())); } } @Override public void onError (@NonNull String query, Bundle extras) { super .onError(query, extras); if (Objects.equals(mSearchQuery, query)) { onSearchData(null ); } } }; private void onSearchData (@Nullable List<MediaItemMetadata> list) { mSearchMediaItems.onDataLoaded(null , list); }
PlaybackViewModel MediaBrowserConnector和MediaItemRepository分别完成了连接和检索功能,接下来就是PlaybackViewModel 中实现的播放控制功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 private class MediaControllerCallback extends MediaControllerCompat .Callback { private MediaBrowserConnector.BrowsingState mBrowsingState; private MediaControllerCompat mMediaController; private MediaMetadataCompat mMediaMetadata; private PlaybackStateCompat mPlaybackState; void onMediaBrowsingStateChanged (MediaBrowserConnector.BrowsingState newBrowsingState) { if (Objects.equals(mBrowsingState, newBrowsingState)) { Log.w(TAG, "onMediaBrowsingStateChanged noop " ); return ; } if (mMediaController != null ) { switch (newBrowsingState.mConnectionStatus) { case DISCONNECTING: case REJECTED: case CONNECTING: case CONNECTED: mMediaController.unregisterCallback(this ); case SUSPENDED: setMediaController(null ); } } mBrowsingState = newBrowsingState; if (mBrowsingState.mConnectionStatus == ConnectionStatus.CONNECTED) { setMediaController(mInputFactory.getControllerForBrowser(mBrowsingState.mBrowser)); } } private void setMediaController (MediaControllerCompat mediaController) { mMediaMetadata = null ; mPlaybackState = null ; mMediaController = mediaController; mPlaybackControls.setValue(new PlaybackController(mediaController)); if (mMediaController != null ) { mMediaController.registerCallback(this ); mColors.setValue(mColorsFactory.extractColors(mediaController.getPackageName())); onMetadataChanged(mMediaController.getMetadata()); onPlaybackStateChanged(mMediaController.getPlaybackState()); onQueueChanged(mMediaController.getQueue()); onQueueTitleChanged(mMediaController.getQueueTitle()); } else { mColors.setValue(null ); onMetadataChanged(null ); onPlaybackStateChanged(null ); onQueueChanged(null ); onQueueTitleChanged(null ); } updatePlaybackStatus(); } @Override public void onSessionDestroyed () { Log.w(TAG, "onSessionDestroyed" ); setMediaController(null ); } @Override public void onMetadataChanged (@Nullable MediaMetadataCompat mmdCompat) { if ((mmdCompat != null ) && EMPTY_MEDIA_METADATA.equals(mmdCompat.getMediaMetadata())) { mMediaMetadata = null ; } else { mMediaMetadata = mmdCompat; } MediaItemMetadata item = (mMediaMetadata != null ) ? new MediaItemMetadata(mMediaMetadata) : null ; mMetadata.setValue(item); updatePlaybackStatus(); } @Override public void onQueueTitleChanged (CharSequence title) { mQueueTitle.setValue(title); } @Override public void onQueueChanged (@Nullable List<MediaSessionCompat.QueueItem> queue) { List<MediaItemMetadata> filtered = queue == null ? Collections.emptyList() : queue.stream() .filter(item -> item != null && item.getDescription() != null && item.getDescription().getTitle() != null ) .map(MediaItemMetadata::new ) .collect(Collectors.toList()); mSanitizedQueue.setValue(filtered); mHasQueue.setValue(filtered.size() > 1 ); } @Override public void onPlaybackStateChanged (PlaybackStateCompat playbackState) { mPlaybackState = playbackState; updatePlaybackStatus(); } private void updatePlaybackStatus () { if (mMediaController != null && mPlaybackState != null ) { mPlaybackStateWrapper.setValue( new PlaybackStateWrapper(mMediaController, mMediaMetadata, mPlaybackState)); } else { mPlaybackStateWrapper.setValue(null ); } } }
拓展 PlaybackState 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 public static final class PlaybackStateWrapper { private final MediaControllerCompat mMediaController; @Nullable private final MediaMetadataCompat mMetadata; private final PlaybackStateCompat mState; PlaybackStateWrapper(@NonNull MediaControllerCompat mediaController, @Nullable MediaMetadataCompat metadata, @NonNull PlaybackStateCompat state) { mMediaController = mediaController; mMetadata = metadata; mState = state; } public boolean shouldDisplay () { return mState.getState() != PlaybackStateCompat.STATE_NONE && ((mMetadata != null ) || ( getMainAction() != ACTION_DISABLED)); } @Action public int getMainAction () { @Actions long actions = mState.getActions(); @Action int stopAction = ACTION_DISABLED; if ((actions & (PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY_PAUSE)) != 0 ) { stopAction = ACTION_PAUSE; } else if ((actions & PlaybackStateCompat.ACTION_STOP) != 0 ) { stopAction = ACTION_STOP; } switch (mState.getState()) { case PlaybackStateCompat.STATE_PLAYING: case PlaybackStateCompat.STATE_BUFFERING: case PlaybackStateCompat.STATE_CONNECTING: case PlaybackStateCompat.STATE_FAST_FORWARDING: case PlaybackStateCompat.STATE_REWINDING: case PlaybackStateCompat.STATE_SKIPPING_TO_NEXT: case PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS: case PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM: return stopAction; case PlaybackStateCompat.STATE_STOPPED: case PlaybackStateCompat.STATE_PAUSED: case PlaybackStateCompat.STATE_NONE: case PlaybackStateCompat.STATE_ERROR: return (actions & PlaybackStateCompat.ACTION_PLAY) != 0 ? ACTION_PLAY : ACTION_DISABLED; default : Log.w(TAG, String.format("Unknown PlaybackState: %d" , mState.getState())); return ACTION_DISABLED; } } public long getSupportedActions () { return mState.getActions(); } public long getMaxProgress () { return mMetadata == null ? 0 : mMetadata.getLong(MediaMetadataCompat.METADATA_KEY_DURATION); } public boolean isPlaying () { return mState.getState() == PlaybackStateCompat.STATE_PLAYING; } public boolean isSkipNextEnabled () { return (mState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_NEXT) != 0 ; } public boolean isSkipPreviousEnabled () { return (mState.getActions() & PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS) != 0 ; } public boolean isSeekToEnabled () { return (mState.getActions() & PlaybackStateCompat.ACTION_SEEK_TO) != 0 ; } public boolean isSkipNextReserved () { return mMediaController.getExtras() != null && (mMediaController.getExtras().getBoolean( MediaConstants.SLOT_RESERVATION_SKIP_TO_NEXT) || mMediaController.getExtras().getBoolean( MediaConstants.PLAYBACK_SLOT_RESERVATION_SKIP_TO_NEXT)); } public boolean iSkipPreviousReserved () { return mMediaController.getExtras() != null && (mMediaController.getExtras().getBoolean( MediaConstants.SLOT_RESERVATION_SKIP_TO_PREV) || mMediaController.getExtras().getBoolean( MediaConstants.PLAYBACK_SLOT_RESERVATION_SKIP_TO_PREV)); } public boolean isLoading () { int state = mState.getState(); return state == PlaybackStateCompat.STATE_BUFFERING || state == PlaybackStateCompat.STATE_CONNECTING || state == PlaybackStateCompat.STATE_FAST_FORWARDING || state == PlaybackStateCompat.STATE_REWINDING || state == PlaybackStateCompat.STATE_SKIPPING_TO_NEXT || state == PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS || state == PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM; } public CharSequence getErrorMessage () { return mState.getErrorMessage(); } public int getErrorCode () { return mState.getErrorCode(); } public long getActiveQueueItemId () { return mState.getActiveQueueItemId(); } @PlaybackStateCompat .State public int getState () { return mState.getState(); } public Bundle getExtras () { return mState.getExtras(); } @VisibleForTesting PlaybackStateCompat getStateCompat () { return mState; } public List<RawCustomPlaybackAction> getCustomActions () { List<RawCustomPlaybackAction> actions = new ArrayList<>(); RawCustomPlaybackAction ratingAction = getRatingAction(); if (ratingAction != null ) actions.add(ratingAction); for (PlaybackStateCompat.CustomAction action : mState.getCustomActions()) { String packageName = mMediaController.getPackageName(); actions.add( new RawCustomPlaybackAction(action.getIcon(), packageName, action.getAction(), action.getExtras())); } return actions; } @Nullable private RawCustomPlaybackAction getRatingAction () { long stdActions = mState.getActions(); if ((stdActions & PlaybackStateCompat.ACTION_SET_RATING) == 0 ) return null ; int ratingType = mMediaController.getRatingType(); if (ratingType != RatingCompat.RATING_HEART) return null ; boolean hasHeart = false ; if (mMetadata != null ) { RatingCompat rating = mMetadata.getRating( MediaMetadataCompat.METADATA_KEY_USER_RATING); hasHeart = rating != null && rating.hasHeart(); } int iconResource = hasHeart ? R.drawable.ic_star_filled : R.drawable.ic_star_empty; Bundle extras = new Bundle(); extras.putBoolean(EXTRA_SET_HEART, !hasHeart); return new RawCustomPlaybackAction(iconResource, null , ACTION_SET_RATING, extras); } }
封装媒体控制类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 public class PlaybackController { private final MediaControllerCompat mMediaController; private PlaybackController (@Nullable MediaControllerCompat mediaController) { mMediaController = mediaController; } public void play () { if (mMediaController != null ) { mMediaController.getTransportControls().play(); } } public void skipToPrevious () { if (mMediaController != null ) { mMediaController.getTransportControls().skipToPrevious(); } } public void skipToNext () { if (mMediaController != null ) { mMediaController.getTransportControls().skipToNext(); } } public void pause () { if (mMediaController != null ) { mMediaController.getTransportControls().pause(); } } public void stop () { if (mMediaController != null ) { mMediaController.getTransportControls().stop(); } } public void seekTo (long pos) { if (mMediaController != null ) { PlaybackStateCompat oldState = mMediaController.getPlaybackState(); PlaybackStateCompat newState = new PlaybackStateCompat.Builder(oldState) .setState(oldState.getState(), pos, oldState.getPlaybackSpeed()) .build(); mMediaControllerCallback.onPlaybackStateChanged(newState); mMediaController.getTransportControls().seekTo(pos); } } public void doCustomAction (String action, Bundle extras) { if (mMediaController == null ) return ; MediaControllerCompat.TransportControls cntrl = mMediaController.getTransportControls(); if (ACTION_SET_RATING.equals(action)) { boolean setHeart = extras != null && extras.getBoolean(EXTRA_SET_HEART, false ); cntrl.setRating(RatingCompat.newHeartRating(setHeart)); } else { cntrl.sendCustomAction(action, extras); } } public void playItem (MediaItemMetadata item) { if (mMediaController != null ) { mMediaController.getTransportControls().playFromMediaId(item.getId(), null ); } } public void skipToQueueItem (long queueId) { if (mMediaController != null ) { mMediaController.getTransportControls().skipToQueueItem(queueId); } } public void prepare () { if (mMediaController != null ) { mMediaController.getTransportControls().prepare(); } } }
PlaybackFragment 如图所示,播放界面分为显示媒体源信息、显示当前的Audio信息以及播放控制。
显示媒体资源信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 private LiveData<MediaSource> mMediaSource;mMediaSource = mMediaSourceViewModel.getPrimaryMediaSource(); mAppName = mapNonNull(mMediaSource, new Function<MediaSource, CharSequence>() { @Override public CharSequence apply (MediaSource mediaSource) { return mediaSource.getDisplayName(); } }); mAppIcon = mapNonNull(mMediaSource, new Function<MediaSource, Bitmap>() { @Override public Bitmap apply (MediaSource mediaSource) { return mediaSource.getCroppedPackageIcon(); } }); public static <T, R> LiveData<R> mapNonNull (@NonNull LiveData<T> source, @NonNull Function<T, R> func) { return mapNonNull(source, null , func); } public static <T, R> LiveData<R> mapNonNull (@NonNull LiveData<T> source, @Nullable R nullValue, @NonNull Function<T, R> func) { return Transformations.map(source, new Function<T, R>() { @Override public R apply (T value) { if (value == null ) { return nullValue; } else { return func.apply(value); } } }); }
从上面的代码可以看出,界面上显示出的『Local Media』和应用的图标 都是从MediaSourceViewModel中的getPrimaryMediaSource()获取。在MediaSourceViewModel中则是通过CarMediaManager这个CarAndroid Framework层封装的API获取的,关于CarAndroid中Framework层的各个Service的实现,我们等应用都说完后再来一一解释。
显示当前播放的媒体信息 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 void init (FragmentActivity activity, MediaSourceViewModel mediaSourceViewModel, PlaybackViewModel playbackViewModel, MediaItemsRepository mediaItemsRepository) {mTitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getTitle); mSubtitle = mapNonNull(playbackViewModel.getMetadata(), MediaItemMetadata::getArtist); mMediaItemsRepository.getRootMediaItems() .observe(activity, this ::onRootMediaItemsUpdate); } private void onRootMediaItemsUpdate (FutureData<List<MediaItemMetadata>> data) { if (data.isLoading()) { mBrowseTreeHasChildren.setValue(null ); return ; } List<MediaItemMetadata> items = MediaBrowserViewModelImpl.filterItems( true , data.getData()); boolean browseTreeHasChildren = items != null && !items.isEmpty(); mBrowseTreeHasChildren.setValue(browseTreeHasChildren); }