Android车载应用开发与分析- 车载多媒体- 多媒体应用架构与MediaSession框架

Posted 林栩link

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android车载应用开发与分析- 车载多媒体- 多媒体应用架构与MediaSession框架相关的知识,希望对你有一定的参考价值。

参考资料
媒体应用架构概览 | Android 开发者 | Android Developers
MediaSession | Android Developers
MediaSession框架全解析_qzns木雨的博客-CSDN博客_mediasession

1. 多媒体应用架构

1.1 传统应用架构

播放音频或视频的多媒体应用通常由两部分组成:

  • 播放器:接收传入的数据多媒体,并输出音频或视频。可以是MediaPlayer、ExoPlayer或其他Player。
  • 界面:用于显示、控制播放器状态界面。


众所周知,如果需要在应用的后台继续播放音频,我们就需要把Player放置在Service中,那么界面播放器之间通信就非常值得研究了。很长一段时间里,都是由Service提供一个Binder来实现与播放器之间的通信。但是往往下拉的状态栏桌面的Widget都需要与Service之间进行通信,这时候Service就不得不通过实现一系列AIDL接口/广播/ContentProvider完成与其它应用之间的通信,而这些通信手段既增加了应用开发者之间的沟通成本,也增加了应用之间的耦合度。

为了解决上面的问题,android官方从Android5.0开始提供了MediaSession框架。

1.2 MediaSession 框架

MediaSession框架规范了音视频应用中界面播放器之间的通信接口,实现界面与播放器之间的完全解耦。框架定义了两个重要的类媒体会话媒体控制器,它们为构建多媒体播放器应用提供了一个完善的结构。

媒体会话媒体控制器通过以下方式相互通信:使用与标准播放器操作(播放、暂停、停止等)相对应的预定义回调,以及用于定义应用独有的特殊行为的可扩展自定义调用。

2. MediaSession 介绍

MediaSession框架属于典型的C/S架构,有四个常用的成员类,是整个MediaSession框架流程控制的核心。

2.1 客户端媒体浏览器 - MediaBrowser

媒体浏览器,用来连接MediaBrowserService订阅数据,通过它的回调接口我们可以获取与Service的连接状态以及获取在Service中的音乐库数据。在客户端(也就是上文我们提到的界面,或者说是控制端)中创建。
媒体浏览器不是线程安全的。所有调用都应在构造MediaBrowser的线程上进行。

@RequiresApi(Build.VERSION_CODES.M)
override fun onCreate(savedInstanceState: Bundle?) 
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val component = ComponentName(this, MediaService::class.java)
    mMediaBrowser = MediaBrowser(this, component, connectionCallback, null);
    mMediaBrowser.connect()

2.1.1 MediaBrowser.ConnectionCallback

用于接收与MediaBrowserService连接事件的回调,在创建MediaBrowser时传入。

@RequiresApi(Build.VERSION_CODES.M)
override fun onCreate(savedInstanceState: Bundle?) 
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    val component = ComponentName(this, MediaService::class.java)
    mMediaBrowser = MediaBrowser(this, component, connectionCallback, null);
    mMediaBrowser.connect()


private val connectionCallback = object : MediaBrowser.ConnectionCallback() 

    override fun onConnected() 
        super.onConnected()
    

    override fun onConnectionFailed() 
        super.onConnectionFailed()
    

    override fun onConnectionSuspended() 
        super.onConnectionSuspended()
    

2.1.2 MediaBrowser.ItemCallback

用于返回MediaBrowser.getItem()的结果。

private val connectionCallback = object : MediaBrowser.ConnectionCallback() 

    override fun onConnected() 
        super.onConnected()
        // ...
        if(mMediaBrowser.isConnected) 
            val mediaId = mMediaBrowser.root
            mMediaBrowser.getItem(mediaId, itemCallback)
        
    


@RequiresApi(Build.VERSION_CODES.M)
private val itemCallback = object : MediaBrowser.ItemCallback()

    override fun onItemLoaded(item: MediaBrowser.MediaItem?) 
        super.onItemLoaded(item)
    

    override fun onError(mediaId: String) 
        super.onError(mediaId)
    

2.1.3 MediaBrowser.MediaItem

包含有关单个媒体项的信息,用于浏览/搜索媒体。MediaItem依赖于服务端提供,因此框架本身无法保证它包含的值都是正确的。

2.1.4 MediaBrowser.SubscriptionCallback

用于订与MediaBrowserServiceMediaBrowser.MediaItem列表变化的回调。

private val connectionCallback = object : MediaBrowser.ConnectionCallback() 

    override fun onConnected() 
        super.onConnected()
        // ...
        if(mMediaBrowser.isConnected) 
            val mediaId = mMediaBrowser.root
            // 需要先取消订阅
            mMediaBrowser.unsubscribe(mediaId)
            // 服务端会调用onLoadChildren
            mMediaBrowser.subscribe(mediaId, subscribeCallback)
        
    


private val subscribeCallback = object : MediaBrowser.SubscriptionCallback()
    override fun onChildrenLoaded(
        parentId: String,
        children: MutableList<MediaBrowser.MediaItem>
    ) 
        super.onChildrenLoaded(parentId, children)
    

    override fun onChildrenLoaded(
        parentId: String,
        children: MutableList<MediaBrowser.MediaItem>,
        options: Bundle
    ) 
        super.onChildrenLoaded(parentId, children, options)
    

    override fun onError(parentId: String) 
        super.onError(parentId)
    

    override fun onError(parentId: String, options: Bundle) 
        super.onError(parentId, options)
    

2.2 客户端媒体控制器 - MediaController

媒体控制器,用来向服务端发送控制指令,例如:播放、暂停等等,在客户端中创建。媒体控制器是线程安全的。MediaController还有一个关联的权限android.permission.MEDIA_CONTENT_CONTROL(不是必须加的权限)必须是系统级应用才可以获取,幸运的是车载应用一般都是系统级应用。
MediaController必须在MediaBrowser连接成功后才可以创建。

private val connectionCallback = object : MediaBrowser.ConnectionCallback() 

    override fun onConnected() 
        super.onConnected()
        // ...
        if(mMediaBrowser.isConnected) 
            val sessionToken = mMediaBrowser.sessionToken
            mMediaController = MediaController(applicationContext,sessionToken)
        
    


2.2.1 MediaController.Callback

用于从MediaSession接收回调。使用方式如下:

private val connectionCallback = object : MediaBrowser.ConnectionCallback() 

    override fun onConnected() 
        super.onConnected()
        // ...
        if(mMediaBrowser.isConnected) 
            val sessionToken = mMediaBrowser.sessionToken
            mMediaController = MediaController(applicationContext,sessionToken)
            mMediaController.registerCallback(controllerCallback)
        
    


private val controllerCallback = object : MediaController.Callback() 

    override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) 
        super.onAudioInfoChanged(info)
    

    override fun onExtrasChanged(extras: Bundle?) 
        super.onExtrasChanged(extras)
    
    // ...

2.2.2 MediaController.PlaybackInfo

保存有关当前播放以及如何处理此会话的音频的信息。使用方式如下:

// 获取当前回话播放的音频信息
val playbackInfo = mMediaController.playbackInfo

2.2.3 MediaController.TransportControls

用于控制会话中媒体播放的接口。这允许客户端向Session发送媒体控制命令。使用方式如下:

