Initial commit
8
android/.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/workspace.xml
|
||||
/.idea/libraries
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
39
android/build.gradle
Normal file
|
@ -0,0 +1,39 @@
|
|||
group 'com.ryanheise.audioservice'
|
||||
version '1.0-SNAPSHOT'
|
||||
|
||||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:3.5.0'
|
||||
}
|
||||
}
|
||||
|
||||
rootProject.allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
}
|
||||
|
||||
apply plugin: 'com.android.library'
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 16
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
lintOptions {
|
||||
disable 'InvalidPackage'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.core:core:1.1.0'
|
||||
implementation 'androidx.media:media:1.1.0'
|
||||
}
|
4
android/gradle.properties
Normal file
|
@ -0,0 +1,4 @@
|
|||
org.gradle.jvmargs=-Xmx1536M
|
||||
android.enableR8=true
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
BIN
android/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
6
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
#Thu Sep 17 20:40:30 CEST 2020
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip
|
172
android/gradlew
vendored
Normal file
|
@ -0,0 +1,172 @@
|
|||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
84
android/gradlew.bat
vendored
Normal file
|
@ -0,0 +1,84 @@
|
|||
@if "%DEBUG%" == "" @echo off
|
||||
@rem ##########################################################################
|
||||
@rem
|
||||
@rem Gradle startup script for Windows
|
||||
@rem
|
||||
@rem ##########################################################################
|
||||
|
||||
@rem Set local scope for the variables with windows NT shell
|
||||
if "%OS%"=="Windows_NT" setlocal
|
||||
|
||||
set DIRNAME=%~dp0
|
||||
if "%DIRNAME%" == "" set DIRNAME=.
|
||||
set APP_BASE_NAME=%~n0
|
||||
set APP_HOME=%DIRNAME%
|
||||
|
||||
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
set DEFAULT_JVM_OPTS=
|
||||
|
||||
@rem Find java.exe
|
||||
if defined JAVA_HOME goto findJavaFromJavaHome
|
||||
|
||||
set JAVA_EXE=java.exe
|
||||
%JAVA_EXE% -version >NUL 2>&1
|
||||
if "%ERRORLEVEL%" == "0" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:findJavaFromJavaHome
|
||||
set JAVA_HOME=%JAVA_HOME:"=%
|
||||
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
|
||||
|
||||
if exist "%JAVA_EXE%" goto init
|
||||
|
||||
echo.
|
||||
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
|
||||
echo.
|
||||
echo Please set the JAVA_HOME variable in your environment to match the
|
||||
echo location of your Java installation.
|
||||
|
||||
goto fail
|
||||
|
||||
:init
|
||||
@rem Get command-line arguments, handling Windows variants
|
||||
|
||||
if not "%OS%" == "Windows_NT" goto win9xME_args
|
||||
|
||||
:win9xME_args
|
||||
@rem Slurp the command line arguments.
|
||||
set CMD_LINE_ARGS=
|
||||
set _SKIP=2
|
||||
|
||||
:win9xME_args_slurp
|
||||
if "x%~1" == "x" goto execute
|
||||
|
||||
set CMD_LINE_ARGS=%*
|
||||
|
||||
:execute
|
||||
@rem Setup the command line
|
||||
|
||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
||||
|
||||
@rem Execute Gradle
|
||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
|
||||
|
||||
:end
|
||||
@rem End local scope for the variables with windows NT shell
|
||||
if "%ERRORLEVEL%"=="0" goto mainEnd
|
||||
|
||||
:fail
|
||||
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
|
||||
rem the _cmd.exe /c_ return code!
|
||||
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
|
||||
exit /b 1
|
||||
|
||||
:mainEnd
|
||||
if "%OS%"=="Windows_NT" endlocal
|
||||
|
||||
:omega
|
1
android/settings.gradle
Normal file
|
@ -0,0 +1 @@
|
|||
rootProject.name = 'audio_service'
|
3
android/src/main/AndroidManifest.xml
Normal file
|
@ -0,0 +1,3 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.ryanheise.audioservice">
|
||||
</manifest>
|
|
@ -0,0 +1,8 @@
|
|||
package com.ryanheise.audioservice;
|
||||
|
||||
public enum AudioInterruption {
|
||||
pause,
|
||||
temporaryPause,
|
||||
temporaryDuck,
|
||||
unknownPause,
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
package com.ryanheise.audioservice;
|
||||
|
||||
public enum AudioProcessingState {
|
||||
none,
|
||||
connecting,
|
||||
ready,
|
||||
buffering,
|
||||
fastForwarding,
|
||||
rewinding,
|
||||
skippingToPrevious,
|
||||
skippingToNext,
|
||||
skippingToQueueItem,
|
||||
completed,
|
||||
stopped,
|
||||
error,
|
||||
}
|
|
@ -0,0 +1,805 @@
|
|||
package com.ryanheise.audioservice;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.media.AudioAttributes;
|
||||
import android.media.AudioFocusRequest;
|
||||
import android.media.AudioManager;
|
||||
import android.media.MediaDescription;
|
||||
import android.media.MediaMetadata;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.PowerManager;
|
||||
import android.support.v4.media.MediaBrowserCompat;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.MediaMetadataCompat;
|
||||
import android.support.v4.media.RatingCompat;
|
||||
import android.support.v4.media.session.MediaControllerCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
import android.util.Log;
|
||||
import android.util.LruCache;
|
||||
import android.view.KeyEvent;
|
||||
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.media.MediaBrowserServiceCompat;
|
||||
import androidx.media.app.NotificationCompat.MediaStyle;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
public class AudioService extends MediaBrowserServiceCompat {
|
||||
private static final int NOTIFICATION_ID = 1124;
|
||||
private static final int REQUEST_CONTENT_INTENT = 1000;
|
||||
private static final String MEDIA_ROOT_ID = "root";
|
||||
// See the comment in onMediaButtonEvent to understand how the BYPASS keycodes work.
|
||||
// We hijack KEYCODE_MUTE and KEYCODE_MEDIA_RECORD since the media session subsystem
|
||||
// considers these keycodes relevant to media playback and will pass them on to us.
|
||||
public static final int KEYCODE_BYPASS_PLAY = KeyEvent.KEYCODE_MUTE;
|
||||
public static final int KEYCODE_BYPASS_PAUSE = KeyEvent.KEYCODE_MEDIA_RECORD;
|
||||
public static final int MAX_COMPACT_ACTIONS = 3;
|
||||
|
||||
private static volatile boolean running;
|
||||
static AudioService instance;
|
||||
private static PendingIntent contentIntent;
|
||||
private static boolean resumeOnClick;
|
||||
private static ServiceListener listener;
|
||||
static String androidNotificationChannelName;
|
||||
static String androidNotificationChannelDescription;
|
||||
static Integer notificationColor;
|
||||
static String androidNotificationIcon;
|
||||
static boolean androidNotificationClickStartsActivity;
|
||||
static boolean androidNotificationOngoing;
|
||||
static boolean androidStopForegroundOnPause;
|
||||
private static List<MediaSessionCompat.QueueItem> queue = new ArrayList<MediaSessionCompat.QueueItem>();
|
||||
private static int queueIndex = -1;
|
||||
private static Map<String, MediaMetadataCompat> mediaMetadataCache = new HashMap<>();
|
||||
private static Set<String> artUriBlacklist = new HashSet<>();
|
||||
private static LruCache<String, Bitmap> artBitmapCache;
|
||||
private static Size artDownscaleSize;
|
||||
private static boolean playing = false;
|
||||
private static AudioProcessingState processingState = AudioProcessingState.none;
|
||||
private static int repeatMode;
|
||||
private static int shuffleMode;
|
||||
private static boolean notificationCreated;
|
||||
|
||||
public static void init(Activity activity, boolean resumeOnClick, String androidNotificationChannelName, String androidNotificationChannelDescription, String action, Integer notificationColor, String androidNotificationIcon, boolean androidNotificationClickStartsActivity, boolean androidNotificationOngoing, boolean androidStopForegroundOnPause, Size artDownscaleSize, ServiceListener listener) {
|
||||
if (running)
|
||||
throw new IllegalStateException("AudioService already running");
|
||||
running = true;
|
||||
|
||||
Context context = activity.getApplicationContext();
|
||||
Intent intent = new Intent(context, activity.getClass());
|
||||
intent.setAction(action);
|
||||
contentIntent = PendingIntent.getActivity(context, REQUEST_CONTENT_INTENT, intent, PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
AudioService.listener = listener;
|
||||
AudioService.resumeOnClick = resumeOnClick;
|
||||
AudioService.androidNotificationChannelName = androidNotificationChannelName;
|
||||
AudioService.androidNotificationChannelDescription = androidNotificationChannelDescription;
|
||||
AudioService.notificationColor = notificationColor;
|
||||
AudioService.androidNotificationIcon = androidNotificationIcon;
|
||||
AudioService.androidNotificationClickStartsActivity = androidNotificationClickStartsActivity;
|
||||
AudioService.androidNotificationOngoing = androidNotificationOngoing;
|
||||
AudioService.androidStopForegroundOnPause = androidStopForegroundOnPause;
|
||||
AudioService.artDownscaleSize = artDownscaleSize;
|
||||
|
||||
notificationCreated = false;
|
||||
playing = false;
|
||||
processingState = AudioProcessingState.none;
|
||||
repeatMode = 0;
|
||||
shuffleMode = 0;
|
||||
|
||||
// Get max available VM memory, exceeding this amount will throw an
|
||||
// OutOfMemory exception. Stored in kilobytes as LruCache takes an
|
||||
// int in its constructor.
|
||||
final int maxMemory = (int)(Runtime.getRuntime().maxMemory() / 1024);
|
||||
|
||||
// Use 1/8th of the available memory for this memory cache.
|
||||
final int cacheSize = maxMemory / 8;
|
||||
|
||||
artBitmapCache = new LruCache<String, Bitmap>(cacheSize) {
|
||||
@Override
|
||||
protected int sizeOf(String key, Bitmap bitmap) {
|
||||
// The cache size will be measured in kilobytes rather than
|
||||
// number of items.
|
||||
return bitmap.getByteCount() / 1024;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public static AudioProcessingState getProcessingState() {
|
||||
return processingState;
|
||||
}
|
||||
|
||||
public static boolean isPlaying() {
|
||||
return playing;
|
||||
}
|
||||
|
||||
public static int getRepeatMode() {
|
||||
return repeatMode;
|
||||
}
|
||||
|
||||
public static int getShuffleMode() {
|
||||
return shuffleMode;
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
running = false;
|
||||
mediaMetadata = null;
|
||||
resumeOnClick = false;
|
||||
listener = null;
|
||||
androidNotificationChannelName = null;
|
||||
androidNotificationChannelDescription = null;
|
||||
notificationColor = null;
|
||||
androidNotificationIcon = null;
|
||||
artDownscaleSize = null;
|
||||
queue.clear();
|
||||
queueIndex = -1;
|
||||
mediaMetadataCache.clear();
|
||||
actions.clear();
|
||||
artBitmapCache.evictAll();
|
||||
compactActionIndices = null;
|
||||
|
||||
mediaSession.setQueue(queue);
|
||||
mediaSession.setActive(false);
|
||||
releaseWakeLock();
|
||||
stopForeground(true);
|
||||
notificationCreated = false;
|
||||
stopSelf();
|
||||
}
|
||||
|
||||
public static boolean isRunning() {
|
||||
return running;
|
||||
}
|
||||
|
||||
private PowerManager.WakeLock wakeLock;
|
||||
private MediaSessionCompat mediaSession;
|
||||
private MediaSessionCallback mediaSessionCallback;
|
||||
private MediaMetadataCompat preparedMedia;
|
||||
private List<NotificationCompat.Action> actions = new ArrayList<NotificationCompat.Action>();
|
||||
private int[] compactActionIndices;
|
||||
private MediaMetadataCompat mediaMetadata;
|
||||
private Object audioFocusRequest;
|
||||
private String notificationChannelId;
|
||||
private Handler handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
int getResourceId(String resource) {
|
||||
String[] parts = resource.split("/");
|
||||
String resourceType = parts[0];
|
||||
String resourceName = parts[1];
|
||||
return getResources().getIdentifier(resourceName, resourceType, getApplicationContext().getPackageName());
|
||||
}
|
||||
|
||||
NotificationCompat.Action action(String resource, String label, long actionCode) {
|
||||
int iconId = getResourceId(resource);
|
||||
return new NotificationCompat.Action(iconId, label,
|
||||
buildMediaButtonPendingIntent(actionCode));
|
||||
}
|
||||
|
||||
PendingIntent buildMediaButtonPendingIntent(long action) {
|
||||
int keyCode = toKeyCode(action);
|
||||
if (keyCode == KeyEvent.KEYCODE_UNKNOWN)
|
||||
return null;
|
||||
Intent intent = new Intent(this, MediaButtonReceiver.class);
|
||||
intent.setAction(Intent.ACTION_MEDIA_BUTTON);
|
||||
intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode));
|
||||
return PendingIntent.getBroadcast(this, keyCode, intent, 0);
|
||||
}
|
||||
|
||||
PendingIntent buildDeletePendingIntent() {
|
||||
Intent intent = new Intent(this, MediaButtonReceiver.class);
|
||||
intent.setAction(MediaButtonReceiver.ACTION_NOTIFICATION_DELETE);
|
||||
return PendingIntent.getBroadcast(this, 0, intent, 0);
|
||||
}
|
||||
|
||||
public static int toKeyCode(long action) {
|
||||
if (action == PlaybackStateCompat.ACTION_PLAY) {
|
||||
return KEYCODE_BYPASS_PLAY;
|
||||
} else if (action == PlaybackStateCompat.ACTION_PAUSE) {
|
||||
return KEYCODE_BYPASS_PAUSE;
|
||||
} else {
|
||||
return PlaybackStateCompat.toKeyCode(action);
|
||||
}
|
||||
}
|
||||
|
||||
void setState(List<NotificationCompat.Action> actions, int actionBits, int[] compactActionIndices, AudioProcessingState processingState, boolean playing, long position, long bufferedPosition, float speed, long updateTime, int repeatMode, int shuffleMode) {
|
||||
this.actions = actions;
|
||||
this.compactActionIndices = compactActionIndices;
|
||||
boolean wasPlaying = AudioService.playing;
|
||||
AudioService.processingState = processingState;
|
||||
AudioService.playing = playing;
|
||||
AudioService.repeatMode = repeatMode;
|
||||
AudioService.shuffleMode = shuffleMode;
|
||||
|
||||
PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder()
|
||||
.setActions(PlaybackStateCompat.ACTION_PLAY_PAUSE | actionBits)
|
||||
.setState(getPlaybackState(), position, speed, updateTime)
|
||||
.setBufferedPosition(bufferedPosition);
|
||||
mediaSession.setPlaybackState(stateBuilder.build());
|
||||
|
||||
if (!running) return;
|
||||
|
||||
if (!wasPlaying && playing) {
|
||||
enterPlayingState();
|
||||
} else if (wasPlaying && !playing) {
|
||||
exitPlayingState();
|
||||
}
|
||||
|
||||
updateNotification();
|
||||
}
|
||||
|
||||
public int getPlaybackState() {
|
||||
switch (processingState) {
|
||||
case none: return PlaybackStateCompat.STATE_NONE;
|
||||
case connecting: return PlaybackStateCompat.STATE_CONNECTING;
|
||||
case ready: return playing ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
|
||||
case buffering: return PlaybackStateCompat.STATE_BUFFERING;
|
||||
case fastForwarding: return PlaybackStateCompat.STATE_FAST_FORWARDING;
|
||||
case rewinding: return PlaybackStateCompat.STATE_REWINDING;
|
||||
case skippingToPrevious: return PlaybackStateCompat.STATE_SKIPPING_TO_PREVIOUS;
|
||||
case skippingToNext: return PlaybackStateCompat.STATE_SKIPPING_TO_NEXT;
|
||||
case skippingToQueueItem: return PlaybackStateCompat.STATE_SKIPPING_TO_QUEUE_ITEM;
|
||||
case completed: return playing ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_PAUSED;
|
||||
case stopped: return PlaybackStateCompat.STATE_STOPPED;
|
||||
case error: return PlaybackStateCompat.STATE_ERROR;
|
||||
default: return PlaybackStateCompat.STATE_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
private Notification buildNotification() {
|
||||
int[] compactActionIndices = this.compactActionIndices;
|
||||
if (compactActionIndices == null) {
|
||||
compactActionIndices = new int[Math.min(MAX_COMPACT_ACTIONS, actions.size())];
|
||||
for (int i = 0; i < compactActionIndices.length; i++) compactActionIndices[i] = i;
|
||||
}
|
||||
NotificationCompat.Builder builder = getNotificationBuilder();
|
||||
if (mediaMetadata != null) {
|
||||
MediaDescriptionCompat description = mediaMetadata.getDescription();
|
||||
if (description.getTitle() != null)
|
||||
builder.setContentTitle(description.getTitle());
|
||||
if (description.getSubtitle() != null)
|
||||
builder.setContentText(description.getSubtitle());
|
||||
if (description.getDescription() != null)
|
||||
builder.setSubText(description.getDescription());
|
||||
if (description.getIconBitmap() != null)
|
||||
builder.setLargeIcon(description.getIconBitmap());
|
||||
}
|
||||
if (androidNotificationClickStartsActivity)
|
||||
builder.setContentIntent(mediaSession.getController().getSessionActivity());
|
||||
if (notificationColor != null)
|
||||
builder.setColor(notificationColor);
|
||||
for (NotificationCompat.Action action : actions) {
|
||||
builder.addAction(action);
|
||||
}
|
||||
builder.setStyle(new MediaStyle()
|
||||
.setMediaSession(mediaSession.getSessionToken())
|
||||
.setShowActionsInCompactView(compactActionIndices)
|
||||
.setShowCancelButton(true)
|
||||
.setCancelButtonIntent(buildMediaButtonPendingIntent(PlaybackStateCompat.ACTION_STOP))
|
||||
);
|
||||
if (androidNotificationOngoing)
|
||||
builder.setOngoing(true);
|
||||
Notification notification = builder.build();
|
||||
return notification;
|
||||
}
|
||||
|
||||
private NotificationCompat.Builder getNotificationBuilder() {
|
||||
NotificationCompat.Builder notificationBuilder = null;
|
||||
if (notificationBuilder == null) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
|
||||
createChannel();
|
||||
int iconId = getResourceId(androidNotificationIcon);
|
||||
notificationBuilder = new NotificationCompat.Builder(this, notificationChannelId)
|
||||
.setSmallIcon(iconId)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setShowWhen(false)
|
||||
.setDeleteIntent(buildDeletePendingIntent())
|
||||
;
|
||||
}
|
||||
return notificationBuilder;
|
||||
}
|
||||
|
||||
public void handleDeleteNotification() {
|
||||
if (listener == null) return;
|
||||
listener.onClose();
|
||||
}
|
||||
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
private void createChannel() {
|
||||
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
NotificationChannel channel = notificationManager.getNotificationChannel(notificationChannelId);
|
||||
if (channel == null) {
|
||||
channel = new NotificationChannel(notificationChannelId, androidNotificationChannelName, NotificationManager.IMPORTANCE_LOW);
|
||||
if (androidNotificationChannelDescription != null)
|
||||
channel.setDescription(androidNotificationChannelDescription);
|
||||
notificationManager.createNotificationChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateNotification() {
|
||||
if (!notificationCreated) return;
|
||||
NotificationManager notificationManager = (NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
notificationManager.notify(NOTIFICATION_ID, buildNotification());
|
||||
}
|
||||
|
||||
private boolean enterPlayingState() {
|
||||
startService(new Intent(AudioService.this, AudioService.class));
|
||||
if (!mediaSession.isActive())
|
||||
mediaSession.setActive(true);
|
||||
|
||||
acquireWakeLock();
|
||||
mediaSession.setSessionActivity(contentIntent);
|
||||
internalStartForeground();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void exitPlayingState() {
|
||||
if (androidStopForegroundOnPause) {
|
||||
exitForegroundState();
|
||||
}
|
||||
}
|
||||
|
||||
private void exitForegroundState() {
|
||||
stopForeground(false);
|
||||
releaseWakeLock();
|
||||
}
|
||||
|
||||
private void internalStartForeground() {
|
||||
startForeground(NOTIFICATION_ID, buildNotification());
|
||||
notificationCreated = true;
|
||||
}
|
||||
|
||||
private void acquireWakeLock() {
|
||||
if (!wakeLock.isHeld())
|
||||
wakeLock.acquire();
|
||||
}
|
||||
|
||||
private void releaseWakeLock() {
|
||||
if (wakeLock.isHeld())
|
||||
wakeLock.release();
|
||||
}
|
||||
|
||||
static MediaMetadataCompat createMediaMetadata(String mediaId, String album, String title, String artist, String genre, Long duration, String artUri, String displayTitle, String displaySubtitle, String displayDescription, RatingCompat rating, Map<?, ?> extras) {
|
||||
MediaMetadataCompat.Builder builder = new MediaMetadataCompat.Builder()
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, mediaId)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album)
|
||||
.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title);
|
||||
if (artist != null)
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, artist);
|
||||
if (genre != null)
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_GENRE, genre);
|
||||
if (duration != null)
|
||||
builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, duration);
|
||||
if (artUri != null) {
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, artUri);
|
||||
String artCacheFilePath = null;
|
||||
if (extras != null) {
|
||||
artCacheFilePath = (String)extras.get("artCacheFile");
|
||||
}
|
||||
if (artCacheFilePath != null) {
|
||||
Bitmap bitmap = loadArtBitmapFromFile(artCacheFilePath);
|
||||
if (bitmap != null) {
|
||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ALBUM_ART, bitmap);
|
||||
builder.putBitmap(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON, bitmap);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (displayTitle != null)
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, displayTitle);
|
||||
if (displaySubtitle != null)
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, displaySubtitle);
|
||||
if (displayDescription != null)
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_DESCRIPTION, displayDescription);
|
||||
if (rating != null) {
|
||||
builder.putRating(MediaMetadataCompat.METADATA_KEY_RATING, rating);
|
||||
}
|
||||
if (extras != null) {
|
||||
for (Object o : extras.keySet()) {
|
||||
String key = (String)o;
|
||||
Object value = extras.get(key);
|
||||
if (value instanceof Long) {
|
||||
builder.putLong("extra_long_" + key, (Long)value);
|
||||
} else if (value instanceof Integer) {
|
||||
builder.putLong("extra_long_" + key, (Integer)value);
|
||||
} else if (value instanceof String) {
|
||||
builder.putString("extra_string_" + key, (String)value);
|
||||
}
|
||||
}
|
||||
}
|
||||
MediaMetadataCompat mediaMetadata = builder.build();
|
||||
mediaMetadataCache.put(mediaId, mediaMetadata);
|
||||
return mediaMetadata;
|
||||
}
|
||||
|
||||
static MediaMetadataCompat getMediaMetadata(String mediaId) {
|
||||
return mediaMetadataCache.get(mediaId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
instance = this;
|
||||
notificationChannelId = getApplication().getPackageName() + ".channel";
|
||||
|
||||
mediaSession = new MediaSessionCompat(this, "media-session");
|
||||
mediaSession.setMediaButtonReceiver(null); // TODO: Make this configurable
|
||||
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
|
||||
PlaybackStateCompat.Builder stateBuilder = new PlaybackStateCompat.Builder()
|
||||
.setActions(PlaybackStateCompat.ACTION_PLAY);
|
||||
mediaSession.setPlaybackState(stateBuilder.build());
|
||||
mediaSession.setCallback(mediaSessionCallback = new MediaSessionCallback());
|
||||
setSessionToken(mediaSession.getSessionToken());
|
||||
mediaSession.setQueue(queue);
|
||||
|
||||
PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
|
||||
wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, AudioService.class.getName());
|
||||
}
|
||||
|
||||
void enableQueue() {
|
||||
mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS | MediaSessionCompat.FLAG_HANDLES_QUEUE_COMMANDS);
|
||||
}
|
||||
|
||||
void setQueue(List<MediaSessionCompat.QueueItem> queue) {
|
||||
this.queue = queue;
|
||||
mediaSession.setQueue(queue);
|
||||
}
|
||||
|
||||
void playMediaItem(MediaDescriptionCompat description) {
|
||||
mediaSessionCallback.onPlayMediaItem(description);
|
||||
}
|
||||
|
||||
void setMetadata(final MediaMetadataCompat mediaMetadata) {
|
||||
this.mediaMetadata = mediaMetadata;
|
||||
mediaSession.setMetadata(mediaMetadata);
|
||||
updateNotification();
|
||||
}
|
||||
|
||||
static Bitmap loadArtBitmapFromFile(String path) {
|
||||
Bitmap bitmap = artBitmapCache.get(path);
|
||||
if (bitmap != null) return bitmap;
|
||||
try {
|
||||
if (artDownscaleSize != null) {
|
||||
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||
options.inJustDecodeBounds = true;
|
||||
BitmapFactory.decodeFile(path, options);
|
||||
int imageHeight = options.outHeight;
|
||||
int imageWidth = options.outWidth;
|
||||
options.inSampleSize = calculateInSampleSize(options, artDownscaleSize.width, artDownscaleSize.height);
|
||||
options.inJustDecodeBounds = false;
|
||||
|
||||
bitmap = BitmapFactory.decodeFile(path, options);
|
||||
} else {
|
||||
bitmap = BitmapFactory.decodeFile(path);
|
||||
}
|
||||
artBitmapCache.put(path, bitmap);
|
||||
return bitmap;
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
|
||||
final int height = options.outHeight;
|
||||
final int width = options.outWidth;
|
||||
int inSampleSize = 1;
|
||||
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
final int halfHeight = height / 2;
|
||||
final int halfWidth = width / 2;
|
||||
while ((halfHeight / inSampleSize) >= reqHeight
|
||||
&& (halfWidth / inSampleSize) >= reqWidth) {
|
||||
inSampleSize *= 2;
|
||||
}
|
||||
}
|
||||
|
||||
return inSampleSize;
|
||||
}
|
||||
|
||||
@Override
|
||||
public BrowserRoot onGetRoot(String clientPackageName, int clientUid, Bundle rootHints) {
|
||||
return new BrowserRoot(MEDIA_ROOT_ID, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadChildren(final String parentMediaId, final Result<List<MediaBrowserCompat.MediaItem>> result) {
|
||||
if (listener == null) {
|
||||
result.sendResult(new ArrayList<MediaBrowserCompat.MediaItem>());
|
||||
return;
|
||||
}
|
||||
listener.onLoadChildren(parentMediaId, result);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(final Intent intent, int flags, int startId) {
|
||||
MediaButtonReceiver.handleIntent(mediaSession, intent);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (listener != null) {
|
||||
listener.onDestroy();
|
||||
}
|
||||
mediaSession.release();
|
||||
instance = null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTaskRemoved(Intent rootIntent) {
|
||||
if (listener != null) {
|
||||
listener.onTaskRemoved();
|
||||
}
|
||||
super.onTaskRemoved(rootIntent);
|
||||
}
|
||||
|
||||
public class MediaSessionCallback extends MediaSessionCompat.Callback {
|
||||
@Override
|
||||
public void onAddQueueItem(MediaDescriptionCompat description) {
|
||||
if (listener == null) return;
|
||||
listener.onAddQueueItem(getMediaMetadata(description.getMediaId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddQueueItem(MediaDescriptionCompat description, int index) {
|
||||
if (listener == null) return;
|
||||
listener.onAddQueueItemAt(getMediaMetadata(description.getMediaId()), index);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveQueueItem(MediaDescriptionCompat description) {
|
||||
if (listener == null) return;
|
||||
listener.onRemoveQueueItem(getMediaMetadata(description.getMediaId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepare() {
|
||||
if (listener == null) return;
|
||||
if (!mediaSession.isActive())
|
||||
mediaSession.setActive(true);
|
||||
listener.onPrepare();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlay() {
|
||||
if (listener == null) return;
|
||||
listener.onPlay();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareFromMediaId(String mediaId, Bundle extras) {
|
||||
if (listener == null) return;
|
||||
if (!mediaSession.isActive())
|
||||
mediaSession.setActive(true);
|
||||
listener.onPrepareFromMediaId(mediaId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPlayFromMediaId(final String mediaId, final Bundle extras) {
|
||||
if (listener == null) return;
|
||||
listener.onPlayFromMediaId(mediaId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onMediaButtonEvent(Intent mediaButtonEvent) {
|
||||
if (listener == null) return false;
|
||||
final KeyEvent event = (KeyEvent)mediaButtonEvent.getExtras().get(Intent.EXTRA_KEY_EVENT);
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
switch (event.getKeyCode()) {
|
||||
case KEYCODE_BYPASS_PLAY:
|
||||
onPlay();
|
||||
break;
|
||||
case KEYCODE_BYPASS_PAUSE:
|
||||
onPause();
|
||||
break;
|
||||
case KeyEvent.KEYCODE_MEDIA_STOP:
|
||||
onStop();
|
||||
break;
|
||||
case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD:
|
||||
onFastForward();
|
||||
break;
|
||||
case KeyEvent.KEYCODE_MEDIA_REWIND:
|
||||
onRewind();
|
||||
break;
|
||||
// Android unfortunately reroutes media button clicks to
|
||||
// KEYCODE_MEDIA_PLAY/PAUSE instead of the expected KEYCODE_HEADSETHOOK
|
||||
// or KEYCODE_MEDIA_PLAY_PAUSE. As a result, we can't genuinely tell if
|
||||
// onMediaButtonEvent was called because a media button was actually
|
||||
// pressed or because a PLAY/PAUSE action was pressed instead! To get
|
||||
// around this, we make PLAY and PAUSE actions use different keycodes:
|
||||
// KEYCODE_BYPASS_PLAY/PAUSE. Now if we get KEYCODE_MEDIA_PLAY/PUASE
|
||||
// we know it is actually a media button press.
|
||||
case KeyEvent.KEYCODE_MEDIA_NEXT:
|
||||
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
|
||||
case KeyEvent.KEYCODE_MEDIA_PLAY:
|
||||
case KeyEvent.KEYCODE_MEDIA_PAUSE:
|
||||
// These are the "genuine" media button click events
|
||||
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
|
||||
case KeyEvent.KEYCODE_HEADSETHOOK:
|
||||
MediaControllerCompat controller = mediaSession.getController();
|
||||
listener.onClick(mediaControl(event));
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private MediaControl mediaControl(KeyEvent event) {
|
||||
switch (event.getKeyCode()) {
|
||||
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
|
||||
case KeyEvent.KEYCODE_HEADSETHOOK:
|
||||
return MediaControl.media;
|
||||
case KeyEvent.KEYCODE_MEDIA_NEXT:
|
||||
return MediaControl.next;
|
||||
case KeyEvent.KEYCODE_MEDIA_PREVIOUS:
|
||||
return MediaControl.previous;
|
||||
default:
|
||||
return MediaControl.media;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
if (listener == null) return;
|
||||
listener.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
if (listener == null) return;
|
||||
listener.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkipToNext() {
|
||||
if (listener == null) return;
|
||||
listener.onSkipToNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkipToPrevious() {
|
||||
if (listener == null) return;
|
||||
listener.onSkipToPrevious();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFastForward() {
|
||||
if (listener == null) return;
|
||||
listener.onFastForward();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRewind() {
|
||||
if (listener == null) return;
|
||||
listener.onRewind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSkipToQueueItem(long id) {
|
||||
if (listener == null) return;
|
||||
listener.onSkipToQueueItem(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSeekTo(long pos) {
|
||||
if (listener == null) return;
|
||||
listener.onSeekTo(pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetRating(RatingCompat rating) {
|
||||
if (listener == null) return;
|
||||
listener.onSetRating(rating);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetRepeatMode(int repeatMode) {
|
||||
if (listener == null) return;
|
||||
listener.onSetRepeatMode(repeatMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetShuffleMode(int shuffleMode) {
|
||||
if (listener == null) return;
|
||||
listener.onSetShuffleMode(shuffleMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetRating(RatingCompat rating, Bundle extras) {
|
||||
if (listener == null) return;
|
||||
listener.onSetRating(rating, extras);
|
||||
}
|
||||
|
||||
//
|
||||
// NON-STANDARD METHODS
|
||||
//
|
||||
|
||||
public void onPlayMediaItem(final MediaDescriptionCompat description) {
|
||||
if (listener == null) return;
|
||||
listener.onPlayMediaItem(getMediaMetadata(description.getMediaId()));
|
||||
}
|
||||
}
|
||||
|
||||
public static interface ServiceListener {
|
||||
void onLoadChildren(String parentMediaId, Result<List<MediaBrowserCompat.MediaItem>> result);
|
||||
|
||||
void onClick(MediaControl mediaControl);
|
||||
|
||||
void onPrepare();
|
||||
|
||||
void onPrepareFromMediaId(String mediaId);
|
||||
|
||||
//void onPrepareFromSearch(String query);
|
||||
//void onPrepareFromUri(String uri);
|
||||
void onPlay();
|
||||
|
||||
void onPlayFromMediaId(String mediaId);
|
||||
|
||||
//void onPlayFromSearch(String query, Map<?,?> extras);
|
||||
//void onPlayFromUri(String uri, Map<?,?> extras);
|
||||
void onSkipToQueueItem(long id);
|
||||
|
||||
void onPause();
|
||||
|
||||
void onSkipToNext();
|
||||
|
||||
void onSkipToPrevious();
|
||||
|
||||
void onFastForward();
|
||||
|
||||
void onRewind();
|
||||
|
||||
void onStop();
|
||||
|
||||
void onDestroy();
|
||||
|
||||
void onSeekTo(long pos);
|
||||
|
||||
void onSetRating(RatingCompat rating);
|
||||
|
||||
void onSetRating(RatingCompat rating, Bundle extras);
|
||||
|
||||
void onSetRepeatMode(int repeatMode);
|
||||
|
||||
//void onSetShuffleModeEnabled(boolean enabled);
|
||||
|
||||
void onSetShuffleMode(int shuffleMode);
|
||||
|
||||
//void onCustomAction(String action, Bundle extras);
|
||||
|
||||
void onAddQueueItem(MediaMetadataCompat metadata);
|
||||
|
||||
void onAddQueueItemAt(MediaMetadataCompat metadata, int index);
|
||||
|
||||
void onRemoveQueueItem(MediaMetadataCompat metadata);
|
||||
|
||||
//
|
||||
// NON-STANDARD METHODS
|
||||
//
|
||||
|
||||
void onPlayMediaItem(MediaMetadataCompat metadata);
|
||||
|
||||
void onTaskRemoved();
|
||||
|
||||
void onClose();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
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_NOTIFICATION_DELETE = "com.ryanheise.audioservice.intent.action.ACTION_NOTIFICATION_DELETE";
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (intent != null
|
||||
&& ACTION_NOTIFICATION_DELETE.equals(intent.getAction())
|
||||
&& AudioService.instance != null) {
|
||||
AudioService.instance.handleDeleteNotification();
|
||||
return;
|
||||
}
|
||||
super.onReceive(context, intent);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package com.ryanheise.audioservice;
|
||||
|
||||
public enum MediaControl {
|
||||
media,
|
||||
next,
|
||||
previous
|
||||
}
|
11
android/src/main/java/com/ryanheise/audioservice/Size.java
Normal file
|
@ -0,0 +1,11 @@
|
|||
package com.ryanheise.audioservice;
|
||||
|
||||
public class Size {
|
||||
public int width;
|
||||
public int height;
|
||||
|
||||
public Size(int width, int height) {
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
}
|
||||
}
|
After Width: | Height: | Size: 561 B |
BIN
android/src/main/res/drawable-hdpi/audio_service_fast_rewind.png
Normal file
After Width: | Height: | Size: 584 B |
BIN
android/src/main/res/drawable-hdpi/audio_service_pause.png
Normal file
After Width: | Height: | Size: 157 B |
BIN
android/src/main/res/drawable-hdpi/audio_service_play_arrow.png
Normal file
After Width: | Height: | Size: 352 B |
BIN
android/src/main/res/drawable-hdpi/audio_service_skip_next.png
Normal file
After Width: | Height: | Size: 379 B |
After Width: | Height: | Size: 410 B |
BIN
android/src/main/res/drawable-hdpi/audio_service_stop.png
Normal file
After Width: | Height: | Size: 121 B |
After Width: | Height: | Size: 241 B |
BIN
android/src/main/res/drawable-mdpi/audio_service_fast_rewind.png
Normal file
After Width: | Height: | Size: 267 B |
BIN
android/src/main/res/drawable-mdpi/audio_service_pause.png
Normal file
After Width: | Height: | Size: 170 B |
BIN
android/src/main/res/drawable-mdpi/audio_service_play_arrow.png
Normal file
After Width: | Height: | Size: 285 B |
BIN
android/src/main/res/drawable-mdpi/audio_service_skip_next.png
Normal file
After Width: | Height: | Size: 344 B |
After Width: | Height: | Size: 354 B |
BIN
android/src/main/res/drawable-mdpi/audio_service_stop.png
Normal file
After Width: | Height: | Size: 114 B |
After Width: | Height: | Size: 389 B |
After Width: | Height: | Size: 460 B |
BIN
android/src/main/res/drawable-xhdpi/audio_service_pause.png
Normal file
After Width: | Height: | Size: 188 B |
BIN
android/src/main/res/drawable-xhdpi/audio_service_play_arrow.png
Normal file
After Width: | Height: | Size: 538 B |
BIN
android/src/main/res/drawable-xhdpi/audio_service_skip_next.png
Normal file
After Width: | Height: | Size: 602 B |
After Width: | Height: | Size: 639 B |
BIN
android/src/main/res/drawable-xhdpi/audio_service_stop.png
Normal file
After Width: | Height: | Size: 152 B |
After Width: | Height: | Size: 684 B |
After Width: | Height: | Size: 770 B |
BIN
android/src/main/res/drawable-xxhdpi/audio_service_pause.png
Normal file
After Width: | Height: | Size: 315 B |
After Width: | Height: | Size: 720 B |
BIN
android/src/main/res/drawable-xxhdpi/audio_service_skip_next.png
Normal file
After Width: | Height: | Size: 832 B |
After Width: | Height: | Size: 857 B |
BIN
android/src/main/res/drawable-xxhdpi/audio_service_stop.png
Normal file
After Width: | Height: | Size: 252 B |
After Width: | Height: | Size: 838 B |
After Width: | Height: | Size: 952 B |
BIN
android/src/main/res/drawable-xxxhdpi/audio_service_pause.png
Normal file
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 |
BIN
android/src/main/res/drawable-xxxhdpi/audio_service_stop.png
Normal file
After Width: | Height: | Size: 316 B |