org.runbuddy.tomahawk.mediaplayers.PluginMediaPlayer.java Source code

Java tutorial

Introduction

Here is the source code for org.runbuddy.tomahawk.mediaplayers.PluginMediaPlayer.java

Source

/* == This file is part of Tomahawk Player - <http://tomahawk-player.org> ===
 *
 *   Copyright 2015, Enno Gottschalk <mrmaffen@googlemail.com>
 *
 *   Tomahawk 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 3 of the License, or
 *   (at your option) any later version.
 *
 *   Tomahawk 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 Tomahawk. If not, see <http://www.gnu.org/licenses/>.
 */
package org.runbuddy.tomahawk.mediaplayers;

import android.content.ComponentName;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.support.v4.media.session.PlaybackStateCompat;
import android.util.Log;

import org.runbuddy.libtomahawk.resolver.PipeLine;
import org.runbuddy.libtomahawk.resolver.Query;
import org.runbuddy.libtomahawk.resolver.ScriptResolver;
import org.runbuddy.tomahawk.services.PlaybackService;
import org.runbuddy.tomahawk.utils.WeakReferenceHandler;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import de.greenrobot.event.EventBus;

public abstract class PluginMediaPlayer extends TomahawkMediaPlayer {

    private static final String TAG = PluginMediaPlayer.class.getSimpleName();

    /**
     * Command to the service to register a client, receiving callbacks from the service. The
     * Message's replyTo field must be a Messenger of the client where callbacks should be sent.
     */
    public static final int MSG_REGISTER_CLIENT = 1;

    /**
     * Command to the service to unregister a client, ot stop receiving callbacks from the service.
     * The Message's replyTo field must be a Messenger of the client as previously given with
     * MSG_REGISTER_CLIENT.
     */
    public static final int MSG_UNREGISTER_CLIENT = 2;

    /**
     * Commands to the service
     */
    protected static final int MSG_PREPARE = 100;

    protected static final String MSG_PREPARE_ARG_URI = "uri";

    protected static final String MSG_PREPARE_ARG_ACCESSTOKEN = "accessToken";

    protected static final String MSG_PREPARE_ARG_ACCESSTOKENEXPIRES = "accessTokenExpires";

    protected static final int MSG_PLAY = 101;

    protected static final int MSG_PAUSE = 102;

    protected static final int MSG_SEEK = 103;

    protected static final String MSG_SEEK_ARG_MS = "ms";

    protected static final int MSG_SETBITRATE = 104;

    protected static final String MSG_SETBITRATE_ARG_MODE = "mode";

    /**
     * Commands to the client
     */
    protected static final int MSG_ONPAUSE = 200;

    protected static final int MSG_ONPLAY = 201;

    protected static final int MSG_ONPREPARED = 202;

    protected static final String MSG_ONPREPARED_ARG_URI = "uri";

    protected static final int MSG_ONPLAYERENDOFTRACK = 203;

    protected static final int MSG_ONPLAYERPOSITIONCHANGED = 204;

    protected static final String MSG_ONPLAYERPOSITIONCHANGED_ARG_POSITION = "position";

    protected static final String MSG_ONPLAYERPOSITIONCHANGED_ARG_TIMESTAMP = "timestamp";

    protected static final int MSG_ONERROR = 205;

    protected static final String MSG_ONERROR_ARG_MESSAGE = "message";

    private String mPluginName;

    private String mPackageName;

    private boolean mIsRequestingService = false;

    /**
     * Messenger for communicating with service.
     */
    private Messenger mService = null;

    private List<Message> mWaitingMessages = new ArrayList<>();

    private TomahawkMediaPlayerCallback mMediaPlayerCallback;

    private boolean mIsPlaying;

    private int mPlayState = PlaybackStateCompat.STATE_NONE;

    private Query mPreparedQuery;

    private Query mPreparingQuery;

    private Query mActuallyPreparingQuery;

    private boolean mShowFakePosition = false;

    private final DisableFakePositionHandler mDisableFakePositionHandler = new DisableFakePositionHandler(this);

    private static class DisableFakePositionHandler extends WeakReferenceHandler<PluginMediaPlayer> {

        public DisableFakePositionHandler(PluginMediaPlayer referencedObject) {
            super(referencedObject);
        }

        @Override
        public void handleMessage(Message msg) {
            if (getReferencedObject() != null) {
                getReferencedObject().mShowFakePosition = false;
            }
        }
    }