private val connectionCallback = object : MediaBrowser.ConnectionCallback() 

    override fun onConnected() 
        super.onConnected()
        // ...
        if(mMediaBrowser.isConnected) 
            val sessionToken = mMediaBrowser.sessionToken
            mMediaController = MediaController(applicationContext,sessionToken)
            // 播放媒体
            mMediaController.transportControls.play()
            // 暂停媒体
            mMediaController.transportControls.pause()
        
    

2.3 服务端媒体浏览服务 - MediaBrowserService

媒体浏览器服务,继承自ServiceMediaBrowserService属于服务端,也是承载播放器(如MediaPlayer、ExoPlayer等)和MediaSession的容器。
实现MediaBrowserService时会要求复写onGetRootonLoadChildren两个方法。
onGetRoot通过的返回值决定是否允许客户端的MediaBrowser连接到MediaBrowserService
当客户端调用MediaBrowser.subscribe时会触发onLoadChildren方法。

const val FOLDERS_ID = "__FOLDERS__"
const val ARTISTS_ID = "__ARTISTS__"
const val ALBUMS_ID = "__ALBUMS__"
const val GENRES_ID = "__GENRES__"
const val ROOT_ID = "__ROOT__"

class MediaService : MediaBrowserService() 

    override fun onGetRoot(
        clientPackageName: String,
        clientUid: Int,
        rootHints: Bundle?
    ): BrowserRoot? 
        // 由MediaBrowser.connect触发,可以通过返回null拒绝客户端的连接。
        return BrowserRoot(ROOT_ID, null)
    

    override fun onLoadChildren(
        parentId: String,
        result: Result<MutableList<MediaBrowser.MediaItem>>
    ) 
    // 由MediaBrowser.subscribe触发
        when (parentId) 
            ROOT_ID -> 
                // 查询本地媒体库
                // ...
                // 将此消息与当前线程分离,并允许稍后进行sendResult调用
                result.detach()
                // 设定到 result 中
                result.sendResult()
            
            FOLDERS_ID -> 

            
            ALBUMS_ID -> 

            
            ARTISTS_ID -> 

            
            GENRES_ID -> 

            
            else -> 

            
        
    

然后还需要在manifest中注册这个Service。

<service
    android:name=".MediaService"
    android:label="@string/service_name">
    <intent-filter>
        <action android:name="android.media.browse.MediaBrowserService" />
    </intent-filter>
</service>

2.3.1 MediaBrowserService.BrowserRoot

包含浏览器服务首次连接时需要返回给客户端的信息。

MediaBrowserService.BrowserRoot API 列表

方法名备注
Bundle getExtras()获取有关浏览器服务的附加信息。
String getRootId()获取用于浏览的根 ID。

2.3.2 MediaBrowserService.Result

包含浏览器服务返回给客户端的结果集。通过调用sendResult()将结果返回给调用方,但是在此之前需要调用detach()

MediaBrowserService.Result API 列表

方法名备注
void detach()将此消息与当前线程分离,并允许稍后进行调用sendResult(T)
void sendResult(T result)将结果发送回调用方。

2.4 服务端媒体会话 - MediaSession

媒体会话,即**受控端。通过设定MediaSession.Callback回调来接收媒体控制器MediaController发送的指令。
创建MediaSession后还需要调用setSessionToken()方法设置用于和
控制器配对的令牌。使用方式如下:

const val FOLDERS_ID = "__FOLDERS__"
const val ARTISTS_ID = "__ARTISTS__"
const val ALBUMS_ID = "__ALBUMS__"
const val GENRES_ID = "__GENRES__"
const val ROOT_ID = "__ROOT__"

class MediaService : MediaBrowserService() 

    private lateinit var mediaSession: MediaSession;

    override fun onCreate() 
        super.onCreate()
        mediaSession = MediaSession(this, "TAG")
        mediaSession.setCallback(callback)
        sessionToken = mediaSession.sessionToken
    

    // 与MediaController.transportControls中的大部分方法都是一一对应的
    // 在该方法中实现对 播放器 的控制,
    private val callback = object : MediaSession.Callback() 

        override fun onPlay() 
            super.onPlay()
            // 处理 播放器 的播放逻辑。
            // 车载应用的话,别忘了处理音频焦点
        

        override fun onPause() 
            super.onPause()
        

    
        override fun onGetRoot(
        clientPackageName: String,
        clientUid: Int,
        rootHints: Bundle?
    ): BrowserRoot? 
        Log.e("TAG", "onGetRoot: $rootHints")
        return BrowserRoot(ROOT_ID, null)
    

    override fun onLoadChildren(
        parentId: String,
        result: Result<MutableList<MediaBrowser.MediaItem>>
    ) 
        Log.e("TAG", "onLoadChildren: $parentId")
        result.detach()
        when (parentId) 
            ROOT_ID -> 
                result.sendResult(null)
            
            FOLDERS_ID -> 

            
            ALBUMS_ID -> 

            
            ARTISTS_ID -> 

            
            GENRES_ID -> 

            
            else -> 

            
        
    

    override fun onLoadItem(itemId: String?, result: Result<MediaBrowser.MediaItem>?) 
        super.onLoadItem(itemId, result)
        Log.e("TAG", "onLoadItem: $itemId")
    

2.4.1 MediaSession.Callback

接收来自控制器和系统的媒体按钮、传输控件和命令。与MediaController.transportControls中的大部分方法都是一一对应的。使用方式如下:

override fun onCreate() 
    super.onCreate()
    mediaSession = MediaSession(this, "TAG")
    mediaSession.setCallback(callback)
    sessionToken = mediaSession.sessionToken


// 与MediaController.transportControls中的方法是一一对应的。
// 在该方法中实现对 播放器 的控制,
private val callback = object : MediaSession.Callback() 

    override fun onPlay() 
        super.onPlay()
        // 处理 播放器 的播放逻辑。
        // 车载应用的话,别忘了处理音频焦点
        // ...
        if (!mediaSession.isActive) 
            mediaSession.isActive = true
        
        // 更新播放状态.
        val state = PlaybackState.Builder()
            .setState(
                PlaybackState.STATE_PLAYING,1,1f
            )
            .build()
        // 此时MediaController.Callback.onPlaybackStateChanged会回调
        mediaSession.setPlaybackState(state)
    

    override fun onPause() 
        super.onPause()
    

    override fun onStop() 
        super.onStop()
    


2.4.2 MediaSession.QueueItem

作为播放队列一部分的单个项目。它包含队列中项目及其 ID 的说明。
MediaSession.QueueItem API 列表

方法名备注
MediaDescription getDescription()返回介质的说明。包含媒体的基础信息如:标题、封面等等。
long getQueueId()获取此项目的队列 ID。

2.4.3 MediaSession.Token

表示正在进行的会话。这可以通过会话所有者传递给客户端,以允许客户端与服务端之间建立通信。

2.6 播放器状态 - PlaybackState

用于承载播放状态的类。如当前播放位置和当前控制功能。
MediaSession.Callback更改状态后需要调用MediaSession.setPlaybackState把状态同步给客户端。使用方式如下:

private val callback = object : MediaSession.Callback() 

    override fun onPlay() 
        super.onPlay()
        // ...
        // 更新状态
        val state = PlaybackState.Builder()
            .setState(
                PlaybackState.STATE_PLAYING,1,1f
            )
            .build()
        mediaSession.setPlaybackState(state)
    

2.6.1 PlaybackState.Builder

基于建造者模式来生成PlaybackState对象。使用方式如下:

PlaybackState state = new PlaybackState.Builder()
        .setState(PlaybackState.STATE_PLAYING,
                mMediaPlayer.getCurrentPosition(), PLAYBACK_SPEED)
        .setActions(PLAYING_ACTIONS)
        .addCustomAction(mShuffle)
        .setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId())
        .build();

