feat(player): add Mornye media notification controls

Sync notification favorites with Library, add Mornye transport icons and an output action on legacy Android, and expose the iOS favorite command. Preserve playback position when controls refresh.

Vendor audio_service 0.18.19 with a small Android patch for custom notification actions. Modern Android keeps its system output switcher and transport layout.
This commit is contained in:
zarzet committed 2026-09-26 03:07:38 +07:00
1 parent 728d179d69
commit 5259c86365
80 files changed
+8973 -13

No files matched your search

@@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>
@@ -0,0 +1,10 @@
package com.ryanheise.audioservice;
public enum AudioProcessingState {
idle,
loading,
buffering,
ready,
completed,
error,
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,15 @@
package com.ryanheise.audioservice;
import android.content.Context;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
public class AudioServiceActivity extends FlutterActivity {
@Override
public FlutterEngine provideFlutterEngine(@NonNull Context context) {
return AudioServicePlugin.getFlutterEngine(context);
}
}
@@ -0,0 +1,120 @@
package com.ryanheise.audioservice;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import java.util.Iterator;
import java.util.Map;
import org.json.JSONObject;
public class AudioServiceConfig {
private static final String SHARED_PREFERENCES_NAME = "audio_service_preferences";
private static final String KEY_ANDROID_RESUME_ON_CLICK = "androidResumeOnClick";
private static final String KEY_ANDROID_NOTIFICATION_CHANNEL_ID = "androidNotificationChannelId";
private static final String KEY_ANDROID_NOTIFICATION_CHANNEL_NAME = "androidNotificationChannelName";
private static final String KEY_ANDROID_NOTIFICATION_CHANNEL_DESCRIPTION = "androidNotificationChannelDescription";
private static final String KEY_NOTIFICATION_COLOR = "notificationColor";
private static final String KEY_ANDROID_NOTIFICATION_ICON = "androidNotificationIcon";
private static final String KEY_ANDROID_SHOW_NOTIFICATION_BADGE = "androidShowNotificationBadge";
private static final String KEY_ANDROID_NOTIFICATION_CLICK_STARTS_ACTIVITY = "androidNotificationClickStartsActivity";
private static final String KEY_ANDROID_NOTIFICATION_ONGOING = "androidNotificationOngoing";
private static final String KEY_ANDROID_STOP_FOREGROUND_ON_PAUSE = "androidStopForegroundOnPause";
private static final String KEY_ART_DOWNSCALE_WIDTH = "artDownscaleWidth";
private static final String KEY_ART_DOWNSCALE_HEIGHT = "artDownscaleHeight";
private static final String KEY_ACTIVITY_CLASS_NAME = "activityClassName";
private static final String KEY_BROWSABLE_ROOT_EXTRAS = "androidBrowsableRootExtras";
private SharedPreferences preferences;
public boolean androidResumeOnClick;
public String androidNotificationChannelId;
public String androidNotificationChannelName;
public String androidNotificationChannelDescription;
public int notificationColor;
public String androidNotificationIcon;
public boolean androidShowNotificationBadge;
public boolean androidNotificationClickStartsActivity;
public boolean androidNotificationOngoing;
public boolean androidStopForegroundOnPause;
public int artDownscaleWidth;
public int artDownscaleHeight;
public String activityClassName;
public String browsableRootExtras;
public AudioServiceConfig(Context context) {
preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
androidResumeOnClick = preferences.getBoolean(KEY_ANDROID_RESUME_ON_CLICK, true);
androidNotificationChannelId = preferences.getString(KEY_ANDROID_NOTIFICATION_CHANNEL_ID, null);
androidNotificationChannelName = preferences.getString(KEY_ANDROID_NOTIFICATION_CHANNEL_NAME, null);
androidNotificationChannelDescription = preferences.getString(KEY_ANDROID_NOTIFICATION_CHANNEL_DESCRIPTION, null);
notificationColor = preferences.getInt(KEY_NOTIFICATION_COLOR, -1);
androidNotificationIcon = preferences.getString(KEY_ANDROID_NOTIFICATION_ICON, "mipmap/ic_launcher");
androidShowNotificationBadge = preferences.getBoolean(KEY_ANDROID_SHOW_NOTIFICATION_BADGE, false);
androidNotificationClickStartsActivity = preferences.getBoolean(KEY_ANDROID_NOTIFICATION_CLICK_STARTS_ACTIVITY, true);
androidNotificationOngoing = preferences.getBoolean(KEY_ANDROID_NOTIFICATION_ONGOING, false);
androidStopForegroundOnPause = preferences.getBoolean(KEY_ANDROID_STOP_FOREGROUND_ON_PAUSE, true);
artDownscaleWidth = preferences.getInt(KEY_ART_DOWNSCALE_WIDTH, -1);
artDownscaleHeight = preferences.getInt(KEY_ART_DOWNSCALE_HEIGHT, -1);
activityClassName = preferences.getString(KEY_ACTIVITY_CLASS_NAME, null);
browsableRootExtras = preferences.getString(KEY_BROWSABLE_ROOT_EXTRAS, null);
}
public void setBrowsableRootExtras(Map<?,?> map) {
if (map != null) {
JSONObject json = new JSONObject(map);
browsableRootExtras = json.toString();
} else {
browsableRootExtras = null;
}
}
public Bundle getBrowsableRootExtras() {
if (browsableRootExtras == null) return null;
try {
JSONObject json = new JSONObject(browsableRootExtras);
Bundle extras = new Bundle();
for (Iterator<String> it = json.keys(); it.hasNext();) {
String key = it.next();
try {
extras.putInt(key, json.getInt(key));
} catch (Exception e1) {
try {
extras.putBoolean(key, json.getBoolean(key));
} catch (Exception e2) {
try {
extras.putDouble(key, json.getDouble(key));
} catch (Exception e3) {
try {
extras.putString(key, json.getString(key));
} catch (Exception e4) {
System.out.println("Unsupported extras value for key " + key);
}
}
}
}
}
return extras;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public void save() {
preferences.edit()
.putBoolean(KEY_ANDROID_RESUME_ON_CLICK, androidResumeOnClick)
.putString(KEY_ANDROID_NOTIFICATION_CHANNEL_ID, androidNotificationChannelId)
.putString(KEY_ANDROID_NOTIFICATION_CHANNEL_NAME, androidNotificationChannelName)
.putString(KEY_ANDROID_NOTIFICATION_CHANNEL_DESCRIPTION, androidNotificationChannelDescription)
.putInt(KEY_NOTIFICATION_COLOR, notificationColor)
.putString(KEY_ANDROID_NOTIFICATION_ICON, androidNotificationIcon)
.putBoolean(KEY_ANDROID_SHOW_NOTIFICATION_BADGE, androidShowNotificationBadge)
.putBoolean(KEY_ANDROID_NOTIFICATION_CLICK_STARTS_ACTIVITY, androidNotificationClickStartsActivity)
.putBoolean(KEY_ANDROID_NOTIFICATION_ONGOING, androidNotificationOngoing)
.putBoolean(KEY_ANDROID_STOP_FOREGROUND_ON_PAUSE, androidStopForegroundOnPause)
.putInt(KEY_ART_DOWNSCALE_WIDTH, artDownscaleWidth)
.putInt(KEY_ART_DOWNSCALE_HEIGHT, artDownscaleHeight)
.putString(KEY_ACTIVITY_CLASS_NAME, activityClassName)
.putString(KEY_BROWSABLE_ROOT_EXTRAS, browsableRootExtras)
.apply();
}
}
@@ -0,0 +1,48 @@
package com.ryanheise.audioservice;
import android.content.Context;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.flutter.embedding.android.FlutterFragmentActivity;
import io.flutter.embedding.engine.FlutterEngine;
public class AudioServiceFragmentActivity extends FlutterFragmentActivity {
@Override
public FlutterEngine provideFlutterEngine(@NonNull Context context) {
return AudioServicePlugin.getFlutterEngine(context);
}
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
AudioServicePlugin.getFlutterEngine(this);
super.onCreate(savedInstanceState);
}
@Override
protected String getCachedEngineId() {
AudioServicePlugin.getFlutterEngine(this);
return AudioServicePlugin.getFlutterEngineId();
}
// The engine is created and managed by AudioServicePlugin,
// it should not be destroyed with the activity.
@Override
public boolean shouldDestroyEngineWithHost() {
return false;
}
// Make this protected method public
@Override
public String getInitialRoute() {
return super.getInitialRoute();
}
// Make this protected method public
@Override
public boolean shouldHandleDeeplinking() {
return super.shouldHandleDeeplinking();
}
}
@@ -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);
}
}
@@ -0,0 +1,7 @@
package com.ryanheise.audioservice;
public enum MediaButton {
media,
next,
previous
}
@@ -0,0 +1,25 @@
package com.ryanheise.audioservice;
import android.content.Context;
import android.content.Intent;
public class MediaButtonReceiver extends androidx.media.session.MediaButtonReceiver {
public static final String ACTION_CUSTOM = "com.ryanheise.audioservice.intent.action.CUSTOM";
public static final String ACTION_NOTIFICATION_DELETE = "com.ryanheise.audioservice.intent.action.ACTION_NOTIFICATION_DELETE";
@Override
public void onReceive(Context context, Intent intent) {
if (intent != null && ACTION_CUSTOM.equals(intent.getAction()) && AudioService.instance != null) {
AudioService.instance.handleCustomNotificationAction(
intent.getStringExtra("action"), intent.getBundleExtra("extras"));
return;
}
if (intent != null
&& ACTION_NOTIFICATION_DELETE.equals(intent.getAction())
&& AudioService.instance != null) {
AudioService.instance.handleDeleteNotification();
return;
}
super.onReceive(context, intent);
}
}
@@ -0,0 +1,27 @@
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, 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 && Objects.equals(customAction, otherControl.customAction);
} else {
return false;
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 410 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 267 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 170 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 354 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 389 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 538 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 602 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 639 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 152 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 684 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 770 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 720 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 832 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 857 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 252 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 838 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 952 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 461 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 316 B