Publish the media3 session controller test app

Issue: androidx/media#78
PiperOrigin-RevId: 642277900
This commit is contained in:
ibaker 2024-06-11 08:24:31 -07:00 committed by Copybara-Service
parent b763673903
commit f54991eea1
80 changed files with 5019 additions and 0 deletions

View File

@ -24,6 +24,8 @@
* Add `SessionError` and use it in `SessionResult` and `LibraryResult` * Add `SessionError` and use it in `SessionResult` and `LibraryResult`
instead of the error code to provide more information about the error instead of the error code to provide more information about the error
and how to resolve the error if possible. and how to resolve the error if possible.
* Publish the code for the media3 controller test app that can be used to
test interactions with apps publishing a media session.
* UI: * UI:
* Add customisation of various icons in `PlayerControlView` through xml * Add customisation of various icons in `PlayerControlView` through xml
attributes to allow different drawables per `PlayerView` instance, attributes to allow different drawables per `PlayerView` instance,

View File

@ -56,4 +56,8 @@ project(modulePrefix + 'test-session-common').projectDir = new File(rootDir, 'li
include modulePrefix + 'test-session-current' include modulePrefix + 'test-session-current'
project(modulePrefix + 'test-session-current').projectDir = new File(rootDir, 'libraries/test_session_current') project(modulePrefix + 'test-session-current').projectDir = new File(rootDir, 'libraries/test_session_current')
// MediaController test app.
include modulePrefix + 'testapp-controller'
project(modulePrefix + 'testapp-controller').projectDir = new File(rootDir, 'testapps/controller')
apply from: 'core_settings.gradle' apply from: 'core_settings.gradle'

5
testapps/README.md Normal file
View File

@ -0,0 +1,5 @@
# Android media test apps
This directory contains applications that can be used to test an application's
integration with Android media APIs. Browse the individual test applications
and their READMEs to learn more.

View File

@ -0,0 +1,4 @@
# Media3 controller test app
This is the media3 controller test application. It allows media apps to verify
their media session implementation.

View File

@ -0,0 +1,70 @@
// Copyright 2021 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
apply from: '../../constants.gradle'
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
android {
namespace 'androidx.media3.testapp.controller'
compileSdk project.ext.compileSdkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
defaultConfig {
versionName project.ext.releaseVersion
versionCode project.ext.releaseVersionCode
minSdkVersion project.ext.minSdkVersion
targetSdkVersion project.ext.appTargetSdkVersion
multiDexEnabled true
vectorDrawables.useSupportLibrary = true
}
buildTypes {
release {
shrinkResources true
minifyEnabled true
proguardFiles = [
'proguard-rules.txt',
getDefaultProguardFile('proguard-android-optimize.txt')
]
signingConfig signingConfigs.debug
}
debug {
jniDebuggable = true
}
}
lintOptions {
// The test app isn't indexed, and doesn't have translations.
disable 'GoogleAppIndexingWarning','MissingTranslation'
}
}
dependencies {
implementation 'androidx.core:core-ktx:' + androidxCoreVersion
implementation 'androidx.appcompat:appcompat:' + androidxAppCompatVersion
implementation 'androidx.multidex:multidex:' + androidxMultidexVersion
implementation 'com.google.android.material:material:' + androidxMaterialVersion
implementation 'androidx.media:media:' + androidxMediaVersion
implementation project(modulePrefix + 'lib-session')
implementation project(modulePrefix + 'lib-datasource')
}

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2023 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<lint>
<issue id="UnsafeOptInUsageError">
<option name="opt-in" value="androidx.media3.common.util.UnstableApi" />
</issue>
</lint>

View File

@ -0,0 +1,2 @@
# Proguard rules specific to the media3 controller test app.

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="androidx.media3.testapp.controller">
<uses-sdk/>
<uses-permission
android:name="android.permission.INTERNET"/>
<uses-permission
tools:ignore="QueryAllPackagesPermission"
android:name="android.permission.QUERY_ALL_PACKAGES"/>
<queries>
<intent>
<action android:name="android.media.browse.MediaBrowserService" />
</intent>
<intent>
<action android:name="androidx.media3.session.MediaSessionService" />
</intent>
<intent>
<action android:name="androidx.media3.session.MediaLibraryService" />
</intent>
</queries>
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".LaunchActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".MediaAppControllerActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<service
android:name=".LaunchActivity$NotificationListener"
android:label="@string/app_name"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE"
android:exported="true">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService" />
</intent-filter>
</service>
</application>
</manifest>

View File

@ -0,0 +1,93 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.app.Activity
import android.media.AudioManager
import android.view.View
import android.widget.AdapterView
import android.widget.Spinner
import android.widget.ToggleButton
import androidx.appcompat.app.AppCompatActivity
import androidx.media.AudioFocusRequestCompat
import androidx.media.AudioManagerCompat
/** Helper class to manage audio focus requests and the UI surrounding this feature. */
class AudioFocusHelper(activity: Activity) :
View.OnClickListener,
AudioManager.OnAudioFocusChangeListener,
AdapterView.OnItemSelectedListener {
private val audioManager: AudioManager =
activity.getSystemService(AppCompatActivity.AUDIO_SERVICE) as AudioManager
private val toggleButton: ToggleButton = activity.findViewById(R.id.audio_focus_button)
private val focusTypeSpinner: Spinner = activity.findViewById(R.id.audio_focus_type)
private val selectedFocusType: Int
get() = FOCUS_TYPES[focusTypeSpinner.selectedItemPosition]
companion object {
private val FOCUS_TYPES =
intArrayOf(
AudioManager.AUDIOFOCUS_GAIN,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
)
}
init {
toggleButton.setOnClickListener(this)
this.focusTypeSpinner.onItemSelectedListener = this
}
override fun onClick(v: View) =
if (toggleButton.isChecked) {
gainAudioFocus()
} else {
abandonAudioFocus()
}
override fun onAudioFocusChange(focusChange: Int) =
when (focusChange) {
AudioManager.AUDIOFOCUS_GAIN,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK -> toggleButton.isChecked = true
else -> toggleButton.isChecked = false
}
override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
// If we're holding audio focus and the type should change, automatically
// request the new type of focus.
if (toggleButton.isChecked) {
gainAudioFocus()
}
}
override fun onNothingSelected(parent: AdapterView<*>?) {
// Nothing to do.
}
private fun gainAudioFocus() {
val audioFocusRequest: AudioFocusRequestCompat =
AudioFocusRequestCompat.Builder(selectedFocusType).setOnAudioFocusChangeListener(this).build()
AudioManagerCompat.requestAudioFocus(audioManager, audioFocusRequest)
}
private fun abandonAudioFocus() {
val audioFocusRequest: AudioFocusRequestCompat =
AudioFocusRequestCompat.Builder(selectedFocusType).setOnAudioFocusChangeListener(this).build()
AudioManagerCompat.abandonAudioFocusRequest(audioManager, audioFocusRequest)
}
}

View File

@ -0,0 +1,100 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
/** Utilities for [Bitmap]s. */
object BitmapUtils {
/**
* Converts a [Drawable] to an appropriately sized [Bitmap].
*
* @param resources Resources for the current [android.content.Context].
* @param drawable The [Drawable] to convert to a Bitmap.
* @param downScale Will downscale the Bitmap to `R.dimen.app_icon_size` dp.
* @return A Bitmap, no larger than `R.dimen.app_icon_size` dp if desired.
*/
fun convertDrawable(resources: Resources, drawable: Drawable, downScale: Boolean): Bitmap {
val bitmap: Bitmap
if (drawable is BitmapDrawable) {
bitmap = drawable.bitmap
} else {
bitmap =
Bitmap.createBitmap(
drawable.intrinsicWidth,
drawable.intrinsicHeight,
Bitmap.Config.ARGB_8888
)
val canvas = Canvas(bitmap)
drawable.setBounds(0, 0, canvas.width, canvas.height)
drawable.draw(canvas)
}
if (!downScale) {
return bitmap
}
val iconSize: Int = resources.getDimensionPixelSize(R.dimen.app_icon_size)
return if (bitmap.height > iconSize || bitmap.width > iconSize) {
// Which needs to be scaled to fit.
val height: Int = bitmap.height
val width: Int = bitmap.width
val scaleHeight: Int
val scaleWidth: Int
// Calculate the new size based on which dimension is larger.
if (height > width) {
scaleHeight = iconSize
scaleWidth = (width * iconSize.toFloat() / height).toInt()
} else {
scaleWidth = iconSize
scaleHeight = (height * iconSize.toFloat() / width).toInt()
}
Bitmap.createScaledBitmap(bitmap, scaleWidth, scaleHeight, false)
} else {
bitmap
}
}
/**
* Creates a Material Design compliant [androidx.appcompat.widget.Toolbar] icon from a given full
* sized icon.
*
* @param resources Resources for the current [android.content.Context].
* @param icon The bitmap to convert.
* @return A scaled Bitmap of the appropriate size and in-built padding.
*/
fun createToolbarIcon(resources: Resources, icon: Bitmap): Bitmap {
val padding: Int = resources.getDimensionPixelSize(R.dimen.margin_small)
val iconSize: Int = resources.getDimensionPixelSize(R.dimen.toolbar_icon_size)
val sizeWithPadding = iconSize + 2 * padding
// Create a Bitmap backed Canvas to be the toolbar icon.
val toolbarIcon: Bitmap =
Bitmap.createBitmap(sizeWithPadding, sizeWithPadding, Bitmap.Config.ARGB_8888)
val canvas = Canvas(toolbarIcon)
canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR)
// Resize the app icon to Material Design size.
val scaledIcon: Bitmap = Bitmap.createScaledBitmap(icon, iconSize, iconSize, false)
canvas.drawBitmap(scaledIcon, padding.toFloat(), padding.toFloat(), null)
return toolbarIcon
}
}

View File

@ -0,0 +1,200 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.app.Activity
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaBrowser
import androidx.media3.session.SessionCommand
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.common.util.concurrent.ListenableFuture
import java.util.Stack
/** Helper class that enables navigation on tree in MediaBrowser. */
class BrowseMediaItemsAdapter(
private val activity: Activity,
private val mediaBrowser: MediaBrowser
) : RecyclerView.Adapter<BrowseMediaItemsAdapter.ViewHolder>() {
private var items: List<MediaItem> = emptyList()
// Stack that holds ancestors of current item.
private val nodes = Stack<String>()
init {
val browseTreeList: RecyclerView = activity.findViewById(R.id.media_items_list)
browseTreeList.layoutManager = LinearLayoutManager(activity)
browseTreeList.setHasFixedSize(true)
browseTreeList.adapter = this
val topButtonView: View = activity.findViewById(R.id.media_browse_tree_top)
topButtonView.setOnClickListener {
if (!supportsSubscribe() || !supportsUnsubscribe()) {
Toast.makeText(activity, R.string.command_not_supported_msg, Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (nodes.size > 1) {
unsubscribe()
while (nodes.size > 1) nodes.pop()
subscribe()
}
}
val upButtonView: View = activity.findViewById(R.id.media_browse_tree_up)
upButtonView.setOnClickListener {
if (!supportsSubscribe() || !supportsUnsubscribe()) {
Toast.makeText(activity, R.string.command_not_supported_msg, Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (nodes.size > 1) {
unsubscribe()
nodes.pop()
subscribe()
}
}
if (
mediaBrowser.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT)
) {
val libraryResult: ListenableFuture<LibraryResult<MediaItem>> =
mediaBrowser.getLibraryRoot(null)
libraryResult.addListener(
{
val result: LibraryResult<MediaItem> = libraryResult.get()
result.value?.let { setRoot(it.mediaId) }
},
ContextCompat.getMainExecutor(activity)
)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.media_browse_item, parent, false)
)
@SuppressWarnings("FutureReturnValueIgnored")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if (items.isEmpty()) {
if (!supportsSubscribe() || !supportsUnsubscribe()) {
setMessageForEmptyList(holder, activity.getString(R.string.command_not_supported_msg))
} else {
setMessageForEmptyList(holder, activity.getString(R.string.media_browse_tree_empty))
}
return
}
val mediaMetadata: MediaMetadata = items[position].mediaMetadata
holder.name.text = mediaMetadata.title ?: "Title metadata empty"
holder.subtitle.text = mediaMetadata.subtitle ?: "Subtitle metadata empty"
holder.subtitle.visibility = View.VISIBLE
holder.icon.visibility = View.VISIBLE
when {
mediaMetadata.artworkUri != null -> {
holder.icon.setImageURI(mediaMetadata.artworkUri)
}
mediaMetadata.artworkData != null -> {
val bitmap: Bitmap =
BitmapFactory.decodeByteArray(
mediaMetadata.artworkData,
0,
mediaMetadata.artworkData!!.size
)
holder.icon.setImageBitmap(bitmap)
}
else -> {
holder.icon.setImageResource(R.drawable.ic_album_black_24dp)
}
}
val item: MediaItem = items[position]
holder.itemView.setOnClickListener {
if (mediaMetadata.isBrowsable == true) {
unsubscribe()
nodes.push(item.mediaId)
subscribe()
}
if (mediaMetadata.isPlayable == true) {
mediaBrowser.setMediaItem(MediaItem.Builder().setMediaId(item.mediaId).build())
mediaBrowser.prepare()
mediaBrowser.play()
}
}
}
override fun getItemCount(): Int {
// Leave one item for message if nodes or items are empty.
if (nodes.size == 0 || items.isEmpty()) return 1
return items.size
}
private fun supportsSubscribe(): Boolean =
mediaBrowser.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE)
private fun supportsUnsubscribe(): Boolean =
mediaBrowser.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE)
private fun setMessageForEmptyList(holder: ViewHolder, message: String) {
holder.name.text = message
holder.subtitle.visibility = View.GONE
holder.icon.visibility = View.GONE
holder.itemView.setOnClickListener {}
}
fun updateItems(newItems: List<MediaItem>) {
items = newItems
notifyDataSetChanged()
}
@SuppressWarnings("FutureReturnValueIgnored")
private fun subscribe() {
if (nodes.isNotEmpty() && supportsSubscribe()) {
mediaBrowser.subscribe(nodes.peek(), null)
}
}
@SuppressWarnings("FutureReturnValueIgnored")
private fun unsubscribe() {
if (nodes.isNotEmpty() && supportsUnsubscribe()) {
mediaBrowser.unsubscribe(nodes.peek())
}
updateItems(emptyList())
}
private fun setRoot(root: String) {
unsubscribe()
nodes.clear()
nodes.push(root)
subscribe()
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.item_name)
val subtitle: TextView = itemView.findViewById(R.id.item_subtitle)
val icon: ImageView = itemView.findViewById(R.id.item_icon)
}
}