2.6.2 PlaybackState.CustomAction

CustomActions可用于通过将特定于应用程序的操作发送给MediaControllers,这样就可以扩展标准传输控件的功能。使用方式如下:

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();

PlaybackState.CustomAction API 说明

方法名备注
String getAction()返回CustomAction的action。
Bundle getExtras()返回附加项,这些附加项提供有关操作的其他特定于应用程序的信息,如果没有,则返回 null。
int getIcon()返回package中图标的资源 ID。
CharSequence getName()返回此操作的显示名称。

2.7 元数据类 - MediaMetadata

包含有关项目的基础数据,例如标题、艺术家等。一般需要服务端从本地数据库或远端查询出原始数据在封装成MediaMetadata再通过MediaSession.setMetadata(metadata)返回到客户端MediaController.Callback.onMetadataChanged中。

MediaMetadata API 说明

方法名备注
boolean containsKey(String key)如果给定的key包含在元数据中,则返回 true
int describeContents()描述此可打包实例的封送处理表示中包含的特殊对象的种类。
Bitmap getBitmap(String key)返回给定的key的Bitmap;如果给定key不存在位图,则返回 null。
int getBitmapDimensionLimit()获取创建此元数据时位图的宽度/高度限制(以像素为单位)。
MediaDescription getDescription()获取此元数据的简单说明以进行显示。
long getLong(String key)返回与给定key关联的值,如果给定key不再存在,则返回 0L。
Rating getRating(String key)对于给定的key返回Rating;如果给定key不存在Rating,则返回 null。
String getString(String key)以 String 格式返回与给定key关联的文本值,如果给定key不存在所需类型的映射,或者null值显式与该key关联,则返回 null。
CharSequence getText(String key)返回与给定键关联的值,如果给定键不存在所需类型的映射,或者与该键显式关联 null 值,则返回 null。
Set keySet()返回一个 Set,其中包含在此元数据中用作key的字符串。
int size()返回此元数据中的字段数。

MediaMetadata 常用Key

方法名备注
METADATA_KEY_ALBUM媒体的唱片集标题。
METADATA_KEY_ALBUM_ART媒体原始来源的相册的插图,Bitmap格式
METADATA_KEY_ALBUM_ARTIST媒体原始来源的专辑的艺术家。
METADATA_KEY_ALBUM_ART_URI媒体原始源的相册的图稿,Uri格式(推荐使用)
METADATA_KEY_ART媒体封面,Bitmap格式
METADATA_KEY_ART_URI媒体的封面,Uri格式。
METADATA_KEY_ARTIST媒体的艺术家。
METADATA_KEY_AUTHOR媒体的作者。
METADATA_KEY_BT_FOLDER_TYPE蓝牙 AVRCP 1.5 的 6.10.2.2 节中指定的媒体的蓝牙文件夹类型。
METADATA_KEY_COMPILATION媒体的编译状态。
METADATA_KEY_COMPOSER媒体的作曲家。
METADATA_KEY_DATE媒体的创建或发布日期。
METADATA_KEY_DISC_NUMBER介质原始来源的光盘编号。
METADATA_KEY_DISPLAY_DESCRIPTION适合向用户显示的说明。
METADATA_KEY_DISPLAY_ICON适合向用户显示的图标或缩略图。
METADATA_KEY_DISPLAY_ICON_URI适合向用户显示的图标或缩略图, Uri格式。
METADATA_KEY_DISPLAY_SUBTITLE适合向用户显示的副标题。
METADATA_KEY_DISPLAY_TITLE适合向用户显示的标题。
METADATA_KEY_DURATION媒体的持续时间(以毫秒为单位)。
METADATA_KEY_GENRE媒体的流派。
METADATA_KEY_MEDIA_ID用于标识内容的字符串Key。
METADATA_KEY_MEDIA_URI媒体内容,Uri格式。
METADATA_KEY_NUM_TRACKS媒体原始源中的曲目数。
METADATA_KEY_RATING媒体的总体评分。
METADATA_KEY_TITLE媒体的标题。
METADATA_KEY_TRACK_NUMBER媒体的磁道编号。
METADATA_KEY_USER_RATING用户对媒体的分级。
METADATA_KEY_WRITER媒体作家。
String METADATA_KEY_YEAR媒体创建或发布为长的年份。

3. MediaSession 简单实践

MediaSession 框架核心类通信过程如下图所示。

客户端源码如下所示:

class MainActivity : AppCompatActivity() 

    private lateinit var mMediaBrowser: MediaBrowser
    private lateinit var mMediaController: MediaController

    @RequiresApi(Build.VERSION_CODES.M)
    override fun onCreate(savedInstanceState: Bundle?) 
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val component = ComponentName(this, MediaService::class.java)
        mMediaBrowser = MediaBrowser(this, component, connectionCallback, null);
        // 连接到MediaBrowserService,会触发MediaBrowserService的onGetRoot方法。
        mMediaBrowser.connect()

        findViewById<Button>(R.id.btn_play).setOnClickListener 
            mMediaController.transportControls.play()
        
    

    private val connectionCallback = object : MediaBrowser.ConnectionCallback() 

        override fun onConnected() 
            super.onConnected()
            if (mMediaBrowser.isConnected) 
                val sessionToken = mMediaBrowser.sessionToken
                mMediaController = MediaController(applicationContext, sessionToken)
                mMediaController.registerCallback(controllerCallback)
                // 获取根mediaId
                val rootMediaId = mMediaBrowser.root
                // 获取根mediaId的item列表,会触发MediaBrowserService.onLoadItem方法
                mMediaBrowser.getItem(rootMediaId,itemCallback)
                mMediaBrowser.unsubscribe(rootMediaId)
                // 订阅服务端 media item的改变,会触发MediaBrowserService.onLoadChildren方法
                mMediaBrowser.subscribe(rootMediaId, subscribeCallback)
            
        
    

    private val controllerCallback = object : MediaController.Callback() 

        override fun onPlaybackStateChanged(state: PlaybackState?) 
            super.onPlaybackStateChanged(state)
            Log.d("TAG", "onPlaybackStateChanged: $state")
            when(state?.state)
                PlaybackState.STATE_PLAYING ->
                    // 处理UI
                
                PlaybackState.STATE_PAUSED ->
                    // 处理UI
                
                // 还有其它状态需要处理
            
        

        // 音频信息,音量
        override fun onAudioInfoChanged(info: MediaController.PlaybackInfo?) 
            super.onAudioInfoChanged(info)
            val currentVolume = info?.currentVolume
            // 显示在UI上
        

        override fun onMetadataChanged(metadata: MediaMetadata?) 
            super.onMetadataChanged(metadata)
            val artUri = metadata?.getString(MediaMetadata.METADATA_KEY_ALBUM_ART_URI)
            // 显示UI上
        

        override fun onSessionEvent(event: String, extras: Bundle?) 
            super.onSessionEvent(event, extras)
            Log.d("TAG", "onSessionEvent: $event")
        
        // ...
    

    private val subscribeCallback = object : MediaBrowser.SubscriptionCallback() 
        override fun onChildrenLoaded(
            parentId: String,
            children: MutableList<MediaBrowser.MediaItem>
        ) 
            super.onChildrenLoaded(parentId, children)
        

        override fun onChildrenLoaded(
            parentId: String,
            children: MutableList<MediaBrowser.MediaItem>,
            options: Bundle
        ) 
            super.onChildrenLoaded(parentId, children, options)
        

        override fun onError(parentId: String) 
            super.onError(parentId)
        
    

    private val itemCallback = object : MediaBrowser.ItemCallback() 

        override fun onItemLoaded(item: MediaBrowser.MediaItem?) 
            super.onItemLoaded(item)
        

        override fun onError(mediaId: String) 
            super.onError(mediaId)
        
    