    private ServiceConnection mConnection = new ServiceConnection() {

        public void onServiceConnected(ComponentName className, IBinder service) {
            // We want to monitor the service for as long as we are
            // connected to it.
            try {
                // This is called when the connection with the service has been
                // established, giving us the service object we can use to
                // interact with the service.  We are communicating with our
                // service through an IDL interface, so get a client-side
                // representation of that from the raw service object.
                Messenger messenger = new Messenger(service);
                Message msg = Message.obtain(null, PluginMediaPlayer.MSG_REGISTER_CLIENT);
                msg.replyTo = getReceivingMessenger();
                messenger.send(msg);
                setService(messenger);
                Log.d(TAG, "Successfully attached to service! :)");
            } catch (RemoteException e) {
                // In this case the service has crashed before we could even
                // do anything with it; we can count on soon being
                // disconnected (and then reconnected if it can be restarted)
                // so there is no need to do anything here.
                Log.e(TAG, "Service crashed before we could do anything."
                        + " Waiting for it to restart and report for duty...");
            }
        }

        public void onServiceDisconnected(ComponentName className) {
            // This is called when the connection with the service has been
            // unexpectedly disconnected -- that is, its process crashed.
            setService(null);
            Log.e(TAG, "Service crashed :(");
        }
    };

    private boolean mRestorePosition = false;

    private String mPreparedUri;

    private long mPositionTimeStamp;

    private int mPositionOffset;

    private long mFakePositionTimeStamp;

    private long mFakePositionOffset;

    private Map<String, Query> mUriToQueryMap = new HashMap<>();

    public PluginMediaPlayer(String pluginName, String packageName) {
        mPluginName = pluginName;
        mPackageName = packageName;
    }

    public ScriptResolver getScriptResolver() {
        ScriptResolver scriptResolver = PipeLine.get().getResolver(mPluginName);
        if (scriptResolver == null) {
            Log.e(TAG, "getScriptResolver - Couldn't find associated ScriptResolver!");
        }
        return scriptResolver;
    }

    /**
     * Handler of incoming messages from service.
     */
    private static class IncomingHandler extends WeakReferenceHandler<PluginMediaPlayer> {

        public IncomingHandler(PluginMediaPlayer referencedObject) {
            super(referencedObject);
        }

        @Override
        public void handleMessage(Message msg) {
            PluginMediaPlayer mp = getReferencedObject();
            switch (msg.what) {
            case MSG_ONPREPARED:
                String uri = msg.getData().getString(MSG_ONPREPARED_ARG_URI);
                Log.d(TAG, "onPrepared() - uri: " + uri);
                if (mp.mPreparingQuery != null && mp.mActuallyPreparingQuery == mp.mPreparingQuery) {
                    mp.mActuallyPreparingQuery = null;
                    mp.mPreparedQuery = mp.mUriToQueryMap.get(uri);
                    mp.mPreparingQuery = null;
                    if (mp.mMediaPlayerCallback != null) {
                        mp.mMediaPlayerCallback.onPrepared(mp, mp.mPreparedQuery);
                    } else {
                        Log.e(TAG, "Wasn't able to call onPrepared because callback object is null");
                    }
                    mp.handlePlayState();
                    if (mp.mRestorePosition && mp.mPreparedUri != null && mp.mPreparedUri.equals(uri)) {
                        mp.mRestorePosition = false;
                        mp.seekTo(mp.mPositionOffset);
                    } else {
                        mp.mPositionOffset = 0;
                        mp.mPositionTimeStamp = System.currentTimeMillis();
                    }
                    mp.mPreparedUri = uri;
                }
                break;
            case MSG_ONPLAY:
                mp.mIsPlaying = true;
                mp.mPositionTimeStamp = System.currentTimeMillis();
                break;
            case MSG_ONPAUSE:
                mp.mIsPlaying = false;
                mp.mPositionOffset = (int) (System.currentTimeMillis() - mp.mPositionTimeStamp)
                        + mp.mPositionOffset;
                mp.mPositionTimeStamp = System.currentTimeMillis();
                break;
            case MSG_ONPLAYERPOSITIONCHANGED:
                long timeStamp = msg.getData().getLong(MSG_ONPLAYERPOSITIONCHANGED_ARG_TIMESTAMP);
                int position = msg.getData().getInt(MSG_ONPLAYERPOSITIONCHANGED_ARG_POSITION);

                mp.mPositionTimeStamp = timeStamp;
                mp.mPositionOffset = position;
                break;
            case MSG_ONPLAYERENDOFTRACK:
                Log.d(TAG, "onCompletion()");
                if (mp.mMediaPlayerCallback != null) {
                    mp.mMediaPlayerCallback.onCompletion(mp, mp.mPreparedQuery);
                } else {
                    Log.e(TAG, "Wasn't able to call onCompletion because callback object is null");
                }
                break;
            case MSG_ONERROR:
                String message = msg.getData().getString(MSG_ONERROR_ARG_MESSAGE);

                if (mp.mMediaPlayerCallback != null) {
                    mp.mMediaPlayerCallback.onError(mp, message);
                } else {
                    Log.e(TAG, "Wasn't able to call onError because callback object is null");
                }
            default:
                super.handleMessage(msg);
            }
        }
    }

    /**
     * Target we publish for clients to send messages to IncomingHandler.
     */
    final Messenger mReceivingMessenger = new Messenger(new IncomingHandler(this));

    public Messenger getReceivingMessenger() {
        return mReceivingMessenger;
    }

