1. LocalMedia
LocalMedia 是 CarAndroid 中自帶的本地音樂播放器,它可以識別出系統中的音樂,并進行播放。本質上屬于一個功能比較完善的Demo,官方的目的可能是為了演示如何使用 MediaSession 框架寫一個音樂播放器。關于MediaSession框架之前已經介紹過了,本篇就簡單解析一下這個Demo。
1.1 LocalMedia 拆解
LocalMedia 運行時分為兩個APP:
-
com.android.car.media.localmediaplayer
該app是一個Service,主要作用是檢索出本地的音樂多媒體,并封裝成元數據。 -
com.android.car.media
主要用于展示HMI和用戶交互,源碼量非常龐大。
除了上面兩個APP,其實還有還有一個進程android.car.media,官方給出的注釋是這么介紹它的:
CarMediaService 管理汽車應用程序當前活動的媒體源。 這與 MediaSessionManager 的活動會話不同,因為汽車中只能有一個活動源,通過瀏覽和播放。在汽車中,活動媒體源不一定有活動的 MediaSession,例如 如果它只是被瀏覽。 但是,該來源仍被視為活動來源,并且應該是任何與媒體相關的 UI(媒體中心、主屏幕等)中顯示的來源。
這里就不介紹CarMediaService,在源碼中被分類在com.android.car目錄下,已經不屬于應用的范疇,本質上屬于Framework。
我們先來看看com.android.car.media.localmediaplayer 是如何實現。
2. localmediaplayer 核心源碼分析
應用的源碼分析討論都是一樣的,先從AndroidManifest開始。
<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
則是負責權限檢查和申請。
2.1 LocalMediaBrowserService
LocalMediaBrowserService
繼承自MediaBrowserService
,作為一個容器,主要就是用來初始化其它組件。
@Override
public void onCreate() {
super.onCreate();
// 創建 DataModel。
mDataModel = new DataModel(this);
// 初始化 RootItem
addRootItems();
// 創建 MediaSession
mSession = new MediaSession(this, MEDIA_SESSION_TAG);
setSessionToken(mSession.getSessionToken());
// 媒體播放器,同時也是 MediaSession.Callback
mPlayer = new Player(this, mSession, mDataModel);
mSession.setCallback(mPlayer);
mSession.setFlags(MediaSession.FLAG_HANDLES_MEDIA_BUTTONS
| MediaSession.FLAG_HANDLES_TRANSPORT_CONTROLS);
mPlayer.maybeRestoreState();
// 廣播,用于監聽Notification的控制動作
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
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()。
@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個動作。
2.2 檢索/封裝Audio - DataModel
DataModel
主要用于檢索設備本地的多媒體數據,提供了以下四種從ContentProvider檢索方式。
2.2.1 文件夾(Folder)檢索
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);
}
2.2.2 專輯(Album)檢索
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);
}
2.2.3 藝術家(Artist)檢索
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);
}
2.2.4 流派(Genre)檢索
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);
}
2.2.5 模糊檢索
該方法主要就是檢索出設備中所有的Audio數據。
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)) {
// Genre來自不同的表,并且不使用通常媒體表中的 where 子句,因此我們需要有這個條件。
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());
}
2.2.6 QueryTask
由于ContentProvider#query是一個耗時方法,所以需要放在子線程中執行,于是就有了QueryTask。
@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更輕量的一些的線程池或協程即可。
2.2.7 FilesystemListTask
FilesystemListTask 與 QueryTask 一樣都是 AsyncTask,FilesystemListTask 主要用于文件夾檢索,為了便于理解代碼,沒有和QueryTask封裝在一起。如果強行寫在一起,看起來非常奇怪并且過度參數化,有可能變得更加冗長。
@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());
//在封裝為 MediaItem
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;
}
3.3 媒體播放器 - Player
Player繼承MediaSession.Callback
,所以需要處理HMI端調用MediaController.transportControls.xxx
的對應方法。在內部邏輯主要就是記錄播放狀態和處理音頻焦點。
官方代碼中Player沒有實現上一曲、下一曲之間的無縫播放,無縫播放至少需要初始化兩個MediaPlayer。
完整的源碼位置 : Github - Player.java
3.3.1 初始化媒體播放器
public Player(Context context, MediaSession session, DataModel dataModel) {
mContext = context;
mDataModel = dataModel;
// 創建AudioManager
mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
mSession = session;
// 創建SharedPreferences用于記錄播放狀態
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);
// 初始化播放器狀態,這里設定為error狀態
mErrorState = new PlaybackState.Builder()
.setState(PlaybackState.STATE_ERROR, 0, 0)
.setErrorMessage(context.getString(R.string.playback_error))
.build();
// 初始化Notification
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);
}
// 創建 Notification.Action
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;
}
private OnCompletionListener mOnCompletionListener = new OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "onCompletion()");
}
safeAdvance();
}
};
3.3.2 處理音頻焦點
音頻焦點的相關內容,在之前的博客Android車載應用開發與分析(6)- 車載多媒體(一)- 音視頻基礎知識與MediaPlayer中已經介紹過了,這里不再贅述。
// 申請音頻焦點
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);
}
}
};
3.3.3 播放指定的媒體
在HMI端調用 MediaController.transportControls.playFromMediaId()
時觸發。
@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);
}
// 嘗試申請音頻焦點,申請成功則執行 startPlayback
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中查詢到的原始數據,封裝成元數據。下面的代碼,演示了如何封裝。
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) {
// Ignored because the albumArt is intialized correctly anyway.
}
metadata.putString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI, albumArt);
break;
}
}
}
} finally {
if (cursor != null) {
cursor.close();
}
}
return metadata.build();
}
3.3.4 恢復播放
@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() {
// 啟動權限申請用的Activity
Intent prefsIntent = new Intent();
prefsIntent.setClass(mContext, PermissionsActivity.class);
prefsIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
PendingIntent pendingIntent = PendingIntent.getActivity(mContext, 0, prefsIntent, 0);
// 將播放狀態設定未ERROR
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端。
// 更新播放狀態
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);
}
3.3.5 暫停
@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);
// 更新媒體的Notification狀態。
postMediaNotification(mPausedNotificationBuilder);
}
3.3.6 終止播放
在Service被銷毀時需要終止播放,并銷毀播放器
// 在Service的onDestroy方法中調用
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);
}
3.3.7 切換下一曲
@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()");
}
// 如果存在,請轉到下一首歌曲。
// 請注意,如果您要支持無縫播放,則必須更改此代碼,
// 以便擁有當前正在播放和正在加載的MediaPlayer,并在它們之間進行切換,同時還調用setNextMediaPlayer。
if (mQueue != null && !mQueue.isEmpty()) {
// 當我們跑出當前隊列的末尾時,繼續循環。
mCurrentQueueIdx = (mCurrentQueueIdx + 1) % mQueue.size();
playCurrentQueueIndex();
} else {
// 終止播放
stopPlayback();
}
}
3.3.8 切換下一曲
@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()");
}
// 如果有下一首歌,請轉到下一首。請注意,如果要支持無間隙播放,則必須更改此代碼,
// 以便在調用setNextMediaPlayer的同時,擁有當前正在播放和正在加載的MediaPlayer,并在兩者之間進行切換。
if (mQueue != null) {
// 當我們跑完當前隊列的末尾時,繼續循環。
mCurrentQueueIdx--;
if (mCurrentQueueIdx < 0) {
mCurrentQueueIdx = mQueue.size() - 1;
}
playCurrentQueueIndex();
} else {
// 終止播放
stopPlayback();
}
}
3.3.9 播放指定的媒體
@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);
}
3.3.10 隨機播放
隨機播放在MediaSession.Callback
中并沒有定義,所以需要使用MediaSession.Callback
中提供的onCustomAction進行拓展。
@Override
public void onCustomAction(String action, Bundle extras) {
switch (action) {
case SHUFFLE:
shuffle();
break;
default:
Log.e(TAG, "Unhandled custom action: " + action);
}
}
/**
* 這是shuffle 的一個簡單實現,之前播放的歌曲可能會在shuffle操作后重復。只能從主線程調用此函數。
* shuffle 可以理解為亂序播放。
*/
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);
// QueueItem 包含一個隊列 id,當用戶選擇當前播放列表時,該 id 用作鍵。
// 這意味著必須重建 QueueItems 以設置其新 ID。
for (int i = 0; i < mQueue.size(); i++) {
mQueue.set(i, new QueueItem(mQueue.get(i).getDescription(), i));
}
mCurrentQueueIdx = 0;
// 更新MediaSession隊列狀態
updateSessionQueueState();
}
}
private void updateSessionQueueState() {
mSession.setQueueTitle(mContext.getString(R.string.playlist));
mSession.setQueue(mQueue);
}
以上就是localMediaPlayer中比較重要的源碼,HMI部分的源碼解讀受限于篇幅,之后再單獨寫一篇。