Skip to content

Commit

Permalink
Merge branch 'feature/custom_actions_android13' into minor
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanheise committed Aug 28, 2023
2 parents 01eb2d2 + 35aa6a5 commit aaa7911
Show file tree
Hide file tree
Showing 26 changed files with 581 additions and 76 deletions.
5 changes: 5 additions & 0 deletions audio_service/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.18.11

* Support custom media controls (@defsub)
* Support fast forward, rewind and stop when targeting Android 13 (@defsub)

## 0.18.10

* Add support for AGP 8 (@theskyblockman).
Expand Down
7 changes: 4 additions & 3 deletions audio_service/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
include: package:flutter_lints/flutter.yaml

analyzer:
strong-mode:
implicit-casts: false
implicit-dynamic: false
language:
strict-casts: true
strict-inference: true
strict-raw-types: true

linter:
rules:
Expand Down
1 change: 1 addition & 0 deletions audio_service/android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ android {
}
defaultConfig {
minSdkVersion 16
targetSdkVersion 33
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public class AudioService extends MediaBrowserServiceCompat {
private static final int NOTIFICATION_ID = 1124;
private static final int REQUEST_CONTENT_INTENT = 1000;
public static final String NOTIFICATION_CLICK_ACTION = "com.ryanheise.audioservice.NOTIFICATION_CLICK";
public static final String CUSTOM_ACTION_STOP = "com.ryanheise.audioservice.action.STOP";
public static final String CUSTOM_ACTION_FAST_FORWARD = "com.ryanheise.audioservice.action.FAST_FORWARD";
public static final String CUSTOM_ACTION_REWIND = "com.ryanheise.audioservice.action.REWIND";
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 @@ -265,8 +268,9 @@ private static int calculateInSampleSize(BitmapFactory.Options options, int reqW
private PowerManager.WakeLock wakeLock;
private MediaSessionCompat mediaSession;
private MediaSessionCallback mediaSessionCallback;
private List<MediaControl> actions = new ArrayList<>();
private List<MediaControl> controls = new ArrayList<>();
private List<NotificationCompat.Action> nativeActions = new ArrayList<>();
private List<PlaybackStateCompat.CustomAction> customActions = new ArrayList<>();
private int[] compactActionIndices;
private MediaMetadataCompat mediaMetadata;
private Bitmap artBitmap;
Expand Down Expand Up @@ -363,7 +367,7 @@ public void onDestroy() {
artBitmap = null;
queue.clear();
mediaMetadataCache.clear();
actions.clear();
controls.clear();
artBitmapCache.evictAll();
compactActionIndices = null;
releaseMediaSession();
Expand Down Expand Up @@ -432,6 +436,57 @@ NotificationCompat.Action createAction(String resource, String label, long actio
buildMediaButtonPendingIntent(actionCode));
}

private boolean needCustomMediaControl(MediaControl control) {
return control.customAction != null;
}

private Bundle mapToBundle(Map<?, ?> map) {
if (map == null) {
return null;
}
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 control) {
int iconId = getResourceId(control.icon);
if (control.customAction != null) {
return new PlaybackStateCompat.CustomAction.Builder(control.customAction.name, control.label, iconId)
.setExtras(mapToBundle(control.customAction.extras))
.build();
} else if (Build.VERSION.SDK_INT >= 33) {
// Android 13 changes MediaControl behavior as documented here:
// https://developer.android.com/about/versions/13/behavior-changes-13
// The below actions will be added to slots 1-3, if included.
// 1 - ACTION_PLAY, ACTION_PLAY
// 2 - ACTION_SKIP_TO_PREVIOUS
// 3 - ACTION_SKIP_TO_NEXT
// Custom actions will use slots 2-5 if included.
// - ACTION_STOP
// - ACTION_FAST_FORWARD
// - ACTION_REWIND
if (control.actionCode == PlaybackStateCompat.ACTION_STOP) {
return new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_STOP, control.label, iconId).build();
} else if (control.actionCode == PlaybackStateCompat.ACTION_FAST_FORWARD) {
return new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_FAST_FORWARD, control.label, iconId).build();
} else if (control.actionCode == PlaybackStateCompat.ACTION_REWIND) {
return new PlaybackStateCompat.CustomAction.Builder(CUSTOM_ACTION_REWIND, control.label, iconId).build();
}
}
return null;
}

PendingIntent buildMediaButtonPendingIntent(long action) {
int keyCode = toKeyCode(action);
if (keyCode == KeyEvent.KEYCODE_UNKNOWN)
Expand All @@ -456,18 +511,24 @@ PendingIntent buildDeletePendingIntent() {
return PendingIntent.getBroadcast(this, 0, intent, flags);
}

void setState(List<MediaControl> actions, long actionBits, int[] compactActionIndices, AudioProcessingState processingState, boolean playing, long position, long bufferedPosition, float speed, long updateTime, Integer errorCode, String errorMessage, int repeatMode, int shuffleMode, boolean captioningEnabled, Long queueIndex) {
void setState(List<MediaControl> controls, long actionBits, int[] compactActionIndices, AudioProcessingState processingState, boolean playing, long position, long bufferedPosition, float speed, long updateTime, Integer errorCode, String errorMessage, int repeatMode, int shuffleMode, boolean captioningEnabled, Long queueIndex) {
boolean notificationChanged = false;
if (!Arrays.equals(compactActionIndices, this.compactActionIndices)) {
notificationChanged = true;
}
if (!actions.equals(this.actions)) {
if (!controls.equals(this.controls)) {
notificationChanged = true;
}
this.actions = actions;
this.controls = controls;
this.nativeActions.clear();
for (MediaControl action : actions) {
nativeActions.add(createAction(action.icon, action.label, action.actionCode));
this.customActions.clear();
for (MediaControl control : controls) {
final PlaybackStateCompat.CustomAction customAction = createCustomAction(control);
if (customAction != null) {
customActions.add(customAction);
} else {
nativeActions.add(createAction(control.icon, control.label, control.actionCode));
}
}
this.compactActionIndices = compactActionIndices;
boolean wasPlaying = this.playing;
Expand All @@ -481,6 +542,11 @@ void setState(List<MediaControl> actions, long actionBits, int[] compactActionIn
.setActions(AUTO_ENABLED_ACTIONS | actionBits)
.setState(getPlaybackState(), position, speed, updateTime)
.setBufferedPosition(bufferedPosition);

for (PlaybackStateCompat.CustomAction action : this.customActions) {
stateBuilder.addCustomAction(action);
}

if (queueIndex != null)
stateBuilder.setActiveQueueItemId(queueIndex);
if (errorCode != null && errorMessage != null)
Expand Down Expand Up @@ -558,7 +624,7 @@ public int getPlaybackState() {
private Notification buildNotification() {
int[] compactActionIndices = this.compactActionIndices;
if (compactActionIndices == null) {
compactActionIndices = new int[Math.min(MAX_COMPACT_ACTIONS, actions.size())];
compactActionIndices = new int[Math.min(MAX_COMPACT_ACTIONS, nativeActions.size())];
for (int i = 0; i < compactActionIndices.length; i++) compactActionIndices[i] = i;
}
NotificationCompat.Builder builder = getNotificationBuilder();
Expand Down Expand Up @@ -1023,7 +1089,15 @@ public void onSetShuffleMode(int shuffleMode) {
@Override
public void onCustomAction(String action, Bundle extras) {
if (listener == null) return;
listener.onCustomAction(action, extras);
if (CUSTOM_ACTION_STOP.equals(action)) {
listener.onStop();
} else if (CUSTOM_ACTION_FAST_FORWARD.equals(action)) {
listener.onFastForward();
} else if (CUSTOM_ACTION_REWIND.equals(action)) {
listener.onRewind();
} else {
listener.onCustomAction(action, extras);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
Expand Down Expand Up @@ -870,7 +871,14 @@ public void onMethodCall(MethodCall call, Result result) {
String label = (String)rawControl.get("label");
long actionCode = 1 << ((Integer)rawControl.get("action"));
actionBits |= actionCode;
actions.add(new MediaControl(resource, label, actionCode));
Map<?, ?> customActionMap = (Map<?, ?>)rawControl.get("customAction");
CustomMediaAction customAction = null;
if (customActionMap != null) {
String name = (String) customActionMap.get("name");
Map<?, ?> extras = (Map<?, ?>) customActionMap.get("extras");
customAction = new CustomMediaAction(name, extras);
}
actions.add(new MediaControl(resource, label, actionCode, customAction));
}
for (Integer rawSystemAction : rawSystemActions) {
long actionCode = 1 << rawSystemAction;
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 CustomMediaAction {
public final String name;
public final Map<?, ?> extras;

public CustomMediaAction(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;
CustomMediaAction that = (CustomMediaAction) 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,21 +1,25 @@
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 CustomMediaAction customAction;

public MediaControl(String icon, String label, long actionCode) {
public MediaControl(String icon, String label, long actionCode, CustomMediaAction customAction) {
this.icon = icon;
this.label = label;
this.actionCode = actionCode;
}
this.customAction = customAction;
}

@Override
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
2 changes: 1 addition & 1 deletion audio_service/example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ android {
defaultConfig {
applicationId "com.ryanheise.audioserviceexample"
minSdkVersion 21
targetSdkVersion 31
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
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>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<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="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>
2 changes: 1 addition & 1 deletion audio_service/example/lib/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ class _SeekBarState extends State<SeekBar> {
.firstMatch("$_remaining")
?.group(1) ??
'$_remaining',
style: Theme.of(context).textTheme.caption),
style: Theme.of(context).textTheme.bodySmall),
),
],
);
Expand Down
Loading

0 comments on commit aaa7911

Please sign in to comment.