net.simno.klingar.playback.LocalPlayback.java Source code

Java tutorial

Introduction

Here is the source code for net.simno.klingar.playback.LocalPlayback.java

Source

/*
 * Copyright (C) 2014 The Android Open Source Project
 * Copyright (C) 2017 Simon Norberg
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.simno.klingar.playback;

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v4.media.session.PlaybackStateCompat.State;

import com.google.android.exoplayer2.DefaultLoadControl;
import com.google.android.exoplayer2.ExoPlaybackException;
import com.google.android.exoplayer2.ExoPlayer;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.Timeline;
import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.TrackGroupArray;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelectionArray;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;

import net.simno.klingar.R;
import net.simno.klingar.data.model.Track;

import timber.log.Timber;

import static android.media.AudioManager.ACTION_AUDIO_BECOMING_NOISY;
import static com.google.android.exoplayer2.C.TIME_UNSET;

/**
 * A class that implements local media playback using
 * {@link com.google.android.exoplayer2.ExoPlayer}
 */
class LocalPlayback implements Playback, ExoPlayer.EventListener, AudioManager.OnAudioFocusChangeListener {

    private static final float VOLUME_DUCK = 0.2f;
    private static final float VOLUME_NORMAL = 1.0f;
    private static final int AUDIO_NO_FOCUS_NO_DUCK = 0;
    private static final int AUDIO_NO_FOCUS_CAN_DUCK = 1;
    private static final int AUDIO_FOCUSED = 2;

    private final IntentFilter audioNoisyIntentFilter = new IntentFilter(ACTION_AUDIO_BECOMING_NOISY);
    private final Context context;
    private final WifiManager.WifiLock wifiLock;
    private final AudioManager audioManager;
    private final MusicController musicController;
    private final DefaultDataSourceFactory dataSourceFactory;
    private final DefaultExtractorsFactory extractorsFactory;
    private SimpleExoPlayer exoPlayer;
    private Callback callback;
    private int audioFocus = AUDIO_NO_FOCUS_NO_DUCK;
    @State
    private int state;
    private int exoPlayerState;
    private boolean playOnFocusGain;
    private boolean configWhenReady;
    private volatile boolean audioNoisyReceiverRegistered;
    private volatile long currentPosition;
    private volatile Track currentTrack;