    protected synchronized void callService(int what) {
        callService(what, null);
    }

    protected synchronized void callService(int what, Bundle bundle) {
        Message message = Message.obtain(null, what);
        message.setData(bundle);
        callService(message);
    }

    private synchronized void callService(Message message) {
        if (mService != null) {
            try {
                mService.send(message);
            } catch (RemoteException e) {
                Log.e(TAG, "Service crashed: ", e);
                mWaitingMessages.add(message);
            }
        } else {
            // cache the message, will be send in setService
            mWaitingMessages.add(message);
            if (!mIsRequestingService) {
                mIsRequestingService = true;
                requestService();
            }
        }
    }

    /**
     * Construct and send off a {@link PlaybackService.RequestServiceBindingEvent} to the {@link
     * PlaybackService}. As soon as the {@link PlaybackService} has been successfully bound to the
     * PluginService {@link #setService} will be called.
     */
    private void requestService() {
        PlaybackService.RequestServiceBindingEvent event = new PlaybackService.RequestServiceBindingEvent(
                mConnection, mPackageName);
        EventBus.getDefault().post(event);
    }

    public synchronized void setService(Messenger service) {
        mIsRequestingService = false;
        mService = service;
        if (mService != null) {
            // send all cached messages
            while (!mWaitingMessages.isEmpty()) {
                callService(mWaitingMessages.remove(0));
            }
        } else {
            mRestorePosition = true;
            mPreparedQuery = null;
            mPreparingQuery = null;
            mIsPlaying = false;
        }
    }

    public synchronized boolean isBound() {
        return mService != null;
    }

    public ServiceConnection getServiceConnection() {
        return mConnection;
    }

    public abstract String getUri(Query query);

    public abstract void prepare(String uri);

    /**
     * Prepare the given {@link Query} for playback
     *
     * @param query    the {@link Query} that should be prepared for playback
     * @param callback a {@link TomahawkMediaPlayerCallback} that should be stored and be used to
     *                 report certain callbacks back to the {@link PlaybackService}
     */
    @Override
    public void prepare(Query query, TomahawkMediaPlayerCallback callback) {
        Log.d(TAG, "prepare()");
        mMediaPlayerCallback = callback;
        mPreparedQuery = null;
        mPreparingQuery = query;
        mActuallyPreparingQuery = query;
        callService(MSG_PAUSE);

        String uri = getUri(query);
        mUriToQueryMap.put(uri, query);
        prepare(uri);
    }

    /**
     * Start playing the previously prepared {@link Query}
     */
    @Override
    public void play() {
        Log.d(TAG, "play()");
        mPlayState = PlaybackStateCompat.STATE_PLAYING;
        handlePlayState();
    }

    /**
     * Pause playing the current {@link Query}
     */
    @Override
    public void pause() {
        Log.d(TAG, "pause()");
        mPlayState = PlaybackStateCompat.STATE_PAUSED;
        handlePlayState();
    }

    /**
     * Seek to the given playback position (in ms)
     */
    @Override
    public void seekTo(final long msec) {
        Log.d(TAG, "seekTo()");
        Bundle args = new Bundle();
        args.putInt(MSG_SEEK_ARG_MS, (int) msec);
        callService(MSG_SEEK, args);

        mFakePositionOffset = msec;
        mFakePositionTimeStamp = System.currentTimeMillis();
        mShowFakePosition = true;
        // After 1 second, we set mShowFakePosition to false again
        mDisableFakePositionHandler.sendEmptyMessageDelayed(1337, 1000);
    }

    /**
     * Release any relevant resources that this {@link PluginMediaPlayer} might hold onto
     */
    @Override
    public void release() {
        Log.d(TAG, "release()");
        mPreparedQuery = null;
        mPreparingQuery = null;
        callService(MSG_PAUSE);
        mMediaPlayerCallback = null;
    }

    /**
     * @return the current track position
     */
    @Override
    public long getPosition() {
        if (mShowFakePosition) {
            if (mIsPlaying) {
                return System.currentTimeMillis() - mFakePositionTimeStamp + mFakePositionOffset;
            } else {
                return mFakePositionOffset;
            }
        } else {
            if (mIsPlaying) {
                return System.currentTimeMillis() - mPositionTimeStamp + mPositionOffset;
            } else {
                return mPositionOffset;
            }
        }
    }

    @Override
    public boolean isPlaying(Query query) {
        return mPreparedQuery == query && mIsPlaying;
    }

    @Override
    public boolean isPreparing(Query query) {
        return mPreparingQuery == query;
    }

    @Override
    public boolean isPrepared(Query query) {
        return mPreparedQuery == query;
    }

    private void handlePlayState() {
        if (mPreparedQuery != null) {
            if (mPlayState == PlaybackStateCompat.STATE_PAUSED && mIsPlaying) {
                callService(MSG_PAUSE);
            } else if (mPlayState == PlaybackStateCompat.STATE_PLAYING && !mIsPlaying) {
                callService(MSG_PLAY);
            }
        }
    }

}