Java tutorial
/* == 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); } } } }