Android车载应用开发与分析(8)- 车载多媒体(三)- 原生音乐播放器(上)

简介:

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 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上显示的四个大类。

![](https://upload-images.jianshu.io/upload_images/3146091-80b83e6bdc5beba0?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)

**mRootItems**会在onLoadChildren()方法中传给HMI端。HMI端需要调用**MediaBrowser**.subscribe才能触发onLoadChildren()。

@Override
public void onLoadChildren(String parentId, Result> 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> 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> 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> 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> 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> 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 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 paths = new HashSet();
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](https://github.com/linux-link/LocalMedia/blob/master/Service/src/main/java/com/android/car/media/localmediaplayer/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](https://www.jianshu.com/p/232dd12e35cb)中已经介绍过了,这里不再赘述。

// 申请音频焦点
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 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部分的源码解读受限于篇幅,之后再单独写一篇。

目录
相关文章
|
5天前
|
开发框架 前端开发 Android开发
Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势
本文深入探讨了 Flutter 与原生模块(Android 和 iOS)之间的通信机制,包括方法调用、事件传递等,分析了通信的必要性、主要方式、数据传递、性能优化及错误处理,并通过实际案例展示了其应用效果,展望了未来的发展趋势。这对于实现高效的跨平台移动应用开发具有重要指导意义。
33 4
|
20天前
|
安全 Android开发 数据安全/隐私保护
深入探讨iOS与Android系统安全性对比分析
在移动操作系统领域,iOS和Android无疑是两大巨头。本文从技术角度出发,对这两个系统的架构、安全机制以及用户隐私保护等方面进行了详细的比较分析。通过深入探讨,我们旨在揭示两个系统在安全性方面的差异,并为用户提供一些实用的安全建议。
|
29天前
|
缓存 Java Shell
Android 系统缓存扫描与清理方法分析
Android 系统缓存从原理探索到实现。
53 15
Android 系统缓存扫描与清理方法分析
|
29天前
|
Java 程序员 开发工具
Android|修复阿里云播放器下载不回调的问题
虽然 GC 带来了很多便利,但在实际编码时,我们也需要注意对象的生命周期管理,该存活的存活,该释放的释放,避免因为 GC 导致的问题。
33 2
|
1月前
|
开发工具 Android开发 Swift
安卓与iOS开发环境的差异性分析
【10月更文挑战第8天】 本文旨在探讨Android和iOS两大移动操作系统在开发环境上的不同,包括开发语言、工具、平台特性等方面。通过对这些差异性的分析,帮助开发者更好地理解两大平台,以便在项目开发中做出更合适的技术选择。
|
6天前
|
开发框架 前端开发 Android开发
安卓与iOS开发中的跨平台策略
在移动应用开发的战场上,安卓和iOS两大阵营各据一方。随着技术的演进,跨平台开发框架成为开发者的新宠,旨在实现一次编码、多平台部署的梦想。本文将探讨跨平台开发的优势与挑战,并分享实用的开发技巧,帮助开发者在安卓和iOS的世界中游刃有余。
|
11天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
13天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
15天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
【10月更文挑战第35天】在数字化时代,安卓应用的开发成为了一个热门话题。本文旨在通过浅显易懂的语言,带领初学者了解安卓开发的基础知识,同时为有一定经验的开发者提供进阶技巧。我们将一起探讨如何从零开始构建第一个安卓应用,并逐步深入到性能优化和高级功能的实现。无论你是编程新手还是希望提升技能的开发者,这篇文章都将为你提供有价值的指导和灵感。
下一篇
无影云桌面