服务端源码如下所示:


const val FOLDERS_ID = "__FOLDERS__"
const val ARTISTS_ID = "__ARTISTS__"
const val ALBUMS_ID = "__ALBUMS__"
const val GENRES_ID = "__GENRES__"
const val ROOT_ID = "__ROOT__"

class MediaService : MediaBrowserService() 

    // 控制是否允许客户端连接,并返回root media id给客户端
    override fun onGetRoot(
        clientPackageName: String,
        clientUid: Int,
        rootHints: Bundle?
    ): BrowserRoot? 
        Log.e("TAG", "onGetRoot: $rootHints")
        return BrowserRoot(ROOT_ID, null)
    

    // 处理客户端的订阅信息
    override fun onLoadChildren(
        parentId: String,
        result: Result<MutableList<MediaBrowser.MediaItem>>
    ) 
        Log.e("TAG", "onLoadChildren: $parentId")
        result.detach()
        when (parentId) 
            ROOT_ID -> 
                result.sendResult(null)
            
            FOLDERS_ID -> 

            
            ALBUMS_ID -> 

            
            ARTISTS_ID -> 

            
            GENRES_ID -> 

            
            else -> 

            
        
    

    override fun onLoadItem(itemId: String?, result: Result<MediaBrowser.MediaItem>?) 
        super.onLoadItem(itemId, result)
        Log.e("TAG", "onLoadItem: $itemId")
        // 根据itemId,返回对用MediaItem
        result?.detach()
        result?.sendResult(null)
    

    private lateinit var mediaSession: MediaSession;

    override fun onCreate() 
        super.onCreate()
        mediaSession = MediaSession(this, "TAG")
        mediaSession.setCallback(callback)
        // 设置token
        sessionToken = mediaSession.sessionToken
    

    // 与MediaController.transportControls中的方法是一一对应的。
    // 在该方法中实现对 播放器 的控制,
    private val callback = object : MediaSession.Callback() 

        override fun onPlay() 
            super.onPlay()
            // 处理 播放器 的播放逻辑。
            // 车载应用的话,别忘了处理音频焦点
            Log.e("TAG", "onPlay:")
            if (!mediaSession.isActive) 
                mediaSession.isActive = true
            
            // 更新状态
            val state = PlaybackState.Builder()
                .setState(
                    PlaybackState.STATE_PLAYING, 1, 1f
                )
                .build()
            mediaSession.setPlaybackState(state)
        

        override fun onPause() 
            super.onPause()
        

        override fun onStop() 
            super.onStop()
        

        // 还有其它方法需要复写
    

上述的代码只是帮助理解MediaSession框架的通信过程,本身的功能非常的简陋。上一篇Android车载应用开发与分析(6)- 车载多媒体(一)- 音视频基础知识与MediaPlayer中介绍了音视频的基础知识和MediaPlayer的生命周期,再通过本篇了解了MediaSession框架的基础使用,下一篇我们就可以开始解析车载Android中的原生LocalMedia应用了。

4. MediaSession API 列表

4.1 MediaBrowser 相关组件 API 列表

4.1.1 MediaBrowser

方法名备注
void connect()连接到媒体浏览器服务。
void disconnect()断开与媒体浏览器服务的连接。
Bundle getExtras()获取介质服务的任何附加信息。
void getItem(String mediaId, MediaBrowser.ItemCallback cb)从连接的服务中检索特定的MediaItem
String getRoot()获取根ID。
ComponentName getServiceComponent()获取媒体浏览器连接到的服务组件。
MediaSession.Token getSessionToken()获取与媒体浏览器关联的媒体会话Token。
boolean isConnected()返回浏览器是否连接到服务。
void subscribe(String parentId,Bundle options, MediaBrowser.SubscriptionCallback callback)使用特定于服务的参数进行查询,以获取有关指定 ID 中包含的媒体项的信息,并订阅以在更新更改时接收更新。
void subscribe(String parentId, MediaBrowser.SubscriptionCallback callback)询有关包含在指定 ID 中的媒体项的信息,并订阅以在更改时接收更新。
void unsubscribe(String parentId)取消订阅指定媒体 ID 。
void unsubscribe(String parentId, MediaBrowser.SubscriptionCallback callback)通过回调取消订阅对指定媒体 ID。

4.1.2 MediaBrowser.ConnectionCallback

方法备注
onConnected()与MediaBrowserService连接成功。在调用MediaBrowser.connect()后才会有回调。
onConnectionFailed()与MediaBrowserService连接失败。
onConnectionSuspended()与MediaBrowserService连接断开。

4.1.3 MediaBrowser. ItemCallback

方法名备注
onError(String mediaId)检索时出错,或者连接的服务不支持时回调。
onItemLoaded(MediaBrowser.MediaItem item)返回Item时调用。

4.1.4 MediaBrowser. MediaItem

方法名备注
int describeContents()描述此可打包实例的封送处理表示中包含的特殊对象的种类。
MediaDescription getDescription()获取介质的说明。包含媒体的基础信息如:标题、封面等等。
int getFlags()获取项的标志。FLAG_BROWSABLE:表示Item具有自己的子项。FLAG_PLAYABLE:表示Item可播放
String getMediaId()返回此项的媒体 ID。
boolean isBrowsable()返回此项目是否可浏览。
boolean isPlayable()返回此项是否可播放。

4.1.5 MediaBrowser.SubscriptionCallback

方法名备注
onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children)在加载或更新子项列表时回调。
onChildrenLoaded(String parentId, List<MediaBrowser.MediaItem> children,Bundle options)在加载或更新子项列表时回调。
onError(String parentId)当 ID 不存在或订阅时出现其他错误时回调。
onError(String parentId, Bundle options)当 ID 不存在或订阅时出现其他错误时回调。

4.2 MediaController 相关组件 API 列表

4.2.1 MediaController

方法名备注
void adjustVolume (int direction, int flags)调整此会话正在播放的输出的音量。
boolean dispatchMediaButtonEvent (KeyEvent keyEvent)将指定的媒体按钮事件发送到会话。
Bundle getExtras()获取此会话的附加内容。
long getFlags()获取此会话的标志。
MediaMetadata getMetadata()获取此会话的当前Metadata。
String getPackageName()获取会话所有者的程序包名称。
MediaController.PlaybackInfo getPlaybackInfo()获取此会话的当前播放信息。
PlaybackState getPlaybackState()获取此会话的当前播放状态。
List<MediaSession.QueueItem> getQueue()获取此会话的当前播放队列(如果已设置)。
CharSequence getQueueTitle()获取此会话的队列标题。
int getRatingType()获取会话支持的评级类型。
PendingIntent getSessionActivity()获取启动与此会话关联的 UI 的意图(如果存在)。
Bundle getSessionInfo()获取创建会话时设置的其他会话信息。
MediaSession.Token getSessionToken()获取连接到的会话的令牌。
String getTag()获取会话的标记以进行调试。
MediaController.TransportControls getTransportControls()获取TransportControls实例以将控制操作发送到关联的会话。
void registerCallback (MediaController.Callback callback, Handler handler)注册回调以从会话接收更新。
void registerCallback (MediaController.Callback callback)注册回调以从会话接收更新。
void sendCommand (String command, Bundle args, ResultReceiver cb)向会话发送通用命令。
void setVolumeTo (int value, int flags)设置此会话正在播放的输出的音量。
void unregisterCallback (MediaController.Callback callback)注销指定的回调。