View File

@ -0,0 +1,98 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.app.Activity
import android.content.res.Resources
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.res.ResourcesCompat
import androidx.media3.session.CommandButton
import androidx.media3.session.MediaController
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
/** Helper class that displays and handles custom commands. */
class CustomCommandsAdapter(
activity: Activity,
private val mediaController: MediaController,
packageName: String,
) : RecyclerView.Adapter<CustomCommandsAdapter.ViewHolder>() {
private var commands: List<CommandButton> = emptyList()
private val resources: Resources = activity.packageManager.getResourcesForApplication(packageName)
init {
val customCommandsList: RecyclerView = activity.findViewById(R.id.custom_commands_list)
customCommandsList.layoutManager = LinearLayoutManager(activity)
customCommandsList.setHasFixedSize(true)
customCommandsList.adapter = this
setCommands(mediaController.customLayout)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.media_custom_command, parent, false)
)
@SuppressWarnings("FutureReturnValueIgnored")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val commandButton: CommandButton = commands[position]
holder.name.text = commandButton.displayName
holder.description.text = commandButton.sessionCommand?.customAction
if (commandButton.iconResId != 0) {
val iconDrawable: Drawable? =
ResourcesCompat.getDrawable(resources, commandButton.iconResId, null)
holder.icon.setImageDrawable(iconDrawable)
}
holder.itemView.setOnClickListener {
commandButton.sessionCommand?.let { mediaController.sendCustomCommand(it, Bundle.EMPTY) }
}
}
override fun getItemCount(): Int = commands.size
fun setCommands(newCommands: List<CommandButton>) {
val diffResult: DiffUtil.DiffResult =
DiffUtil.calculateDiff(
object : DiffUtil.Callback() {
override fun getOldListSize(): Int = commands.size
override fun getNewListSize(): Int = newCommands.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
commands.size == newCommands.size &&
commands[oldItemPosition] == newCommands[newItemPosition]
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
commands[oldItemPosition] == newCommands[newItemPosition]
}
)
commands = newCommands
diffResult.dispatchUpdatesTo(this)
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.action_name)
val description: TextView = itemView.findViewById(R.id.action_description)
val icon: ImageView = itemView.findViewById(R.id.action_icon)
}
}

View File

@ -0,0 +1,187 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.media.session.MediaSessionManager as ActiveSessionManager
import android.os.Build
import android.os.Build.VERSION_CODES
import android.os.Bundle
import android.service.notification.NotificationListenerService
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.app.NotificationManagerCompat
import androidx.media3.testapp.controller.findapps.FindActiveMediaSessionApps
import androidx.media3.testapp.controller.findapps.FindMediaApps
import androidx.media3.testapp.controller.findapps.FindMediaServiceApps
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
/**
* App entry point. Presents a list of apps that implement
* [androidx.media3.session.MediaSessionService], [androidx.media3.session.MediaLibraryService], or
* [androidx.media.MediaBrowserServiceCompat]. Also presents a separate list of active media session
* apps.
*/
class LaunchActivity : AppCompatActivity() {
private lateinit var mediaAppListAdapter: MediaAppListAdapter
private lateinit var mediaSessionApps: MediaAppListAdapter.Section
private val sessionAppsUpdated =
object : FindMediaApps.AppListUpdatedCallback {
override fun onAppListUpdated(mediaAppEntries: List<MediaAppDetails>) {
if (mediaAppEntries.isEmpty()) {
mediaSessionApps.setError(
R.string.no_apps_found,
R.string.no_apps_reason_no_media_services,
)
} else {
mediaSessionApps.setAppsList(mediaAppEntries)
}
}
}
private var activeSessionListener: ActiveSessionListener? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_launch)
val toolbar: Toolbar? = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
mediaAppListAdapter =
MediaAppListAdapter(
object : MediaAppListAdapter.MediaAppSelectedListener {
override fun onMediaAppClicked(mediaAppDetails: MediaAppDetails) {
startActivity(
MediaAppControllerActivity.buildIntent(this@LaunchActivity, mediaAppDetails)
)
}
}
)
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
activeSessionListener = ActiveSessionListener()
}
mediaSessionApps = mediaAppListAdapter.addSection(R.string.media_app_header_media_service)
val mediaAppsList: RecyclerView? = findViewById(R.id.app_list)
mediaAppsList?.layoutManager = LinearLayoutManager(this)
mediaAppsList?.setHasFixedSize(true)
mediaAppsList?.adapter = mediaAppListAdapter
}
override fun onStart() {
super.onStart()
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
activeSessionListener!!.onStart()
}
// Finds apps that implement MediaSessionService, MediaLibraryService, or
// MediaBrowserServiceCompat.
FindMediaServiceApps(context = this, this.packageManager, this.resources, sessionAppsUpdated)
.execute()
}
override fun onStop() {
super.onStop()
if (Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
activeSessionListener!!.onStop()
}
}
/**
* Encapsulates the API 21+ functionality of looking for and observing updates to active media
* sessions. We only construct an instance of this class if the device is running L or later, to
* avoid any ClassNotFoundExceptions due to the use of MediaSession and related classes.
*/
@RequiresApi(VERSION_CODES.LOLLIPOP)
private inner class ActiveSessionListener {
private val activeSessionApps: MediaAppListAdapter.Section =
mediaAppListAdapter.addSection(R.string.media_app_header_active_session)
private val activeSessionManager: ActiveSessionManager =
getSystemService(Context.MEDIA_SESSION_SERVICE) as ActiveSessionManager
private val sessionAppsUpdated =
object : FindMediaApps.AppListUpdatedCallback {
override fun onAppListUpdated(mediaAppEntries: List<MediaAppDetails>) =
if (mediaAppEntries.isEmpty()) {
activeSessionApps.setError(
R.string.no_apps_found,
R.string.no_apps_reason_no_active_sessions,
)
} else {
activeSessionApps.setAppsList(mediaAppEntries)
}
}
private lateinit var findActiveMediaSessionApps: FindActiveMediaSessionApps
private val sessionsChangedListener =
ActiveSessionManager.OnActiveSessionsChangedListener { findActiveMediaSessionApps.execute() }
fun onStart() {
if (!NotificationListener.isEnabled(this@LaunchActivity)) {
activeSessionApps.setError(
R.string.no_apps_found,
R.string.no_apps_reason_missing_permission,
R.string.action_notification_permissions_settings,
) {
startActivity(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"))
}
} else {
val listenerComponent = ComponentName(this@LaunchActivity, NotificationListener::class.java)
findActiveMediaSessionApps =
FindActiveMediaSessionApps(
activeSessionManager,
listenerComponent,
packageManager,
resources,
this@LaunchActivity,
sessionAppsUpdated,
)
activeSessionManager.addOnActiveSessionsChangedListener(
sessionsChangedListener,
listenerComponent,
)
findActiveMediaSessionApps.execute()
}
}
fun onStop() {
activeSessionManager.removeOnActiveSessionsChangedListener(sessionsChangedListener)
}
}
/**
* A notification listener service that allows us to grab active media sessions from their
* notifications. This class is only used on API 21+ because the Android media framework added
* getActiveSessions in API 21.
*/
@RequiresApi(VERSION_CODES.LOLLIPOP)
class NotificationListener : NotificationListenerService() {
companion object {
// Helper method to check if our notification listener is enabled. In order to get active
// media sessions, we need an enabled notification listener component.
fun isEnabled(context: Context): Boolean {
return NotificationManagerCompat.getEnabledListenerPackages(context)
.contains(context.packageName)
}
}
}
}

View File

@ -0,0 +1,340 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.app.Activity
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.text.TextUtils
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.common.util.BitmapLoader
import androidx.media3.common.util.Log
import androidx.media3.datasource.DataSourceBitmapLoader
import androidx.media3.session.CacheBitmapLoader
import androidx.media3.session.CommandButton
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaBrowser
import androidx.media3.session.MediaController
import androidx.media3.session.MediaLibraryService
import androidx.media3.session.SessionCommand
import androidx.media3.session.SessionCommands
import androidx.media3.session.SessionToken
import androidx.viewpager.widget.PagerAdapter
import androidx.viewpager.widget.ViewPager
import com.google.android.material.tabs.TabLayout
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.FutureCallback
import com.google.common.util.concurrent.Futures
import com.google.common.util.concurrent.ListenableFuture
class MediaAppControllerActivity : AppCompatActivity() {
private lateinit var mediaAppDetails: MediaAppDetails
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
private val browser: MediaBrowser?
get() = if (browserFuture.isDone && !browserFuture.isCancelled) browserFuture.get() else null
private lateinit var viewPager: ViewPager
private lateinit var ratingViewGroup: ViewGroup
private lateinit var mediaInfoText: TextView
private lateinit var mediaTitleView: TextView
private lateinit var mediaArtistView: TextView
private lateinit var mediaAlbumView: TextView
private lateinit var mediaAlbumArtView: ImageView
private lateinit var transportControlHelper: TransportControlHelper
private lateinit var shuffleModeHelper: ShuffleModeHelper
private lateinit var repeatModeHelper: RepeatModeHelper
private lateinit var ratingHelper: RatingHelper
private lateinit var customCommandsAdapter: CustomCommandsAdapter
private lateinit var timelineAdapter: TimelineAdapter
private lateinit var browseMediaItemsAdapter: BrowseMediaItemsAdapter
private lateinit var searchMediaItemsAdapter: SearchMediaItemsAdapter
private lateinit var bitmapLoader: BitmapLoader
companion object {
private const val TAG = "ControllerActivity"
// Key name for Intent extras.
private const val APP_DETAILS_EXTRA = "androidx.media3.testapp.controller.APP_DETAILS_EXTRA"
/**
* Builds an [Intent] to launch this Activity with a set of extras.
*
* @param activity The Activity building the Intent.
* @param appDetails The app details about the media app to connect to.
* @return An Intent that can be used to start the Activity.
*/
fun buildIntent(activity: Activity, appDetails: MediaAppDetails): Intent {
val intent = Intent(activity, MediaAppControllerActivity::class.java)
intent.putExtra(APP_DETAILS_EXTRA, appDetails.toBundle())
return intent
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_media_app_controller)
val toolbar: Toolbar = findViewById(R.id.toolbar)
setSupportActionBar(toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
toolbar.setNavigationOnClickListener { finish() }
bitmapLoader = CacheBitmapLoader(DataSourceBitmapLoader(this))
viewPager = findViewById(R.id.view_pager)
ratingViewGroup = findViewById(R.id.rating)
mediaInfoText = findViewById(R.id.media_info)
mediaAlbumArtView = findViewById(R.id.media_art)
mediaTitleView = findViewById(R.id.media_title)
mediaArtistView = findViewById(R.id.media_artist)
mediaAlbumView = findViewById(R.id.media_album)
mediaAppDetails = parseIntent(intent)
val pages: Array<Int> =
arrayOf(
R.id.prepare_play_page,
R.id.controls_page,
R.id.custom_commands_page,
R.id.timeline_list_page,
R.id.browse_tree_page,
R.id.media_search_page,
)
viewPager.offscreenPageLimit = pages.size
viewPager.adapter =
object : PagerAdapter() {
override fun getCount(): Int = pages.size
override fun isViewFromObject(view: View, obj: Any): Boolean = (view === obj)
override fun instantiateItem(container: ViewGroup, position: Int): Any =
findViewById(pages[position])
}
val pageIndicator: TabLayout = findViewById(R.id.page_indicator)
pageIndicator.setupWithViewPager(viewPager)
setupToolbar()
setupMediaBrowser()
}
override fun onDestroy() {
MediaBrowser.releaseFuture(browserFuture)
super.onDestroy()
}
private fun parseIntent(intent: Intent): MediaAppDetails {
val extras: Bundle? = intent.extras
return MediaAppDetails.fromBundle(extras!!.getBundle(APP_DETAILS_EXTRA)!!)
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBundle(APP_DETAILS_EXTRA, mediaAppDetails.toBundle())
}
private fun setupMediaBrowser() {
browserFuture = getMediaBrowser(mediaAppDetails.sessionToken)
val listener: Player.Listener =
object : Player.Listener {
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
updateMediaInfoText()
updateMediaMetadataView(mediaMetadata)
}
override fun onPlaybackStateChanged(state: Int) = updateMediaInfoText()
override fun onAvailableCommandsChanged(availableCommands: Player.Commands) =
updateMediaInfoText()
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) =
updateMediaInfoText()
}
Futures.addCallback(
browserFuture,
object : FutureCallback<MediaBrowser> {
override fun onSuccess(browser: MediaBrowser) {
browser.addListener(listener)
PreparePlayHelper(this@MediaAppControllerActivity, browser)
AudioFocusHelper(this@MediaAppControllerActivity)
customCommandsAdapter =
CustomCommandsAdapter(
this@MediaAppControllerActivity,
browser,
mediaAppDetails.packageName,
)
transportControlHelper = TransportControlHelper(this@MediaAppControllerActivity, browser)
shuffleModeHelper = ShuffleModeHelper(this@MediaAppControllerActivity, browser)
repeatModeHelper = RepeatModeHelper(this@MediaAppControllerActivity, browser)
ratingHelper = RatingHelper(ratingViewGroup, browser)
timelineAdapter = TimelineAdapter(this@MediaAppControllerActivity, browser)
browseMediaItemsAdapter =
BrowseMediaItemsAdapter(this@MediaAppControllerActivity, browser)
searchMediaItemsAdapter =
SearchMediaItemsAdapter(this@MediaAppControllerActivity, browser)
updateMediaInfoText()
updateMediaMetadataView(browser.mediaMetadata)
}
override fun onFailure(t: Throwable) {
mediaInfoText.text = getString(R.string.controller_connection_failed_msg, t.message)
}
},
ContextCompat.getMainExecutor(this),
)
}
private fun getMediaBrowser(token: SessionToken): ListenableFuture<MediaBrowser> {
val listener =
object : MediaBrowser.Listener {
override fun onAvailableSessionCommandsChanged(
controller: MediaController,
commands: SessionCommands,
) = updateMediaInfoText()
override fun onCustomLayoutChanged(
controller: MediaController,
layout: MutableList<CommandButton>,
) {
customCommandsAdapter.setCommands(layout)
}
override fun onChildrenChanged(
browser: MediaBrowser,
parentId: String,
itemCount: Int,
params: MediaLibraryService.LibraryParams?,
) {
if (
itemCount >= 1 &&
browser.isSessionCommandAvailable(SessionCommand.COMMAND_CODE_LIBRARY_GET_CHILDREN)
) {
val future: ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> =
browser.getChildren(parentId, 0, itemCount, params)
future.addListener(
{
val items: List<MediaItem> = future.get().value ?: emptyList()
browseMediaItemsAdapter.updateItems(items)
},
ContextCompat.getMainExecutor(this@MediaAppControllerActivity),
)
}
}
override fun onDisconnected(controller: MediaController) {
mediaInfoText.text = getString(R.string.controller_disconnected_msg)
browseMediaItemsAdapter.updateItems(emptyList())
searchMediaItemsAdapter.updateItems(emptyList())
}
}
return MediaBrowser.Builder(this, token).setListener(listener).buildAsync()
}
private fun updateMediaInfoText() {
val browser = this.browser ?: return
val mediaInfos = HashMap<String, CharSequence>()
mediaInfos[getString(R.string.info_state_string)] =
MediaIntToString.playbackStateMap.getValue(browser.playbackState)
val mediaMetadata: MediaMetadata = browser.mediaMetadata
mediaInfos[getString(R.string.info_title_string)] =
mediaMetadata.title ?: "Title metadata empty"
mediaInfos[getString(R.string.info_artist_string)] =
mediaMetadata.artist ?: "Artist metadata empty"
mediaInfos[getString(R.string.info_album_string)] =
mediaMetadata.albumTitle ?: "Album title metadata empty"
mediaInfos[getString(R.string.info_play_when_ready)] = browser.playWhenReady.toString()
var infoCharSequence: CharSequence = ""
val keys: List<String> = mediaInfos.keys.sorted()
for (key in keys) {
infoCharSequence = TextUtils.concat(infoCharSequence, key, "=", mediaInfos[key], "\n")
}
infoCharSequence = TextUtils.concat(infoCharSequence, "\nSupported Commands=\n")
val playerCommands: Player.Commands = browser.availableCommands
MediaIntToString.playerCommandMap.forEach { (command, string) ->
if (playerCommands.contains(command)) {
infoCharSequence = TextUtils.concat(infoCharSequence, string, "\n")
}
}
val sessionCommands: SessionCommands = browser.availableSessionCommands
MediaIntToString.sessionCommandMap.forEach { (command, string) ->
if (sessionCommands.contains(command)) {
infoCharSequence = TextUtils.concat(infoCharSequence, string, "\n")
}
}
mediaInfoText.text = infoCharSequence
}
private fun updateMediaMetadataView(mediaMetadata: MediaMetadata) {
mediaTitleView.text = mediaMetadata.title ?: "Title metadata empty"
mediaArtistView.text = mediaMetadata.artist ?: "Artist metadata empty"
mediaAlbumView.text = mediaMetadata.albumTitle ?: "Album title metadata empty"
bitmapLoader.loadBitmapFromMetadata(mediaMetadata)?.let {
Futures.addCallback(
it,
object : FutureCallback<Bitmap> {
override fun onSuccess(result: Bitmap?) {
mediaAlbumArtView.setImageBitmap(result)
}
override fun onFailure(t: Throwable) {
mediaAlbumArtView.setImageResource(R.drawable.ic_album_black_24dp)
t.message?.let { msg -> Log.e("BitmapLoader", msg, t) }
}
},
ContextCompat.getMainExecutor(this),
)
} ?: mediaAlbumArtView.setImageResource(R.drawable.ic_album_black_24dp)
}
private fun setupToolbar() {
val actionBar: ActionBar? = supportActionBar
if (actionBar != null) {
val toolbarIcon = BitmapUtils.createToolbarIcon(resources, mediaAppDetails.icon)
with(actionBar) {
setIcon(BitmapDrawable(resources, toolbarIcon))
title = mediaAppDetails.appName
}
}
}
}

