今天就開始實作音樂播放器整體架構了,還記得在第一天最後介紹到的架構圖嗎,這篇會從MediaBrowserService 開始實作,先從比較底層元件開始實作,播歌時需要 Android 架構的 Service 元件,讓音樂在背景能繼續播放,首先先 import androidx media 相關的 library。
implementation 'androidx.media:media:1.1.0'
實作的 Service 繼承 MediaBrowserServiceCompat,為官方為了播歌和瀏覽歌曲實作的 Service,並搭配之後會使用到 MediaBrowser,實作 Client / Server 架構。
同時在 AndroidManifest 內要註冊 Service,就像是 Activity 一樣,其中比較特別的是要加入 intent-filter,和 Android 系統註冊, App 有 MediaBrowser 的功能。
<service
android:name=".media.MusicService"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
繼承 MediaBrowserServiceCompat 後,有兩個 abstract function 是要實作的:
class MusicService: MediaBrowserServiceCompat() {
override fun onLoadChildren(
parentId: String,
result: Result<MutableList<MediaBrowserCompat.MediaItem>>
) {
TODO("Not yet implemented")
}
override fun onGetRoot(
clientPackageName: String,
clientUid: Int,
rootHints: Bundle?
): BrowserRoot? {
TODO("Not yet implemented")
}
}
首先是 onLoadChildren 屬於瀏覽歌曲相關,可以看到會傳入 parentId,然後再將 result 傳出,在 uamp 專案內點擊專輯後,會顯示專輯內的歌曲,收到 Id 後,去 local 的 repository 尋找相關的歌曲,然後將結果透過 result.sendResult(items) 回傳,但這個 function 目前用不到,因此就先不實作。
onGetRoot 有很重要的功能,可以決定要不要讓其他 App 或是服務連接上,連上後可以瀏覽音樂檔案並進行音樂播放相關的操作,聽起來有點抽象,就像是第一天提到的可以透過手錶或是其他 App 來控制或是取得相關的音樂資料,就是透過這個 function。
val isKnownCaller = packageValidator.isKnownCaller(clientPackageName, clientUid)
return if (isKnownCaller) {
/**
* By default return the browsable root. Treat the EXTRA_RECENT flag as a special case
* and return the recent root instead.
*/
val isRecentRequest = rootHints?.getBoolean(EXTRA_RECENT) ?: false
val browserRootPath = if (isRecentRequest) UAMP_RECENT_ROOT else UAMP_BROWSABLE_ROOT
BrowserRoot(browserRootPath, rootExtras)
} else {
/**
* Unknown caller. There are two main ways to handle this:
* 1) Return a root without any content, which still allows the connecting client
* to issue commands.
* 2) Return `null`, which will cause the system to disconnect the app.
*
* UAMP takes the first approach for a variety of reasons, but both are valid
* options.
*/
BrowserRoot(UAMP_EMPTY_ROOT, rootExtras)
}
那我們要怎麼決定規則呢?先來看 uamp 專案的寫法,會有一個進行確認的 function,裡面仔細看還蠻複雜的,會判一些可以連接的 Package 名稱的白名單(Android Auto, WearOS, Android Auto Simulator, Google Assistant),還會判斷 Sign key 的 signature 是否符合。用 uid 可以判斷是否為自己本身得 App 想要連接或是系統端要連:
val isCallerKnown = when {
// If it's our own app making the call, allow it.
callingUid == Process.myUid() -> true
// If it's one of the apps on the allow list, allow it.
isPackageInAllowList -> true
// If the system is making the call, allow it.
callingUid == Process.SYSTEM_UID -> true
...
那如果判斷是非預期的呼叫端呢,從 uamp 上的註解可以得知有兩個處理方式,一個方是回傳空的 BrowserRoot,就不能瀏覽音樂檔案,但可以進行播放的操作,另外一個方式直接回傳 null,完全不能連上,也就不能控制音樂的播放行為了。
目前先採用比較簡單的做法,只判斷是否為自己本身的 App 端呼叫,如果非自己本身的 App 呼叫,在 debug 版才能連上,release 版就先連不上。之後有其他的服務(ex: Google Assistant)有需要連上,就再來這邊加入。
val isKnownCaller = allowBrowsing(clientPackageName)
return if (isKnownCaller) {
BrowserRoot(FANCY_BROWSABLE_ROOT, null)
} else {
if (BuildConfig.DEBUG) {
BrowserRoot(FANCY_EMPTY_ROOT, null)
} else {
null
}
}
private fun allowBrowsing(clientPackageName: String, clientUid: Int): Boolean {
return clientPackageName == packageName
}
程式碼在這邊,分支名稱(day8_media_browser_service):Fancy/day8_media_browser_service