Skip to content

Commit

Permalink
Update custom action support to use CustomAction
Browse files Browse the repository at this point in the history
Support name and extras within CustomAction.
Updated example CustomAction favorite and custom icon for fast forward.
Updated platform interface version to 0.2.0.
  • Loading branch information
defsub committed Feb 15, 2023
1 parent 90aa658 commit 2060d0d
Show file tree
Hide file tree
Showing 10 changed files with 167 additions and 36 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ public class AudioService extends MediaBrowserServiceCompat {
public static final String NOTIFICATION_CLICK_ACTION = "com.ryanheise.audioservice.NOTIFICATION_CLICK";
public static final String CUSTOM_MEDIA_BUTTON_ACTION = "com.ryanheise.audioservice.CUSTOM_MEDIA_BUTTON";
public static final String EXTRA_ACTION_CODE = "actionCode";
public static final String EXTRA_CUSTOM_ACTION = "customAction";
public static final String EXTRA_CUSTOM_ACTION_NAME = "actionName";
public static final String EXTRA_CUSTOM_ACTION_EXTRAS = "actionExtras";
private static final String BROWSABLE_ROOT_ID = "root";
private static final String RECENT_ROOT_ID = "recent";
// See the comment in onMediaButtonEvent to understand how the BYPASS keycodes work.
Expand Down Expand Up @@ -437,7 +438,7 @@ NotificationCompat.Action createAction(String resource, String label, long actio
}

private boolean needCustomMediaControl(MediaControl control) {
if (control.customAction != null && control.customAction.length() > 0) {
if (control.customAction != null) {
return true;
}

Expand All @@ -457,11 +458,32 @@ private boolean needCustomMediaControl(MediaControl control) {
control.actionCode == PlaybackStateCompat.ACTION_REWIND));
}

private Bundle mapToBundle(Map<?, ?> map) {
Bundle bundle = new Bundle();
for (Map.Entry<?, ?> entry : map.entrySet()) {
String key = entry.getKey().toString();
Object value = entry.getValue();
if (value instanceof Integer) {
bundle.putInt(key, (Integer)value);
} else if (value instanceof Long) {
bundle.putLong(key, (Long)value);
} else {
bundle.putString(key, value.toString());
}
}
return bundle;
}

PlaybackStateCompat.CustomAction createCustomAction(MediaControl action) {
int iconId = getResourceId(action.icon);
Bundle extras = new Bundle();
extras.putLong(EXTRA_ACTION_CODE, action.actionCode);
extras.putString(EXTRA_CUSTOM_ACTION, action.customAction);
if (action.customAction != null) {
extras.putString(EXTRA_CUSTOM_ACTION_NAME, action.customAction.name);
if (action.customAction.extras != null) {
extras.putBundle(EXTRA_CUSTOM_ACTION_EXTRAS, mapToBundle(action.customAction.extras));
}
}
PlaybackStateCompat.CustomAction.Builder builder =
new PlaybackStateCompat.CustomAction.Builder(
CUSTOM_MEDIA_BUTTON_ACTION, action.label, iconId).setExtras(extras);
Expand Down Expand Up @@ -1071,9 +1093,13 @@ public void onCustomAction(String action, Bundle extras) {
if (listener == null) return;
if (CUSTOM_MEDIA_BUTTON_ACTION.equals(action)) {
long actionCode = extras.getLong(EXTRA_ACTION_CODE, -1);
String customAction = extras.getString(EXTRA_CUSTOM_ACTION);
if (customAction != null && customAction.length() > 0) {
listener.onCustomAction(customAction, Bundle.EMPTY);
String actionName = extras.getString(EXTRA_CUSTOM_ACTION_NAME);
if (actionName != null && actionName.length() > 0) {
Bundle actionExtras = extras.getBundle(EXTRA_CUSTOM_ACTION_EXTRAS);
if (actionExtras == null) {
actionExtras = Bundle.EMPTY;
}
listener.onCustomAction(actionName, actionExtras);
}
else if (actionCode == PlaybackStateCompat.ACTION_STOP) {
listener.onStop();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -869,8 +869,14 @@ public void onMethodCall(MethodCall call, Result result) {
String resource = (String)rawControl.get("androidIcon");
String label = (String)rawControl.get("label");
long actionCode = 1 << ((Integer)rawControl.get("action"));
String customAction = (String)rawControl.get("customAction");
actionBits |= actionCode;
Map<?, ?> customActionMap = (Map<?, ?>)rawControl.get("customAction");
CustomAction customAction = null;
if (customActionMap != null) {
String name = (String) customActionMap.get("name");
Map<?, ?> extras = (Map<?, ?>) customActionMap.get("extras");
customAction = new CustomAction(name, extras);
}
actions.add(new MediaControl(resource, label, actionCode, customAction));
}
for (Integer rawSystemAction : rawSystemActions) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.ryanheise.audioservice;

import java.util.Map;
import java.util.Objects;

public class CustomAction {
public final String name;
public final Map<?, ?> extras;

public CustomAction(String name, Map<?, ?> extras) {
this.name = name;
this.extras = extras;
}

@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
CustomAction that = (CustomAction) o;
return name.equals(that.name) && Objects.equals(extras, that.extras);
}

@Override
public int hashCode() {
return Objects.hash(name, extras);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.ryanheise.audioservice;

import java.util.Objects;

public class MediaControl {
public final String icon;
public final String label;
public final long actionCode;
public final String customAction;
public final CustomAction customAction;

public MediaControl(String icon, String label, long actionCode, String customAction) {
public MediaControl(String icon, String label, long actionCode, CustomAction customAction) {
this.icon = icon;
this.label = label;
this.actionCode = actionCode;
Expand All @@ -17,7 +19,7 @@ public MediaControl(String icon, String label, long actionCode, String customAct
public boolean equals(Object other) {
if (other instanceof MediaControl) {
MediaControl otherControl = (MediaControl)other;
return icon.equals(otherControl.icon) && label.equals(otherControl.label) && actionCode == otherControl.actionCode;
return icon.equals(otherControl.icon) && label.equals(otherControl.label) && actionCode == otherControl.actionCode && Objects.equals(customAction, otherControl.customAction);
} else {
return false;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<vector android:height="24dp" android:tint="#000000"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M18,13c0,3.31 -2.69,6 -6,6s-6,-2.69 -6,-6s2.69,-6 6,-6v4l5,-5l-5,-5v4c-4.42,0 -8,3.58 -8,8c0,4.42 3.58,8 8,8s8,-3.58 8,-8H18z"/>
<path android:fillColor="@android:color/white" android:pathData="M10.06,15.38c-0.29,0 -0.62,-0.17 -0.62,-0.54H8.59c0,0.97 0.9,1.23 1.45,1.23c0.87,0 1.51,-0.46 1.51,-1.25c0,-0.66 -0.45,-0.9 -0.71,-1c0.11,-0.05 0.65,-0.32 0.65,-0.92c0,-0.21 -0.05,-1.22 -1.44,-1.22c-0.62,0 -1.4,0.35 -1.4,1.16h0.85c0,-0.34 0.31,-0.48 0.57,-0.48c0.59,0 0.58,0.5 0.58,0.54c0,0.52 -0.41,0.59 -0.63,0.59H9.56v0.66h0.45c0.65,0 0.7,0.42 0.7,0.64C10.71,15.11 10.5,15.38 10.06,15.38z"/>
<path android:fillColor="@android:color/white" android:pathData="M13.85,11.68c-0.14,0 -1.44,-0.08 -1.44,1.82v0.74c0,1.9 1.31,1.82 1.44,1.82c0.14,0 1.44,0.09 1.44,-1.82V13.5C15.3,11.59 13.99,11.68 13.85,11.68zM14.45,14.35c0,0.77 -0.21,1.03 -0.59,1.03c-0.38,0 -0.6,-0.26 -0.6,-1.03v-0.97c0,-0.75 0.22,-1.01 0.59,-1.01c0.38,0 0.6,0.26 0.6,1.01V14.35z"/>
</vector>
34 changes: 18 additions & 16 deletions audio_service/example/lib/example_custom_action.dart
Original file line number Diff line number Diff line change
@@ -1,21 +1,14 @@
// ignore_for_file: public_member_api_docs

// FOR MORE EXAMPLES, VISIT THE GITHUB REPOSITORY AT:
// This example demonstrates:
//
// https://github.com/ryanheise/audio_service
//
// This example implements a minimal audio handler that renders the current
// media item and playback state to the system notification and responds to 4
// media actions:
//
// - play
// - pause
// - seek
// - stop
// - Android 13 fast forward, rewind, and stop working with custom actions
// - Fast forward interval of 30 seconds with custom icons
// - Android notification with custom "favorite" icon and custom handler.
//
// To run this example, use:
//
// flutter run
// flutter run -t lib/example_custom_action.dart

import 'dart:async';

Expand All @@ -37,6 +30,7 @@ Future<void> main() async {
androidNotificationChannelId: 'com.ryanheise.myapp.channel.audio',
androidNotificationChannelName: 'Audio playback',
androidNotificationOngoing: true,
fastForwardInterval: Duration(seconds: 30),
),
);
runApp(const MyApp());
Expand Down Expand Up @@ -92,7 +86,7 @@ class MainScreen extends StatelessWidget {
else
_button(Icons.play_arrow, _audioHandler.play),
_button(Icons.stop, _audioHandler.stop),
_button(Icons.fast_forward, _audioHandler.fastForward),
_button(Icons.forward_30, _audioHandler.fastForward),
],
);
},
Expand Down Expand Up @@ -198,7 +192,9 @@ class AudioPlayerHandler extends BaseAudioHandler with SeekHandler {
@override
Future<dynamic> customAction(String name, [Map<String, dynamic>? extras]) {
if (kDebugMode) {
print(name);
if (name == 'favoriteAction') {
print('Click favorite, level is ${extras?['level']}');
}
}
return super.customAction(name, extras);
}
Expand All @@ -214,9 +210,15 @@ class AudioPlayerHandler extends BaseAudioHandler with SeekHandler {
MediaControl.rewind,
if (_player.playing) MediaControl.pause else MediaControl.play,
MediaControl.stop,
MediaControl.fastForward,
// add fast forward with custom icon
const MediaControl(androidIcon: "drawable/baseline_forward_30_24",
label: "fast forward",
action: MediaAction.fastForward),
// add custom action with heart icon
MediaControl.custom(androidIcon: "drawable/ic_baseline_favorite_24",
label: "favorite", customAction: "favorite"),
label: "favorite",
name: "favoriteAction",
extras: <String, dynamic>{'level':1}),
],
systemActions: const {
MediaAction.seek,
Expand Down
54 changes: 47 additions & 7 deletions audio_service/lib/audio_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,42 @@ class _MediaItemCopyWith extends MediaItemCopyWith {
);
}

/// Custom action information used to define an action name and optional extras
/// that are sent to [AudioHandler.customAction] when the associated media control is used.
class CustomAction {
/// Custom action name
final String name;

/// A map of additional data for the custom action.
///
/// The values must be integers or strings.
final Map<String, dynamic>? extras;

/// Creates a [CustomAction].
CustomAction({required this.name, this.extras});

/// Convert to a Map.
Map<String, dynamic> toMap() => <String, dynamic>{
'name': name,
'extras': extras,
};

CustomActionMessage _toMessage() => CustomActionMessage(
name: name,
extras: extras,
);

@override
int get hashCode => Object.hash(name, extras);

@override
bool operator ==(Object other) =>
other.runtimeType == runtimeType &&
other is CustomAction &&
name == other.name &&
mapEquals<String, dynamic>(extras, other.extras);
}

/// A button to appear in the Android notification, lock screen, Android smart
/// watch, or Android Auto device. The set of buttons you would like to display
/// at any given moment should be streamed via [AudioHandler.playbackState].
Expand Down Expand Up @@ -846,14 +882,18 @@ class MediaControl {
/// The action to be executed by this control
final MediaAction action;

/// The action name to receive in [MediaAction.customAction]
final String? customAction;
/// The custom action name and optional extras to receive in
/// [AudioHandler.customAction]
final CustomAction? customAction;

/// Creates a custom [MediaControl].
MediaControl.custom({
required this.androidIcon,
required this.label,
required this.customAction}) : action = MediaAction.custom;
MediaControl.custom(
{required this.androidIcon,
required this.label,
required String name,
Map<String, dynamic>? extras})
: action = MediaAction.custom,
customAction = CustomAction(name: name, extras: extras);

/// Creates a custom [MediaControl].
const MediaControl({
Expand All @@ -867,7 +907,7 @@ class MediaControl {
androidIcon: androidIcon,
label: label,
action: MediaActionMessage.values[action.index],
customAction: customAction,
customAction: customAction?._toMessage(),
);

@override
Expand Down
4 changes: 4 additions & 0 deletions audio_service_platform_interface/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.2.0

* Add customAction to MediaControlMessage (@defsub)

## 0.1.0

* Remove unused androidEnableQueue option
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ class MediaControlMessage {

/// The action string used in [customAction] when the action occurs. This should
/// be used along with [MediaAction.custom].
final String? customAction;
final CustomActionMessage? customAction;

@literal
const MediaControlMessage({
Expand All @@ -292,10 +292,27 @@ class MediaControlMessage {
'androidIcon': androidIcon,
'label': label,
'action': action.index,
if (customAction != null) 'customAction': customAction
if (customAction != null) 'customAction': customAction?.toMap()
};
}

class CustomActionMessage {
/// Custom action name
final String name;

/// A map of additional data for the custom action.
///
/// The values must be integers or strings.
final Map<String, dynamic>? extras;

CustomActionMessage({required this.name, this.extras});

Map<String, dynamic> toMap() => <String, dynamic>{
'name': name,
'extras': extras,
};
}

/// The playback state which includes a [playing] boolean state, a processing
/// state such as [AudioProcessingStateMessage.buffering], the playback position and
/// the currently enabled actions to be shown in the Android notification or the
Expand Down
2 changes: 1 addition & 1 deletion audio_service_platform_interface/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ description: A common platform interface for the audio_service plugin. Different
homepage: https://github.com/ryanheise/audio_service/tree/master/audio_service_platform_interface
# NOTE: We strongly prefer non-breaking changes, even at the expense of a
# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes
version: 0.1.0
version: 0.2.0

dependencies:
flutter:
Expand Down

0 comments on commit 2060d0d

Please sign in to comment.