View File

@ -0,0 +1,128 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Resources
import android.graphics.Bitmap
import android.media.session.MediaController
import android.os.Build
import android.os.Bundle
import androidx.annotation.RequiresApi
import androidx.media3.common.util.Util
import androidx.media3.session.SessionToken
/** Stores details about a media app. */
class MediaAppDetails
private constructor(
val packageName: String,
val appName: String,
val icon: Bitmap,
val sessionToken: SessionToken,
val supportsAutomotive: Boolean,
val supportsAuto: Boolean,
) {
companion object {
fun create(
packageManager: PackageManager,
resources: Resources,
sessionToken: SessionToken,
): MediaAppDetails {
val packageName = sessionToken.packageName
val appInfo = packageManager.getApplicationInfo(packageName, /* flags= */ 0)
val appName = appInfo.loadLabel(packageManager).toString()
val icon =
BitmapUtils.convertDrawable(resources, appInfo.loadIcon(packageManager), downScale = true)
return MediaAppDetails(
packageName,
appName,
icon,
sessionToken,
getSupportsAutomotive(packageManager, packageName),
getSupportsAuto(packageManager, packageName),
)
}
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
fun create(
packageManager: PackageManager,
resources: Resources,
controller: MediaController,
context: Context,
): MediaAppDetails {
val sessionToken = SessionToken.createSessionToken(context, controller.sessionToken).get()
val packageName = sessionToken.packageName
val appInfo = packageManager.getApplicationInfo(packageName, 0)
val appName = appInfo.loadLabel(packageManager).toString()
val icon =
BitmapUtils.convertDrawable(resources, appInfo.loadIcon(packageManager), downScale = true)
return MediaAppDetails(
packageName,
appName,
icon,
sessionToken,
getSupportsAutomotive(packageManager, packageName),
getSupportsAuto(packageManager, packageName),
)
}
private fun getSupportsAutomotive(
packageManager: PackageManager,
packageName: String,
): Boolean {
val features =
packageManager.getPackageInfo(packageName, PackageManager.GET_CONFIGURATIONS).reqFeatures
return features?.any { it?.name == "android.hardware.type.automotive" } == true
}
private fun getSupportsAuto(packageManager: PackageManager, packageName: String): Boolean {
val metaData =
packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA).metaData
return metaData?.containsKey("com.google.android.gms.car.application") == true
}
private val PACKAGE_NAME = Util.intToStringMaxRadix(0)
private val APP_NAME = Util.intToStringMaxRadix(1)
private val ICON = Util.intToStringMaxRadix(2)
private val SESSION_TOKEN = Util.intToStringMaxRadix(3)
private val SUPPORTS_AUTO = Util.intToStringMaxRadix(4)
private val SUPPORTS_AUTOMOTIVE = Util.intToStringMaxRadix(5)
fun fromBundle(bundle: Bundle): MediaAppDetails {
return MediaAppDetails(
bundle.getString(PACKAGE_NAME)!!,
bundle.getString(APP_NAME)!!,
(bundle.getParcelable(ICON) as Bitmap?)!!,
SessionToken.fromBundle(bundle.getBundle(SESSION_TOKEN)!!),
bundle.getBoolean(SUPPORTS_AUTO),
bundle.getBoolean(SUPPORTS_AUTOMOTIVE),
)
}
}
fun toBundle(): Bundle {
val bundle = Bundle()
bundle.putString(PACKAGE_NAME, packageName)
bundle.putString(APP_NAME, appName)
bundle.putParcelable(ICON, icon)
bundle.putBundle(SESSION_TOKEN, sessionToken.toBundle())
bundle.putBoolean(SUPPORTS_AUTO, supportsAuto)
bundle.putBoolean(SUPPORTS_AUTOMOTIVE, supportsAutomotive)
return bundle
}
}

View File