4.2.2 MediaController.Callback

方法名备注
void onAudioInfoChanged (MediaController.PlaybackInfo info)当前音频信息发生改变。
void onExtrasChanged (Bundle extras)当前附加内容发生改变。
void onMetadataChanged (MediaMetadata metadata)当前Metadata发生改变。
void onPlaybackStateChanged(PlaybackState state)当前播放状态发生改变。客户端通过该回调来显示界面上音视频的播放状态。
void onQueueChanged (List<MediaSession.QueueItem> queue)当前队列中项目发生改变。
void onQueueTitleChanged (CharSequence title)当前队列标题发生改变。
void onSessionDestroyed()会话销毁。
void onSessionEvent (String event, Bundle extras)MediaSession所有者发送的自定义事件。

4.2.3 MediaController. PlaybackInfo

方法名备注
AudioAttributes getAudioAttributes()获取此会话的音频属性。
int getCurrentVolume()获取此会话的当前音量。
int getMaxVolume()获取可为此会话设置的最大音量。
int getPlaybackType()获取影响音量处理的播放类型。
int getVolumeControl()获取可以使用的音量控件的类型。
String getVolumeControlId()获取此会话的音量控制 ID。

4.2.4 MediaController. TransportControls

方法名备注
void fastForward()开始快进。
void pause()请求播放器暂停播放并保持在当前位置。
void play()请求播放器在其当前位置开始播放。
void playFromMediaId (String mediaId, Bundle extras)请求播放器开始播放特定媒体 ID。
void playFromSearch (String query, Bundle extras)请求播放器开始播放特定的搜索查询。
void playFromUri (Uri uri, Bundle extras)请求播放器开始播放特定Uri。
void prepare()请求播放器准备播放。
void prepareFromMediaId (String mediaId, Bundle extras)请求播放器为特定媒体 ID 准备播放。
void prepareFromSearch (String query, Bundle extras)请求播放器为特定搜索查询准备播放。
void prepareFromUri (Uri uri, Bundle extras)请求播放器为特定Uri。
void rewind()开始倒带。
void seekTo(long pos)移动到媒体流中的新位置。
void sendCustomAction (PlaybackState.CustomAction customAction, Bundle args)发送自定义操作以供MediaSession执行。
void sendCustomAction (String action,Bundle args)将自定义操作中的 id 和 args 发送回去,以便MediaSession执行。
void setPlaybackSpeed (float speed)设置播放速度。
void setRating(Rating rating)对当前内容进行评级。
void skipToNext()跳到下一项。
void skipToPrevious()跳到上一项。
void skipToQueueItem(long id)在播放队列中播放具有特定 ID 的项目。
void stop()请求播放器停止播放;它可以以任何适当的方式清除其状态。

4.3 MediaBrowserService 相关组件 API 列表

4.3.1 MediaBrowserService

方法名备注
final Bundle getBrowserRootHints()获取从当前连接 MediaBrowser的发送的根提示。
final MediaSessionManager.RemoteUserInfo getCurrentBrowserInfo()获取发送当前请求的浏览器信息。
MediaSession.Token getSessionToken()获取会话令牌,如果尚未创建会话令牌或已销毁会话令牌,则获取 null。
void notifyChildrenChanged(String parentId)通知所有连接的媒体浏览器指定父 ID 的子级已经更改。
void notifyChildrenChanged(String parentId, Bundle options)通知所有连接的媒体浏览器指定父 ID 的子级已经更改。
abstract MediaBrowserService.BrowserRoot onGetRoot(String clientPackageName,int clientUid, Bundle rootHints)获取供特定客户端浏览的根信息。由MediaBrowser.connect触发,可以通过返回null拒绝客户端的连接。
abstract void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result)获取有关媒体项的子项的信息。由MediaBrowser.subscribe触发。
void onLoadChildren(String parentId, Result<List<MediaBrowser.MediaItem>> result,Bundle options)获取有关媒体项的子项的信息。由MediaBrowser.subscribe触发。
void onLoadItem(String itemId, Result<MediaBrowser.MediaItem> result)获取有关特定媒体项的信息。由MediaBrowser.getItem触发。
void setSessionToken(MediaSession.Token token)设置媒体会话。

4.3.2 MediaBrowserService.BrowserRoot

方法名备注
Bundle getExtras()获取有关浏览器服务的附加信息。
String getRootId()获取用于浏览的根 ID。

4.3.3 MediaBrowserService.Result

方法名备注
void detach()将此消息与当前线程分离,并允许稍后进行调用sendResult(T)
void sendResult(T result)将结果发送回调用方。

4.4 MediaSession 相关组件 API 列表

4.4.1 MediaSession

方法名备注
MediaController getController()获取此会话的控制器。
MediaSessionManager.RemoteUserInfo getCurrentControllerInfo()获取发送当前请求的控制器信息。
MediaSession.Token getSessionToken()获取此会话令牌对象。
boolean isActive()获取此会话的当前活动状态。
void release()当应用完成播放时,必须调用此项。
void sendSessionEvent (String event, Bundle extras)将专有事件发送给监听此会话的所有MediaController。会触发MediaController.Callback.onSessionEvent。
void setActive(boolean active)设置此会话当前是否处于活动状态并准备好接收命令。
void setCallback (MediaSession.Callback callback)设置回调以接收媒体会话的更新。
void setCallback (MediaSession.Callback callback,Handler handler)设置回调以接收媒体会话的更新。
void setExtras(Bundle extras)设置一些可与MediaSession关联的附加功能。
void setFlags(int flags)为会话设置标志。
void setMediaButtonBroadcastReceiver(ComponentName broadcastReceiver)设置应接收媒体按钮的清单声明类的组件名称。
void setMediaButtonReceiver(PendingIntent mbr)此方法在 API 级别 31 中已弃用。改用setMediaButtonBroadcastReceiver(android.content.ComponentName)。
void setMetadata(MediaMetadata metadata)更新当前MediaMetadata。
void setPlaybackState(PlaybackState state)更新当前播放状态。
void setPlaybackToLocal(AudioAttributes attributes)设置此会话音频的属性。
void setPlaybackToRemote(VolumeProvider volumeProvider)将此会话配置为使用远程音量处理。
void setQueue(List<MediaSession.QueueItem> queue)更新播放队列中的项目列表。
void setQueueTitle(CharSequence title)设置播放队列的标题。
void setRatingType(int type)设置此会话使用的评级样式。
void setSessionActivity(PendingIntent pi)设置启动此会话的Activity的Intent。

4.4.2 MediaSession.Callback

