I am trying to build an extremely simple radio streaming app which stores a list of web radio URLs, which can be selected to stream audio; using a service to allow playing to continue when app not active + control from notification.
The controls I require are extremely simple: play / pause, and stop, which should kill the service and be induced when the notification is cleared or stop button pressed in-app.
I apologise for the large amount of code but this is where I'm at:
public class StreamingService extends Service
implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener {
// .. snipped out fields
private AudioManager.OnAudioFocusChangeListener mOnAudioFocusChangeListener =
new AudioManager.OnAudioFocusChangeListener() {
@Override
public void onAudioFocusChange(int focusChange) {
switch (focusChange) {
case AudioManager.AUDIOFOCUS_GAIN:
// set mCurrentAudioFocusState field
}
if (mMediaPlayer != null)
configurePlayerState();
}
};
private int mCurrentAudioFocusState = AUDIO_NO_FOCUS_NO_DUCK;
private final IntentFilter mAudioNoisyIntentFilter =
new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
private BroadcastReceiver mNoisyReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
// Pause when headphones unplugged
mMediaPlayer.pause();
}
};
private boolean mAudioNoisyReceiverRegistered = false;
@Override
public void onCreate() {
super.onCreate();
AudioManager mAudioManager = (AudioManager)
getSystemService(Context.AUDIO_SERVICE);
int result = mAudioManager.requestAudioFocus(
mOnAudioFocusChangeListener,
AudioManager.STREAM_MUSIC,
AudioManager.AUDIOFOCUS_GAIN
);
if (result != AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
stopSelf();
} else {
mCurrentAudioFocusState = AUDIO_FOCUSED;
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setOnPreparedListener(this);
mMediaPlayer.setOnErrorListener(this);
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
WifiManager.WifiLock wifiLock =
((WifiManager) Objects.requireNonNull(
getApplicationContext().getSystemService(Context.WIFI_SERVICE)))
.createWifiLock(WifiManager.WIFI_MODE_FULL, "wifi_lock");
wifiLock.acquire();
try {
mMediaPlayer.setDataSource(intent.getStringExtra(STREAM_URI));
} catch (IOException e) {
e.printStackTrace();
}
mMediaPlayer.prepareAsync();
onStartIntent = intent;
return Service.START_STICKY;
}
@Override
public void onDestroy() {
mMediaPlayer.release();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public boolean onError(MediaPlayer mediaPlayer, int i, int i1) {
mMediaPlayer.reset();
return true;
}
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
handleIntent(onStartIntent);
}
private void handleIntent(Intent intent) {
String action = intent.getAction();
String command = intent.getStringExtra(CMD_NAME);
if (ACTION_CMD.equals(action)) {
switch (command) {
case CMD_PLAY:
registerAudioNoisyReceiver();
mMediaPlayer.start();
startForeground(NOTIFICATION_ID, buildNotification());
case CMD_PAUSE:
unregisterAudioNoisyReceiver();
mMediaPlayer.pause();
startForeground(NOTIFICATION_ID, buildNotification());
case CMD_STOP:
unregisterAudioNoisyReceiver();
mMediaPlayer.stop();
stopSelf();
}
}
}
private Notification buildNotification() {
createNotificationChannel();
NotificationCompat.Builder builder =
new NotificationCompat.Builder(getApplicationContext(), NOTIFICATION_CHANNEL);
builder
.setContentTitle(onStartIntent.getStringExtra(STREAM_TITLE))
.setContentIntent(PendingIntent.getActivity(
this,
0,
new Intent(getApplicationContext(), MainActivity.class),
0))
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setDeleteIntent(getActionIntent(CMD_STOP));
builder
.setSmallIcon(android.R.drawable.ic_media_play)
.setColor(ContextCompat.getColor(this, R.color.colorPrimaryDark));
builder
.addAction(new NotificationCompat.Action(
android.R.drawable.ic_media_pause, getString(R.string.pause),
getActionIntent(CMD_PAUSE)));
builder
.setStyle(new android.support.v4.media.app.NotificationCompat.MediaStyle()
.setShowActionsInCompactView(0)
.setShowCancelButton(true)
.setCancelButtonIntent(
getActionIntent(CMD_STOP)));
return builder.build();
}
private PendingIntent getActionIntent(String action) {
Intent s = new Intent(getApplicationContext(), StreamingService.class);
s.putExtra(
STREAM_TITLE,
onStartIntent.getStringExtra(STREAM_TITLE)
);
s.putExtra(
STREAM_URI,
onStartIntent.getStringExtra(STREAM_URI)
);
s.setAction(ACTION_CMD);
s.putExtra(
CMD_NAME,
action
);
s.setPackage(getApplicationContext().getPackageName());
return PendingIntent.getService(
getApplicationContext(), 0, s, 0);
}
// snipped methods to register and unregister noisy receiver
private void configurePlayerState() {
switch(mCurrentAudioFocusState) {
case AUDIO_NO_FOCUS_CAN_DUCK:
registerAudioNoisyReceiver();
mMediaPlayer.setVolume(VOLUME_DUCK, VOLUME_DUCK);
case AUDIO_NO_FOCUS_LOST:
unregisterAudioNoisyReceiver();
mMediaPlayer.stop();
case AUDIO_NO_FOCUS_NO_DUCK:
unregisterAudioNoisyReceiver();
mMediaPlayer.pause();
case AUDIO_FOCUSED:
registerAudioNoisyReceiver();
mMediaPlayer.setVolume(VOLUME_NORMAL, VOLUME_NORMAL);
}
}
private void createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = getString(R.string.channel_name);
String description = getString(R.string.channel_description);
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel =
new NotificationChannel(NOTIFICATION_CHANNEL, name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
assert notificationManager != null;
notificationManager.createNotificationChannel(channel);
}
}
}
This is collated from ideas using Google's lectures on media playback, Android docs, and example apps like UAMP and other online examples.
The code as it currently stands: launches, seems to set up the audio, but then seems to pause, stop and destroy, also destroying the notification. No notification ever appears in-app and no audio plays. Here's a logcat:
05-06 12:41:21.407 1903 1994 I ActivityManager: Displayed com.ojm.pinstream/.activities.MainActivity: +727ms
05-06 12:41:23.955 1903 2517 D AudioService: Stream muted, skip playback
05-06 12:41:23.962 1903 3205 I ActivityManager: START u0 {cmp=com.ojm.pinstream/.activities.PlayActivity} from uid 10191
05-06 12:41:23.979 12786 12786 W AudioManager: Use of stream types is deprecated for operations other than volume control
05-06 12:41:23.979 12786 12786 W AudioManager: See the documentation of requestAudioFocus() for what to use instead with android.media.AudioAttributes to qualify your playback use case
05-06 12:41:23.980 1903 3205 I MediaFocusControl: requestAudioFocus() from uid/pid 10191/12786 clientId=android.media.AudioManager@6badb4bcom.ojm.pinstream.services.StreamingService$1@3626928 callingPack=com.ojm.pinstream req=1 flags=0x0 sdk=27
05-06 12:41:23.986 12786 12786 W MediaPlayer: Use of stream types is deprecated for operations other than volume control
05-06 12:41:23.986 12786 12786 W MediaPlayer: See the documentation of setAudioStreamType() for what to use instead with android.media.AudioAttributes to qualify your playback use case
05-06 12:41:23.990 12786 12786 V MediaHTTPService: MediaHTTPService(android.media.MediaHTTPService@9e12641): Cookies: null
05-06 12:41:23.992 1808 25066 D NuPlayerDriver: NuPlayerDriver(0xe8513800) created, clientPid(12786)
05-06 12:41:23.996 12786 12808 V MediaHTTPService: makeHTTPConnection: CookieManager created: java.net.CookieManager@5cb47e6
05-06 12:41:23.997 12786 12808 V MediaHTTPService: makeHTTPConnection(android.media.MediaHTTPService@9e12641): cookieHandler: java.net.CookieManager@5cb47e6 Cookies: null
05-06 12:41:24.005 12786 12808 D NetworkSecurityConfig: No Network Security Config specified, using platform default
05-06 12:41:24.053 1903 4685 E NotificationService: Suppressing notification from package by user request.
05-06 12:41:24.056 1903 1966 E NotificationService: Suppressing notification from package by user request.
05-06 12:41:24.076 12786 12791 I zygote64: Do partial code cache collection, code=60KB, data=45KB
05-06 12:41:24.076 12786 12791 I zygote64: After code cache collection, code=60KB, data=45KB
05-06 12:41:24.078 12786 12791 I zygote64: Increasing code cache capacity to 256KB
05-06 12:41:24.203 1903 1994 I ActivityManager: Displayed com.ojm.pinstream/.activities.PlayActivity: +203ms
05-06 12:41:24.227 12786 12807 D OpenGLRenderer: endAllActiveAnimators on 0x7bd8b64c00 (ListView) with handle 0x7be64b8340
05-06 12:41:27.025 1903 8861 E NotificationService: Suppressing notification from package by user request.
05-06 12:41:27.031 1903 1966 E NotificationService: Suppressing notification from package by user request.
05-06 12:41:28.257 5051 5051 V ApiRequest: Performing request to https://127.0.0.1:8384/rest/events?since=0&limit=1
05-06 12:41:28.322 5051 5051 D EventProcessor: Reading events starting with id 1675
05-06 12:41:28.322 5051 5051 V ApiRequest: Performing request to https://127.0.0.1:8384/rest/events?since=1675&limit=0
05-06 12:41:28.733 1903 8861 D WificondControl: Scan result ready event
05-06 12:41:29.020 1808 12827 D GenericSource: stopBufferingIfNecessary_l, mBuffering=0
05-06 12:41:29.020 1808 12818 D NuPlayerDriver: notifyListener_l(0xe8513800), (1, 0, 0, -1), loop setting(0, 0)
05-06 12:41:29.039 1903 3205 V MediaRouterService: restoreBluetoothA2dp(false)
05-06 12:41:29.039 1711 6225 D AudioPolicyManagerCustom: setForceUse() usage 1, config 10, mPhoneState 0
05-06 12:41:29.040 1808 2811 D NuPlayerDriver: start(0xe8513800), state is 4, eos is 0
05-06 12:41:29.041 1808 12818 I GenericSource: start
05-06 12:41:29.061 1808 12834 I OMXClient: Treble IOmx obtained
05-06 12:41:29.061 1812 1902 I OMXMaster: makeComponentInstance(OMX.google.mp3.decoder) in [email protected] process
05-06 12:41:29.067 1812 1902 E OMXNodeInstance: setConfig(0xf362a720:google.mp3.decoder, ConfigPriority(0x6f800002)) ERROR: Undefined(0x80001001)
05-06 12:41:29.068 1808 12834 I ACodec : codec does not support config priority (err -2147483648)
05-06 12:41:29.068 1812 6179 E OMXNodeInstance: getConfig(0xf362a720:google.mp3.decoder, ConfigAndroidVendorExtension(0x6f100004)) ERROR: Undefined(0x80001001)
05-06 12:41:29.069 1808 12834 I MediaCodec: MediaCodec will operate in async mode
05-06 12:41:29.081 1808 2811 D NuPlayerDriver: pause(0xe8513800)
05-06 12:41:29.081 1808 2811 D NuPlayerDriver: notifyListener_l(0xe8513800), (7, 0, 0, -1), loop setting(0, 0)
05-06 12:41:29.082 1903 1966 E NotificationService: Suppressing notification from package by user request.
05-06 12:41:29.082 1903 8861 V MediaRouterService: restoreBluetoothA2dp(false)
05-06 12:41:29.084 1711 6225 D AudioPolicyManagerCustom: setForceUse() usage 1, config 10, mPhoneState 0
05-06 12:41:29.097 1808 2811 D NuPlayerDriver: stop(0xe8513800)
05-06 12:41:29.097 1808 2811 D NuPlayerDriver: notifyListener_l(0xe8513800), (8, 0, 0, -1), loop setting(0, 0)
05-06 12:41:29.101 12786 12786 V MediaPlayer: resetDrmState: mDrmInfo=null mDrmProvisioningThread=null mPrepareDrmInProgress=false mActiveDrmScheme=false
05-06 12:41:29.102 12786 12786 V MediaPlayer: cleanDrmObj: mDrmObj=null mDrmSessionId=null
05-06 12:41:29.102 1808 2811 D NuPlayerDriver: reset(0xe8513800) at state 8
05-06 12:41:29.103 1903 1903 I NotificationService: Cannot find enqueued record for key: 0|com.ojm.pinstream|576|null|10191
05-06 12:41:29.108 1808 12826 I NuCachedSource2: caching reached eos.
05-06 12:41:29.108 1903 1966 E NotificationService: Suppressing notification from package by user request.
05-06 12:41:29.117 1903 3205 E NotificationService: Suppressing notification from package by user request.
05-06 12:41:29.117 1808 12818 D NuPlayerDriver: notifyResetComplete(0xe8513800)
05-06 12:41:29.121 1903 1966 E NotificationService: Suppressing notification from package by user request.
05-06 12:41:29.123 2663 2663 W StatusBar: removeNotification for unknown key: 0|com.ojm.pinstream|576|null|10191
I'm not exactly experienced with Android development. If anyone could give any help at all it'd be hugely appreciated.
Here is the chain of methods being called in your code:
startForeground(NOTIFICATION_ID, buildNotification())
method is calledbuildNotification()
method adds a CMD_PAUSE action via the method getActionIntent(CMD_PAUSE)
getActionIntent()
method calls the method PendingIntent.getService(getApplicationContext(), 0, s, 0)
The problem is that you are getting your PendingIntent via a method that goes and starts the service straight away - PendingIntent.getService()
, as per the documentation:
Retrieve a PendingIntent that will start a service, like calling {@link Context#startService Context.startService()}. The start arguments given to the service will come from the extras of the Intent.
When your audio starts playing, it creates the notification, fetches the pending intent for the CMD_PAUSE action, this pending intent starts the service, the handleIntent()
method is called with the action set via the pending intent and your audio is then paused...
From personal experience, you should investigate using the following:
MediaButtonReceiver.buildMediaButtonPendingIntent(context,
PlaybackStateCompat.ACTION_PLAY)
See the MediaButtonReceiver documentation for more detail.
onStartCommand() is called when a media event, such as pressing the pause button, occurs for your media service - so you should implement a simple callback to handle the intent:
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
MediaButtonReceiver.handleIntent(mSession, intent)
return super.onStartCommand(intent, flags, startId)
}
You need to find a different way of passing in a new URI to your service, such as using a MediaBrowser - or a simpler way, binding to the service and calling a method to refresh the URI from your activity. You shouldn't be calling startService() from your activity which triggers onStartCommand().