@ -0,0 +1,221 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.StringRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
/** A sectioned RecyclerView Adapter that displays list(s) of media apps. */
class MediaAppListAdapter(val mediaAppSelectedListener: MediaAppSelectedListener) :
RecyclerView.Adapter<ViewHolder>() {
/** Click listener for when an app is selected. */
interface MediaAppSelectedListener {
fun onMediaAppClicked(mediaAppDetails: MediaAppDetails)
}
/** The types of views that this recycler view adapter displays. */
enum class ViewType(val layoutId: Int) {
/**
* A media app entry, with icon, app name, and package name. Tapping on one of these entries
* will fire the MediaAppSelectedListener callback.
*/
AppView(R.layout.media_app_item) {
override fun create(itemLayout: View): ViewHolder = AppEntry.ViewHolder(itemLayout)
},
/** A section header, only displayed if the adapter has multiple sections. */
HeaderView(R.layout.media_app_list_header) {
override fun create(itemLayout: View): ViewHolder = Header.ViewHolder(itemLayout)
},
/** An error, such as "no apps", or "missing permission". Can optionally provide an action. */
ErrorView(R.layout.media_app_list_error) {
override fun create(itemLayout: View): ViewHolder = Error.ViewHolder(itemLayout)
};
abstract fun create(itemLayout: View): ViewHolder
}
/** An interface for items in the recycler view. */
interface RecyclerViewItem {
fun viewType(): ViewType
fun bindTo(holder: ViewHolder)
}
/** An implementation of [RecyclerViewItem] for media apps. */
class AppEntry(
private val appDetails: MediaAppDetails,
private val appSelectedListener: MediaAppSelectedListener,
) : RecyclerViewItem {
override fun viewType(): ViewType = ViewType.AppView
override fun bindTo(holder: RecyclerView.ViewHolder) {
if (holder is ViewHolder) {
holder.appIconView?.setImageBitmap(appDetails.icon)
holder.appIconView?.contentDescription =
holder.appIconView?.context?.getString(R.string.app_icon_desc, appDetails.appName)
holder.appNameView?.text = appDetails.appName
holder.appPackageView?.text = appDetails.packageName
holder.controlButton?.setOnClickListener {
appSelectedListener.onMediaAppClicked(appDetails)
}
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val appIconView: ImageView? = itemView.findViewById(R.id.app_icon)
val appNameView: TextView? = itemView.findViewById(R.id.app_name)
val appPackageView: TextView? = itemView.findViewById(R.id.package_name)
val controlButton: Button? = itemView.findViewById(R.id.app_control)
}
}
/** An implementation of [RecyclerViewItem] for headers. */
class Header(@StringRes private val labelResId: Int) : RecyclerViewItem {
override fun viewType(): ViewType = ViewType.HeaderView
override fun bindTo(holder: RecyclerView.ViewHolder) {
if (holder is ViewHolder) {
holder.headerView?.setText(labelResId)
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val headerView: TextView? = itemView.findViewById(R.id.header_text)
}
}
/** An implementation of [RecyclerViewItem] for error states, with an optional action. */
class Error(
@StringRes private val errorMsgId: Int,
@StringRes private val errorDetailId: Int,
@StringRes private val errorButtonId: Int,
private val clickListener: View.OnClickListener?,
) : RecyclerViewItem {
override fun viewType(): ViewType = ViewType.ErrorView
override fun bindTo(holder: RecyclerView.ViewHolder) {
if (holder is ViewHolder) {
holder.errorMessage?.setText(errorMsgId)
holder.errorDetail?.setText(errorDetailId)
holder.errorMessage?.visibility = if (errorDetailId == 0) View.GONE else View.VISIBLE
holder.actionButton?.setOnClickListener(clickListener)
if (errorButtonId == 0 || clickListener == null) {
holder.actionButton?.visibility = View.GONE
} else {
holder.actionButton?.visibility = View.VISIBLE
holder.actionButton?.setText(errorButtonId)
}
}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val errorMessage: TextView? = itemView.findViewById(R.id.error_message)
val errorDetail: TextView? = itemView.findViewById(R.id.error_detail)
val actionButton: Button? = itemView.findViewById(R.id.error_action)
}
}
/** Represents a section of items in the recycler view. */
inner class Section(@StringRes internal val label: Int) {
internal val items = mutableListOf<RecyclerViewItem>()
val size: Int
get() = items.size
fun setError(@StringRes message: Int, @StringRes detail: Int) =
setError(message, detail, 0, null)
fun setError(
@StringRes message: Int,
@StringRes detail: Int,
@StringRes buttonText: Int,
onClickListener: View.OnClickListener?,
) {
items.clear()
items.add(Error(message, detail, buttonText, onClickListener))
updateData()
}
fun setAppsList(appEntries: List<MediaAppDetails?>) {
items.clear()
for (appEntry in appEntries) {
if (appEntry != null) {
items.add(AppEntry(appEntry, mediaAppSelectedListener))
}
}
updateData()
}
}
private val sections = ArrayList<Section>()
private val recyclerViewEntries = ArrayList<RecyclerViewItem>()
fun addSection(@StringRes label: Int): Section {
val section = Section(label)
sections.add(section)
return section
}
fun updateData() {
val oldEntries = ArrayList<RecyclerViewItem>(recyclerViewEntries)
recyclerViewEntries.clear()
for (section in sections) {
if (section.size > 0) {
recyclerViewEntries.add(Header(section.label))
}
recyclerViewEntries.addAll(section.items)
}
val diffResult: DiffUtil.DiffResult =
DiffUtil.calculateDiff(
object : DiffUtil.Callback() {
override fun getOldListSize(): Int = oldEntries.size
override fun getNewListSize(): Int = recyclerViewEntries.size
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
oldEntries[oldItemPosition] == recyclerViewEntries[newItemPosition]
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
areItemsTheSame(oldItemPosition, newItemPosition)
}
)
diffResult.dispatchUpdatesTo(this)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val type: ViewType = ViewType.values()[viewType]
val itemLayout: View = LayoutInflater.from(parent.context).inflate(type.layoutId, parent, false)
return type.create(itemLayout)
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) =
recyclerViewEntries[position].bindTo(holder)
override fun getItemViewType(position: Int) = recyclerViewEntries[position].viewType().ordinal
override fun getItemCount(): Int = recyclerViewEntries.size
}

View File

@ -0,0 +1,74 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import androidx.media3.common.Player
import androidx.media3.session.SessionCommand
object MediaIntToString {
val playbackStateMap =
mapOf(
Player.STATE_IDLE to "STATE_IDLE",
Player.STATE_BUFFERING to "STATE_BUFFERING",
Player.STATE_READY to "STATE_READY",
Player.STATE_ENDED to "STATE_ENDED"
)
val playerCommandMap =
mapOf(
Player.COMMAND_INVALID to "COMMAND_INVALID",
Player.COMMAND_PLAY_PAUSE to "COMMAND_PLAY_PAUSE",
Player.COMMAND_PREPARE to "COMMAND_PREPARE",
Player.COMMAND_STOP to "COMMAND_STOP",
Player.COMMAND_SEEK_TO_DEFAULT_POSITION to "COMMAND_SEEK_TO_DEFAULT_POSITION",
Player.COMMAND_SEEK_TO_DEFAULT_POSITION to "COMMAND_SEEK_TO_DEFAULT_POSITION",
Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM to "COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM",
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM to "COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM",
Player.COMMAND_SEEK_TO_PREVIOUS to "COMMAND_SEEK_TO_PREVIOUS",
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM to "COMMAND_SEEK_TO_NEXT_MEDIA_ITEM",
Player.COMMAND_SEEK_TO_NEXT to "COMMAND_SEEK_TO_NEXT",
Player.COMMAND_SEEK_TO_MEDIA_ITEM to "COMMAND_SEEK_TO_MEDIA_ITEM",
Player.COMMAND_SEEK_BACK to "COMMAND_SEEK_BACK",
Player.COMMAND_SEEK_FORWARD to "COMMAND_SEEK_FORWARD",
Player.COMMAND_SET_SPEED_AND_PITCH to "COMMAND_SET_SPEED_AND_PITCH",
Player.COMMAND_SET_SHUFFLE_MODE to "COMMAND_SET_SHUFFLE_MODE",
Player.COMMAND_SET_REPEAT_MODE to "COMMAND_SET_REPEAT_MODE",
Player.COMMAND_GET_CURRENT_MEDIA_ITEM to "COMMAND_GET_CURRENT_MEDIA_ITEM",
Player.COMMAND_GET_TIMELINE to "COMMAND_GET_TIMELINE",
Player.COMMAND_GET_METADATA to "COMMAND_GET_METADATA",
Player.COMMAND_SET_PLAYLIST_METADATA to "COMMAND_SET_PLAYLIST_METADATA",
Player.COMMAND_SET_MEDIA_ITEM to "COMMAND_SET_MEDIA_ITEM",
Player.COMMAND_CHANGE_MEDIA_ITEMS to "COMMAND_CHANGE_MEDIA_ITEMS",
Player.COMMAND_GET_AUDIO_ATTRIBUTES to "COMMAND_GET_AUDIO_ATTRIBUTES",
Player.COMMAND_GET_VOLUME to "COMMAND_GET_VOLUME",
Player.COMMAND_GET_DEVICE_VOLUME to "COMMAND_GET_DEVICE_VOLUME",
Player.COMMAND_SET_VOLUME to "COMMAND_SET_VOLUME",
Player.COMMAND_SET_DEVICE_VOLUME to "COMMAND_SET_DEVICE_VOLUME",
Player.COMMAND_ADJUST_DEVICE_VOLUME to "COMMAND_ADJUST_DEVICE_VOLUME",
Player.COMMAND_SET_VIDEO_SURFACE to "COMMAND_SET_VIDEO_SURFACE",
Player.COMMAND_GET_TEXT to "COMMAND_GET_TEXT"
)
val sessionCommandMap =
mapOf(
SessionCommand.COMMAND_CODE_SESSION_SET_RATING to "COMMAND_SESSION_SET_RATING",
SessionCommand.COMMAND_CODE_LIBRARY_GET_LIBRARY_ROOT to "COMMAND_LIBRARY_GET_LIBRARY_ROOT",
SessionCommand.COMMAND_CODE_LIBRARY_SUBSCRIBE to "COMMAND_LIBRARY_SUBSCRIBE",
SessionCommand.COMMAND_CODE_LIBRARY_UNSUBSCRIBE to "COMMAND_LIBRARY_UNSUBSCRIBE",
SessionCommand.COMMAND_CODE_LIBRARY_GET_CHILDREN to "COMMAND_LIBRARY_GET_CHILDREN",
SessionCommand.COMMAND_CODE_LIBRARY_GET_ITEM to "COMMAND_LIBRARY_GET_ITEM",
SessionCommand.COMMAND_CODE_LIBRARY_SEARCH to "COMMAND_LIBRARY_SEARCH",
SessionCommand.COMMAND_CODE_LIBRARY_GET_SEARCH_RESULT to "COMMAND_LIBRARY_GET_SEARCH_RESULT"
)
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.app.Activity
import android.net.Uri
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.Spinner
import androidx.media3.common.MediaItem
import androidx.media3.session.MediaController
/** Helper class which handles prepare and play actions. */
class PreparePlayHelper(activity: Activity, private val mediaController: MediaController) :
View.OnClickListener {
private val inputType: Spinner = activity.findViewById(R.id.input_type)
private val uriInputText: EditText = activity.findViewById(R.id.uri_id_query)
private val prepareButton: Button = activity.findViewById(R.id.action_prepare)
private val playButton: Button = activity.findViewById(R.id.action_play)
init {
prepareButton.setOnClickListener(this)
playButton.setOnClickListener(this)
}
companion object {
// Indices of the values in the "input_options" string array.
private const val INDEX_SEARCH = 0
private const val INDEX_MEDIA_ID = 1
private const val INDEX_URI = 2
}
@SuppressWarnings("FutureReturnValueIgnored")
override fun onClick(v: View) {
mediaController.apply {
setMediaItem(buildMediaItem())
playWhenReady = v.id == R.id.action_play
prepare()
}
}
private fun buildMediaItem(): MediaItem {
val value: String = uriInputText.text.toString()
val mediaItemBuilder = MediaItem.Builder()
when (inputType.selectedItemPosition) {
INDEX_MEDIA_ID -> mediaItemBuilder.setMediaId(value)
INDEX_SEARCH ->
mediaItemBuilder.setRequestMetadata(
MediaItem.RequestMetadata.Builder().setSearchQuery(value).build()
)
INDEX_URI ->
mediaItemBuilder.setRequestMetadata(
MediaItem.RequestMetadata.Builder().setMediaUri(Uri.parse(value)).build()
)
}
return mediaItemBuilder.build()
}
}

View File

@ -0,0 +1,281 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.text.Editable
import android.text.TextUtils
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import android.widget.ImageView
import androidx.annotation.IdRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.media3.common.HeartRating
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PercentageRating
import androidx.media3.common.Player
import androidx.media3.common.Rating
import androidx.media3.common.StarRating
import androidx.media3.common.ThumbRating
import androidx.media3.session.MediaController
/** Helper class to manage displaying and setting different kinds of media ratings. */
class RatingHelper(private val rootView: ViewGroup, private val mediaController: MediaController) {
private var ratingUiHelper: RatingUiHelper?
init {
ratingUiHelper = ratingUiHelperFor(rootView, mediaController.mediaMetadata)
val listener: Player.Listener =
object : Player.Listener {
override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) =
updateRating(mediaMetadata)
}
mediaController.addListener(listener)
updateRating(mediaController.mediaMetadata)
}
fun updateRating(mediaMetadata: MediaMetadata) {
val rating: Rating? = mediaMetadata.userRating ?: mediaMetadata.overallRating
if (rating != null) {
if (ratingUiHelper == null) {
ratingUiHelper = ratingUiHelperFor(rootView, mediaMetadata)
}
ratingUiHelper?.setRating(rating)
} else {
ratingUiHelper?.let { it.setRating(it.unrated()) }
}
}
private fun ratingUiHelperFor(
viewGroup: ViewGroup,
mediaMetadata: MediaMetadata
): RatingUiHelper? {
val rating: Rating? = mediaMetadata.userRating ?: mediaMetadata.overallRating
viewGroup.visibility = View.VISIBLE
return when (rating) {
is ThumbRating -> RatingUiHelper.Thumbs(viewGroup, mediaController)
is HeartRating -> RatingUiHelper.Heart(viewGroup, mediaController)
is PercentageRating -> RatingUiHelper.Percentage(viewGroup, mediaController)
is StarRating ->
when (rating.maxStars) {
3 -> RatingUiHelper.Stars3(viewGroup, mediaController)
4 -> RatingUiHelper.Stars4(viewGroup, mediaController)
5 -> RatingUiHelper.Stars5(viewGroup, mediaController)
else -> {
viewGroup.visibility = View.GONE
null
}
}
else -> {
viewGroup.visibility = View.GONE
null
}
}
}
}
@SuppressWarnings("FutureReturnValueIgnored")
private abstract class RatingUiHelper(
private val rootView: ViewGroup,
mediaController: MediaController
) {
private var currentRating: Rating = unrated()
init {
for (i in 0 until rootView.childCount) {
val ratingView: View = rootView.getChildAt(i)
ratingView.visibility = if (visible(ratingView.id)) View.VISIBLE else View.GONE
if (ratingView !is Editable) {
ratingView.setOnClickListener { view ->
val newRating: Rating = ratingFor(view.id, currentRating)
val mediaItem: MediaItem? = mediaController.currentMediaItem
if (mediaItem != null && !TextUtils.isEmpty(mediaItem.mediaId)) {
mediaController.setRating(mediaItem.mediaId, newRating)
} else {
mediaController.setRating(newRating)
}
}
}
}
}
/** Returns whether the given view is enabled with the current rating */
abstract fun enabled(@IdRes viewId: Int, rating: Rating): Boolean
/**
* Returns whether the given view is visible for the type of rating. For example, a thumbs up/down
* rating will not display stars or heart. And a 4-star rating will not display the fifth star.
*/
abstract fun visible(@IdRes viewId: Int): Boolean
/** Returns the rating that should be set when the given view is tapped. */
abstract fun ratingFor(@IdRes viewId: Int, currentRating: Rating): Rating
/** Returns unrated rating of the current rating type. */
abstract fun unrated(): Rating
fun setRating(rating: Rating) {
for (i in 0 until rootView.childCount) {
val view: View = rootView.getChildAt(i)
if (view is ImageView) {
val tint: Int =
if (enabled(view.id, rating)) R.color.colorPrimary else R.color.colorInactive
DrawableCompat.setTint(view.drawable, ContextCompat.getColor(rootView.context, tint))
} else {
view.isEnabled = enabled(view.id, rating)
}
}
currentRating = rating
}
open class Stars3(viewGroup: ViewGroup, controller: MediaController) :
RatingUiHelper(viewGroup, controller) {
override fun enabled(viewId: Int, rating: Rating): Boolean {
if (rating is StarRating) {
val starRating: Float = rating.starRating
return when (viewId) {
R.id.rating_star_1 -> starRating >= 1.0f
R.id.rating_star_2 -> starRating >= 2.0f
R.id.rating_star_3 -> starRating >= 3.0f
else -> false
}
}
return false
}
override fun visible(viewId: Int): Boolean =
viewId == R.id.rating_star_1 || viewId == R.id.rating_star_2 || viewId == R.id.rating_star_3
override fun ratingFor(viewId: Int, currentRating: Rating): Rating =
when (viewId) {
R.id.rating_star_1 -> StarRating(3, 1.0f)
R.id.rating_star_2 -> StarRating(3, 2.0f)
R.id.rating_star_3 -> StarRating(3, 3.0f)
else -> StarRating(3)
}
override fun unrated(): Rating = StarRating(3)
}
open class Stars4(viewGroup: ViewGroup, controller: MediaController) :
Stars3(viewGroup, controller) {
override fun enabled(viewId: Int, rating: Rating): Boolean {
if (rating is StarRating && viewId == R.id.rating_star_4) {
return rating.starRating >= 4.0f
}
return super.enabled(viewId, rating)
}
override fun visible(viewId: Int): Boolean =
viewId == R.id.rating_star_4 || super.visible(viewId)
override fun ratingFor(viewId: Int, currentRating: Rating): Rating =
when (viewId) {
R.id.rating_star_1 -> StarRating(4, 1.0f)
R.id.rating_star_2 -> StarRating(4, 2.0f)
R.id.rating_star_3 -> StarRating(4, 3.0f)
R.id.rating_star_4 -> StarRating(4, 4.0f)
else -> StarRating(4)
}
override fun unrated(): Rating = StarRating(4)
}
class Stars5(viewGroup: ViewGroup, controller: MediaController) : Stars4(viewGroup, controller) {
override fun enabled(viewId: Int, rating: Rating): Boolean {
if (rating is StarRating && viewId == R.id.rating_star_5) {
return rating.starRating >= 5.0f
}
return super.enabled(viewId, rating)
}
override fun visible(viewId: Int): Boolean =
viewId == R.id.rating_star_5 || super.visible(viewId)
override fun ratingFor(viewId: Int, currentRating: Rating): Rating =
when (viewId) {
R.id.rating_star_1 -> StarRating(5, 1.0f)
R.id.rating_star_2 -> StarRating(5, 2.0f)
R.id.rating_star_3 -> StarRating(5, 3.0f)
R.id.rating_star_4 -> StarRating(5, 4.0f)
R.id.rating_star_5 -> StarRating(5, 5.0f)
else -> StarRating(5)
}
override fun unrated(): Rating = StarRating(5)
}
class Thumbs(viewGroup: ViewGroup, controller: MediaController) :
RatingUiHelper(viewGroup, controller) {
override fun enabled(viewId: Int, rating: Rating): Boolean {
if (rating is ThumbRating) {
if (rating.isThumbsUp && viewId == R.id.rating_thumb_up) return true
if (isThumbDown(rating) && viewId == R.id.rating_thumb_down) return true
}
return false
}
override fun visible(viewId: Int): Boolean =
viewId == R.id.rating_thumb_up || viewId == R.id.rating_thumb_down
override fun ratingFor(viewId: Int, currentRating: Rating): Rating {
// User tapped on current thumb rating, so reset the rating.
if (enabled(viewId, currentRating)) return ThumbRating()
return when (viewId) {
R.id.rating_thumb_up -> ThumbRating(true)
R.id.rating_thumb_down -> ThumbRating(false)
else -> ThumbRating()
}
}
override fun unrated(): Rating = ThumbRating()
private fun isThumbDown(rating: ThumbRating): Boolean = rating.isRated && !rating.isThumbsUp
}
class Heart(viewGroup: ViewGroup, controller: MediaController) :
RatingUiHelper(viewGroup, controller) {
override fun enabled(viewId: Int, rating: Rating): Boolean =
rating is HeartRating && rating.isHeart
override fun visible(viewId: Int): Boolean = viewId == R.id.rating_heart
override fun ratingFor(viewId: Int, currentRating: Rating): Rating =
if (currentRating is HeartRating) HeartRating(!currentRating.isHeart) else HeartRating()
override fun unrated(): Rating = HeartRating()
}
class Percentage(viewGroup: ViewGroup, controller: MediaController) :
RatingUiHelper(viewGroup, controller) {
private val percentageEditText: EditText = viewGroup.findViewById(R.id.rating_percentage)
override fun enabled(viewId: Int, rating: Rating): Boolean = true
override fun visible(viewId: Int): Boolean =
viewId == R.id.rating_percentage || viewId == R.id.rating_percentage_set
override fun ratingFor(viewId: Int, currentRating: Rating): Rating {
val percentage: Int = Integer.parseInt(percentageEditText.text.toString(), 10)
return PercentageRating(percentage / 100.0f)
}
override fun unrated(): Rating = PercentageRating()
}
}

View File

@ -0,0 +1,83 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.app.Activity
import android.view.View
import android.view.ViewGroup
import android.widget.AdapterView
import android.widget.ImageView
import android.widget.Spinner
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.media3.common.Player
import androidx.media3.session.MediaController
/** Helper class which handles repeat mode changes and the UI surrounding this feature. */
class RepeatModeHelper(activity: Activity, mediaController: MediaController) {
private val container: ViewGroup = activity.findViewById(R.id.group_toggle_repeat)
private val spinner: Spinner = container.findViewById(R.id.repeat_mode_spinner)
private val icon: ImageView = container.findViewById(R.id.repeat_mode_icon)
private val modes: List<Int> =
listOf(Player.REPEAT_MODE_OFF, Player.REPEAT_MODE_ONE, Player.REPEAT_MODE_ALL)
init {
spinner.onItemSelectedListener =
object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(p0: AdapterView<*>?, p1: View?, p2: Int, p3: Long) {
mediaController.repeatMode = modes[p2]
}
override fun onNothingSelected(parent: AdapterView<*>?) {}
}
val listener: Player.Listener =
object : Player.Listener {
override fun onRepeatModeChanged(repeatMode: Int) {
spinner.setSelection(repeatMode)
updateColor(repeatMode)
}
override fun onAvailableCommandsChanged(availableCommands: Player.Commands) =
updateBackground(availableCommands.contains(Player.COMMAND_SET_REPEAT_MODE))
}
mediaController.addListener(listener)
val isSupported: Boolean =
mediaController.availableCommands.contains(Player.COMMAND_SET_REPEAT_MODE)
updateBackground(isSupported)
}
fun updateBackground(isSupported: Boolean) {
if (isSupported) {
container.background = null
spinner.visibility = View.VISIBLE
} else {
container.setBackgroundResource(R.drawable.bg_unsupported_action)
spinner.visibility = View.GONE
}
}
fun updateColor(mode: Int) {
val tint: Int =
if (mode == Player.REPEAT_MODE_ONE || mode == Player.REPEAT_MODE_ALL) {
R.color.colorPrimary
} else {
R.color.colorInactive
}
DrawableCompat.setTint(icon.drawable, ContextCompat.getColor(container.context, tint))
}
}

View File

@ -0,0 +1,166 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.app.Activity
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.session.LibraryResult
import androidx.media3.session.MediaBrowser
import androidx.media3.session.SessionCommand
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.common.collect.ImmutableList
import com.google.common.util.concurrent.ListenableFuture
class SearchMediaItemsAdapter(
private val activity: Activity,
private val mediaBrowser: MediaBrowser
) : RecyclerView.Adapter<SearchMediaItemsAdapter.ViewHolder>() {
private var items: List<MediaItem> = emptyList()
init {
val searchItemsList: RecyclerView = activity.findViewById(R.id.search_items_list)
searchItemsList.layoutManager = LinearLayoutManager(activity)
searchItemsList.setHasFixedSize(true)
searchItemsList.adapter = this
val searchButton: Button = activity.findViewById(R.id.search_button)
val queryTextView: TextView = activity.findViewById(R.id.search_query)
searchButton.setOnClickListener {
if (!supportSearch()) {
Toast.makeText(activity, R.string.command_not_supported_msg, Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val query: String = queryTextView.text.toString()
if (query.isEmpty()) {
Toast.makeText(activity, R.string.search_query_empty_msg, Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
val future: ListenableFuture<LibraryResult<Void>> = mediaBrowser.search(query, null)
future.addListener(
{
if (future.get().resultCode == LibraryResult.RESULT_SUCCESS) {
val searchFuture: ListenableFuture<LibraryResult<ImmutableList<MediaItem>>> =
mediaBrowser.getSearchResult(
query,
/* page= */ 0,
/* pageSize= */ Int.MAX_VALUE,
/* params= */ null
)
searchFuture.addListener(
{
val mediaItems: List<MediaItem>? = searchFuture.get().value
updateItems(mediaItems ?: emptyList())
},
ContextCompat.getMainExecutor(activity)
)
} else {
updateItems(emptyList())
}
},
ContextCompat.getMainExecutor(activity)
)
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.media_browse_item, parent, false)
)
@SuppressWarnings("FutureReturnValueIgnored")
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
if (items.isEmpty()) {
if (!supportSearch()) {
setMessageForEmptyList(holder, activity.getString(R.string.command_not_supported_msg))
} else {
setMessageForEmptyList(holder, activity.getString(R.string.search_result_empty))
}
return
}
val mediaMetadata: MediaMetadata = items[position].mediaMetadata
holder.name.text = mediaMetadata.title ?: "Title metadata empty"
holder.subtitle.text = mediaMetadata.subtitle ?: "Subtitle metadata empty"
holder.subtitle.visibility = View.VISIBLE
holder.icon.visibility = View.VISIBLE
when {
mediaMetadata.artworkUri != null -> {
holder.icon.setImageURI(mediaMetadata.artworkUri)
}
mediaMetadata.artworkData != null -> {
val bitmap: Bitmap =
BitmapFactory.decodeByteArray(
mediaMetadata.artworkData,
0,
mediaMetadata.artworkData!!.size
)
holder.icon.setImageBitmap(bitmap)
}
else -> {
holder.icon.setImageResource(R.drawable.ic_album_black_24dp)
}
}
val item: MediaItem = items[position]
holder.itemView.setOnClickListener {
if (mediaMetadata.isPlayable == true) {
mediaBrowser.setMediaItem(MediaItem.Builder().setMediaId(item.mediaId).build())
mediaBrowser.prepare()
mediaBrowser.play()
}
}
}
override fun getItemCount(): Int {
if (items.isEmpty()) return 1
return items.size
}
private fun supportSearch(): Boolean =
mediaBrowser.availableSessionCommands.contains(SessionCommand.COMMAND_CODE_LIBRARY_SEARCH)
fun updateItems(newItems: List<MediaItem>) {
items = newItems
notifyDataSetChanged()
}
private fun setMessageForEmptyList(holder: ViewHolder, message: String) {
holder.name.text = message
holder.subtitle.visibility = View.GONE
holder.icon.visibility = View.GONE
holder.itemView.setOnClickListener {}
}
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.item_name)
val subtitle: TextView = itemView.findViewById(R.id.item_subtitle)
val icon: ImageView = itemView.findViewById(R.id.item_icon)
}
}

View File

@ -0,0 +1,72 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.app.Activity
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.ToggleButton
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.media3.common.Player
import androidx.media3.session.MediaController
/** Helper class which handles shuffle mode changes and the UI surrounding this feature. */
class ShuffleModeHelper(activity: Activity, mediaController: MediaController) {
private val container: ViewGroup = activity.findViewById(R.id.group_toggle_shuffle)
private val icon: ImageView = container.findViewById(R.id.shuffle_mode_icon)
private val shuffleButton: ToggleButton = container.findViewById(R.id.shuffle_mode_button)
init {
shuffleButton.setOnClickListener {
mediaController.shuffleModeEnabled = shuffleButton.isChecked
}
val listener: Player.Listener =
object : Player.Listener {
override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {
shuffleButton.isChecked = shuffleModeEnabled
updateColor(shuffleModeEnabled)
}
override fun onAvailableCommandsChanged(availableCommands: Player.Commands) =
updateBackground(availableCommands.contains(Player.COMMAND_SET_SHUFFLE_MODE))
}
mediaController.addListener(listener)
val isSupported: Boolean =
mediaController.availableCommands.contains(Player.COMMAND_SET_SHUFFLE_MODE)
updateBackground(isSupported)
val isEnabled: Boolean = mediaController.shuffleModeEnabled
updateColor(isEnabled)
shuffleButton.isChecked = isEnabled
}
fun updateBackground(isSupported: Boolean) {
if (isSupported) {
container.background = null
shuffleButton.visibility = View.VISIBLE
} else {
container.setBackgroundResource(R.drawable.bg_unsupported_action)
shuffleButton.visibility = View.GONE
}
}
fun updateColor(isEnabled: Boolean) {
val tint: Int = if (isEnabled) R.color.colorPrimary else R.color.colorInactive
DrawableCompat.setTint(icon.drawable, ContextCompat.getColor(container.context, tint))
}
}

View File

@ -0,0 +1,140 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.app.Activity
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.core.content.res.ResourcesCompat
import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.session.MediaController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
class TimelineAdapter(
private val activity: Activity,
private val mediaController: MediaController
) : RecyclerView.Adapter<TimelineAdapter.ViewHolder>() {
private var timeline: Timeline = mediaController.currentTimeline
private var currentIndex: Int = -1
init {
val timelineList: RecyclerView = activity.findViewById(R.id.timeline_items_list)
timelineList.layoutManager = LinearLayoutManager(activity)
timelineList.setHasFixedSize(true)
timelineList.adapter = this
val refreshButton: Button = activity.findViewById(R.id.refresh_button)
refreshButton.setOnClickListener {
refreshTimeline(mediaController.currentTimeline, mediaController.currentMediaItemIndex)
}
val listener =
object : Player.Listener {
override fun onTimelineChanged(newTimeline: Timeline, reason: Int) {
timeline = newTimeline
notifyDataSetChanged()
}
override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo,
reason: Int
) {
currentIndex = newPosition.mediaItemIndex
notifyItemChanged(oldPosition.mediaItemIndex)
notifyItemChanged(newPosition.mediaItemIndex)
}
override fun onAvailableCommandsChanged(availableCommands: Player.Commands) {
notifyDataSetChanged()
}
}
mediaController.addListener(listener)
}
fun refreshTimeline(newTimeline: Timeline, index: Int) {
timeline = newTimeline
currentIndex = index
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder =
ViewHolder(
LayoutInflater.from(parent.context).inflate(R.layout.media_timeline_item, parent, false)
)
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val window = timeline.getWindow(position, Timeline.Window())
val mediaMetadata = window.mediaItem.mediaMetadata
holder.name.text = mediaMetadata.title ?: "Title metadata empty"
holder.subtitle.text = mediaMetadata.subtitle ?: "Subtitle metadata empty"
when {
mediaMetadata.artworkUri != null -> {
holder.icon.setImageURI(mediaMetadata.artworkUri)
}
mediaMetadata.artworkData != null -> {
val bitmap: Bitmap =
BitmapFactory.decodeByteArray(
mediaMetadata.artworkData,
0,
mediaMetadata.artworkData!!.size
)
holder.icon.setImageBitmap(bitmap)
}
else -> {
holder.icon.setImageResource(R.drawable.ic_album_black_24dp)
}
}
holder.itemView.setOnClickListener { mediaController.seekToDefaultPosition(position) }
holder.removeButton.apply {
if (mediaController.availableCommands.contains(Player.COMMAND_CHANGE_MEDIA_ITEMS)) {
visibility = View.VISIBLE
setOnClickListener { mediaController.removeMediaItem(position) }
} else {
visibility = View.GONE
setOnClickListener {}
}
}
val colorId =
if (position == currentIndex) {
R.color.background_grey
} else {
R.color.background_transparent
}
holder.itemView.setBackgroundColor(ResourcesCompat.getColor(activity.resources, colorId, null))
}
override fun getItemCount(): Int = timeline.windowCount
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val name: TextView = itemView.findViewById(R.id.item_name)
val subtitle: TextView = itemView.findViewById(R.id.item_subtitle)
val icon: ImageView = itemView.findViewById(R.id.item_icon)
val removeButton: Button = itemView.findViewById(R.id.remove_button)
}
}