    private final BroadcastReceiver audioNoisyReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
                if (isPlaying()) {
                    musicController.pause();
                }
            }
        }
    };

    LocalPlayback(Context context, MusicController musicController, AudioManager audioManager,
            WifiManager wifiManager) {
        this.context = context;
        this.musicController = musicController;
        this.audioManager = audioManager;
        this.wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "klingar_lock");
        this.state = PlaybackStateCompat.STATE_NONE;
        String agent = Util.getUserAgent(context, context.getResources().getString(R.string.app_name));
        this.dataSourceFactory = new DefaultDataSourceFactory(context, agent, null);
        this.extractorsFactory = new DefaultExtractorsFactory();
    }

    private static String getExoPlayerState(int state) {
        switch (state) {
        case ExoPlayer.STATE_IDLE:
            return "STATE_IDLE";
        case ExoPlayer.STATE_BUFFERING:
            return "STATE_BUFFERING";
        case ExoPlayer.STATE_READY:
            return "STATE_READY";
        case ExoPlayer.STATE_ENDED:
            return "STATE_ENDED";
        default:
            return "UNKNOWN";
        }
    }

    @Override
    public void start() {
    }

    @Override
    public void stop(boolean notifyListeners) {
        state = PlaybackStateCompat.STATE_STOPPED;
        if (notifyListeners && callback != null) {
            callback.onPlaybackStatusChanged();
        }
        currentPosition = getCurrentStreamPosition();
        giveUpAudioFocus();
        unregisterAudioNoisyReceiver();
        relaxResources(true);
    }

    @Override
    @State
    public int getState() {
        return state;
    }

    @Override
    public boolean isConnected() {
        return true;
    }

    @Override
    public boolean isPlaying() {
        return playOnFocusGain || (exoPlayer != null && exoPlayer.getPlayWhenReady());
    }

    @Override
    public int getCurrentStreamPosition() {
        return exoPlayer != null ? (int) exoPlayer.getCurrentPosition() : (int) currentPosition;
    }

    @Override
    public void setCurrentStreamPosition(int position) {
        this.currentPosition = position;
    }

    @Override
    public void updateLastKnownStreamPosition() {
        if (exoPlayer != null) {
            currentPosition = exoPlayer.getCurrentPosition();
        }
    }

    @Override
    public void play(Track track) {
        Timber.d("play %s", track);
        playOnFocusGain = true;
        tryToGetAudioFocus();
        registerAudioNoisyReceiver();
        boolean mediaHasChanged = !track.equals(currentTrack);
        if (mediaHasChanged) {
            currentPosition = 0;
            currentTrack = track;
        }

        if (state == PlaybackStateCompat.STATE_PAUSED && !mediaHasChanged && exoPlayer != null) {
            configExoPlayerState();
        } else {
            state = PlaybackStateCompat.STATE_STOPPED;
            relaxResources(false); // release everything except ExoPlayer

            createExoPlayerIfNeeded();

            state = PlaybackStateCompat.STATE_BUFFERING;
            if (callback != null) {
                callback.onPlaybackStatusChanged();
            }

            Uri uri = Uri.parse(track.source());
            ExtractorMediaSource source = new ExtractorMediaSource(uri, dataSourceFactory, extractorsFactory, null,
                    null);
            configWhenReady = true;
            exoPlayer.setPlayWhenReady(false);
            exoPlayer.prepare(source);

            // If we are streaming from the internet, we want to hold a Wifi lock, which prevents the
            // Wifi radio from going to sleep while the song is playing.
            wifiLock.acquire();
        }
    }

    @Override
    public void pause() {
        Timber.d("pause");
        if (state == PlaybackStateCompat.STATE_PLAYING) {
            // Pause ExoPlayer and cancel the 'foreground service' state.
            if (exoPlayer != null && exoPlayer.getPlayWhenReady()) {
                exoPlayer.setPlayWhenReady(false);
                currentPosition = exoPlayer.getCurrentPosition();
            }
            // while paused, retain ExoPlayer but give up audio focus
            relaxResources(false);
        }
        state = PlaybackStateCompat.STATE_PAUSED;
        if (callback != null) {
            callback.onPlaybackStatusChanged();
        }
        unregisterAudioNoisyReceiver();
    }

    @Override
    public void seekTo(int position) {
        Timber.d("seekTo %s", position);
        if (exoPlayer == null) {
            // If we do not have a current ExoPlayer, simply update the current position
            currentPosition = position;
        } else {
            if (exoPlayer.getPlayWhenReady()) {
                state = PlaybackStateCompat.STATE_BUFFERING;
                if (callback != null) {
                    callback.onPlaybackStatusChanged();
                }
            }
            registerAudioNoisyReceiver();
            long duration = exoPlayer.getDuration();
            long seekPosition = duration == TIME_UNSET ? 0 : Math.min(Math.max(0, position), duration);
            exoPlayer.seekTo(seekPosition);
        }
    }

    @Override
    public Track getCurrentTrack() {
        return currentTrack;
    }

    @Override
    public void setCurrentTrack(Track track) {
        this.currentTrack = track;
    }

    @Override
    public void setCallback(Callback callback) {
        this.callback = callback;
    }

    @Override
    public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
        boolean playerStateChanged = exoPlayerState != playbackState;
        exoPlayerState = playbackState;

        Timber.d("onPlayerStateChanged %s playWhenReady %s playerStateChanged %s configWhenReady %s",
                getExoPlayerState(playbackState), playWhenReady, playerStateChanged, configWhenReady);

        switch (playbackState) {
        case ExoPlayer.STATE_READY:
            if (configWhenReady) {
                configWhenReady = false;
                configExoPlayerState();
            } else if (playWhenReady) { // seek complete
                currentPosition = exoPlayer.getCurrentPosition();
                state = PlaybackStateCompat.STATE_PLAYING;
                if (callback != null) {
                    callback.onPlaybackStatusChanged();
                }
            }
            break;
        case ExoPlayer.STATE_ENDED:
            if (playerStateChanged) { // only call onCompletion once
                currentPosition = 0;
                callback.onCompletion();
            }
            break;
        default:
            break;
        }
    }

    @Override
    public void onTimelineChanged(Timeline timeline, Object manifest) {
    }

    @Override
    public void onTracksChanged(TrackGroupArray trackGroups, TrackSelectionArray trackSelections) {
    }

    @Override
    public void onLoadingChanged(boolean isLoading) {
    }

    @Override
    public void onPlayerError(ExoPlaybackException error) {
        Timber.e(error, "Exception playing song");
        state = PlaybackStateCompat.STATE_ERROR;
        if (callback != null) {
            callback.onPlaybackStatusChanged();
        }
    }

    @Override
    public void onPositionDiscontinuity() {
    }

    private void tryToGetAudioFocus() {
        Timber.d("tryToGetAudioFocus");
        int result = audioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            audioFocus = AUDIO_FOCUSED;
        } else {
            audioFocus = AUDIO_NO_FOCUS_NO_DUCK;
        }
    }

    private void giveUpAudioFocus() {
        Timber.d("giveUpAudioFocus");
        if (audioManager.abandonAudioFocus(this) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
            audioFocus = AUDIO_NO_FOCUS_NO_DUCK;
        }
    }

    /**
     * Reconfigures ExoPlayer according to audio focus settings and starts/restarts it.
     * This method starts/restarts the ExoPlayer respecting the current audio focus state.
     * So if we have focus, it will play normally; if we don't have focus, it will either leave
     * ExoPlayer paused or set it to a low volume, depending on what is  allowed by the current
     * focus settings. This method assumes exoPlayer != null, so if you are calling it,
     * you have to do so from a context where you are sure this is the case.
     */
    private void configExoPlayerState() {
        Timber.d("configExoPlayerState audioFocus %s", audioFocus);
        if (audioFocus == AUDIO_NO_FOCUS_NO_DUCK) {
            // If we don't have audio focus and can't duck, we have to pause,
            if (state == PlaybackStateCompat.STATE_PLAYING) {
                pause();
            }
        } else { // we have audio focus
            registerAudioNoisyReceiver();
            if (audioFocus == AUDIO_NO_FOCUS_CAN_DUCK) {
                if (exoPlayer != null) {
                    exoPlayer.setVolume(VOLUME_DUCK); // we'll be relatively quiet
                }
            } else {
                if (exoPlayer != null) {
                    exoPlayer.setVolume(VOLUME_NORMAL); // we can be loud again
                }
            }
            // If we were playing when we lost focus, we need to resume playing.
            if (playOnFocusGain) {
                if (exoPlayer != null && !exoPlayer.getPlayWhenReady()) {
                    Timber.d("configExoPlayerState seeking to %s", currentPosition);
                    if (currentPosition == exoPlayer.getCurrentPosition()) {
                        state = PlaybackStateCompat.STATE_PLAYING;
                        exoPlayer.setPlayWhenReady(true);
                    } else {
                        state = PlaybackStateCompat.STATE_BUFFERING;
                        seekTo((int) currentPosition);
                        exoPlayer.setPlayWhenReady(true);
                    }
                    if (callback != null) {
                        callback.onPlaybackStatusChanged();
                    }
                }
                playOnFocusGain = false;
            }
        }
    }

    @Override
    public void onAudioFocusChange(int focusChange) {
        Timber.d("onAudioFocusChange focusChange %s", focusChange);
        if (focusChange == AudioManager.AUDIOFOCUS_GAIN) {
            audioFocus = AUDIO_FOCUSED; // We have gained focus
        } else if (focusChange == AudioManager.AUDIOFOCUS_LOSS
                || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT
                || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK) {
            // We have lost focus. If we can duck (low playback volume), we can keep playing.
            // Otherwise, we need to pause the playback.
            boolean canDuck = focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK;
            audioFocus = canDuck ? AUDIO_NO_FOCUS_CAN_DUCK : AUDIO_NO_FOCUS_NO_DUCK;

            // If we are playing, we need to reset ExoPlayer by calling configExoPlayerState
            // with audioFocus properly set.
            if (state == PlaybackStateCompat.STATE_PLAYING && !canDuck) {
                // If we don't have audio focus and can't duck, we save the information that
                // we were playing, so that we can resume playback once we get the focus back.
                playOnFocusGain = true;
            }
        } else {
            Timber.e("onAudioFocusChange: Ignoring unsupported focusChange: %s", focusChange);
        }
        configExoPlayerState();
    }

    private void createExoPlayerIfNeeded() {
        Timber.d("createExoPlayerIfNeeded %s", exoPlayer == null);
        if (exoPlayer == null) {
            exoPlayer = ExoPlayerFactory.newSimpleInstance(context, new DefaultTrackSelector(),
                    new DefaultLoadControl());
            exoPlayer.addListener(this);
        }
    }

    /**
     * Releases resources used by the service for playback. This includes the
     * "foreground service" status, the wake locks and possibly the ExoPlayer.
     *
     * @param releaseExoPlayer Indicates whether ExoPlayer should also be released or not
     */
    private void relaxResources(boolean releaseExoPlayer) {
        Timber.d("relaxResources releaseExoPlayer %s", releaseExoPlayer);
        // stop and release the ExoPlayer, if it's available
        if (releaseExoPlayer && exoPlayer != null) {
            exoPlayer.removeListener(this);
            exoPlayer.release();
            exoPlayer = null;
        }

        // we can also release the Wifi lock, if we're holding it
        if (wifiLock.isHeld()) {
            wifiLock.release();
        }
    }

    private void registerAudioNoisyReceiver() {
        if (!audioNoisyReceiverRegistered) {
            context.registerReceiver(audioNoisyReceiver, audioNoisyIntentFilter);
            audioNoisyReceiverRegistered = true;
        }
    }

    private void unregisterAudioNoisyReceiver() {
        if (audioNoisyReceiverRegistered) {
            context.unregisterReceiver(audioNoisyReceiver);
            audioNoisyReceiverRegistered = false;
        }
    }
}