方法名备注
void onCommand(String command,Bundle args,ResultReceiver cb)当控制器已向此会话发送命令时调用。
void onCustomAction(String action, Bundle extras)当要执行MediaControllerPlaybackState.CustomAction时调用。
void onFastForward()处理快进请求。
boolean onMediaButtonEvent(Intent mediaButtonIntent)当按下媒体按钮并且此会话具有最高优先级或控制器向会话发送媒体按钮事件时调用。
void onPause()处理暂停播放的请求。
void onPlay()处理开始播放的请求。
void onPlayFromMediaId(String mediaId, Bundle extras)处理播放应用提供的特定mediaId的播放请求。
void onPlayFromSearch(String query, Bundle extras)处理从搜索查询开始播放的请求。
void onPlayFromUri(Uri uri, Bundle extras)处理播放由URI表示的特定媒体项的请求。
void onPrepare()处理准备播放的请求。
void onPrepareFromMediaId(String mediaId, Bundle extras)处理应用提供的特定mediaId的准备播放请求
void onPrepareFromSearch(String query, Bundle extras)处理准备从搜索查询播放的请求。
void onPrepareFromUri(Uri uri, Bundle extras)处理由URI表示的特定媒体项的准备请求。
void onRewind()处理倒带请求。
void onSeekTo(long pos)处理跳转到特定位置的请求。
void onSetPlaybackSpeed(float speed)处理修改播放速度的请求。
void onSetRating(Rating rating)处理设定评级的请求。
void onSkipToNext()处理要跳到下一个媒体项的请求。
void onSkipToPrevious()处理要跳到上一个媒体项的请求。
void onSkipToQueueItem(long id)处理跳转到播放队列中具有给定 ID 的项目的请求。
void onStop()处理停止播放的请求。

4.4.3 MediaSession.QueueItem

方法名备注
MediaDescription getDescription()返回介质的说明。包含媒体的基础信息如:标题、封面等等。
long getQueueId()获取此项目的队列 ID。

4.5 PlaybackState 相关组件 API 列表

4.5.1 PlaybackState

方法名备注
long getActions()获取此会话上可用的当前操作。
long getActiveQueueItemId()获取队列中当前活动项的 ID。
long getBufferedPosition()获取当前缓冲位置(以毫秒为单位)。
List<PlaybackState.CustomAction> getCustomActions()获取自定义操作的列表。
CharSequence getErrorMessage()获取用户可读的错误消息。
Bundle getExtras()获取在此播放状态下设置的任何自定义附加内容。
long getLastPositionUpdateTime()获取上次更新位置的经过的实时时间。
float getPlaybackSpeed()获取当前播放速度作为正常播放的倍数。
long getPosition()获取当前播放位置(以毫秒为单位)。
int getState()获取当前播放状态。
boolean isActive()返回是否将其视为活动播放状态。

4.5.2 PlaybackState.Builder

方法名备注
PlaybackState.Builder addCustomAction(String action, String name, int icon)将自定义操作添加到播放状态。
PlaybackState.Builder addCustomAction (PlaybackState.CustomAction customAction)将自定义操作添加到播放状态。
PlaybackState.Builder setActions(long actions)设置此会话上可用的当前操作。
PlaybackState.Builder setActiveQueueItemId(long id)通过指定活动项目的 id 来设置播放队列中的活动项目。
PlaybackState.Builder setBufferedPosition(long bufferedPosition)设置当前缓冲位置(以毫秒为单位)。
PlaybackState.Builder setErrorMessage(CharSequence error)设置用户可读的错误消息。
PlaybackState.Builder setExtras(Bundle extras)设置要包含在播放状态中的任何自定义附加内容。
PlaybackState.Builder setState(int state, long position, float playbackSpeed)设置当前播放状态。
PlaybackState.Builder setState(int state, long position, float playbackSpeed, long updateTime)设置当前播放状态。
PlaybackState build()生成并返回具有这些值的PlaybackState实例。

4.5.3 PlaybackState.CustomAction

方法名备注
String getAction()返回CustomAction的action。
Bundle getExtras()返回附加项,这些附加项提供有关操作的其他特定于应用程序的信息,如果没有,则返回 null。
int getIcon()返回package中图标的资源 ID。
CharSequence getName()返回此操作的显示名称。

Android车载应用开发与分析(11)- 车载Android应用开发入门指南

1. 前言 - 移动互联网退潮下的汽车大战

将时间回退到2017年我大学刚毕业时,彼时移动互联网就已经开始退潮,各大个培训机构也纷纷停止了Android相关的培训,曾经热火朝天的应用开发从那时起,就开始走向下坡路,小程序以及众多跨平台框架也让市场对Android原生开发的需求逐年降低,市场需求的降低也造就了Android开发的面试变得史无前例的“卷”。

终于我在2019年选择离开了互联网,投身当时还不是非常火热的车载Android领域继续从事Android原生开发。而这一年中国首个外商独资的整车制造项目,“上海特斯拉超级工厂”开工了。

特斯拉在智能化和电子化上的巨大优势将智能汽车推向了一个全新的高度,先进的自动驾驶以及BMS电池管理系统,深深震撼了全世界的人,在当时的国人眼中特斯拉几乎就是新能源汽车的代名词,时至今日,Model Y和Model 3已也依然是新能源汽车领域的畅销车型。

众所周知汽车工业是发达国家重要的经济支柱,而中国是世界上最大汽车生产和销售国,特斯拉的热销立马就引发了一场 鲶鱼效应 ,国内外的汽车制造商纷纷开始布局智能化汽车,汽车工业走向了软件定义汽车的时代。软件定义汽车的核心思想是,决定未来汽车的是以人工智能为核心的软件技术,车载软件在汽车领域的重要性首次被拔高到了前所未有的高度,就这样一场轰轰烈烈的车载软件技术大战上演了。

2022.10.17 又更新了一篇,删减了部分内容,请参考:https://blog.csdn.net/linkwj/article/details/127398839

2. 智能汽车座舱基本结构

在从事车载Android应用开发前,必须要对汽车座舱的基本结构有一个大体的认知,只有意识到汽车座舱是一种与手机完全不同的架构,才能更好的助力我们日后学习车载Android应用的开发。下面就来介绍一个比较主流的车载操作系统架构。

注意:并不是所有的车载操作系统都采用了下面的架构,比如,特斯拉采用的是基于Linux一套架构。

上面就是目前主流汽车座舱采用技术架构,我们从上到下依次介绍:

T-BOX

T-Box又称TCU(车联网控制单元),指安装在汽车上用于控制跟踪汽车的嵌入式系统,是车载信息交互系统核心部件,有了它汽车才能实现联网功能,所以也起到中央网关的作用。通常包括GPS单元、移动通讯外部接口电子处理单元、微控制器、移动通讯单元以及存储器等。
对车辆,T-Box可提供车辆故障监控、电源管理、远程升级、数据采集、智慧交通等功能,对车主,T-Box可为提供车辆远程控制、安防服务等功能。
T-BOX属于外围硬件,与中控、仪表并不集成在一个主板上。

SOC

SoC的定义多种多样,由于其内涵丰富、应用范围广,很难给出准确定义。一般说来, SoC称为系统级芯片,也有称片上系统(System on Chip),意指它是一个产品,是一个有专用目标的集成电路,其中包含完整系统并有嵌入软件的全部内容。
车载Soc和我们最常见的手机Soc非常类似,内部集成了CPU和GPU。目前最主流的车载Soc是高通的SA8155,它就是高通在手机Soc骁龙855的基础上发展而来的。

MCU

微控制单元(Microcontroller Unit;MCU) ,又称单片微型计算机(Single Chip Microcomputer )或者单片机,是把中央处理器(Central Process Unit;CPU)的频率与规格做适当缩减,并将内存(memory)、计数器(Timer)、USB、A/D转换、UART、PLC、DMA等周边接口,甚至LCD驱动电路都整合在单一芯片上,形成芯片级的计算机。

一般汽车座舱内,集成SOC的主板上也会额外集成一个或多个MCU。

AutoSAR