View File

@ -0,0 +1,111 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller
import android.app.Activity
import android.widget.ImageButton
import androidx.media3.common.Player
import androidx.media3.session.MediaController
/** Helper class which handles transport controls and the UI surrounding this feature. */
class TransportControlHelper(activity: Activity, mediaController: MediaController) {
private val buttonCommandList: List<Pair<ImageButton, Int>>
init {
val controls =
listOf(
Control(
{ controller: MediaController -> controller.play() },
activity.findViewById(R.id.action_resume),
Player.COMMAND_PLAY_PAUSE
),
Control(
{ controller: MediaController -> controller.pause() },
activity.findViewById(R.id.action_pause),
Player.COMMAND_PLAY_PAUSE
),
Control(
{ controller: MediaController -> controller.stop() },
activity.findViewById(R.id.action_stop),
Player.COMMAND_STOP
),
Control(
{ controller: MediaController -> controller.seekToNext() },
activity.findViewById(R.id.action_skip_next),
Player.COMMAND_SEEK_TO_NEXT_MEDIA_ITEM
),
Control(
{ controller: MediaController -> controller.seekToPrevious() },
activity.findViewById(R.id.action_skip_previous),
Player.COMMAND_SEEK_TO_PREVIOUS_MEDIA_ITEM
),
Control(
{ controller: MediaController ->
val positionMs: Long = controller.currentPosition
controller.seekTo(positionMs - 1000 * 30)
},
activity.findViewById(R.id.action_skip_30s_backward),
Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM
),
Control(
{ controller: MediaController ->
val positionMs: Long = controller.currentPosition
controller.seekTo(positionMs + 1000 * 30)
},
activity.findViewById(R.id.action_skip_30s_forward),
Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM
),
Control(
{ controller: MediaController -> controller.seekForward() },
activity.findViewById(R.id.action_fast_forward),
Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM
),
Control(
{ controller: MediaController -> controller.seekBack() },
activity.findViewById(R.id.action_fast_rewind),
Player.COMMAND_SEEK_IN_CURRENT_MEDIA_ITEM
)
)
for (control in controls) {
control.button.setOnClickListener { control.action(mediaController) }
}
buttonCommandList = controls.map { it.button to it.command }.toList()
updateBackground(mediaController.availableCommands)
val listener: Player.Listener =
object : Player.Listener {
override fun onAvailableCommandsChanged(availableCommands: Player.Commands) =
updateBackground(availableCommands)
}
mediaController.addListener(listener)
}
private class Control(
val action: (MediaController) -> Unit,
val button: ImageButton,
@Player.Command val command: Int
)
fun updateBackground(availableCommands: Player.Commands) =
buttonCommandList.forEach { (button: ImageButton, command: Int) ->
if (availableCommands.contains(command)) {
button.background = null
} else {
button.setBackgroundResource(R.drawable.bg_unsupported_action)
}
}
}

View File

