dk.nota.lyt.libvlc.PlaybackService.java Source code

Java tutorial

Introduction

Here is the source code for dk.nota.lyt.libvlc.PlaybackService.java

Source

/*****************************************************************************
 * PlaybackService.java
 *****************************************************************************
 * Copyright  2011-2015 VLC authors and VideoLAN
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
 *****************************************************************************/

/**
 * Heavily modified to fit our needs for the LYT project.
 */

package dk.nota.lyt.libvlc;

import android.annotation.TargetApi;
import android.app.Activity;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.AudioManager.OnAudioFocusChangeListener;
import android.net.Uri;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.support.annotation.MainThread;
import android.support.annotation.Nullable;
import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.support.v7.app.NotificationCompat;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import android.widget.Toast;

import com.bumptech.glide.Glide;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;

import org.videolan.libvlc.IVLCVout;
import org.videolan.libvlc.LibVLC;
import org.videolan.libvlc.Media;
import org.videolan.libvlc.MediaList;
import org.videolan.libvlc.MediaPlayer;
import org.videolan.libvlc.util.AndroidUtil;

import java.io.File;
import java.lang.ref.WeakReference;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.Random;
import java.util.Stack;
import java.util.concurrent.atomic.AtomicBoolean;

import dk.nota.lyt.libvlc.media.MediaWrapper;
import dk.nota.lyt.libvlc.media.MediaWrapperList;
import dk.nota.lyt.libvlc.media.MediaEvent;
import dk.nota.lyt.libvlc.media.MediaPlayerEvent;

public class PlaybackService extends Service {

    private static final String TAG = PlaybackService.class.getCanonicalName();

    private static final int SHOW_PROGRESS = 0;
    private static final int SHOW_TOAST = 1;
    public static final String ACTION_REMOTE_GENERIC = PlaybackService.class.getPackage().getName() + ".remote.";
    public static final String ACTION_REMOTE_BACKWARD = ACTION_REMOTE_GENERIC + "Backward";
    public static final String ACTION_REMOTE_PLAY = ACTION_REMOTE_GENERIC + "Play";
    public static final String ACTION_REMOTE_PLAYPAUSE = ACTION_REMOTE_GENERIC + "PlayPause";
    public static final String ACTION_REMOTE_PAUSE = ACTION_REMOTE_GENERIC + "Pause";
    public static final String ACTION_REMOTE_STOP = ACTION_REMOTE_GENERIC + "Stop";
    public static final String ACTION_REMOTE_FORWARD = ACTION_REMOTE_GENERIC + "Forward";

    /* Binder for Service RPC */
    private class LocalBinder extends Binder {
        PlaybackService getService() {
            return PlaybackService.this;
        }
    }

    public static PlaybackService getService(IBinder iBinder) {
        LocalBinder binder = (LocalBinder) iBinder;
        return binder.getService();
    }

    private SharedPreferences mSettings;
    private final IBinder mBinder = new LocalBinder();
    private MediaWrapperList mMediaList = new MediaWrapperList();
    private MediaPlayer mMediaPlayer;
    private Activity mNotificationActivity;
    private String mNotificationAction;

    final private ArrayList<PlaybackEventHandler> mPlaybackEventHandlers = new ArrayList<>();
    private boolean mDetectHeadset = true;
    private PowerManager.WakeLock mWakeLock;
    private final AtomicBoolean mExpanding = new AtomicBoolean(false);

    // Index management
    private Stack<Integer> mPrevious;
    private int mCurrentIndex; // Set to -1 if no media is currently loaded
    private int mPrevIndex; // Set to -1 if no previous media
    private int mNextIndex; // Set to -1 if no next media

    // Playback management
    MediaSessionCompat mMediaSession;
    protected MediaSessionCallback mSessionCallback;
    private static final long PLAYBACK_ACTIONS = PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_PLAY
            | PlaybackStateCompat.ACTION_STOP | PlaybackStateCompat.ACTION_SKIP_TO_NEXT
            | PlaybackStateCompat.ACTION_SKIP_TO_PREVIOUS;

    public static final int TYPE_AUDIO = 0;
    public static final int REPEAT_NONE = 0;
    public static final int REPEAT_ONE = 1;
    public static final int REPEAT_ALL = 2;
    private int mRepeating = REPEAT_NONE;
    private boolean mShuffling = false;
    private Random mRandom = null; // Used in shuffling process
    private long mSavedTime = 0l;
    private boolean mHasAudioFocus = false;
    private boolean mParsed = false;
    private boolean mSeekable = false;
    private boolean mPausable = false;
    /**
     * RemoteControlClient is for lock screen playback control.
     */
    private RemoteControlEventReceiver mRemoteControlClientReceiver = null;
    private ComponentName mRemoteControlClientReceiverComponent;

    private LibVLC LibVLC() {
        Log.i(TAG, "=== Creating LibVLC with options! ===");
        Log.i(TAG, "NetworkCache: " + DefaultOptions.NetworkCaching);
        Log.i(TAG, "FileCache: " + DefaultOptions.FileCaching);
        ArrayList<String> defaultOptions = new ArrayList<String>();
        if (DefaultOptions.AutoReconnect) {
            defaultOptions.add("--http-reconnect");
        }
        defaultOptions.add("--network-caching=" + DefaultOptions.NetworkCaching);
        defaultOptions.add("--file-caching=" + DefaultOptions.FileCaching);
        return Utils.GetLibVLC(this.getApplicationContext(), defaultOptions);
    }

    private MediaPlayer newMediaPlayer() {
        final MediaPlayer mp = new MediaPlayer(LibVLC());
        return mp;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        mSettings = PreferenceManager.getDefaultSharedPreferences(this);
        mMediaPlayer = newMediaPlayer();
        if (!Utils.testCompatibleCPU(this)) {
            stopSelf();
            return;
        }

        mDetectHeadset = mSettings.getBoolean("enable_headset_detection", true);

        mCurrentIndex = -1;
        mPrevIndex = -1;
        mNextIndex = -1;
        mPrevious = new Stack<Integer>();
        mRemoteControlClientReceiverComponent = new ComponentName(this.getApplicationContext(),
                RemoteControlEventReceiver.class.getName());

        // Make sure the audio player will acquire a wake-lock while playing. If we don't do
        // that, the CPU might go to sleep while the song is playing, causing playback to stopService.
        PowerManager pm = (PowerManager) this.getApplicationContext().getSystemService(Context.POWER_SERVICE);
        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);

