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.
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,
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
After Width: | Height: | Size: 561 B |
|
After Width: | Height: | Size: 584 B |
|
After Width: | Height: | Size: 157 B |
|
After Width: | Height: | Size: 352 B |
|
After Width: | Height: | Size: 379 B |
|
After Width: | Height: | Size: 410 B |
|
After Width: | Height: | Size: 121 B |
|
After Width: | Height: | Size: 241 B |
|
After Width: | Height: | Size: 267 B |
|
After Width: | Height: | Size: 170 B |
|
After Width: | Height: | Size: 285 B |
|
After Width: | Height: | Size: 344 B |
|
After Width: | Height: | Size: 354 B |
|
After Width: | Height: | Size: 114 B |
|
After Width: | Height: | Size: 389 B |
|
After Width: | Height: | Size: 460 B |
|
After Width: | Height: | Size: 188 B |
|
After Width: | Height: | Size: 538 B |
|
After Width: | Height: | Size: 602 B |
|
After Width: | Height: | Size: 639 B |
|
After Width: | Height: | Size: 152 B |
|
After Width: | Height: | Size: 684 B |
|
After Width: | Height: | Size: 770 B |
|
After Width: | Height: | Size: 315 B |
|
After Width: | Height: | Size: 720 B |
|
After Width: | Height: | Size: 832 B |
|
After Width: | Height: | Size: 857 B |
|
After Width: | Height: | Size: 252 B |
|
After Width: | Height: | Size: 838 B |
|
After Width: | Height: | Size: 952 B |
|
After Width: | Height: | Size: 461 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 316 B |