Search code examples
javaandroidandroid-serviceandroid-mediaplayerandroid-notifications

How to create an Android foreground service for web radio streaming with interactive notification?


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.


Solution

  • Here is the chain of methods being called in your code:

    1. Audio starts
    2. startForeground(NOTIFICATION_ID, buildNotification()) method is called
    3. buildNotification() method adds a CMD_PAUSE action via the method getActionIntent(CMD_PAUSE)
    4. 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().