Adaptive AutoSAR 是一种适用于高级自动驾驶的软件架构平台,提要提供高性能的计算和通信,提供灵活的软件配置,支撑应用的更新。
Adaptive AutoSAR 的主要架构分为硬件层、ARA(AutoSAR Run-timeFor Adaptive实时运行环境)以及应用层。
应用层包含的应用程序模块(AA)运行在ARA之上,每个AA以独立的进程运行。ARA由功能集群提供的应用接口组成,他们属于自适应平台。自适应平台提供Adaptive AutoSAR 的基本功能和标准服务。
每个AA可以向其他AA发生服务。基于这种架构,整车的功能之间可以解耦。

Hypervisor

一种运行在基础物理服务器和操作系统之间的中间软件层,可允许多个操作系统和应用共享硬件。也可叫做VMM( virtual machine monitor ),即虚拟机监视器。
目前主流的汽车座舱,都是同时在一个Soc上运行着两个不同的操作系统,一个是显示汽车仪表盘的QNX系统,另一个用于车载信息娱乐的Android系统,其底层技术原理就是Hypervisor。

QNX

QNX是一种商用的、遵从POSIX规范的类Unix实时操作系统,目标市场主要是面向嵌入式系统,具备高运行效率、高可靠性特点,并在工控领域拥有近40年的使用经验,被广泛应用于汽车、轨道交通、航空航天等对安全性、实时性要求较高的领域。
QNX在车载操作系统市场的占有率超过75%,在更注重生态和内容的车载娱乐系统占有率也超过60%,而在强调安全性的仪表盘以及驾驶辅助领域,QNX的市占率更是达到了近100%。

2010年QNX被加拿大RIM公司收购,而这家公司就是黑莓BlackBerry的母公司。

SOA

SOA(Service-OrientedArchitecture)是一种基于业务实现的粗粒度松耦合的面向服务的分布式架构,即实现业务和技术的分离,又实现业务和技术的自由组合。
以位置服务为例,很多车内应用会用到位置信息,像天气、拍照、导航,这些应用根据自身服务有不同的需求,对位置信息的处理各不相同,SOA就可以很好地解决这个问题。
SOA原本是服务器开发中用到的技术,现如今也被用在车载操作系统领域,但是目前关于SOA的技术规范比较混乱,国内主机厂商外对于SOA的实现方式也有区别。
SOA并不车载操作系统必须的,其实目前为止已经上市的车型中,很少采用了SOA架构,所以它还只是车载操作系统未来的一个发展方向。

2021年上汽零束率先发布业界首个车载SOA软件架构规范。威马汽车科技集团旗下的W6号称国内首款采用SOA的量产车。

车载以太网

车载以太网是一种用以太网连接车内电子单元的新型局域网技术,与传统以太网使用4对非屏蔽双绞线电缆不同,车载以太网在单对非屏蔽双绞线上可实现100Mbit/s,甚至1Gbit/s的传输速率,同时还满足汽车行业对高可靠性、低电磁辐射、低功耗、带宽分配、低延迟以及同步实时性等方面的要求。

车载以太网的设计是为了满足车载环境中的一些特殊需求。例如:满足车载设备对于电气特性的要求(EMI/RF);满足车载设备对高带宽、低延迟以及音视频同步等应用的要求;满足车载系统对网络管理的需求等。因此可以理解为,车载以太网在民用以太网协议的基础上,改变了物理接口的电气特性,并结合车载网络需求专门定制了一些新标准。针对车载以太网标准,IEEE组织也对IEEE 802.1和IEEE 802.3标准进行了相应的补充和修订。

CAN

CAN是控制器域网 (Controller Area Network, CAN) 的简称,是由研发和生产汽车电子产品著称的德国BOSCH公司开发了的,并最终成为国际标准(ISO11898)。是国际上应用最广泛的现场总线之一。 在北美和西欧,CAN总线协议已经成为汽车计算机控制系统和嵌入式工业控制局域网的标准总线,并且拥有以CAN为底层协议专为大型货车和重工机械车辆设计的J1939协议。近年来,其所具有的高可靠性和良好的错误检测能力受到重视,被广泛应用于汽车计算机控制系统和环境温度恶劣、电磁辐射强和振动大的工业环境。
CAN在车载操作系统&应用开发中使用非常广泛,车载Android的核心服务之一 - CarService本质上就是将外部硬件通信报文解析成上层应用可以识别的数据,这里的通信报文目前普遍都是CAN报文。

CAN通信在车载中使用的是如此广泛,以至于作为Android程序员,我们都不得不去学习CAN仿真测试工具的使用,有时候甚至需要我们去阅读、解析CAN报文。
值得一提的是CAN仿真测试工具非常昂贵,虽有国产替代,但目前依然普遍采用德国维克多公司出品的各类工具和软件,价格在数万元到数十万元不等。

3D HMI设计工具 & 嵌入式图形引擎

随着车载Soc算力的提高,现代座舱越来越喜欢引入3D化的图形界面,3D化的界面可以实时生成动画反馈,大大提升了界面的美观性和易用性。目前车载开发中主流的3D HMI设计工具&图形引擎有老牌的游戏开发工具如Unity 3d、Unreal(虚幻),也有专用于汽车HMI设计&图形显示的 — Kanzi 。

2016年芬兰汽车软件公司Rightware以及旗下产品Kanzi,被国内的汽车软件供应商中科创达收购。

上面介绍了汽车座舱的基础知识,Android应用程序员说到底还是负责在座舱中控,编写各类型的应用,下面就来介绍车载应用与互联网应用的不同之处。

3. 车载应用开发

车载Android应用说到底就是,在车载Android系统中嵌入一系列系统级应用,这里既包含与用户存在交互的HMI应用,也包含在后台运行没有HMI的Service应用。

一般而言,车载应用复杂度比一般的互联网应用还要低一些。

常见有HMI的车载应用如,车载空调、多媒体应用、桌面、SystemUI、系统设置、车控车设、蓝牙电话以及一些第三方应用等等。

没有HMI的应用有,CarService、AudioService、AccountService等等。在车载应用开发中需要定制大量的Service,这也是应用开发中工作量比较大的一部分。

3.1 系统级应用与普通应用的区别

系统应用需要嵌入到Android ROM中运行,虽然普通的应用也可以嵌入到ROM中,但是系统应用可以调用Android SDK的内部API,而这一点是普通应用做不到的,总得来说系统应用具有以下特点

  • 可以访问Android SDK内部的API
  • 不需要申请动态权限
  • 可配置开机自启动
  • 必须对应用进行签名

接下来我们实际上手编写一个系统级应用。

3.2 编写一个系统级应用

编写Android系统应用与普通的Android应用基本相同,我们首先在AndroidStudio中编写一个demo,只需要一个空白的Activity和Application即可。

public class DemoApp extends Application 

    private Handler handler;

    @Override
    public void onCreate() 
        super.onCreate();
        Log.e("TAG", "onCreate: start");
        handler = new Handler(Looper.getMainLooper());
        handler.postDelayed(new Runnable() 
            @Override
            public void run() 
                showView();
            
        ,5000);
    

    private void showView()
        WindowManager manager = getSystemService(WindowManager.class);
        View view = new View(this);
        WindowManager.LayoutParams params = new WindowManager.LayoutParams(WindowManager.LayoutParams.MATCH_PARENT,WindowManager.LayoutParams.MATCH_PARENT);
        params.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
        manager.addView(view,params);
    

上面的application逻辑很简单,app启动5秒后,弹出一个全屏的Window的。

接下来在AndroidManifest.xml中注册application。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.car"
    android:sharedUserId="android.uid.system">

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:persistent="true"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.First">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