@ -0,0 +1,60 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller.findapps
import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Resources
import android.media.session.MediaController
import android.media.session.MediaSessionManager
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.media3.testapp.controller.MediaAppDetails
/**
* Implementation of [FindMediaApps] that uses [MediaSessionManager] to populate the list of active
* media session apps.
*/
@RequiresApi(Build.VERSION_CODES.LOLLIPOP)
class FindActiveMediaSessionApps
constructor(
private val mediaSessionManager: MediaSessionManager,
private val componentName: ComponentName,
private val packageManager: PackageManager,
private val resources: Resources,
private val context: Context,
callback: AppListUpdatedCallback
) : FindMediaApps(callback) {
override val mediaApps: List<MediaAppDetails>
get() {
return getMediaAppsFromMediaControllers(
mediaSessionManager.getActiveSessions(componentName),
packageManager,
resources
)
}
private fun getMediaAppsFromMediaControllers(
sessionTokens: List<MediaController>,
packageManager: PackageManager,
resources: Resources
): List<MediaAppDetails> {
return sessionTokens.map {
MediaAppDetails.create(packageManager, resources, controller = it, context)
}
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller.findapps
import androidx.media3.testapp.controller.MediaAppDetails
/** Base class that fetches a list of media apps. */
abstract class FindMediaApps(private val callback: AppListUpdatedCallback) {
/** Callback used by [FindMediaApps]. */
interface AppListUpdatedCallback {
fun onAppListUpdated(mediaAppEntries: List<MediaAppDetails>)
}
protected abstract val mediaApps: List<MediaAppDetails>
fun execute() {
callback.onAppListUpdated(mediaApps)
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.media3.testapp.controller.findapps
import android.content.Context
import android.content.pm.PackageManager
import android.content.res.Resources
import androidx.media3.session.SessionToken
import androidx.media3.testapp.controller.MediaAppDetails
/**
* Implementation of [FindMediaApps] that uses [MediaSessionManager] to populate the list of media
* service apps.
*/
class FindMediaServiceApps
constructor(
private val context: Context,
private val packageManager: PackageManager,
private val resources: Resources,
callback: AppListUpdatedCallback
) : FindMediaApps(callback) {
override val mediaApps: List<MediaAppDetails>
get() {
return getMediaAppsFromSessionTokens(
SessionToken.getAllServiceTokens(context),
packageManager,
resources
)
}
private fun getMediaAppsFromSessionTokens(
sessionTokens: Set<SessionToken>,
packageManager: PackageManager,
resources: Resources
): List<MediaAppDetails> {
return sessionTokens.map {
MediaAppDetails.create(packageManager, resources, sessionToken = it)
}
}
}

View File

@ -0,0 +1 @@
../../proguard-rules.txt

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_checked="true" android:color="@color/color_white"/>
<item android:color="@color/colorInactive" />
</selector>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:bottom="@dimen/margin_inner_drawable"
android:left="@dimen/margin_inner_drawable"
android:right="@dimen/margin_inner_drawable"
android:top="@dimen/margin_inner_drawable">
<shape android:shape="oval">
<size
android:width="24dp"
android:height="24dp" />
<stroke
android:width="1dp"
android:color="@color/border_unsupported_action"
android:dashGap="2dp"
android:dashWidth="3dp" />
<solid android:color="@color/background_unsupported_action" />
</shape>
</item>
</layer-list>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="@color/colorAccent"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,16.5c-2.49,0 -4.5,-2.01 -4.5,-4.5S9.51,7.5 12,7.5s4.5,2.01 4.5,4.5 -2.01,4.5 -4.5,4.5zM12,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1 1,-0.45 1,-1 -0.45,-1 -1,-1z"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M4,18l8.5,-6L4,6v12zM13,6v12l8.5,-6L13,6z"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M11,18L11,6l-8.5,6 8.5,6zM11.5,12l8.5,6L20,6l-8.5,6z"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M9.6,13.5h0.4c0.2,0 0.4,-0.1 0.5,-0.2s0.2,-0.2 0.2,-0.4v-0.2s-0.1,-0.1 -0.1,-0.2 -0.1,-0.1 -0.2,-0.1h-0.5s-0.1,0.1 -0.2,0.1 -0.1,0.1 -0.1,0.2v0.2h-1c0,-0.2 0,-0.3 0.1,-0.5s0.2,-0.3 0.3,-0.4 0.3,-0.2 0.4,-0.2 0.4,-0.1 0.5,-0.1c0.2,0 0.4,0 0.6,0.1s0.3,0.1 0.5,0.2 0.2,0.2 0.3,0.4 0.1,0.3 0.1,0.5v0.3s-0.1,0.2 -0.1,0.3 -0.1,0.2 -0.2,0.2 -0.2,0.1 -0.3,0.2c0.2,0.1 0.4,0.2 0.5,0.4s0.2,0.4 0.2,0.6c0,0.2 0,0.4 -0.1,0.5s-0.2,0.3 -0.3,0.4 -0.3,0.2 -0.5,0.2 -0.4,0.1 -0.6,0.1c-0.2,0 -0.4,0 -0.5,-0.1s-0.3,-0.1 -0.5,-0.2 -0.2,-0.2 -0.3,-0.4 -0.1,-0.4 -0.1,-0.6h0.8v0.2s0.1,0.1 0.1,0.2 0.1,0.1 0.2,0.1h0.5s0.1,-0.1 0.2,-0.1 0.1,-0.1 0.1,-0.2v-0.5s-0.1,-0.1 -0.1,-0.2 -0.1,-0.1 -0.2,-0.1h-0.6v-0.7zM15.3,14.2c0,0.3 0,0.6 -0.1,0.8l-0.3,0.6s-0.3,0.3 -0.5,0.3 -0.4,0.1 -0.6,0.1 -0.4,0 -0.6,-0.1 -0.3,-0.2 -0.5,-0.3 -0.2,-0.3 -0.3,-0.6 -0.1,-0.5 -0.1,-0.8v-0.7c0,-0.3 0,-0.6 0.1,-0.8l0.3,-0.6s0.3,-0.3 0.5,-0.3 0.4,-0.1 0.6,-0.1 0.4,0 0.6,0.1 0.3,0.2 0.5,0.3 0.2,0.3 0.3,0.6 0.1,0.5 0.1,0.8v0.7zM14.4,13.4v-0.5s-0.1,-0.2 -0.1,-0.3 -0.1,-0.1 -0.2,-0.2 -0.2,-0.1 -0.3,-0.1 -0.2,0 -0.3,0.1l-0.2,0.2s-0.1,0.2 -0.1,0.3v2s0.1,0.2 0.1,0.3 0.1,0.1 0.2,0.2 0.2,0.1 0.3,0.1 0.2,0 0.3,-0.1l0.2,-0.2s0.1,-0.2 0.1,-0.3v-1.5zM4,13c0,4.4 3.6,8 8,8s8,-3.6 8,-8h-2c0,3.3 -2.7,6 -6,6s-6,-2.7 -6,-6 2.7,-6 6,-6v4l5,-5 -5,-5v4c-4.4,0 -8,3.6 -8,8z"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,21.35l-1.45,-1.32C5.4,15.36 2,12.28 2,8.5 2,5.42 4.42,3 7.5,3c1.74,0 3.41,0.81 4.5,2.09C13.09,3.81 14.76,3 16.5,3 19.58,3 22,5.42 22,8.5c0,3.78 -3.4,6.86 -8.55,11.54L12,21.35z" />
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M17.56,14.24c0.28,-0.69 0.44,-1.45 0.44,-2.24 0,-3.31 -2.69,-6 -6,-6 -0.79,0 -1.55,0.16 -2.24,0.44l1.62,1.62c0.2,-0.03 0.41,-0.06 0.62,-0.06 2.21,0 4,1.79 4,4 0,0.21 -0.02,0.42 -0.05,0.63l1.61,1.61zM12,4c4.42,0 8,3.58 8,8 0,1.35 -0.35,2.62 -0.95,3.74l1.47,1.47C21.46,15.69 22,13.91 22,12c0,-5.52 -4.48,-10 -10,-10 -1.91,0 -3.69,0.55 -5.21,1.47l1.46,1.46C9.37,4.34 10.65,4 12,4zM3.27,2.5L2,3.77l2.1,2.1C2.79,7.57 2,9.69 2,12c0,3.7 2.01,6.92 4.99,8.65l1,-1.73C5.61,17.53 4,14.96 4,12c0,-1.76 0.57,-3.38 1.53,-4.69l1.43,1.44C6.36,9.68 6,10.8 6,12c0,2.22 1.21,4.15 3,5.19l1,-1.74c-1.19,-0.7 -2,-1.97 -2,-3.45 0,-0.65 0.17,-1.25 0.44,-1.79l1.58,1.58L10,12c0,1.1 0.9,2 2,2l0.21,-0.02 0.01,0.01 7.51,7.51L21,20.23 4.27,3.5l-1,-1z"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M8,5v14l11,-7z"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M7,7h10v3l4,-4 -4,-4v3L5,5v6h2L7,7zM17,17L7,17v-3l-4,4 4,4v-3h12v-6h-2v4z" />
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,5L12,1L7,6l5,5L12,7c3.3,0 6,2.7 6,6s-2.7,6 -6,6 -6,-2.7 -6,-6L4,13c0,4.4 3.6,8 8,8s8,-3.6 8,-8 -3.6,-8 -8,-8zM9.6,13.5h0.4c0.2,0 0.4,-0.1 0.5,-0.2s0.2,-0.2 0.2,-0.4v-0.2s-0.1,-0.1 -0.1,-0.2 -0.1,-0.1 -0.2,-0.1h-0.5s-0.1,0.1 -0.2,0.1 -0.1,0.1 -0.1,0.2v0.2h-1c0,-0.2 0,-0.3 0.1,-0.5s0.2,-0.3 0.3,-0.4 0.3,-0.2 0.4,-0.2 0.4,-0.1 0.5,-0.1c0.2,0 0.4,0 0.6,0.1s0.3,0.1 0.5,0.2 0.2,0.2 0.3,0.4 0.1,0.3 0.1,0.5v0.3s-0.1,0.2 -0.1,0.3 -0.1,0.2 -0.2,0.2 -0.2,0.1 -0.3,0.2c0.2,0.1 0.4,0.2 0.5,0.4s0.2,0.4 0.2,0.6c0,0.2 0,0.4 -0.1,0.5s-0.2,0.3 -0.3,0.4 -0.3,0.2 -0.5,0.2 -0.4,0.1 -0.6,0.1c-0.2,0 -0.4,0 -0.5,-0.1s-0.3,-0.1 -0.5,-0.2 -0.2,-0.2 -0.3,-0.4 -0.1,-0.4 -0.1,-0.6h0.8v0.2s0.1,0.1 0.1,0.2 0.1,0.1 0.2,0.1h0.5s0.1,-0.1 0.2,-0.1 0.1,-0.1 0.1,-0.2v-0.5s-0.1,-0.1 -0.1,-0.2 -0.1,-0.1 -0.2,-0.1h-0.6v-0.7zM15.3,14.2c0,0.3 0,0.6 -0.1,0.8l-0.3,0.6s-0.3,0.3 -0.5,0.3 -0.4,0.1 -0.6,0.1 -0.4,0 -0.6,-0.1 -0.3,-0.2 -0.5,-0.3 -0.2,-0.3 -0.3,-0.6 -0.1,-0.5 -0.1,-0.8v-0.7c0,-0.3 0,-0.6 0.1,-0.8l0.3,-0.6s0.3,-0.3 0.5,-0.3 0.4,-0.1 0.6,-0.1 0.4,0 0.6,0.1 0.3,0.2 0.5,0.3 0.2,0.3 0.3,0.6 0.1,0.5 0.1,0.8v0.7zM14.5,13.4v-0.5c0,-0.1 -0.1,-0.2 -0.1,-0.3s-0.1,-0.1 -0.2,-0.2 -0.2,-0.1 -0.3,-0.1 -0.2,0 -0.3,0.1l-0.2,0.2s-0.1,0.2 -0.1,0.3v2s0.1,0.2 0.1,0.3 0.1,0.1 0.2,0.2 0.2,0.1 0.3,0.1 0.2,0 0.3,-0.1l0.2,-0.2s0.1,-0.2 0.1,-0.3v-1.5z"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M10.59,9.17L5.41,4 4,5.41l5.17,5.17 1.42,-1.41zM14.5,4l2.04,2.04L4,18.59 5.41,20 17.96,7.46 20,9.5L20,4h-5.5zM14.83,13.41l-1.41,1.41 3.13,3.13L14.5,20L20,20v-5.5l-2.04,2.04 -3.13,-3.13z" />
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportHeight="24.0"
android:viewportWidth="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,17.27L18.18,21l-1.64,-7.03L22,9.24l-7.19,-0.61L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21z" />
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M6,6h12v12H6z"/>
</vector>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF"
android:alpha="0.8">
<path
android:fillColor="#FF000000"
android:pathData="M19,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.11,0 2,-0.9 2,-2L21,5c0,-1.1 -0.89,-2 -2,-2zM10,17l-5,-5 1.41,-1.41L10,14.17l7.59,-7.59L19,8l-9,9z"/>
</vector>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="#FFFFFF"
android:alpha="0.8">
<path
android:fillColor="#FF000000"
android:pathData="M4,15h16v-2L4,13v2zM4,19h16v-2L4,17v2zM4,11h16L20,9L4,9v2zM4,5v2h16L20,5L4,5z"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M15,3L6,3c-0.83,0 -1.54,0.5 -1.84,1.22l-3.02,7.05c-0.09,0.23 -0.14,0.47 -0.14,0.73v1.91l0.01,0.01L1,14c0,1.1 0.9,2 2,2h6.31l-0.95,4.57 -0.03,0.32c0,0.41 0.17,0.79 0.44,1.06L9.83,23l6.59,-6.59c0.36,-0.36 0.58,-0.86 0.58,-1.41L17,5c0,-1.1 -0.9,-2 -2,-2zM19,3v12h4L23,3h-4z"/>
</vector>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M1,21h4L5,9L1,9v12zM23,10c0,-1.1 -0.9,-2 -2,-2h-6.31l0.95,-4.57 0.03,-0.32c0,-0.41 -0.17,-0.79 -0.44,-1.06L14.17,1 7.59,7.59C7.22,7.95 7,8.45 7,9v10c0,1.1 0.9,2 2,2h9c0.83,0 1.54,-0.5 1.84,-1.22l3.02,-7.05c0.09,-0.23 0.14,-0.47 0.14,-0.73v-1.91l-0.01,-0.01L23,10z"/>
</vector>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thickness="3dp"
android:useLevel="false">
<solid android:color="@color/colorAccent" />
</shape>
</item>
<item>
<shape
android:innerRadius="0dp"
android:shape="ring"
android:thickness="3dp"
android:useLevel="false">
<solid android:color="@color/colorInactive" />
</shape>
</item>
</selector>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size
android:width="0dp"
android:height="8dp" />
</shape>

View File

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:orientation="vertical"
tools:context="androidx.media3.testapp.LaunchActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layoutManager=""
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<androidx.core.widget.ContentLoadingProgressBar
android:id="@+id/spinner"
style="@style/Base.Widget.AppCompat.ProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:visibility="gone" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="androidx.media3.testapp.controller.MediaAppControllerActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
app:popupTheme="@style/AppTheme.PopupOverlay" />
</com.google.android.material.appbar.AppBarLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.viewpager.widget.ViewPager
android:id="@+id/view_pager"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include
android:id="@+id/prepare_play_page"
layout="@layout/media_info" />
<include
android:id="@+id/controls_page"
layout="@layout/media_controls" />
<include
android:id="@+id/custom_commands_page"
layout="@layout/media_custom_commands" />
<include
android:id="@+id/timeline_list_page"
layout="@layout/media_timeline" />
<include
android:id="@+id/browse_tree_page"
layout="@layout/media_browse_tree" />
<include
android:id="@+id/media_search_page"
layout="@layout/media_search_controls" />
</androidx.viewpager.widget.ViewPager>
<com.google.android.material.tabs.TabLayout
android:id="@+id/page_indicator"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@android:color/transparent"
app:tabBackground="@drawable/tab_indicator"
app:tabGravity="center"
app:tabIndicatorHeight="0dp" />
</FrameLayout>
</LinearLayout>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/margin_small"
android:paddingTop="@dimen/margin_small">
<ImageView
android:id="@+id/app_icon"
android:layout_width="@dimen/app_icon_size"
android:layout_height="@dimen/app_icon_size"
android:layout_marginEnd="@dimen/margin_small"
android:layout_marginRight="@dimen/margin_small"
android:layout_marginStart="@dimen/activity_vertical_margin"
android:layout_marginLeft="@dimen/activity_vertical_margin"
android:contentDescription="@string/app_icon_desc"
android:layout_gravity="center_vertical"
android:scaleType="fitCenter"
app:srcCompat="@mipmap/ic_launcher"/>
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="@dimen/activity_vertical_margin"
android:layout_marginRight="@dimen/activity_vertical_margin"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/app_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:maxLines="1"
android:singleLine="true"
android:textColor="@color/text_dark"
android:textSize="@dimen/app_name_text_size"
android:textStyle="bold"
tools:text="Media App" />
<TextView
android:id="@+id/package_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:ellipsize="middle"
android:singleLine="true"
android:textColor="@color/text_light"
android:textSize="@dimen/app_package_text_size"
tools:text="com.example.mediaapp" />
</LinearLayout>
<Button
android:id="@+id/app_control"
android:layout_width="@dimen/app_control_button_width"
android:layout_height="@dimen/app_button_height"
android:layout_weight="0"
android:text="@string/app_control_button" />
</LinearLayout>

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/margin_small"
android:paddingTop="@dimen/margin_small">
<ImageView
android:id="@+id/error_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/activity_vertical_margin"
android:layout_marginStart="@dimen/activity_vertical_margin"
app:layout_constraintBottom_toBottomOf="@id/error_message"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/error_message"
app:srcCompat="@drawable/ic_no_apps_black_24dp"
tools:ignore="contentDescription" />
<TextView
android:id="@+id/error_message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_small"
android:layout_marginStart="@dimen/margin_small"
android:textSize="@dimen/error_text_size"
app:layout_constraintBottom_toTopOf="@+id/error_detail"
app:layout_constraintEnd_toStartOf="@+id/error_action"
app:layout_constraintStart_toEndOf="@+id/error_icon"
app:layout_constraintTop_toTopOf="parent"
tools:text="Error message" />
<TextView
android:id="@+id/error_detail"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:textSize="@dimen/error_details_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/error_action"
app:layout_constraintStart_toStartOf="@+id/error_message"
tools:text="Longer error message with more detail" />
<Button
android:id="@+id/error_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="@dimen/activity_vertical_margin"
android:layout_marginEnd="@dimen/activity_vertical_margin"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Action" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="@dimen/margin_small"
android:paddingBottom="@dimen/margin_small">
<TextView
android:id="@+id/header_text"
android:textStyle="bold"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/activity_vertical_margin"
android:layout_marginLeft="@dimen/activity_vertical_margin"
android:layout_marginEnd="@dimen/margin_small"
android:layout_marginRight="@dimen/margin_small"
android:gravity="center_vertical"
android:textColor="@color/colorPrimaryDark"
android:textSize="@dimen/list_header_text_size"
tools:text="Header"/>
</LinearLayout>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/margin_small"
android:paddingTop="@dimen/margin_small">
<ImageView
android:id="@+id/item_icon"
android:layout_width="@dimen/app_icon_size"
android:layout_height="@dimen/app_icon_size"
android:layout_gravity="center_vertical"
android:layout_marginRight="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:scaleType="fitCenter" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/item_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="@color/text_dark"
android:textSize="@dimen/app_name_text_size"
android:textStyle="bold"
tools:text="Item Title" />
<TextView
android:id="@+id/item_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:ellipsize="middle"
android:textColor="@color/text_light"
android:textSize="@dimen/app_package_text_size"
tools:text="Item Subtitle" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<Button
android:id="@+id/media_browse_tree_top"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/media_browse_tree_top" />
<Button
android:id="@+id/media_browse_tree_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/media_browse_tree_up" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/media_browse_tree_header"
android:textColor="@color/colorPrimaryDark"
android:textSize="@dimen/list_header_text_size"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/media_items_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -0,0 +1,275 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MediaAppControllerActivity">
<ImageView
android:id="@+id/media_art"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginTop="@dimen/margin_large"
android:contentDescription="@string/media_art_string"
android:background="@color/background_grey"
android:scaleType="centerCrop"
app:layout_constraintBottom_toTopOf="@+id/media_title"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
tools:src="@drawable/ic_album_black_24dp" />
<TextView
android:id="@+id/media_title"
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/margin_small"
android:gravity="center"
android:paddingTop="@dimen/padding_large"
android:textStyle="bold"
app:layout_constraintLeft_toLeftOf="@+id/primaryGuidelineStart"
app:layout_constraintRight_toLeftOf="@+id/primaryGuidelineEnd"
app:layout_constraintBottom_toTopOf="@+id/media_artist"
tools:text="Title" />
<TextView
android:id="@+id/media_artist"
style="@style/TextAppearance.AppCompat.Small"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:gravity="center"
app:layout_constraintLeft_toLeftOf="@+id/primaryGuidelineStart"
app:layout_constraintRight_toLeftOf="@+id/primaryGuidelineEnd"
app:layout_constraintBottom_toTopOf="@+id/media_album"
tools:text="Artist" />
<TextView
android:id="@+id/media_album"
style="@style/TextAppearance.AppCompat.Medium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:gravity="center"
android:paddingBottom="@dimen/padding_large"
app:layout_constraintLeft_toLeftOf="@+id/primaryGuidelineStart"
app:layout_constraintRight_toLeftOf="@+id/primaryGuidelineEnd"
app:layout_constraintBottom_toTopOf="@+id/rating"
tools:text="Album" />
<include
android:id="@+id/rating"
layout="@layout/media_ratings"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:layout_marginLeft="@dimen/margin_small"
android:layout_marginRight="@dimen/margin_small"
android:layout_marginTop="@dimen/margin_small"
app:layout_constraintLeft_toLeftOf="@+id/primaryGuidelineStart"
app:layout_constraintBottom_toTopOf="@+id/group_toggle_repeat"
app:layout_constraintRight_toRightOf="@+id/primaryGuidelineEnd" />
<ImageButton
android:id="@+id/action_skip_30s_backward"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/margin_small"
android:contentDescription="@string/action_skip_30s_backward"
app:layout_constraintBottom_toTopOf="@+id/mediaControlsGuideline"
app:layout_constraintEnd_toStartOf="@id/centerGuideline"
app:srcCompat="@drawable/ic_replay_30_black_32dp" />
<ImageButton
android:id="@+id/action_skip_30s_forward"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_small"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginBottom="@dimen/margin_small"
android:contentDescription="@string/action_skip_30s_forward"
app:layout_constraintBottom_toTopOf="@+id/mediaControlsGuideline"
app:layout_constraintLeft_toLeftOf="@+id/centerGuideline"
app:layout_constraintStart_toStartOf="@id/centerGuideline"
app:srcCompat="@drawable/ic_forward_30_black_32dp" />
<LinearLayout
android:id="@+id/group_toggle_repeat"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/mediaControlsGuideline"
app:layout_constraintEnd_toStartOf="@+id/action_skip_30s_backward"
app:layout_constraintLeft_toLeftOf="@+id/centerGuideline"
app:layout_constraintStart_toEndOf="@+id/primaryGuidelineStart">
<ImageView
android:id="@+id/repeat_mode_icon"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/action_set_repeat"
app:srcCompat="@drawable/ic_repeat_black_32dp" />
<Spinner
android:id="@+id/repeat_mode_spinner"
android:layout_height="@dimen/app_button_height"
android:layout_width="wrap_content"
android:entries="@array/repeat_modes" />
</LinearLayout>
<LinearLayout
android:id="@+id/group_toggle_shuffle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:gravity="center"
android:orientation="vertical"
app:layout_constraintBottom_toTopOf="@+id/mediaControlsGuideline"
app:layout_constraintEnd_toStartOf="@+id/primaryGuidelineEnd"
app:layout_constraintLeft_toLeftOf="@+id/centerGuideline"
app:layout_constraintStart_toEndOf="@+id/action_skip_30s_forward">
<ImageView
android:id="@+id/shuffle_mode_icon"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/action_set_shuffle"
app:srcCompat="@drawable/ic_shuffle_toggle_32dp" />
<ToggleButton
android:id="@+id/shuffle_mode_button"
android:layout_width="wrap_content"
android:layout_height="@dimen/app_button_height"
android:textOff="@string/shuffle_off"
android:textOn="@string/shuffle_on" />
</LinearLayout>
<ImageButton
android:id="@+id/action_skip_previous"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/action_skip_previous"
app:layout_constraintTop_toBottomOf="@+id/mediaControlsGuideline"
app:layout_constraintRight_toLeftOf="@+id/action_fast_rewind"
app:srcCompat="@drawable/ic_skip_previous_black_32dp" />
<ImageButton
android:id="@+id/action_fast_rewind"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/action_fast_rewind"
app:layout_constraintTop_toBottomOf="@+id/mediaControlsGuideline"
app:layout_constraintRight_toLeftOf="@+id/action_resume"
app:srcCompat="@drawable/ic_fast_rewind_black_32dp" />
<ImageButton
android:id="@+id/action_resume"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/action_resume"
app:layout_constraintTop_toBottomOf="@+id/mediaControlsGuideline"
app:layout_constraintRight_toLeftOf="@+id/action_pause"
app:srcCompat="@drawable/ic_play_arrow_black_32dp" />
<ImageButton
android:id="@+id/action_pause"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/action_pause"
app:layout_constraintTop_toBottomOf="@+id/mediaControlsGuideline"
app:layout_constraintLeft_toLeftOf="@+id/centerGuideline"
app:layout_constraintRight_toLeftOf="@+id/centerGuideline"
app:srcCompat="@drawable/ic_pause_black_32dp" />
<ImageButton
android:id="@+id/action_stop"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/action_stop"
app:layout_constraintTop_toBottomOf="@+id/mediaControlsGuideline"
app:layout_constraintLeft_toRightOf="@+id/action_pause"
app:srcCompat="@drawable/ic_stop_black_32dp"
tools:background="@drawable/bg_unsupported_action" />
<ImageButton
android:id="@+id/action_fast_forward"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/action_fast_forward"
app:layout_constraintTop_toBottomOf="@+id/mediaControlsGuideline"
app:layout_constraintLeft_toRightOf="@+id/action_stop"
app:srcCompat="@drawable/ic_fast_forward_black_32dp" />
<ImageButton
android:id="@+id/action_skip_next"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/action_skip_next"
app:layout_constraintTop_toBottomOf="@+id/mediaControlsGuideline"
app:layout_constraintLeft_toRightOf="@+id/action_fast_forward"
app:srcCompat="@drawable/ic_skip_next_black_32dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/centerGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5"
app:layout_constraintTop_toTopOf="@+id/mediaControlsGuideline" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/mediaControlsGuideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_percent="0.8" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/primaryGuidelineStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="@dimen/padding_large" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/primaryGuidelineEnd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="@dimen/padding_large" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/margin_small"
android:paddingTop="@dimen/margin_small">
<ImageView
android:id="@+id/action_icon"
android:layout_width="@dimen/app_icon_size"
android:layout_height="@dimen/app_icon_size"
android:layout_gravity="center_vertical"
android:layout_marginRight="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:scaleType="fitCenter"
app:tint="@color/text_dark"
android:contentDescription="@string/custom_command_description" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/action_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textColor="@color/text_dark"
android:textSize="@dimen/app_name_text_size"
android:textStyle="bold"
tools:text="Custom Command" />
<TextView
android:id="@+id/action_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/margin_small"
android:ellipsize="middle"
android:textColor="@color/text_light"
android:textSize="@dimen/app_package_text_size"
tools:text="CustomActionDescription" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/custom_commands_header"
android:textColor="@color/colorPrimaryDark"
android:textSize="@dimen/list_header_text_size"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/custom_commands_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Spinner
android:id="@+id/input_type"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:entries="@array/input_options" />
<EditText
android:id="@+id/uri_id_query"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:hint="@string/id_hint"
android:inputType="textUri" />
</LinearLayout>
<LinearLayout
style="?android:attr/buttonBarStyle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:weightSum="3">
<Button
android:id="@+id/action_prepare"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/action_prepare" />
<Button
android:id="@+id/action_play"
style="?android:attr/buttonBarButtonStyle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/action_play" />
</LinearLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:orientation="vertical">
<TextView
android:id="@+id/media_info"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="top|start"
android:text="@string/media_info_default" />
</LinearLayout>
</ScrollView>
<TextView
style="@style/TextAppearance.AppCompat.Large"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/audio_focus_title" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/margin_large">
<ToggleButton
android:id="@+id/audio_focus_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textOff="@string/audio_focus_gain_focus"
android:textOn="@string/audio_focus_abandon_focus" />
<Spinner
android:id="@+id/audio_focus_type"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:entries="@array/audio_focus_types" />
</LinearLayout>
</LinearLayout>

View File

@ -0,0 +1,102 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">
<ImageButton
android:id="@+id/rating_thumb_up"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/rating_thumb_up"
app:srcCompat="@drawable/ic_thumb_up_black_32dp" />
<ImageButton
android:id="@+id/rating_thumb_down"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/rating_thumb_down"
app:srcCompat="@drawable/ic_thumb_down_black_32dp" />
<ImageButton
android:id="@+id/rating_heart"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/rating_heart"
app:srcCompat="@drawable/ic_heart_black_32dp" />
<ImageButton
android:id="@+id/rating_star_1"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/rating_star_1"
app:srcCompat="@drawable/ic_star_black_32dp" />
<ImageButton
android:id="@+id/rating_star_2"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/rating_star_2"
app:srcCompat="@drawable/ic_star_black_32dp" />
<ImageButton
android:id="@+id/rating_star_3"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/rating_star_3"
app:srcCompat="@drawable/ic_star_black_32dp" />
<ImageButton
android:id="@+id/rating_star_4"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/rating_star_4"
app:srcCompat="@drawable/ic_star_black_32dp" />
<ImageButton
android:id="@+id/rating_star_5"
style="@style/AppTheme.MediaControl"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/rating_star_5"
app:srcCompat="@drawable/ic_star_black_32dp" />
<EditText
android:id="@+id/rating_percentage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/rating_percentage"
android:inputType="numberDecimal"
android:maxEms="6"
android:minEms="4" />
<Button
android:id="@+id/rating_percentage_set"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/rating_set_percentage" />
</LinearLayout>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Button
android:id="@+id/search_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/search_button" />
<EditText
android:id="@+id/search_query"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="@dimen/activity_vertical_margin"
android:hint="@string/search_query_placeholder"
android:inputType="textUri" />
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/media_browse_tree_header"
android:textColor="@color/colorPrimaryDark"
android:textSize="@dimen/list_header_text_size"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_items_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/test_suite_options_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/test_suite_options_header"
android:paddingLeft="@dimen/padding_small"
android:paddingStart="@dimen/padding_small"
style="@style/TestHeader" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:id="@+id/test_suite_iteration_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/test_suite_iteration_header"
android:paddingLeft="@dimen/padding_small"
android:paddingStart="@dimen/padding_small"
style="@style/SubHeader"/>
<EditText
android:id="@+id/test_suite_iteration"
android:layout_weight="1"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="@dimen/test_subheader_text_size"
android:text="@string/test_suite_iteration_default"
android:hint="@string/test_suite_iteration_default"
android:gravity="center"
android:inputType="number" />
</LinearLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/test_suite_options_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="4"
android:fillViewport="true"
android:background="@color/background_grey"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<TextView
android:id="@+id/test_suite_results_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/test_results_header"
android:paddingLeft="@dimen/padding_small"
android:paddingStart="@dimen/padding_small"
style="@style/TestHeader" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/test_suite_results_container"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="4"
android:fillViewport="true"
android:background="@color/background_grey"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
</LinearLayout>

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/test_options_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/test_options_header"
android:paddingLeft="@dimen/padding_small"
android:paddingStart="@dimen/padding_small"
style="@style/TestHeader"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/test_options_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="4"
android:fillViewport="true"
android:background="@color/background_grey"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<EditText
android:id="@+id/test_query"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="@dimen/margin_small"
android:hint="@string/test_query_placeholder"
android:inputType="textUri" />
<TextView
android:id="@+id/test_results_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/test_results_header"
android:paddingLeft="@dimen/padding_small"
android:paddingStart="@dimen/padding_small"
style="@style/TestHeader" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="@dimen/margin_small"
android:layout_weight="3"
android:fillViewport="true">
<LinearLayout
android:id="@+id/test_results_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:showDividers="middle"
android:divider="@drawable/test_result_divider">
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<Button
android:id="@+id/refresh_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/timeline_refresh_button" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/timeline_list_header"
android:textColor="@color/colorPrimaryDark"
android:textSize="@dimen/list_header_text_size"
android:textStyle="bold" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/timeline_items_list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="@dimen/margin_small"
android:paddingTop="@dimen/margin_small">
<ImageView
android:id="@+id/item_icon"
android:layout_width="@dimen/app_icon_size"
android:layout_height="@dimen/app_icon_size"
android:layout_marginLeft="@dimen/margin_small"
android:layout_marginRight="@dimen/margin_small"
android:layout_marginStart="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
app:layout_constraintBottom_toBottomOf="@id/item_subtitle"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/item_name" />
<TextView
android:id="@+id/item_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/margin_small"
android:layout_marginStart="@dimen/margin_small"
app:layout_constraintBottom_toTopOf="@+id/item_subtitle"
app:layout_constraintEnd_toStartOf="@+id/remove_button"
app:layout_constraintStart_toEndOf="@+id/item_icon"
app:layout_constraintTop_toTopOf="parent"
android:gravity="center_vertical"
android:textColor="@color/text_dark"
android:textSize="@dimen/app_name_text_size"
android:textStyle="bold"
tools:text="Item title" />
<TextView
android:id="@+id/item_subtitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/remove_button"
app:layout_constraintStart_toStartOf="@+id/item_name"
android:ellipsize="middle"
android:textColor="@color/text_light"
android:textSize="@dimen/app_package_text_size"
tools:text="Item subtitle" />
<Button
android:id="@+id/remove_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="@dimen/margin_small"
android:layout_marginEnd="@dimen/margin_small"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:text="@string/timeline_remove_button" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:id="@+id/navigation_test"
android:enabled="true"
android:title="@string/navigation_test_text"
android:icon="@drawable/ic_test"
app:showAsAction="ifRoom" />
<item
android:id="@+id/navigation_suite"
android:enabled="true"
android:title="@string/navigation_suite_text"
android:icon="@drawable/ic_test_suite"
app:showAsAction="ifRoom" />
</menu>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<color name="colorPrimary">#3f51b5</color>
<color name="colorPrimaryDark">#303f9f</color>
<color name="colorAccent">#ff4081</color>
<color name="colorInactive">#a0a0a0</color>
<color name="border_unsupported_action">#ff8080</color>
<color name="background_unsupported_action">#80ff8080</color>
<color name="background_grey">#e0e0e0</color>
<color name="background_transparent">#00000000</color>
<color name="text_dark">#101010</color>
<color name="text_light">#201E1E</color>
<color name="color_white">#fff</color>
<color name="test_result_fail">#FF8483</color>
<color name="test_result_pass">#92FF8F</color>
<color name="test_timeout">#FFDC00</color>
</resources>

View File

@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="padding_small">8dp</dimen>
<dimen name="padding_large">16dp</dimen>
<dimen name="margin_inner_drawable">4dp</dimen>
<dimen name="margin_small">8dp</dimen>
<dimen name="margin_large">32dp</dimen>
<dimen name="toolbar_icon_size">24dp</dimen>
<dimen name="list_header_text_size">14sp</dimen>
<dimen name="app_icon_size">48dp</dimen>
<dimen name="app_name_text_size">18sp</dimen>
<dimen name="app_package_text_size">12sp</dimen>
<dimen name="app_button_height">48dp</dimen>
<dimen name="app_control_button_width">96dp</dimen>
<dimen name="app_test_button_width">72dp</dimen>
<dimen name="error_text_size">18sp</dimen>
<dimen name="error_details_text_size">12sp</dimen>
<dimen name="test_header_text_size">28sp</dimen>
<dimen name="test_subheader_text_size">24sp</dimen>
<dimen name="test_details_text_size">20sp</dimen>
</resources>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- LINT.IfChange -->
<string-array name="input_options">
<item>Search</item>
<item>Media ID</item>
<item>URI</item>
<item>No Input</item>
</string-array>
<!-- LINT.ThenChange(../../java/androidx/media3/testapp/controller/PreparePlayHelper.kt) -->
<!-- LINT.IfChange -->
<string-array name="audio_focus_types">
<item>GAIN</item>
<item>GAIN_TRANSIENT</item>
<item>GAIN_TRANSIENT_MAY_DUCK</item>
</string-array>
<!-- LINT.ThenChange(../../java/androidx/media3/testapp/controller/AudioFocusHelper.kt) -->
<!-- LINT.IfChange -->
<string-array name="repeat_modes">
<item>None</item>
<item>One</item>
<item>All</item>
</string-array>
<!-- LINT.ThenChange(../../java/androidx/media3/testapp/controller/RepeatModeHelper.kt) -->
</resources>

View File

@ -0,0 +1,105 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<string name="app_name">Media3 Controller Test</string>
<string name="app_icon_desc">App icon for %1$s.</string>
<string name="no_apps_found">No Media Apps Found</string>
<string name="no_apps_reason_no_media_services">Could not locate any apps with media services.</string>
<string name="no_apps_reason_no_active_sessions">Could not locate any apps with active media sessions.</string>
<string name="no_apps_reason_missing_permission">Notification Listener permission is required to scan for active media sessions.</string>
<string name="action_notification_permissions_settings">Settings</string>
<string name="media_app_header_media_service">Media Service Implementations</string>
<string name="media_app_header_active_session">Active MediaSessions</string>
<string name="id_hint">URI, Media ID, or Query</string>
<string name="app_control_button">Control</string>
<string name="app_test_button">Test</string>
<string name="media_app_details_update_failed">Couldn\'t update MediaAppDetails object</string>
<string name="media_controller_failed_msg">Failed to create a MediaController from session token</string>
<string name="action_prepare">Prepare</string>
<string name="action_play">Play</string>
<string name="audio_focus_title">Audio Focus</string>
<string name="audio_focus_gain_focus">Gain</string>
<string name="audio_focus_abandon_focus">Abandon</string>
<string name="media_info_default">Empty Media Info</string>
<string name="info_state_string">PlaybackState</string>
<string name="info_title_string">Title</string>
<string name="info_artist_string">Artist</string>
<string name="info_album_string">Album</string>
<string name="info_play_when_ready">PlayWhenReady</string>
<string name="media_art_string">Album art</string>
<string name="action_pause">Pause</string>
<string name="action_resume">Resume</string>
<string name="action_stop">Stop</string>
<string name="action_skip_previous">Previous</string>
<string name="action_skip_next">Next</string>
<string name="action_fast_forward">Fast forward</string>
<string name="action_fast_rewind">Rewind</string>
<string name="action_skip_30s_forward">Skip 30s</string>
<string name="action_skip_30s_backward">Skip back 30s</string>
<string name="action_set_shuffle">Set shuffle mode</string>
<string name="action_set_repeat">Set repeat mode</string>
<string name="shuffle_on">Shuffle</string>
<string name="shuffle_off">None</string>
<string name="rating_thumb_up">Thumb Up</string>
<string name="rating_thumb_down">Thumb Down</string>
<string name="rating_heart">Heart</string>
<string name="rating_star_1">1 star</string>
<string name="rating_star_2">2 stars</string>
<string name="rating_star_3">3 stars</string>
<string name="rating_star_4">4 stars</string>
<string name="rating_star_5">5 stars</string>
<string name="rating_percentage">Rating %</string>
<string name="rating_set_percentage">Set</string>
<string name="custom_commands_header">App-provided Custom Commands</string>
<string name="custom_command_description">Send custom command</string>
<string name="media_browse_tree_top">Browse to top</string>
<string name="media_browse_tree_up">Browse up</string>
<string name="media_browse_tree_header">App-provided MediaItems</string>
<string name="media_browse_tree_empty">Empty tree</string>
<string name="controller_disconnected_msg">MediaController disconnected</string>
<string name="controller_connection_failed_msg">Failed connecting browser: %s</string>
<string name="search_query_placeholder">Query</string>
<string name="search_query_empty_msg">Query is empty</string>
<string name="search_button">Search</string>
<string name="search_result_empty">Result empty</string>
<string name="timeline_list_header">MediaItems in Timeline</string>
<string name="timeline_refresh_button">Refresh</string>
<string name="timeline_remove_button">Remove</string>
<string name="command_not_supported_msg">Command not supported</string>
</resources>

View File

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="AppTheme.MediaControl">
<item name="android:background">?attr/selectableItemBackgroundBorderless</item>
<item name="android:padding">@dimen/padding_small</item>
<item name="android:tint">@color/colorPrimary</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
<style name="TestHeader" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/text_dark</item>
<item name="android:textSize">@dimen/test_header_text_size</item>
<item name="android:textStyle">bold</item>
</style>
<style name="SubHeader" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/text_dark</item>
<item name="android:textSize">@dimen/test_subheader_text_size</item>
</style>
<style name="SubText" parent="TextAppearance.AppCompat">
<item name="android:textColor">@color/text_light</item>
<item name="android:textSize">@dimen/test_details_text_size</item>
</style>
<style name="TestLogDivider" parent="TextAppearance.AppCompat">
<item name="android:textSize">@dimen/test_details_text_size</item>
<item name="android:textStyle">bold</item>
</style>
</resources>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Copyright 2021 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<resources>
<!-- Tests UI -->
<string name="test_options_header">Tests</string>
<string name="test_results_header">Results</string>
<string name="test_query_placeholder">Query</string>
<string name="configure_button">Configure</string>
<string name="test_run_button">Run Test</string>
<string name="test_empty_query_message">"Query is required but empty"</string>
<string name="test_running_title">Test is running</string>
<string name="navigation_test_text">Tests</string>
<string name="navigation_suite_text">Test Suites</string>
<!-- Test Suite UI -->
<string name="test_suite_options_header">Test Suites</string>
<string name="test_suite_iteration_header"># Iterations</string>
<string name="test_suite_iteration_default">1</string>
<string name="run_suite_button">Run Suite</string>
<string name="done_configure_button">Done</string>
<string name="reset_button">Reset</string>
<string name="cancel_suite_button">Cancel Suite</string>
<string name="close_results_button">Close</string>
<string name="test_suite_invalid_iteration_msg">Invalid iteration number, must be between (1..10).</string>
<string name="configure_dialog_title">%1$s Configuration</string>
<string name="suite_is_running_title">Suite is running…</string>
<string name="test_suite_result">Passing: (%d/%d)</string>
<string name="failing_logs_header">Failing Logs:</string>
<string name="passing_logs_header">Passing Logs:</string>
<string name="test_iteration_divider">--- Iteration ---</string>
<!-- Test Titles -->
<string name="play_test_title">Play Test</string>
<string name="pause_test_title">Pause Test</string>
<string name="stop_test_title">Stop Test</string>
<string name="play_search_test_title">Play From Search Test</string>
<!-- Test Descriptions -->
<string name="play_test_description">This tests the \'play\' functionality.</string>
<string name="pause_test_description">This tests the \'pause\' functionality.</string>
<string name="stop_test_description">This tests the \'stop\' functionality.</string>
<string name="play_search_test_description">This tests the \'play from search\' functionality. Enter the search query in the query field.</string>
</resources>