Java tutorial
/* * Copyright (c) 2015 OpenSilk Productions LLC * * 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 3 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, see <http://www.gnu.org/licenses/>. */ package org.opensilk.music.playback.control; import android.content.ComponentName; import android.content.Context; import android.content.ContextWrapper; import android.content.Intent; import android.content.ServiceConnection; import android.media.MediaMetadata; import android.media.Rating; import android.media.audiofx.AudioEffect; import android.media.session.MediaController; import android.media.session.MediaSession; import android.media.session.PlaybackState; import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; import android.support.v4.media.MediaMetadataCompat; import android.support.v4.media.session.MediaSessionCompat; import android.support.v4.media.session.PlaybackStateCompat; import org.opensilk.common.core.dagger2.ForApplication; import org.opensilk.music.playback.BundleHelper; import org.opensilk.music.playback.PlaybackConstants; import org.opensilk.music.playback.PlaybackConstants.CMD; import org.opensilk.music.playback.service.IPlaybackService; import org.opensilk.music.playback.service.PlaybackService; import java.io.Closeable; import java.util.ArrayList; import java.util.List; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import javax.inject.Inject; import javax.inject.Singleton; import rx.Subscription; import rx.functions.Action1; import rx.subjects.BehaviorSubject; import timber.log.Timber; /** * Created by drew on 5/6/15. */ @Singleton @SuppressWarnings("NewApi") public class PlaybackController { final Context mAppContext; final Handler mCallbackHandler = new Handler(Looper.getMainLooper()); boolean mWaitingForService = false; IPlaybackService mPlaybackService; MediaController mMediaController; MediaController.TransportControls mTransportControls; @Inject public PlaybackController(@ForApplication Context mAppContext) { this.mAppContext = new ContextWrapper(mAppContext); } /* * Transport controls */ public void play() { if (hasController()) { getTransportControls().play(); } } public void playFromMediaId(String mediaId, Bundle extras) { if (hasController()) { getTransportControls().playFromMediaId(mediaId, extras); } } public void playFromSearch(String query, Bundle extras) { if (hasController()) { getTransportControls().playFromSearch(query, extras); } } public void skipToQueueItem(long id) { if (hasController()) { getTransportControls().skipToQueueItem(id); } } public void pause() { if (hasController()) { getTransportControls().pause(); } } public void stop() { if (hasController()) { getTransportControls().stop(); } } public void seekTo(long pos) { if (hasController()) { getTransportControls().seekTo(pos); } } public void fastForward() { if (hasController()) { getTransportControls().fastForward(); } } public void skipToNext() { if (hasController()) { getTransportControls().skipToNext(); } } public void rewind() { if (hasController()) { getTransportControls().rewind(); } } public void skipToPrevious() { if (hasController()) { getTransportControls().skipToPrevious(); } } public void setRating(Rating rating) { if (hasController()) { getTransportControls().setRating(rating); } } public void sendCustomAction(PlaybackState.CustomAction customAction, Bundle args) { if (hasController()) { getTransportControls().sendCustomAction(customAction, args); } } public void sendCustomAction(String action, Bundle args) { if (hasController()) { getTransportControls().sendCustomAction(action, args); } } /* * End transport controls */ /* * Custom commands */ public void playorPause() { sendCustomAction(CMD.TOGGLE_PLAYBACK, null); } public void cycleRepeateMode() { sendCustomAction(CMD.CYCLE_REPEAT, null); } public void shuffleQueue() { sendCustomAction(CMD.SHUFFLE_QUEUE, null); } public void enqueueAll(List<Uri> queue, int where) { sendCustomAction(CMD.ENQUEUE, BundleHelper.builder().putInt(where).putList(queue).get()); } public void enqueueAllNext(List<Uri> list) { enqueueAll(list, PlaybackConstants.ENQUEUE_NEXT); } public void addAllToQueue(List<Uri> list) { enqueueAll(list, PlaybackConstants.ENQUEUE_LAST); } public void playAll(List<Uri> list, int startpos) { sendCustomAction(CMD.PLAY_ALL, BundleHelper.builder().putList(list).putInt(startpos).get()); } public void shuffleAll(List<Uri> list) { playAll(list, -1); } public void enqueueTracksFrom(Uri uri, int where, String sortorder) { sendCustomAction(CMD.ENQUEUE_TRACKS_FROM, BundleHelper.builder().putUri(uri).putInt(where).putString(sortorder).get()); } public void enqueueTracksNextFrom(Uri uri, String sortorder) { enqueueTracksFrom(uri, PlaybackConstants.ENQUEUE_NEXT, sortorder); } public void addTracksToQueueFrom(Uri uri, String sortorder) { enqueueTracksFrom(uri, PlaybackConstants.ENQUEUE_LAST, sortorder); } public void playTracksFrom(Uri uri, int startpos, String sortorder) { sendCustomAction(CMD.PLAY_TRACKS_FROM, BundleHelper.builder().putUri(uri).putInt(startpos).putString(sortorder).get()); } public void shuffleTracksFrom(Uri uri) { playTracksFrom(uri, -1, null); } public void removeQueueItem(Uri uri) { sendCustomAction(CMD.REMOVE_QUEUE_ITEM, BundleHelper.builder().putUri(uri).get()); } public void removeQueueItemAt(int pos) { sendCustomAction(CMD.REMOVE_QUEUE_ITEM_AT, BundleHelper.builder().putInt(pos).get()); } public void clearQueue() { sendCustomAction(CMD.CLEAR_QUEUE, null); } public void moveQueueItemTo(Uri uri, int newPos) { sendCustomAction(CMD.MOVE_QUEUE_ITEM_TO, BundleHelper.builder().putUri(uri).putInt(newPos).get()); } /* * End custom commands */ /* * Misc */ public int getAudioSessionId() { if (hasController()) { try { return mPlaybackService.getAudioSessionId(); } catch (RemoteException e) { } } return AudioEffect.ERROR_BAD_VALUE; } /* * Subscriptions */ final BehaviorSubject<PlaybackStateCompat> mPlayStateSubject = BehaviorSubject.create(); public Subscription subscribePlayStateChanges(Action1<PlaybackStateCompat> onNext) { return mPlayStateSubject.asObservable().subscribe(onNext); } final BehaviorSubject<MediaMetadataCompat> mMetaSubject = BehaviorSubject.create(); public Subscription subscribeMetaChanges(Action1<MediaMetadataCompat> onNext) { return mMetaSubject.asObservable().subscribe(onNext); } final BehaviorSubject<List<MediaSessionCompat.QueueItem>> mQueueSubject = BehaviorSubject.create(); public Subscription subscribeQueueChanges(Action1<List<MediaSessionCompat.QueueItem>> onNext) { return mQueueSubject.asObservable().subscribe(onNext); } /* * end subscriptions */ final MediaController.Callback mCallback = new MediaController.Callback() { @Override public void onSessionDestroyed() { onDisconnect(); } @Override public void onSessionEvent(String event, Bundle extras) { super.onSessionEvent(event, extras); } @Override public void onPlaybackStateChanged(PlaybackState state) { mPlayStateSubject.onNext(PlaybackStateCompat.fromPlaybackState(state)); } @Override public void onMetadataChanged(MediaMetadata metadata) { mMetaSubject.onNext(MediaMetadataCompat.fromMediaMetadata(metadata)); } @Override public void onQueueChanged(List<MediaSession.QueueItem> queue) { List<MediaSessionCompat.QueueItem> list = new ArrayList<>(queue.size()); for (MediaSession.QueueItem item : queue) { list.add(MediaSessionCompat.QueueItem.obtain(item)); } mQueueSubject.onNext(list); } @Override public void onQueueTitleChanged(CharSequence title) { super.onQueueTitleChanged(title); } @Override public void onExtrasChanged(Bundle extras) { super.onExtrasChanged(extras); } @Override public void onAudioInfoChanged(MediaController.PlaybackInfo info) { super.onAudioInfoChanged(info); } }; boolean hasController() { if (mMediaController == null) { if (!mWaitingForService) { connect(); } return false; } else { return true; } } MediaController.TransportControls getTransportControls() { if (!hasController()) { throw new IllegalStateException("called getTransportControls without checking hasController"); } return mTransportControls; } public void connect() { if (mMediaController != null || mWaitingForService) { return; } mWaitingForService = true; mAppContext.startService(new Intent(mAppContext, PlaybackService.class)); mAppContext.bindService(new Intent(mAppContext, PlaybackService.class), mServiceConnection, Context.BIND_IMPORTANT); } public void disconnect() { mAppContext.unbindService(mServiceConnection); onDisconnect(); } void onDisconnect() { mPlaybackService = null; mMediaController = null; mTransportControls = null; mWaitingForService = false; } final ServiceConnection mServiceConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { try { mPlaybackService = IPlaybackService.Stub.asInterface(service); mMediaController = new MediaController(mAppContext, mPlaybackService.getToken()); mMediaController.registerCallback(mCallback, mCallbackHandler); mTransportControls = mMediaController.getTransportControls(); final PlaybackState state = mMediaController.getPlaybackState(); if (state != null) { mCallbackHandler.post(new Runnable() { @Override public void run() { mCallback.onPlaybackStateChanged(state); } }); } final MediaMetadata meta = mMediaController.getMetadata(); if (meta != null) { mCallbackHandler.post(new Runnable() { @Override public void run() { mCallback.onMetadataChanged(meta); } }); } final List<MediaSession.QueueItem> queue = mMediaController.getQueue(); if (queue != null) { mCallbackHandler.post(new Runnable() { @Override public void run() { mCallback.onQueueChanged(queue); } }); } } catch (RemoteException e) { Timber.e(e, "Bind service"); mMediaController = null; mTransportControls = null; } finally { mWaitingForService = false; } } @Override public void onServiceDisconnected(ComponentName name) { onDisconnect(); } }; public final static class PlaybackServiceConnection implements Closeable { private final Context context; private final ServiceConnection serviceConnection; private final IPlaybackService service; private PlaybackServiceConnection(Context context, ServiceConnection serviceConnection, IPlaybackService service) { this.context = context; this.serviceConnection = serviceConnection; this.service = service; } @Override public void close() { context.unbindService(serviceConnection); } public IPlaybackService getService() { return service; } } public static PlaybackServiceConnection bindService(Context context) throws InterruptedException { if (context == null) { throw new NullPointerException("context == null"); } ensureNotOnMainThread(context); final BlockingQueue<IPlaybackService> q = new LinkedBlockingQueue<IPlaybackService>(1); ServiceConnection keyChainServiceConnection = new ServiceConnection() { volatile boolean mConnectedAtLeastOnce = false; @Override public void onServiceConnected(ComponentName name, IBinder service) { if (!mConnectedAtLeastOnce) { mConnectedAtLeastOnce = true; try { q.put(IPlaybackService.Stub.asInterface(service)); } catch (InterruptedException e) { // will never happen, since the queue starts with one available slot } } } @Override public void onServiceDisconnected(ComponentName name) { } }; Intent intent = new Intent(context, PlaybackService.class); boolean isBound = context.bindService(intent, keyChainServiceConnection, Context.BIND_AUTO_CREATE); if (!isBound) { throw new AssertionError("could not bind to KeyChainService"); } return new PlaybackServiceConnection(context, keyChainServiceConnection, q.take()); } private static void ensureNotOnMainThread(Context context) { Looper looper = Looper.myLooper(); if (looper != null && looper == context.getMainLooper()) { throw new IllegalStateException("calling this from your main thread can lead to deadlock"); } } }