Initial commit

This commit is contained in:
exttex 2020-09-18 19:25:00 +02:00
commit 73fce9905f
87 changed files with 7529 additions and 0 deletions

8
android/.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
*.iml
.gradle
/local.properties
/.idea/workspace.xml
/.idea/libraries
.DS_Store
/build
/captures

39
android/build.gradle Normal file
View 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'
}

View file

@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true

Binary file not shown.

View 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
View 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
View 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
View file

@ -0,0 +1 @@
rootProject.name = 'audio_service'

View file

@ -0,0 +1,3 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.ryanheise.audioservice">
</manifest>

View file

@ -0,0 +1,8 @@
package com.ryanheise.audioservice;
public enum AudioInterruption {
pause,
temporaryPause,
temporaryDuck,
unknownPause,
}

View file

@ -0,0 +1,16 @@
package com.ryanheise.audioservice;
public enum AudioProcessingState {
none,
connecting,
ready,
buffering,
fastForwarding,
rewinding,
skippingToPrevious,
skippingToNext,
skippingToQueueItem,
completed,
stopped,
error,
}

View file

@ -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();
}
}

File diff suppressed because it is too large Load diff

View file

@ -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);
}
}

View file

@ -0,0 +1,7 @@
package com.ryanheise.audioservice;
public enum MediaControl {
media,
next,
previous
}

View 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;
}
}

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