在上面源码中我们需要关注两个普通应用用不到的属性:
android:sharedUserId
将与其他应用程序共享的 Linux 用户 ID 的名称。默认情况下,Android 会为每个应用分配自己唯一的用户 ID。但是,如果为两个或多个应用将此属性设置为相同的值,则它们将共享相同的 ID,前提是它们的证书集相同。具有相同用户 ID 的应用可以访问彼此的数据,如果需要,可以在同一进程中运行。
开发系统应用时,此项不是必须配置的。配置为android.uid.system后,该应用会变成system用户,可以访问一些system用户才能访问的空间。

android:persistent
配置应用程序是否应始终保持运行,默认为false。设为true之后,应用在开机广播发出之前就会自行启动,而且应用被杀死后,也会立即重启。
开发系统应用时,此项不是必须配置的。

3.3 测试系统应用

3.3.1 准备测试环境

测试系统应用就比较麻烦了,由于手边没有开发板,只能基于模拟器进行测试,所以就必须下载Android的源码,并使用Android源码环境编译出带有系统签名的APK。
下载、编译Android源码 请参考 :Android车载应用开发与分析(1) - Android Automotive概述与编译
完成Android源码编译后,我们将编写好的FirstCarApp部分源码拷贝到 /aosp/packages/apps/Car/ 下,
基于Android源码环境的app工程结构与基于Gradle的AndroidStudio工程结构是完全不一样的,目录结构如下:

你应该注意到了 src 目录下没有androids studio工程结构中的main/java
需要强调的是,这种基于原生的写法,并不常用。实际开发中,我们依然是在Android Studio中开发完毕,将源码提交到gerrit上,后续的编译、签名、复制的过程会有jenkins帮我们完成。

3.3.2 编译&运行应用

源码环境下编译出Android应用,需要编写一个Android.bp或Android.mk脚本,如果你对Android.bp或Android.mk并不了解的话请参考:Android.mk 上手指南 | Android.bp入门教程

本篇测试用的Android.bp脚本如下

package 
    default_applicable_licenses: ["Android-Apache-2.0"],


android_app 
    name: "CarFirstApp",
    srcs: ["src/**/*.java"],
    resource_dirs: ["res"],
    platform_apis: true,
    certificate: "platform",
    privileged: true,
    static_libs: [
    "androidx.appcompat_appcompat",
    "com.google.android.material_material",
    ],
    optimize: 
        enabled: false,
    ,
    dex_preopt: 
        enabled: false,
    ,
    product_variables: 
        pdk: 
            enabled: false,
        ,
    ,


然后完整编译一次Android的源码

# 编译Android源码
/aosp$ source build/envsetup.sh 
/aosp$ lunch 12
/aosp$ make -j 32
/aosp$ emulator -writable-system -netdelay none -netspeed full

一般情况下我们可以直接使用emulator指令就可以启动编译好的模拟器,但是此时的模拟器的文件系统还是read-only模式,并且不可以执行remount指令,通过添加-writable-system -netdelay none -netspeed full,我们就可以正常使用remount指令了。

/aosp$ adb root
/aosp$ adb remount
/aosp$ adb shell reboot

等模拟器重启后,我们继续编译出CarFristApp的apk。

link@link-PC:/aosp$ make CarFirstApp
...
## 编译后输出的apk路径
============================================
[100% 4/4] Install: out/target/product/generic_car_x86/system/priv-app/CarFirstApp/CarFirstApp.apk

#### build completed successfully (2 seconds) ####

然后使用adb指令在模拟器中创建一个CarFristApp目录,将编译好的apk push到system/priv-app/CarFristApp/目录下。

/CarFirstApp$ adb root
/CarFirstApp$ adb remount
# 创建目录
/CarFirstApp$ adb shell mkdir /system/priv-app/CarFirstApp
/CarFirstApp$ adb push CarFirstApp.apk /system/priv-app/CarFirstApp
# 重启
/CarFirstApp$ adb shell reboot

等待模拟器重启结束后,就可以看到,app会自行启动,然后会弹出一个WindowView遮住屏幕。不知道你是否注意到了,无论是自启动,还是弹出一个遮住屏幕的Window,都没有申请权限的窗口显示出来,这就系统级应用的一个重要特点。

在上面的操作中我们选择把apk push到priv-app下面,除此以外Android应用还有以下几种安装路径,可以根据实际需要安装到不同的目录中去。
/system/priv-app
该路径存放一些系统底层的应用,比如Setting,systemUI等。该目录中的app拥有较高的系统权限,而且如果要使用android:protectionLevel=signatureOrSystem,那么该app必须放到priv-app目录中去。

/system/app
该目录中存放的系统app权限相对较低,而且当拥有root权限时,就有可能卸载掉这些app。

/vendor/app
该目录存放vendor厂商的app

/data/app
用户安装的第三方app

3.4 车载应用的难点

车载应用开发过程中,往往都会遇到以下几个难点:

  • 调试耗时且费力
    车载应用开发难度其实并不大,但是很烦!特别是调试,不同于开发手机应用,车载应用的运行环境是基于AOSP定制的,而且大多数时候都会存在数不清的BUG,有时系统底层的bug会在上层应用中体现,这就要求应用开发者必须有能力准确识别出这个bug的归属方。

  • 复杂的UI
    现如今的车载应用都会有着一个套复杂且炫酷交互UI,同时,由于车载Android与QNX共享一个Soc和内存,所以多数时候系统资源都比主流的手机要差不少,对应用开发者来说,实现一套复杂且高性能的HMI,往往会非常有挑战性。

  • 对系统API理解不够
    开发车载应用多数时候都会要求重新定制一个原本系统中已经存在的应用,比如系统设置。这就要求开发者对于原生应用的运行方式、调用的API都有一定的了解。

4. 车载Android开发的前景

读完以上的内容,相信你已经对车载Android的开发有一个浅显的认识了。不知道你会不会认为我在劝你转行做车载Android的开发?答案是NO!

单纯的Android应用工程师在整车座舱上只能负责非常小的一个技术领域,这就已经决定了这个职业的发展高度,如果想突破这层天花板,就必须要深入到Android系统的底层,掌握Framework、HAL甚至于Native的一些运行原理。除此以外,Linux、汽车相关的知识也是需要额外学习的。

就目前而言,车载Android开发依然有着不错的前景,但还远没有达到曾经的移动互联网的热度,甚至可能以后也不会达到,并且就像曾经热火朝天的移动互联网一样,随着大量开发人员的涌入、汽车制造业的重新洗牌、供需关系的改变,总有一天它也会不可避免的走向下坡路。

我曾经后悔过入行车载开发,因为相比手机应用开发,所需要学习知识实在太多太杂,调试过程也比手机应用复杂,但是人这一辈何尝不是在后悔中度过的呢?

参考资料

智能座舱:智能化基础平台及架构(下)
2020年中国T-BOX行业现状分析,乘用车T-Box装配率迅速提升「图」
车载操作系统(三):智能座舱操作系统
首个专为先进智能驾舱打造的一体化HMI工具——Kanzi One重磅发布
Automotive | Android 开源项目 | Android Open Source Project
车载以太网-电子发烧友网

以上是关于Android车载应用开发与分析- 车载多媒体- 多媒体应用架构与MediaSession框架的主要内容,如果未能解决你的问题,请参考以下文章

Android车载应用开发与分析(11)- 车载Android应用开发入门指南

风口上的车载,最新Android车载开发工程师进阶笔记开源

车载系统应用开发入门指南——车载基础知识与Framework开发揭秘

基于Android车载系统模块资料

收藏这份《Android车载系统应用指南》,助你轻松入门,斩获高薪

大厂都在进军车载应用?Android车载应用开发,从零开始一起学