Publish the media3 session controller test app
Issue: androidx/media#78 PiperOrigin-RevId: 642277900
This commit is contained in:
parent
b763673903
commit
f54991eea1
@ -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,
|
||||||
|
@ -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
5
testapps/README.md
Normal 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.
|
4
testapps/controller/README.md
Normal file
4
testapps/controller/README.md
Normal 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.
|
70
testapps/controller/build.gradle
Normal file
70
testapps/controller/build.gradle
Normal 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')
|
||||||
|
}
|
20
testapps/controller/lint.xml
Normal file
20
testapps/controller/lint.xml
Normal 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>
|
2
testapps/controller/proguard-rules.txt
Normal file
2
testapps/controller/proguard-rules.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Proguard rules specific to the media3 controller test app.
|
||||||
|
|
76
testapps/controller/src/main/AndroidManifest.xml
Normal file
76
testapps/controller/src/main/AndroidManifest.xml
Normal 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>
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
@ -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"
|
||||||
|
)
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
1
testapps/controller/src/main/proguard-rules.txt
Symbolic link
1
testapps/controller/src/main/proguard-rules.txt
Symbolic link
@ -0,0 +1 @@
|
|||||||
|
../../proguard-rules.txt
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
26
testapps/controller/src/main/res/drawable/ic_test.xml
Normal file
26
testapps/controller/src/main/res/drawable/ic_test.xml
Normal 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>
|
26
testapps/controller/src/main/res/drawable/ic_test_suite.xml
Normal file
26
testapps/controller/src/main/res/drawable/ic_test_suite.xml
Normal 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>
|
@ -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>
|
@ -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>
|
37
testapps/controller/src/main/res/drawable/tab_indicator.xml
Normal file
37
testapps/controller/src/main/res/drawable/tab_indicator.xml
Normal 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>
|
@ -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>
|
57
testapps/controller/src/main/res/layout/activity_launch.xml
Normal file
57
testapps/controller/src/main/res/layout/activity_launch.xml
Normal 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>
|
@ -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>
|
||||||
|
|
76
testapps/controller/src/main/res/layout/media_app_item.xml
Normal file
76
testapps/controller/src/main/res/layout/media_app_item.xml
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
275
testapps/controller/src/main/res/layout/media_controls.xml
Normal file
275
testapps/controller/src/main/res/layout/media_controls.xml
Normal 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>
|
@ -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>
|
@ -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>
|
118
testapps/controller/src/main/res/layout/media_info.xml
Normal file
118
testapps/controller/src/main/res/layout/media_info.xml
Normal 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>
|
102
testapps/controller/src/main/res/layout/media_ratings.xml
Normal file
102
testapps/controller/src/main/res/layout/media_ratings.xml
Normal 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>
|
@ -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>
|
@ -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>
|
77
testapps/controller/src/main/res/layout/media_tests.xml
Normal file
77
testapps/controller/src/main/res/layout/media_tests.xml
Normal 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>
|
44
testapps/controller/src/main/res/layout/media_timeline.xml
Normal file
44
testapps/controller/src/main/res/layout/media_timeline.xml
Normal 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>
|
@ -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>
|
@ -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>
|
BIN
testapps/controller/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
BIN
testapps/controller/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
BIN
testapps/controller/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
BIN
testapps/controller/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
BIN
testapps/controller/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
BIN
testapps/controller/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.8 KiB |
BIN
testapps/controller/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
testapps/controller/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 7.3 KiB |
BIN
testapps/controller/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
BIN
testapps/controller/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 10 KiB |
36
testapps/controller/src/main/res/values/colors.xml
Normal file
36
testapps/controller/src/main/res/values/colors.xml
Normal 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>
|
45
testapps/controller/src/main/res/values/dimens.xml
Normal file
45
testapps/controller/src/main/res/values/dimens.xml
Normal 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>
|
41
testapps/controller/src/main/res/values/options.xml
Normal file
41
testapps/controller/src/main/res/values/options.xml
Normal 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>
|
105
testapps/controller/src/main/res/values/strings.xml
Normal file
105
testapps/controller/src/main/res/values/strings.xml
Normal 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>
|
59
testapps/controller/src/main/res/values/styles.xml
Normal file
59
testapps/controller/src/main/res/values/styles.xml
Normal 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>
|
56
testapps/controller/src/main/res/values/test_strings.xml
Normal file
56
testapps/controller/src/main/res/values/test_strings.xml
Normal 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>
|
Loading…
x
Reference in New Issue
Block a user