react-native-media-notification 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/LICENSE +20 -0
  2. package/MediaControls.podspec +30 -0
  3. package/README.md +237 -0
  4. package/android/build.gradle +89 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +29 -0
  7. package/android/src/main/java/com/mediacontrols/AudioFocusListener.kt +79 -0
  8. package/android/src/main/java/com/mediacontrols/Controls.kt +22 -0
  9. package/android/src/main/java/com/mediacontrols/CustomCommandButton.kt +72 -0
  10. package/android/src/main/java/com/mediacontrols/MediaControlsModule.kt +188 -0
  11. package/android/src/main/java/com/mediacontrols/MediaControlsPackage.kt +36 -0
  12. package/android/src/main/java/com/mediacontrols/MediaControlsPlayer.kt +321 -0
  13. package/android/src/main/java/com/mediacontrols/MediaControlsService.kt +233 -0
  14. package/android/src/main/java/com/mediacontrols/MediaNotificationProvider.kt +74 -0
  15. package/ios/MediaControls.h +5 -0
  16. package/ios/MediaControls.mm +300 -0
  17. package/lib/module/NativeMediaControls.js +7 -0
  18. package/lib/module/NativeMediaControls.js.map +1 -0
  19. package/lib/module/index.js +75 -0
  20. package/lib/module/index.js.map +1 -0
  21. package/lib/module/package.json +1 -0
  22. package/lib/typescript/package.json +1 -0
  23. package/lib/typescript/src/NativeMediaControls.d.ts +31 -0
  24. package/lib/typescript/src/NativeMediaControls.d.ts.map +1 -0
  25. package/lib/typescript/src/index.d.ts +34 -0
  26. package/lib/typescript/src/index.d.ts.map +1 -0
  27. package/package.json +169 -0
  28. package/src/NativeMediaControls.ts +54 -0
  29. package/src/index.tsx +87 -0
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Marius Butz
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ of this software and associated documentation files (the "Software"), to deal
6
+ in the Software without restriction, including without limitation the rights
7
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the Software is
9
+ furnished to do so, subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20
+ SOFTWARE.
@@ -0,0 +1,30 @@
1
+ require "json"
2
+
3
+ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
+
5
+ Pod::Spec.new do |s|
6
+ s.name = "MediaControls"
7
+ s.version = package["version"]
8
+ s.summary = package["description"]
9
+ s.homepage = package["homepage"]
10
+ s.license = package["license"]
11
+ s.authors = package["author"]
12
+
13
+ s.platforms = { :ios => min_ios_version_supported }
14
+ s.source = { :git => "https://github.com/mbpictures/react-native-media-notification.git", :tag => "#{s.version}" }
15
+
16
+ s.source_files = "ios/**/*.{h,m,mm,swift}"
17
+ s.private_header_files = "ios/**/*.h"
18
+
19
+
20
+ install_modules_dependencies(s)
21
+
22
+ # iOS frameworks
23
+ s.frameworks = 'MediaPlayer', 'AVFoundation'
24
+
25
+ s.pod_target_xcconfig = {
26
+ 'DEFINES_MODULE' => 'YES'
27
+ }
28
+
29
+ install_modules_dependencies(s)
30
+ end
package/README.md ADDED
@@ -0,0 +1,237 @@
1
+ <h1 align="center">
2
+ Welcome to react-native-media-notification👋<br />
3
+ </h1>
4
+ <h3 align="center">
5
+ Your React Native library for media notifications and controls
6
+ </h3>
7
+ <p align="center">
8
+ <a href="LICENSE" target="_blank">
9
+ <img alt="License: MIT" src="https://img.shields.io/badge/License-MIT-green.svg?style=for-the-badge" />
10
+ </a>
11
+ <img alt="Build Status" src="https://img.shields.io/github/actions/workflow/status/mbpictures/react-native-media-notification/ci.yml?style=for-the-badge" />
12
+ <a href="https://badge.fury.io/js/react-native-media-notification">
13
+ <img src="https://img.shields.io/npm/v/react-native-media-notification?style=for-the-badge" alt="npm version">
14
+ </a>
15
+ </p>
16
+
17
+ > A react native package for media notifications and controls, using AndroidX Media3 for Android and the ControlCenter API for iOS.
18
+
19
+ ## Features
20
+
21
+ - 🎵 Media Notifications with Play/Pause/Stop Controls
22
+ - ⏭️ Skip Forward/Backward Support
23
+ - 🎨 Album Artwork Support (URL-based)
24
+ - 🔊 Audio Interruption Handling
25
+ - 📱 iOS Control Center Integration
26
+ - 🤖 Android Media3 Session Support
27
+ - 🎯 TypeScript Support
28
+ - ⚡ New Architecture (Turbo Modules) Ready
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ npm install react-native-media-notification
34
+ ```
35
+
36
+ ```bash
37
+ yarn add react-native-media-notification
38
+ ```
39
+
40
+ ### iOS
41
+
42
+ ```bash
43
+ cd ios && pod install
44
+ ```
45
+
46
+ ### Android
47
+
48
+ #### Android Auto
49
+ To enable Android Auto support, you need to add the following in the application tag of your `android/app/src/main/AndroidManifest.xml`:
50
+
51
+ ```xml
52
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android"
53
+ package="com.yourapp">
54
+ <!--...-->
55
+ <application>
56
+ <!--...-->
57
+ <meta-data
58
+ android:name="com.google.android.gms.car.application"
59
+ android:resource="@xml/automotive_app_desc" />
60
+ </application>
61
+ </manifest>
62
+ ```
63
+
64
+ And create the file `android/app/src/main/res/xml/automotive_app_desc.xml` with the following content:
65
+
66
+ ```xml
67
+ <automotiveApp>
68
+ <uses name="media"/>
69
+ </automotiveApp>
70
+ ```
71
+
72
+ #### Customize Appearance
73
+
74
+ If you want to customize the small media notification icon on Android, you can add the following to your `android/app/src/main/res/values/styles.xml`:
75
+
76
+ ```xml
77
+ <resources>
78
+ <!--...-->
79
+ <drawable name="media3_notification_small_icon">@drawable/my_custom_icon</drawable>
80
+ </resources>
81
+ ```
82
+
83
+ ## Verwendung
84
+
85
+ ### Basic Setup
86
+
87
+ ```typescript
88
+ import * as MediaControls from 'react-native-media-notification';
89
+
90
+ // register event listeners
91
+ const removePlayListener = MediaControls.addEventListener('play', () => {
92
+ console.log('Play button pressed');
93
+ });
94
+
95
+ const removePauseListener = MediaControls.addEventListener('pause', () => {
96
+ console.log('Pause button pressed');
97
+ });
98
+
99
+ // Cleanup
100
+ useEffect(() => {
101
+ return () => {
102
+ removePlayListener.remove();
103
+ removePauseListener.remove();
104
+ };
105
+ }, []);
106
+ ```
107
+
108
+ ### Update Metadata or Create Media Notification
109
+
110
+ ```typescript
111
+ await MediaControls.updateMetadata({
112
+ title: 'Song Title',
113
+ artist: 'Artist Name',
114
+ album: 'Album Name',
115
+ duration: 240, // in seconds
116
+ position: 30, // current position in seconds
117
+ isPlaying: true,
118
+ artwork: 'https://example.com/artwork.jpg',
119
+ shuffle: false, // optional, default is false
120
+ repeat: 'one', // optional, default is 'off', can be 'all', 'one', or 'off'
121
+ });
122
+ ```
123
+
124
+ ### All available Events
125
+
126
+ ```typescript
127
+ // Playback Controls
128
+ MediaControls.addEventListener('play', () => {});
129
+ MediaControls.addEventListener('pause', () => {});
130
+ MediaControls.addEventListener('stop', () => {});
131
+ MediaControls.addEventListener('shuffle', () => {});
132
+ MediaControls.addEventListener('repeatMode', () => {});
133
+
134
+ // Navigation
135
+ MediaControls.addEventListener('skipToNext', () => {});
136
+ MediaControls.addEventListener('skipToPrevious', () => {});
137
+
138
+ // Seeking
139
+ MediaControls.addEventListener('seekForward', () => {});
140
+ MediaControls.addEventListener('seekBackward', () => {});
141
+ MediaControls.addEventListener('seek', (data) => {
142
+ console.log('Seek to position:', data?.position);
143
+ });
144
+
145
+ // Interruptions
146
+ MediaControls.addEventListener('duck', () => {}); // reduce volume for interruption
147
+ MediaControls.addEventListener('unduck', () => {}); // restore volume after interruption
148
+ ```
149
+
150
+ ### Stop Media Notification
151
+
152
+ ```typescript
153
+ await MediaControls.stopMediaNotification();
154
+ ```
155
+
156
+ ## API Reference
157
+
158
+ ### Functions
159
+
160
+ #### `updateMetadata(metadata: MediaTrackMetadata): Promise<void>`
161
+
162
+ Updates the media track metadata for the notification. When called for the first time, it creates the media notification.
163
+
164
+ **Parameter:**
165
+ - `metadata`: Object with media track information, including title, artist, album, duration, artwork URL, current position, and playback state.
166
+
167
+ #### `stopMediaNotification(): Promise<void>`
168
+
169
+ Stops media notification and removes audio focus (if enabled)
170
+
171
+ #### `enableAudioInterruption(enabled: boolean): Promise<void>`
172
+
173
+ Enable or disable audio interruption handling. When enabled, the media controls will respond to audio interruptions (like incoming calls) by pausing playback and resuming when the interruption ends.
174
+
175
+ #### `enableBackgroundMode(enabled: boolean): Promise<void>`
176
+
177
+ **iOS ONLY**. Enable or disable background mode for iOS.
178
+
179
+ #### `addEventListener(event: MediaControlEvent, handler: Function): EventSubscription`
180
+
181
+ Registers an event listener for a specific media control event. Returns a function to remove the listener.
182
+
183
+ #### `removeAllListeners(event?: MediaControlEvent): void`
184
+
185
+ Entfernt alle Event Listener für ein bestimmtes Event oder alle Events, wenn kein Event angegeben ist.
186
+
187
+ ### Types
188
+
189
+ ```typescript
190
+ interface MediaTrackMetadata {
191
+ title: string;
192
+ artist: string;
193
+ album?: string;
194
+ duration?: number; // in seconds
195
+ artwork?: string; // URL for album artwork
196
+ position?: number; // current position in seconds
197
+ isPlaying?: boolean;
198
+ shuffle?: boolean; // optional, default is false
199
+ repeat?: 'off' | 'all' | 'one'; // optional, default is
200
+ }
201
+
202
+ type MediaControlEvent =
203
+ | 'play'
204
+ | 'pause'
205
+ | 'stop'
206
+ | 'skipToNext'
207
+ | 'skipToPrevious'
208
+ | 'seekForward'
209
+ | 'seekBackward'
210
+ | 'seek';
211
+
212
+ type MediaControlEventData = {
213
+ position?: number; // for seek events
214
+ };
215
+ ```
216
+
217
+ ## Platform specific notes
218
+
219
+ ### Android
220
+
221
+ - Uses AndroidX Media3
222
+ - Creates notification channel automatically
223
+
224
+ ### iOS
225
+
226
+ - Uses MPNowPlayingInfoCenter and MPRemoteCommandCenter
227
+ - Integrates in Control Center and Lock Screen
228
+ - Supports ear bud and other external controls
229
+ - Requires Background Audio Capability for Background Playback
230
+
231
+ ## License
232
+
233
+ MIT
234
+
235
+ ## Contributing
236
+
237
+ Contributions are welcome! Please open an issue or submit a pull request.
@@ -0,0 +1,89 @@
1
+ buildscript {
2
+ ext.getExtOrDefault = {name ->
3
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties['MediaControls_' + name]
4
+ }
5
+
6
+ repositories {
7
+ google()
8
+ mavenCentral()
9
+ }
10
+
11
+ dependencies {
12
+ classpath "com.android.tools.build:gradle:8.7.2"
13
+ // noinspection DifferentKotlinGradleVersion
14
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:${getExtOrDefault('kotlinVersion')}"
15
+ }
16
+ }
17
+
18
+
19
+ apply plugin: "com.android.library"
20
+ apply plugin: "kotlin-android"
21
+
22
+ apply plugin: "com.facebook.react"
23
+
24
+ def getExtOrIntegerDefault(name) {
25
+ return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["MediaControls_" + name]).toInteger()
26
+ }
27
+
28
+ android {
29
+ namespace "com.mediacontrols"
30
+
31
+ compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
32
+
33
+ defaultConfig {
34
+ minSdkVersion getExtOrIntegerDefault("minSdkVersion")
35
+ targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
36
+ }
37
+
38
+ buildFeatures {
39
+ buildConfig true
40
+ }
41
+
42
+ buildTypes {
43
+ release {
44
+ minifyEnabled false
45
+ }
46
+ }
47
+
48
+ lintOptions {
49
+ disable "GradleCompatible"
50
+ }
51
+
52
+ compileOptions {
53
+ sourceCompatibility JavaVersion.VERSION_1_8
54
+ targetCompatibility JavaVersion.VERSION_1_8
55
+ }
56
+
57
+ sourceSets {
58
+ main {
59
+ java.srcDirs += [
60
+ "generated/java",
61
+ "generated/jni"
62
+ ]
63
+ }
64
+ }
65
+ }
66
+
67
+ repositories {
68
+ mavenCentral()
69
+ google()
70
+ }
71
+
72
+ def kotlin_version = getExtOrDefault("kotlinVersion")
73
+
74
+ dependencies {
75
+ implementation "com.facebook.react:react-android"
76
+ implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
77
+
78
+ // androidx Media3 dependencies
79
+ implementation "androidx.media3:media3-session:1.8.0"
80
+ implementation "androidx.media3:media3-common:1.8.0"
81
+ implementation "androidx.media3:media3-ui:1.8.0"
82
+ implementation "androidx.core:core-ktx:1.16.0"
83
+ }
84
+
85
+ react {
86
+ jsRootDir = file("../src/")
87
+ libraryName = "MediaControls"
88
+ codegenJavaPackageName = "com.mediacontrols"
89
+ }
@@ -0,0 +1,5 @@
1
+ MediaControls_kotlinVersion=2.0.21
2
+ MediaControls_minSdkVersion=24
3
+ MediaControls_targetSdkVersion=34
4
+ MediaControls_compileSdkVersion=35
5
+ MediaControls_ndkVersion=27.1.12297006
@@ -0,0 +1,29 @@
1
+ <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+
3
+ <!-- Permissions for media playback and notifications -->
4
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
5
+ <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
6
+ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
7
+
8
+ <application>
9
+ <!-- MediaSessionService for background playback -->
10
+ <service
11
+ android:name=".MediaControlsService"
12
+ android:enabled="true"
13
+ android:exported="true"
14
+ android:foregroundServiceType="mediaPlayback">
15
+ <intent-filter>
16
+ <action android:name="androidx.media3.session.MediaSessionService" />
17
+ </intent-filter>
18
+ <!-- Android Auto discovery -->
19
+ <intent-filter>
20
+ <action android:name="android.media.browse.MediaBrowserService" />
21
+ </intent-filter>
22
+ </service>
23
+
24
+ <meta-data
25
+ android:name="com.samsung.android.support.ongoing_activity"
26
+ android:value="true" />
27
+ </application>
28
+
29
+ </manifest>
@@ -0,0 +1,79 @@
1
+ package com.mediacontrols
2
+
3
+ import android.content.Context
4
+ import android.media.AudioFocusRequest
5
+ import android.media.AudioManager
6
+ import android.media.AudioManager.OnAudioFocusChangeListener
7
+ import android.os.Build
8
+ import android.os.Handler
9
+ import androidx.media3.common.util.UnstableApi
10
+ import com.facebook.react.bridge.ReactApplicationContext
11
+
12
+ @UnstableApi
13
+ class AudioFocusListener(
14
+ context: ReactApplicationContext,
15
+ private val module: MediaControlsModule,
16
+ private val player: MediaControlsPlayer
17
+ ) : OnAudioFocusChangeListener {
18
+
19
+ private val mAudioManager: AudioManager =
20
+ context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
21
+ private var mFocusRequest: AudioFocusRequest? = null
22
+
23
+ private var mPlayOnAudioFocus = false
24
+ private var ducked = false
25
+
26
+ private var hasFocus = false
27
+
28
+ override fun onAudioFocusChange(focusChange: Int) {
29
+ if (focusChange == AudioManager.AUDIOFOCUS_LOSS) {
30
+ abandonAudioFocus()
31
+ mPlayOnAudioFocus = false
32
+ module.sendEvent(Controls.STOP, null)
33
+ } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT) {
34
+ Handler(player.applicationLooper).post {
35
+ if (player.isPlaying) {
36
+ mPlayOnAudioFocus = true
37
+ module.sendEvent(Controls.PAUSE, null)
38
+ }
39
+ }
40
+ } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
41
+ module.sendEvent(Controls.DUCK, null)
42
+ ducked = true
43
+ } else if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
44
+ if (mPlayOnAudioFocus) {
45
+ module.sendEvent(Controls.PLAY, null)
46
+ }
47
+ if (ducked) {
48
+ module.sendEvent(Controls.UN_DUCK, null)
49
+ }
50
+ mPlayOnAudioFocus = false
51
+ ducked = false
52
+ }
53
+ }
54
+
55
+ fun requestAudioFocus() {
56
+ if (hasFocus) return
57
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
58
+ mFocusRequest = AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN)
59
+ .setOnAudioFocusChangeListener(this).build()
60
+
61
+ hasFocus = mAudioManager.requestAudioFocus(mFocusRequest!!) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
62
+ } else {
63
+ hasFocus = mAudioManager.requestAudioFocus(
64
+ this,
65
+ AudioManager.STREAM_MUSIC,
66
+ AudioManager.AUDIOFOCUS_GAIN
67
+ ) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED
68
+ }
69
+ }
70
+
71
+ fun abandonAudioFocus() {
72
+ if (!hasFocus) return
73
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && mFocusRequest != null) {
74
+ mAudioManager.abandonAudioFocusRequest(mFocusRequest!!)
75
+ } else mAudioManager.abandonAudioFocus(this)
76
+
77
+ hasFocus = false
78
+ }
79
+ }
@@ -0,0 +1,22 @@
1
+ package com.mediacontrols
2
+
3
+ enum class Controls(val code: String) {
4
+ PLAY("play"),
5
+ PAUSE("pause"),
6
+ STOP("stop"),
7
+ NEXT("skipToNext"),
8
+ PREVIOUS("skipToPrevious"),
9
+ SEEK("seek"),
10
+ SEEK_BACKWARD("seekBackward"),
11
+ SEEK_FORWARD("seekForward"),
12
+ DUCK("duck"),
13
+ UN_DUCK("unDuck"),
14
+ SHUFFLE("shuffle"),
15
+ REPEAT_MODE("repeatMode");
16
+
17
+ companion object {
18
+ fun fromString(value: String): Controls? {
19
+ return entries.find { it.code == value }
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,72 @@
1
+ package com.mediacontrols
2
+
3
+ import android.os.Bundle
4
+ import androidx.media3.session.CommandButton
5
+ import androidx.media3.session.CommandButton.ICON_FAST_FORWARD
6
+ import androidx.media3.session.CommandButton.ICON_REWIND
7
+ import androidx.media3.session.CommandButton.ICON_SHUFFLE_OFF
8
+ import androidx.media3.session.CommandButton.ICON_SHUFFLE_ON
9
+ import androidx.media3.session.SessionCommand
10
+
11
+ private const val CUSTOM_COMMAND_REWIND_ACTION_ID = "REWIND_15"
12
+ private const val CUSTOM_COMMAND_FORWARD_ACTION_ID = "FAST_FWD_15"
13
+ private const val CUSTOM_COMMAND_SHUFFLE_ON_ACTION_ID = "SHUFFLE_ON"
14
+ private const val CUSTOM_COMMAND_SHUFFLE_OFF_ACTION_ID = "SHUFFLE_OFF"
15
+ private const val CUSTOM_COMMAND_REPEAT_ONE_ACTION_ID = "REPEAT_ONE"
16
+ private const val CUSTOM_COMMAND_REPEAT_ALL_ACTION_ID = "REPEAT_ALL"
17
+ private const val CUSTOM_COMMAND_REPEAT_OFF_ACTION_ID = "REPEAT_OFF"
18
+
19
+ enum class CustomCommandButton(
20
+ val customAction: String,
21
+ val commandButton: CommandButton,
22
+ ) {
23
+ REWIND(
24
+ customAction = CUSTOM_COMMAND_REWIND_ACTION_ID,
25
+ commandButton = CommandButton.Builder(ICON_REWIND)
26
+ .setDisplayName("Rewind")
27
+ .setSessionCommand(SessionCommand(CUSTOM_COMMAND_REWIND_ACTION_ID, Bundle()))
28
+ .build(),
29
+ ),
30
+ FORWARD(
31
+ customAction = CUSTOM_COMMAND_FORWARD_ACTION_ID,
32
+ commandButton = CommandButton.Builder(ICON_FAST_FORWARD)
33
+ .setDisplayName("Forward")
34
+ .setSessionCommand(SessionCommand(CUSTOM_COMMAND_FORWARD_ACTION_ID, Bundle()))
35
+ .build(),
36
+ ),
37
+ SHUFFLE_ON(
38
+ customAction = CUSTOM_COMMAND_SHUFFLE_ON_ACTION_ID,
39
+ commandButton = CommandButton.Builder(ICON_SHUFFLE_ON)
40
+ .setDisplayName("ShuffleOn")
41
+ .setSessionCommand(SessionCommand(CUSTOM_COMMAND_SHUFFLE_ON_ACTION_ID, Bundle()))
42
+ .build(),
43
+ ),
44
+ SHUFFLE_OFF(
45
+ customAction = CUSTOM_COMMAND_SHUFFLE_OFF_ACTION_ID,
46
+ commandButton = CommandButton.Builder(ICON_SHUFFLE_OFF)
47
+ .setDisplayName("ShuffleOn")
48
+ .setSessionCommand(SessionCommand(CUSTOM_COMMAND_SHUFFLE_OFF_ACTION_ID, Bundle()))
49
+ .build(),
50
+ ),
51
+ REPEAT_ONE(
52
+ customAction = CUSTOM_COMMAND_REPEAT_ONE_ACTION_ID,
53
+ commandButton = CommandButton.Builder(CommandButton.ICON_REPEAT_ONE)
54
+ .setDisplayName("Repeat One")
55
+ .setSessionCommand(SessionCommand(CUSTOM_COMMAND_REPEAT_ONE_ACTION_ID, Bundle()))
56
+ .build(),
57
+ ),
58
+ REPEAT_ALL(
59
+ customAction = CUSTOM_COMMAND_REPEAT_ALL_ACTION_ID,
60
+ commandButton = CommandButton.Builder(CommandButton.ICON_REPEAT_ALL)
61
+ .setDisplayName("Repeat All")
62
+ .setSessionCommand(SessionCommand(CUSTOM_COMMAND_REPEAT_ALL_ACTION_ID, Bundle()))
63
+ .build(),
64
+ ),
65
+ REPEAT_OFF(
66
+ customAction = CUSTOM_COMMAND_REPEAT_OFF_ACTION_ID,
67
+ commandButton = CommandButton.Builder(CommandButton.ICON_REPEAT_OFF)
68
+ .setDisplayName("Repeat Off")
69
+ .setSessionCommand(SessionCommand(CUSTOM_COMMAND_REPEAT_OFF_ACTION_ID, Bundle()))
70
+ .build(),
71
+ ),;
72
+ }