        IntentFilter filter = new IntentFilter();
        filter.setPriority(Integer.MAX_VALUE);
        filter.addAction(ACTION_REMOTE_BACKWARD);
        filter.addAction(ACTION_REMOTE_PLAYPAUSE);
        filter.addAction(ACTION_REMOTE_PLAY);
        filter.addAction(ACTION_REMOTE_PAUSE);
        filter.addAction(ACTION_REMOTE_STOP);
        filter.addAction(ACTION_REMOTE_FORWARD);
        filter.addAction(Intent.ACTION_HEADSET_PLUG);
        filter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
        registerReceiver(mReceiver, filter);
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent == null)
            return START_STICKY;
        if (ACTION_REMOTE_PLAYPAUSE.equals(intent.getAction())) {
            if (hasCurrentMedia())
                return START_STICKY;
            else
                loadLastPlaylist(TYPE_AUDIO);
        } else if (ACTION_REMOTE_PLAY.equals(intent.getAction())) {
            if (hasCurrentMedia())
                play();
            else
                loadLastPlaylist(TYPE_AUDIO);
        }
        return super.onStartCommand(intent, flags, startId);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        stopService();
        if (mWakeLock.isHeld()) {
            mWakeLock.release();
        }
        if (mRemoteControlClientReceiver != null) {
            unregisterReceiver(mRemoteControlClientReceiver);
            mRemoteControlClientReceiver = null;
        }
        unregisterReceiver(mReceiver);
        mMediaPlayer.release();
    }

    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        if (!hasCurrentMedia())
            stopSelf();
        return true;
    }

    public IVLCVout getVLCVout() {
        return mMediaPlayer.getVLCVout();
    }

    private final OnAudioFocusChangeListener mAudioFocusListener = AndroidUtil.isFroyoOrLater()
            ? createOnAudioFocusChangeListener()
            : null;

    private OnAudioFocusChangeListener createOnAudioFocusChangeListener() {
        return new OnAudioFocusChangeListener() {
            private boolean mLossTransient = false;
            private boolean mLossTransientCanDuck = false;
            private boolean wasPlaying = false;

            @Override
            public void onAudioFocusChange(int focusChange) {
                /*
                 * Pause playback during alerts and notifications
                 */
                switch (focusChange) {
                case AudioManager.AUDIOFOCUS_LOSS:
                    Log.d(TAG, "AUDIOFOCUS_LOSS");
                    // Pause playback
                    changeAudioFocus(false);
                    pause();
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
                    Log.d(TAG, "AUDIOFOCUS_LOSS_TRANSIENT");
                    // Pause playback
                    mLossTransient = true;
                    wasPlaying = isPlaying();
                    if (wasPlaying)
                        pause();
                    break;
                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
                    Log.d(TAG, "AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK");
                    // Lower the volume
                    if (mMediaPlayer.isPlaying()) {
                        mMediaPlayer.setVolume(36);
                        mLossTransientCanDuck = true;
                    }
                    break;
                case AudioManager.AUDIOFOCUS_GAIN:
                    Log.d(TAG, "AUDIOFOCUS_GAIN: " + mLossTransientCanDuck + ", " + mLossTransient);
                    // Resume playback
                    if (mLossTransientCanDuck) {
                        mMediaPlayer.setVolume(100);
                        mLossTransientCanDuck = false;
                    } else if (mLossTransient) {
                        if (wasPlaying)
                            mMediaPlayer.play();
                        mLossTransient = false;
                    }
                    break;
                }
            }
        };
    }

    private void changeAudioFocus(boolean acquire) {
        final AudioManager am = (AudioManager) getSystemService(AUDIO_SERVICE);
        if (am == null)
            return;

        if (acquire) {
            if (!mHasAudioFocus) {
                final int result = am.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
                        AudioManager.AUDIOFOCUS_GAIN);
                if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
                    am.setParameters("bgm_state=true");
                    mHasAudioFocus = true;
                }
            }
        } else {
            if (mHasAudioFocus) {
                final int result = am.abandonAudioFocus(mAudioFocusListener);
                am.setParameters("bgm_state=false");
                mHasAudioFocus = false;
            }
        }
    }

    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        private boolean wasPlaying = false;

        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            int state = intent.getIntExtra("state", 0);
            if (mMediaPlayer == null) {
                Log.w(TAG, "Intent received, but VLC is not loaded, skipping.");
                return;
            }

            // skip all headsets events if there is a call
            TelephonyManager telManager = (TelephonyManager) PlaybackService.this.getApplicationContext()
                    .getSystemService(Context.TELEPHONY_SERVICE);
            if (telManager != null && telManager.getCallState() != TelephonyManager.CALL_STATE_IDLE)
                return;

            /*
             * Launch the activity if needed
             */
            if (action.startsWith(ACTION_REMOTE_GENERIC) && !mMediaPlayer.isPlaying() && !hasCurrentMedia()) {
                context.startActivity(getPackageManager().getLaunchIntentForPackage(getPackageName()));
            }

            /*
             * Remote / headset control events
             */
            if (action.equalsIgnoreCase(ACTION_REMOTE_PLAYPAUSE)) {
                if (!hasCurrentMedia())
                    return;
                if (mMediaPlayer.isPlaying())
                    pause();
                else
                    play();
            } else if (action.equalsIgnoreCase(ACTION_REMOTE_PLAY)) {
                if (!mMediaPlayer.isPlaying() && hasCurrentMedia())
                    play();
            } else if (action.equalsIgnoreCase(ACTION_REMOTE_PAUSE)) {
                if (hasCurrentMedia())
                    pause();
            } else if (action.equalsIgnoreCase(ACTION_REMOTE_BACKWARD)) {
                previous();
            } else if (action.equalsIgnoreCase(ACTION_REMOTE_STOP)) {
                stopService();
            } else if (action.equalsIgnoreCase(ACTION_REMOTE_FORWARD)) {
                next();
            } else if (mDetectHeadset) {
                if (action.equalsIgnoreCase(AudioManager.ACTION_AUDIO_BECOMING_NOISY)) {
                    Log.d(TAG, "Headset Removed.");
                    wasPlaying = isPlaying();
                    if (wasPlaying && hasCurrentMedia())
                        pause();
                } else if (action.equalsIgnoreCase(Intent.ACTION_HEADSET_PLUG) && state != 0) {
                    Log.d(TAG, "Headset Inserted.");
                    if (wasPlaying && hasCurrentMedia())
                        play();
                }
            }
        }
    };

    private final Media.EventListener mMediaListener = new Media.EventListener() {
        @Override
        public void onEvent(Media.Event event) {
            boolean update = true;
            switch (event.type) {
            case Media.Event.MetaChanged:
                /* Update Meta if file is already parsed */
                if (mParsed && updateCurrentMeta(event.getMetaId()))
                    executeUpdate();
                Log.d(TAG, "Media.Event.MetaChanged: " + event.getMetaId());
                break;
            case Media.Event.ParsedChanged:
                Log.d(TAG, "Media.Event.ParsedChanged");
                updateCurrentMeta(-1);
                mParsed = true;
                break;
            default:
                update = false;

            }
            if (update) {
                try {
                    for (PlaybackEventHandler handler : mPlaybackEventHandlers)
                        handler.onMediaEvent(new MediaEvent(event));
                } catch (Exception ex) {
                    Log.d(TAG, "Error notifying PlaybackEventHandlers.onMediaEvent: " + ex.getMessage());
                }
                if (mParsed)
                    showNotification();
            }
        }
    };

    /**
     * Update current media meta and return true if player needs to be updated
     *
     * @param id of the Meta event received, -1 for none
     * @return true if UI needs to be updated
     */
    private boolean updateCurrentMeta(int id) {
        if (id == Media.Meta.Publisher)
            return false;
        final MediaWrapper mw = getCurrentMedia();
        if (mw != null)
            mw.updateMeta(mMediaPlayer);
        return id != Media.Meta.NowPlaying || getCurrentMedia().getNowPlaying() != null;
    }

    private final MediaPlayer.EventListener mMediaPlayerListener = new MediaPlayer.EventListener() {

        @Override
        public void onEvent(MediaPlayer.Event event) {
            switch (event.type) {
            case MediaPlayer.Event.Playing:

                Log.d(TAG, "MediaPlayer.Event.Playing");
                executeUpdate();
                publishState(event.type);
                executeUpdateProgress();

                final MediaWrapper mw = mMediaList.getMedia(mCurrentIndex);
                changeAudioFocus(true);
                if (!mWakeLock.isHeld())
                    mWakeLock.acquire();
                showNotification();
                break;
            case MediaPlayer.Event.Paused:
                Log.d(TAG, "MediaPlayer.Event.Paused");
                executeUpdate();
                publishState(event.type);
                executeUpdateProgress();
                showNotification();
                if (mWakeLock.isHeld())
                    mWakeLock.release();
                break;
            case MediaPlayer.Event.Stopped:
                Log.d(TAG, "MediaPlayer.Event.Stopped");
                executeUpdate();
                publishState(event.type);
                executeUpdateProgress();
                if (mWakeLock.isHeld())
                    mWakeLock.release();
                changeAudioFocus(false);
                break;
            case MediaPlayer.Event.EndReached:
                Log.d(TAG, "MediaPlayer.Event.EndReached");
                executeUpdateProgress();
                determinePrevAndNextIndices(true);
                next();
                if (mWakeLock.isHeld())
                    mWakeLock.release();
                changeAudioFocus(false);
                break;
            case MediaPlayer.Event.EncounteredError:
                showToast("MediaPlayer error with MRL: " + mMediaList.getMRL(mCurrentIndex), Toast.LENGTH_SHORT);
                executeUpdate();
                executeUpdateProgress();
                next();
                if (mWakeLock.isHeld())
                    mWakeLock.release();
                break;
            case MediaPlayer.Event.TimeChanged:
                break;
            case MediaPlayer.Event.PositionChanged:
                break;
            case MediaPlayer.Event.Vout:
                break;
            case MediaPlayer.Event.ESAdded:
                updateMetadata();
                break;
            case MediaPlayer.Event.ESDeleted:
                break;
            case MediaPlayer.Event.PausableChanged:
                mPausable = event.getPausable();
                break;
            case MediaPlayer.Event.SeekableChanged:
                mSeekable = event.getSeekable();
                break;
            }
            try {
                for (PlaybackEventHandler handler : mPlaybackEventHandlers)
                    handler.onMediaPlayerEvent(new MediaPlayerEvent(event));
            } catch (Exception ex) {
                Log.d(TAG, "Error notifying PlaybackEventHandlers.onMediaPlayerEvent: " + ex.getMessage());
            }
        }
    };

    private final MediaWrapperList.EventListener mListEventListener = new MediaWrapperList.EventListener() {

        @Override
        public void onItemAdded(int index, String mrl) {
            Log.d(TAG, "CustomMediaListItemAdded");
            if (mCurrentIndex >= index && !mExpanding.get())
                mCurrentIndex++;

            determinePrevAndNextIndices();
            executeUpdate();
        }

        @Override
        public void onItemRemoved(int index, String mrl) {
            Log.d(TAG, "CustomMediaListItemDeleted");
            if (mCurrentIndex == index && !mExpanding.get()) {
                // The current item has been deleted
                mCurrentIndex--;
                determinePrevAndNextIndices();
                if (mNextIndex != -1)
                    next();
                else if (mCurrentIndex != -1) {
                    playIndex(mCurrentIndex, 0);
                } else
                    stopService();
            }

            if (mCurrentIndex > index && !mExpanding.get())
                mCurrentIndex--;
            determinePrevAndNextIndices();
            executeUpdate();
        }

        @Override
        public void onItemMoved(int indexBefore, int indexAfter, String mrl) {
            Log.d(TAG, "CustomMediaListItemMoved");
            if (mCurrentIndex == indexBefore) {
                mCurrentIndex = indexAfter;
                if (indexAfter > indexBefore)
                    mCurrentIndex--;
            } else if (indexBefore > mCurrentIndex && indexAfter <= mCurrentIndex)
                mCurrentIndex++;
            else if (indexBefore < mCurrentIndex && indexAfter > mCurrentIndex)
                mCurrentIndex--;

            // If we are in random mode, we completely reset the stored previous track
            // as their indices changed.
            mPrevious.clear();

            determinePrevAndNextIndices();
            executeUpdate();
        }
    };

    private void executeUpdate() {
        try {
            for (PlaybackEventHandler handler : mPlaybackEventHandlers) {
                handler.update();
            }
        } catch (Exception ex) {
            Log.d(TAG, "Error notifying PlaybackEventHandlers.update: " + ex.getMessage());
        }

        updateMetadata();
    }

    private void executeUpdateProgress() {
        try {
            for (PlaybackEventHandler handler : mPlaybackEventHandlers) {
                handler.updateProgress();
            }
        } catch (Exception ex) {
            Log.d(TAG, "Error notifying PlaybackEventHandlers.updateProgress: " + ex.getMessage());
        }
    }

    /**
     * Return the current media.
     *
     * @return The current media or null if there is not any.
     */
    @Nullable
    private MediaWrapper getCurrentMedia() {
        return mMediaList.getMedia(mCurrentIndex);
    }

    /**
     * Alias for mCurrentIndex >= 0
     *
     * @return True if a media is currently loaded, false otherwise
     */
    private boolean hasCurrentMedia() {
        return mCurrentIndex >= 0 && mCurrentIndex < mMediaList.size();
    }

    private final Handler mHandler = new AudioServiceHandler(this);

    private class AudioServiceHandler extends WeakHandler<PlaybackService> {
        public AudioServiceHandler(PlaybackService fragment) {
            super(fragment);
        }

        @Override
        public void handleMessage(Message msg) {
            PlaybackService service = getOwner();
            if (service == null)
                return;

            switch (msg.what) {
            case SHOW_PROGRESS:
                if (service.mPlaybackEventHandlers.size() > 0) {
                    removeMessages(SHOW_PROGRESS);
                    service.executeUpdateProgress();
                    sendEmptyMessageDelayed(SHOW_PROGRESS, 1000);
                }
                break;
            case SHOW_TOAST:
                final Bundle bundle = msg.getData();
                final String text = bundle.getString("text");
                Toast.makeText(PlaybackService.this.getApplicationContext(), text, Toast.LENGTH_LONG).show();
                break;
            }
        }
    }

    private static final int REQ_CODE = 123;
    private static final int NOTIFICATION_ID = 99;

    @TargetApi(Build.VERSION_CODES.LOLLIPOP)
    private void showNotification() {
        PendingIntent piStop = PendingIntent.getBroadcast(this, REQ_CODE, new Intent(ACTION_REMOTE_STOP),
                PendingIntent.FLAG_UPDATE_CURRENT);
        PendingIntent piBackward = PendingIntent.getBroadcast(this, REQ_CODE, new Intent(ACTION_REMOTE_BACKWARD),
                PendingIntent.FLAG_UPDATE_CURRENT);
        PendingIntent piPlay = PendingIntent.getBroadcast(this, REQ_CODE, new Intent(ACTION_REMOTE_PLAYPAUSE),
                PendingIntent.FLAG_UPDATE_CURRENT);
        PendingIntent piForward = PendingIntent.getBroadcast(this, REQ_CODE, new Intent(ACTION_REMOTE_FORWARD),
                PendingIntent.FLAG_UPDATE_CURRENT);

        NotificationCompat.Builder bob = new NotificationCompat.Builder(this);
        bob.addAction(R.drawable.ic_skip_previous_white_24dp, getText(R.string.previous), piBackward);
        if (mMediaPlayer.isPlaying()) {
            bob.addAction(R.drawable.ic_pause_white_24dp, getText(R.string.pause), piPlay);
        } else {
            bob.addAction(R.drawable.ic_play_arrow_white_24dp, this.getText(R.string.play), piPlay);
        }
        bob.addAction(R.drawable.ic_skip_next_white_24dp, getText(R.string.next), piForward);

        boolean isPlaying = mMediaPlayer.isPlaying();
        long playbackPosition = mMediaPlayer.getTime();
        MediaWrapper currentMedia = getCurrentMedia();

        Log.d(TAG, "Playback position: " + playbackPosition);

        bob.setStyle(new NotificationCompat.MediaStyle().setMediaSession(mMediaSession.getSessionToken())
                .setShowActionsInCompactView(1).setShowCancelButton(true).setCancelButtonIntent(piStop))
                .setSmallIcon(R.drawable.ic_notification).setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
                .setContentTitle(currentMedia.getTitle())
                .setContentText(currentMedia.getArtist() + " - " + currentMedia.getAlbum())
                .setTicker(currentMedia.getTitle()) //TODO: used by accessibility services!
                .setShowWhen(false).setDeleteIntent(piStop);

        if (mNotificationActivity != null) {
            Intent onClickIntent = new Intent(this, mNotificationActivity.getClass());
            onClickIntent.setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
            onClickIntent.setAction(mNotificationAction);
            onClickIntent.addCategory(Intent.CATEGORY_LAUNCHER);
            PendingIntent piOnClick = PendingIntent.getActivity(this, REQ_CODE, onClickIntent,
                    PendingIntent.FLAG_UPDATE_CURRENT);
            bob.setContentIntent(piOnClick);
        }

        if (currentMedia.isPictureParsed()) {
            bob.setLargeIcon(currentMedia.getPicture());
        } else if (currentMedia.getArtworkURL() != null) {
            loadArtworkFromUrlAsync(currentMedia.getArtworkURL(), bob);
        } else {
            bob.setLargeIcon(getDefaultArtwork());
        }

        Notification notification = bob.build();
        if (!AndroidUtil.isLolliPopOrLater() || isPlaying) {
            startForeground(NOTIFICATION_ID, notification);
        } else {
            stopForeground(false);
            NotificationManagerCompat.from(this).notify(NOTIFICATION_ID, notification);
        }
    }

    private Bitmap mDefaultArtworkBitmap;

    private Bitmap getDefaultArtwork() {
        if (mDefaultArtworkBitmap == null) {
            mDefaultArtworkBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.default_album_artwork);
        }
        return mDefaultArtworkBitmap;
    }

    private void loadArtworkFromUrlAsync(final String artworkUrl, final NotificationCompat.Builder builder) {
        Glide.with(getApplicationContext()).load(artworkUrl).asBitmap().fitCenter()
                .placeholder(R.drawable.default_album_artwork).into(new SimpleTarget<Bitmap>() {
                    @Override
                    public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
                        builder.setLargeIcon(resource);
                        NotificationManagerCompat.from(PlaybackService.this).notify(NOTIFICATION_ID,
                                builder.build());
                        getCurrentMedia().setPicture(resource);
                        getCurrentMedia().setPictureParsed(true);
                    }
                });
    }

    private void hideNotification() {
        hideNotification(true);
    }

    /**
     * Hides the VLC notification and stops the service.
     *
     * @param stopPlayback True to also stopService playback at the same time. Set to false to preserve playback (e.g. for vout events)
     */
    private void hideNotification(boolean stopPlayback) {
        stopForeground(true);
        NotificationManagerCompat.from(this).cancel(NOTIFICATION_ID);
        if (stopPlayback)
            stopSelf();
    }

    /**
     * Sets Activity to show on Notification click
     * @param notificationActivity Activity to send intent to
     * @param action String identifier for action sent with intent
     */
    public <T extends Activity> void setNotificationActivity(T notificationActivity, String action) {
        mNotificationActivity = notificationActivity;
        mNotificationAction = action;
    }

    @MainThread
    public void pause() {
        if (mPausable) {
            savePosition();
            mHandler.removeMessages(SHOW_PROGRESS);
            // hideNotification(); // <-- see event handler
            mMediaPlayer.pause();
            broadcastMetadata();
        }
    }

    @MainThread
    public void play() {
        if (!hasCurrentMedia() && mMediaList.size() > 0) {
            playIndex(0);
        } else if (hasCurrentMedia()) {
            mMediaPlayer.play();
            mHandler.sendEmptyMessage(SHOW_PROGRESS);
            updateMetadata();
            broadcastMetadata();
        }
    }

    @MainThread
    public void stopPlayback() {
        if (mMediaSession != null) {
            mMediaSession.setActive(false);
            mMediaSession.release();
            mMediaSession = null;
        }
        if (mMediaPlayer == null)
            return;
        savePosition();
        final Media media = mMediaPlayer.getMedia();
        if (media != null) {
            media.setEventListener(null);
            mMediaPlayer.setEventListener(null);
            mMediaPlayer.stop();
            mMediaPlayer.setMedia(null);
            media.release();
        }
        mMediaList.removeEventListener(mListEventListener);
        mCurrentIndex = -1;
        mPrevious.clear();
        mHandler.removeMessages(SHOW_PROGRESS);
        hideNotification();
        broadcastMetadata();
        executeUpdate();
        executeUpdateProgress();
        changeAudioFocus(false);
    }

    @MainThread
    public void stopService() {
        removeAllCallbacks();
        stopForeground(true);
        stopPlayback();
        stopSelf();
    }

    private void determinePrevAndNextIndices() {
        determinePrevAndNextIndices(false);
    }

    private void determinePrevAndNextIndices(boolean expand) {
        if (expand) {
            mExpanding.set(true);
            mNextIndex = expand();
            mExpanding.set(false);
        } else {
            mNextIndex = -1;
        }
        mPrevIndex = -1;

        if (mNextIndex == -1) {
            // No subitems; play the next item.
            int size = mMediaList.size();
            mShuffling &= size > 2;

            // Repeating once doesn't change the index
            if (mRepeating == REPEAT_ONE) {
                mPrevIndex = mNextIndex = mCurrentIndex;
            } else {

                if (mShuffling) {
                    if (mPrevious.size() > 0)
                        mPrevIndex = mPrevious.peek();
                    // If we've played all songs already in shuffle, then either
                    // reshuffle or stopService (depending on RepeatType).
                    if (mPrevious.size() + 1 == size) {
                        if (mRepeating == REPEAT_NONE) {
                            mNextIndex = -1;
                            return;
                        } else {
                            mPrevious.clear();
                            mRandom = new Random(System.currentTimeMillis());
                        }
                    }
                    if (mRandom == null)
                        mRandom = new Random(System.currentTimeMillis());
                    // Find a new index not in mPrevious.
                    do {
                        mNextIndex = mRandom.nextInt(size);
                    } while (mNextIndex == mCurrentIndex || mPrevious.contains(mNextIndex));

                } else {
                    // normal playback
                    if (mCurrentIndex > 0)
                        mPrevIndex = mCurrentIndex - 1;
                    if (mCurrentIndex + 1 < size)
                        mNextIndex = mCurrentIndex + 1;
                    else {
                        if (mRepeating == REPEAT_NONE) {
                            mNextIndex = -1;
                        } else {
                            mNextIndex = 0;
                        }
                    }
                }
            }
        }
    }

    private void initMediaSession() {
        ComponentName mediaButtonEventReceiver = new ComponentName(this, RemoteControlEventReceiver.class);
        mSessionCallback = new MediaSessionCallback();
        mMediaSession = new MediaSessionCompat(this, "Lyt", mediaButtonEventReceiver, null);
        mMediaSession.setFlags(
                MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS | MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
        mMediaSession.setCallback(mSessionCallback);
        try {
            mMediaSession.setActive(true);
        } catch (NullPointerException e) {
            // Some versions of KitKat do not support AudioManager.registerMediaButtonIntent
            // with a PendingIntent. They will throw a NullPointerException, in which case
            // they should be able to activate a MediaSessionCompat with only transport controls.
            mMediaSession.setActive(false);
            mMediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS);
            mMediaSession.setActive(true);
        }
    }

    public MediaSessionCompat.Token getSessionToken() {
        if (mMediaSession == null)
            initMediaSession();
        return mMediaSession.getSessionToken();
    }

    private final class MediaSessionCallback extends MediaSessionCompat.Callback {
        @Override
        public void onPlay() {
            play();
        }

        @Override
        public void onPause() {
            pause();
        }

        @Override
        public void onStop() {
            stopService();
        }

        @Override
        public void onSkipToNext() {
            next();
        }

        @Override
        public void onSkipToPrevious() {
            previous();
        }

        @Override
        public void onSeekTo(long pos) {
            setTime(pos);
        }

        @Override
        public void onFastForward() {
            next();
        }

        @Override
        public void onRewind() {
            previous();
        }
    }

    protected void updateMetadata() {
        MediaWrapper media = getCurrentMedia();
        if (media == null)
            return;
        if (mMediaSession == null)
            initMediaSession();
        String title = media.getNowPlaying();
        if (title == null)
            title = media.getTitle();
        MediaMetadataCompat.Builder bob = new MediaMetadataCompat.Builder();
        bob.putString(MediaMetadataCompat.METADATA_KEY_TITLE, title)
                .putString(MediaMetadataCompat.METADATA_KEY_GENRE, Utils.getMediaGenre(media))
                .putLong(MediaMetadataCompat.METADATA_KEY_TRACK_NUMBER, media.getTrackNumber())
                .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, Utils.getMediaArtist(media))
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM_ARTIST, Utils.getMediaReferenceArtist(media))
                .putString(MediaMetadataCompat.METADATA_KEY_ALBUM, Utils.getMediaAlbum(media))
                .putString(MediaMetadataCompat.METADATA_KEY_ART_URI, media.getArtworkURL())
                .putLong(MediaMetadataCompat.METADATA_KEY_DURATION, media.getLength());
        mMediaSession.setMetadata(bob.build());
    }

    protected void publishState(int state) {
        if (mMediaSession == null)
            return;
        PlaybackStateCompat.Builder bob = new PlaybackStateCompat.Builder();
        bob.setActions(PLAYBACK_ACTIONS);
        switch (state) {
        case MediaPlayer.Event.Playing:
            bob.setState(PlaybackStateCompat.STATE_PLAYING, getCurrentMediaPosition(), getRate());
            break;
        case MediaPlayer.Event.Stopped:
            bob.setState(PlaybackStateCompat.STATE_STOPPED, getCurrentMediaPosition(), getRate());
            break;
        default:
            bob.setState(PlaybackStateCompat.STATE_PAUSED, getCurrentMediaPosition(), getRate());
        }
        PlaybackStateCompat pbState = bob.build();
        mMediaSession.setPlaybackState(pbState);
        mMediaSession.setActive(state != PlaybackStateCompat.STATE_STOPPED);
    }

    private void notifyTrackChanged() {
        mHandler.sendEmptyMessage(SHOW_PROGRESS);
        updateMetadata();
        broadcastMetadata();
    }

    private void onMediaChanged() {
        notifyTrackChanged();

        saveCurrentMedia();
        determinePrevAndNextIndices();
    }

    private void onMediaListChanged() {
        saveMediaList();
        determinePrevAndNextIndices();
        executeUpdate();
    }

    @MainThread
    public void next() {
        int size = mMediaList.size();

        mPrevious.push(mCurrentIndex);
        mCurrentIndex = mNextIndex;
        if (size == 0 || mCurrentIndex < 0 || mCurrentIndex >= size) {
            if (mCurrentIndex < 0)
                saveCurrentMedia();
            Log.w(TAG, "Warning: invalid next index, aborted !");
            stopService();
            return;
        }
        playIndex(mCurrentIndex, 0);
        saveCurrentMedia();
    }

    @MainThread
    public void previous() {
        int size = mMediaList.size();
        if (hasPrevious() && mCurrentIndex > 0 && (!mMediaPlayer.isSeekable() || mMediaPlayer.getTime() < 5000l)) {
            mCurrentIndex = mPrevIndex;
            if (mPrevious.size() > 0)
                mPrevious.pop();
            if (size == 0 || mPrevIndex < 0 || mCurrentIndex >= size) {
                Log.w(TAG, "Warning: invalid previous index, aborted !");
                stopService();
                return;
            }
        } else
            setPosition(0f);

        playIndex(mCurrentIndex, 0);
        saveCurrentMedia();
    }

    @MainThread
    public void shuffle() {
        if (mShuffling)
            mPrevious.clear();
        mShuffling = !mShuffling;
        savePosition();
        determinePrevAndNextIndices();
    }

    @MainThread
    public void setRepeatType(int repeatType) {
        mRepeating = repeatType;
        savePosition();
        determinePrevAndNextIndices();
    }

    private void broadcastMetadata() {
        MediaWrapper media = getCurrentMedia();
        if (media == null || media.getType() != MediaWrapper.TYPE_AUDIO)
            return;

        boolean playing = mMediaPlayer.isPlaying();

        Intent broadcast = new Intent("com.android.music.metachanged");
        broadcast.putExtra("track", media.getTitle());
        broadcast.putExtra("artist", media.getArtist());
        broadcast.putExtra("album", media.getAlbum());
        broadcast.putExtra("duration", media.getLength());
        broadcast.putExtra("playing", playing);
        sendBroadcast(broadcast);
    }

    public synchronized void loadLastPlaylist(int type) {
        boolean audio = type == TYPE_AUDIO;
        String currentMedia = mSettings.getString(audio ? "current_song" : "current_media", "");
        if (currentMedia.equals(""))
            return;
        String[] locations = mSettings.getString(audio ? "audio_list" : "media_list", "").split(" ");
        if (locations.length == 0)
            return;

        List<String> mediaPathList = new ArrayList<String>(locations.length);
        for (int i = 0; i < locations.length; ++i)
            mediaPathList.add(Uri.decode(locations[i]));

        mShuffling = mSettings.getBoolean(audio ? "audio_shuffling" : "media_shuffling", false);
        mRepeating = mSettings.getInt(audio ? "audio_repeating" : "media_repeating", REPEAT_NONE);
        int position = mSettings.getInt(audio ? "position_in_audio_list" : "position_in_media_list",
                Math.max(0, mediaPathList.indexOf(currentMedia)));
        long time = mSettings.getLong(audio ? "position_in_song" : "position_in_media", -1);
        mSavedTime = time;
        // load playlist
        loadLocations(mediaPathList);
        if (time > 0)
            setTime(time);
        SharedPreferences.Editor editor = mSettings.edit();
        editor.putInt(audio ? "position_in_audio_list" : "position_in_media_list", 0);
        editor.putLong(audio ? "position_in_song" : "position_in_media", 0);
        editor.apply();
    }

    private synchronized void saveCurrentMedia() {
        boolean audio = true;
        for (int i = 0; i < mMediaList.size(); i++) {
            if (mMediaList.getMedia(i).getType() == MediaWrapper.TYPE_VIDEO)
                audio = false;
        }
        SharedPreferences.Editor editor = mSettings.edit();
        editor.putString(audio ? "current_song" : "current_media", mMediaList.getMRL(Math.max(mCurrentIndex, 0)));
        editor.apply();
    }

    private synchronized void saveMediaList() {
        if (getCurrentMedia() == null)
            return;
        StringBuilder locations = new StringBuilder();
        boolean audio = true;
        for (int i = 0; i < mMediaList.size(); i++) {
            if (mMediaList.getMedia(i).getType() == MediaWrapper.TYPE_VIDEO)
                audio = false;
            locations.append(" ").append(Uri.encode(mMediaList.getMRL(i)));
        }
        //We save a concatenated String because putStringSet is APIv11.
        SharedPreferences.Editor editor = mSettings.edit();
        editor.putString(audio ? "audio_list" : "media_list", locations.toString().trim());
        editor.apply();
    }

    private synchronized void savePosition() {
        if (getCurrentMedia() == null)
            return;
        SharedPreferences.Editor editor = mSettings.edit();
        boolean audio = true;
        for (int i = 0; i < mMediaList.size(); i++) {
            if (mMediaList.getMedia(i).getType() == MediaWrapper.TYPE_VIDEO)
                audio = false;
        }
        editor.putBoolean(audio ? "audio_shuffling" : "media_shuffling", mShuffling);
        editor.putInt(audio ? "audio_repeating" : "media_repeating", mRepeating);
        editor.putInt(audio ? "position_in_audio_list" : "position_in_media_list", mCurrentIndex);
        editor.putLong(audio ? "position_in_song" : "position_in_media", mMediaPlayer.getTime());
        editor.apply();
    }

    private boolean validateLocation(String location) {
        /* Check if the MRL contains a scheme */
        if (!location.matches("\\w+://.+"))
            location = "file://".concat(location);
        if (location.toLowerCase(Locale.ENGLISH).startsWith("file://")) {
            /* Ensure the file exists */
            File f;
            try {
                f = new File(new URI(location));
            } catch (URISyntaxException e) {
                return false;
            } catch (IllegalArgumentException e) {
                return false;
            }
            if (!f.isFile())
                return false;
        }
        return true;
    }

    private void showToast(String text, int duration) {
        Message msg = new Message();
        Bundle bundle = new Bundle();
        bundle.putString("text", text);
        bundle.putInt("duration", duration);
        msg.setData(bundle);
        msg.what = SHOW_TOAST;
        mHandler.sendMessage(msg);
    }

    @MainThread
    public boolean isPlaying() {
        return mMediaPlayer.isPlaying();
    }

    @MainThread
    public boolean isSeekable() {
        return mSeekable;
    }

    @MainThread
    public boolean isPausable() {
        return mPausable;
    }

    @MainThread
    public boolean isShuffling() {
        return mShuffling;
    }

    @MainThread
    public boolean canShuffle() {
        return (getMediaListSize() > 2) ? true : false;
    }

    @MainThread
    public int getRepeatType() {
        return mRepeating;
    }

    @MainThread
    public boolean hasMedia() {
        return hasCurrentMedia();
    }

    @MainThread
    public boolean hasPlaylist() {
        return (getMediaListSize() > 1) ? true : false;
    }

    @MainThread
    public String getAlbum() {
        if (hasCurrentMedia())
            return Utils.getMediaAlbum(getCurrentMedia());
        else
            return null;
    }

    @MainThread
    public String getArtist() {
        if (hasCurrentMedia()) {
            final MediaWrapper media = getCurrentMedia();
            return media.getNowPlaying() != null ? media.getTitle() : Utils.getMediaArtist(media);
        } else
            return null;
    }

    @MainThread
    public String getArtistPrev() {
        if (mPrevIndex != -1)
            return Utils.getMediaArtist(mMediaList.getMedia(mPrevIndex));
        else
            return null;
    }

    @MainThread
    public String getArtistNext() {
        if (mNextIndex != -1)
            return Utils.getMediaArtist(mMediaList.getMedia(mNextIndex));
        else
            return null;
    }

    @MainThread
    public String getTitle() {
        if (hasCurrentMedia())
            return getCurrentMedia().getNowPlaying() != null ? getCurrentMedia().getNowPlaying()
                    : getCurrentMedia().getTitle();
        else
            return null;
    }

    @MainThread
    public String getTitlePrev() {
        if (mPrevIndex != -1)
            return mMediaList.getMedia(mPrevIndex).getTitle();
        else
            return null;
    }

    @MainThread
    public String getTitleNext() {
        if (mNextIndex != -1)
            return mMediaList.getMedia(mNextIndex).getTitle();
        else
            return null;
    }

    @MainThread
    public synchronized void addCallback(PlaybackEventHandler handler) {
        if (!mPlaybackEventHandlers.contains(handler)) {
            mPlaybackEventHandlers.add(handler);
            if (hasCurrentMedia())
                mHandler.sendEmptyMessage(SHOW_PROGRESS);
        }
    }

    @MainThread
    public synchronized void removeCallback(PlaybackEventHandler handler) {
        mPlaybackEventHandlers.remove(handler);
    }

    @MainThread
    public synchronized void removeAllCallbacks() {
        mPlaybackEventHandlers.clear();
    }

    @MainThread
    public long getTime() {
        return mMediaPlayer.getTime();
    }

    @MainThread
    public long getLength() {
        return mMediaPlayer.getLength();
    }

    /**
     * Loads a selection of files (a non-user-supplied collection of media)
     * into the primary or "currently playing" playlist.
     *
     * @param mediaPathList A list of locations to load
     */
    @MainThread
    public void loadLocations(List<String> mediaPathList) {
        ArrayList<MediaWrapper> mediaList = new ArrayList<MediaWrapper>();
        //        MediaDatabase db = MediaDatabase.getInstance();

        for (int i = 0; i < mediaPathList.size(); i++) {
            String location = mediaPathList.get(i);
            MediaWrapper mediaWrapper = null; //db.getMedia(Uri.parse(location));
            if (mediaWrapper == null) {
                if (!validateLocation(location)) {
                    Log.w(TAG, "Invalid location " + location);
                    showToast("Invalid media location", Toast.LENGTH_SHORT);
                    continue;
                }
                Log.v(TAG, "Creating on-the-fly Media object for " + location);
                mediaWrapper = new MediaWrapper(Uri.parse(location));
            }
            mediaList.add(mediaWrapper);
        }
        load(mediaList);
    }

    @MainThread
    public void loadUri(Uri uri) {
        String path = uri.toString();
        if (TextUtils.equals(uri.getScheme(), "content")) {
            path = "file://" + Utils.getPathFromURI(uri);
        }
        loadLocation(path);
    }

    @MainThread
    public void loadLocation(String mediaPath) {
        loadLocations(Collections.singletonList(mediaPath));
    }

    @MainThread
    public void load(List<MediaWrapper> mediaList) {
        Log.v(TAG, "Loading list: " + mediaList.toString());

        if (hasCurrentMedia())
            savePosition();

        mMediaList.removeEventListener(mListEventListener);
        mMediaList.clear();
        MediaWrapperList currentMediaList = mMediaList;

        mPrevious.clear();

        for (int i = 0; i < mediaList.size(); i++) {
            currentMediaList.add(mediaList.get(i));
        }

        if (mMediaList.size() == 0) {
            Log.w(TAG, "Warning: empty media list, nothing to play !");
            return;
        }

        // Add handler after loading the list
        mMediaList.addEventListener(mListEventListener);

        // Autoplay disabled
        //playIndex(mCurrentIndex, 0);
        saveMediaList();
        //onMediaChanged();
    }

    @MainThread
    public void load(MediaWrapper media) {
        ArrayList<MediaWrapper> arrayList = new ArrayList<MediaWrapper>();
        arrayList.add(media);
        load(arrayList);
    }

    /**
     * Play a media from the media list (playlist)
     *
     * @param index The index of the media
     * @param flags LibVLC.MEDIA_* flags
     */
    public void playIndex(int index, int flags) {
        if (mMediaList.size() == 0) {
            Log.w(TAG, "Warning: empty media list, nothing to play !");
            return;
        }
        if (index >= 0 && index < mMediaList.size()) {
            mCurrentIndex = index;
        } else {
            Log.w(TAG, "Warning: index " + index + " out of bounds");
            mCurrentIndex = 0;
        }

        String mrl = mMediaList.getMRL(index);
        if (mrl == null)
            return;
        final MediaWrapper mw = mMediaList.getMedia(index);
        if (mw == null)
            return;

        /* Pausable and seekable are true by default */
        mParsed = false;
        mPausable = mSeekable = true;
        final Media media = new Media(LibVLC(), mw.getUri());
        //        VLCOptions.setMediaOptions(media, this, flags | mw.getFlags());
        media.setEventListener(mMediaListener);
        mMediaPlayer.setMedia(media);
        media.release();

        mMediaPlayer.setVideoTitleDisplay(MediaPlayer.Position.Disable, 0);
        changeAudioFocus(true);
        mMediaPlayer.setEventListener(mMediaPlayerListener);
        mMediaPlayer.play();
        if (mSavedTime != 0l)
            mMediaPlayer.setTime(mSavedTime);
        mSavedTime = 0l;

        determinePrevAndNextIndices();
    }

    /**
     * Use this function to play a media inside whatever MediaList LibVLC is following.
     *
     * Unlike load(), it does not import anything into the primary list.
     */
    @MainThread
    public void playIndex(int index) {
        playIndex(index, 0);
    }

    /**
     * Append to the current existing playlist
     */
    @MainThread
    public void append(List<MediaWrapper> mediaList) {
        if (!hasCurrentMedia()) {
            load(mediaList);
            return;
        }

        for (int i = 0; i < mediaList.size(); i++) {
            MediaWrapper mediaWrapper = mediaList.get(i);
            mMediaList.add(mediaWrapper);
        }
        onMediaListChanged();
    }

    @MainThread
    public void append(MediaWrapper media) {
        ArrayList<MediaWrapper> arrayList = new ArrayList<MediaWrapper>();
        arrayList.add(media);
        append(arrayList);
    }

    /**
     * Move an item inside the playlist.
     */
    @MainThread
    public void moveItem(int positionStart, int positionEnd) {
        mMediaList.move(positionStart, positionEnd);
        PlaybackService.this.saveMediaList();
    }

    @MainThread
    public void insertItem(int position, MediaWrapper mw) {
        mMediaList.insert(position, mw);
        saveMediaList();
        determinePrevAndNextIndices();
    }

    @MainThread
    public void remove(int position) {
        mMediaList.remove(position);
        saveMediaList();
        determinePrevAndNextIndices();
    }

    @MainThread
    public void removeLocation(String location) {
        mMediaList.remove(location);
        saveMediaList();
        determinePrevAndNextIndices();
    }

    public int getMediaListSize() {
        return mMediaList.size();
    }

    @MainThread
    public List<MediaWrapper> getMedias() {
        final ArrayList<MediaWrapper> ml = new ArrayList<MediaWrapper>();
        for (int i = 0; i < mMediaList.size(); i++) {
            ml.add(mMediaList.getMedia(i));
        }
        return ml;
    }

    @MainThread
    public List<String> getMediaLocations() {
        ArrayList<String> medias = new ArrayList<String>();
        for (int i = 0; i < mMediaList.size(); i++) {
            medias.add(mMediaList.getMRL(i));
        }
        return medias;
    }

    @MainThread
    public String getCurrentMediaLocation() {
        return mMediaList.getMRL(mCurrentIndex);
    }

    @MainThread
    public int getCurrentMediaPosition() {
        return mCurrentIndex;
    }

    @MainThread
    public MediaWrapper getCurrentMediaWrapper() {
        return PlaybackService.this.getCurrentMedia();
    }

    @MainThread
    public void setTime(long time) {
        if (mSeekable)
            mMediaPlayer.setTime(time);
    }

    @MainThread
    public boolean hasNext() {
        return mNextIndex != -1;
    }

    @MainThread
    public boolean hasPrevious() {
        return mPrevIndex != -1;
    }

    @MainThread
    public void detectHeadset(boolean enable) {
        mDetectHeadset = enable;
    }

    @MainThread
    public float getRate() {
        return mMediaPlayer.getRate();
    }

    @MainThread
    public void setRate(float rate) {
        mMediaPlayer.setRate(rate);
    }

    @MainThread
    public void navigate(int where) {
        mMediaPlayer.navigate(where);
    }

    @MainThread
    public MediaPlayer.Chapter[] getChapters(int title) {
        return mMediaPlayer.getChapters(title);
    }

    @MainThread
    public MediaPlayer.Title[] getTitles() {
        return mMediaPlayer.getTitles();
    }

    @MainThread
    public int getChapterIdx() {
        return mMediaPlayer.getChapter();
    }

    @MainThread
    public void setChapterIdx(int chapter) {
        mMediaPlayer.setChapter(chapter);
    }

    @MainThread
    public int getTitleIdx() {
        return mMediaPlayer.getTitle();
    }

    @MainThread
    public void setTitleIdx(int title) {
        mMediaPlayer.setTitle(title);
    }

    @MainThread
    public int getVolume() {
        return mMediaPlayer.getVolume();
    }

    @MainThread
    public int setVolume(int volume) {
        return mMediaPlayer.setVolume(volume);
    }

    @MainThread
    public void setPosition(float pos) {
        if (mSeekable)
            mMediaPlayer.setPosition(pos);
    }

    @MainThread
    public int getAudioTracksCount() {
        return mMediaPlayer.getAudioTracksCount();
    }

    @MainThread
    public MediaPlayer.TrackDescription[] getAudioTracks() {
        return mMediaPlayer.getAudioTracks();
    }

    @MainThread
    public int getAudioTrack() {
        return mMediaPlayer.getAudioTrack();
    }

    @MainThread
    public boolean setAudioTrack(int index) {
        return mMediaPlayer.setAudioTrack(index);
    }

    @MainThread
    public int getVideoTracksCount() {
        return mMediaPlayer.getVideoTracksCount();
    }

    @MainThread
    public boolean addSubtitleTrack(String path) {
        return mMediaPlayer.setSubtitleFile(path);
    }

    @MainThread
    public MediaPlayer.TrackDescription[] getSpuTracks() {
        return mMediaPlayer.getSpuTracks();
    }

    @MainThread
    public int getSpuTrack() {
        return mMediaPlayer.getSpuTrack();
    }

    @MainThread
    public boolean setSpuTrack(int index) {
        return mMediaPlayer.setSpuTrack(index);
    }

    @MainThread
    public int getSpuTracksCount() {
        return mMediaPlayer.getSpuTracksCount();
    }

    @MainThread
    public boolean setAudioDelay(long delay) {
        return mMediaPlayer.setAudioDelay(delay);
    }

    @MainThread
    public long getAudioDelay() {
        return mMediaPlayer.getAudioDelay();
    }

    @MainThread
    public boolean setSpuDelay(long delay) {
        return mMediaPlayer.setSpuDelay(delay);
    }

    @MainThread
    public long getSpuDelay() {
        return mMediaPlayer.getSpuDelay();
    }

    @MainThread
    public void setEqualizer(MediaPlayer.Equalizer equalizer) {
        mMediaPlayer.setEqualizer(equalizer);
    }

    /**
     * Expand the current media.
     * @return the index of the media was expanded, and -1 if no media was expanded
     */
    @MainThread
    public int expand() {
        final Media media = mMediaPlayer.getMedia();
        if (media == null)
            return -1;
        final MediaList ml = media.subItems();
        media.release();
        int ret;

        if (ml.getCount() > 0) {
            mMediaList.remove(mCurrentIndex);
            for (int i = ml.getCount() - 1; i >= 0; --i) {
                final Media child = ml.getMediaAt(i);
                child.parse();
                mMediaList.insert(mCurrentIndex, new MediaWrapper(child));
                child.release();
            }
            ret = 0;
        } else {
            ret = -1;
        }
        ml.release();
        return ret;
    }

    public void restartMediaPlayer() {
        stopService();
        mMediaPlayer.release();
        mMediaPlayer = newMediaPlayer();
        /* TODO RESUME */
    }

    public abstract class WeakHandler<T> extends Handler {
        private WeakReference<T> mOwner;

        public WeakHandler(T owner) {
            mOwner = new WeakReference<T>(owner);
        }

        public T getOwner() {
            return mOwner.get();
        }
    }
}