package com.marverenic.music.player;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.Bitmap;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.SystemClock;
import com.marverenic.music.IPlayerService;
import com.marverenic.music.JockeyApplication;
import com.marverenic.music.data.store.ImmutablePreferenceStore;
import com.marverenic.music.data.store.MediaStoreUtil;
import com.marverenic.music.data.store.PreferenceStore;
import com.marverenic.music.data.store.ReadOnlyPreferenceStore;
import com.marverenic.music.model.Song;
import com.marverenic.music.utils.ObservableQueue;
import com.marverenic.music.utils.Optional;
import com.marverenic.music.utils.Util;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import rx.Observable;
import rx.Single;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subjects.BehaviorSubject;
import rx.subjects.PublishSubject;
import timber.log.Timber;
/**
* An implementation of {@link PlayerController} used in release builds to communicate with the
* media player throughout the application. This implementation uses AIDL to send commands through
* IPC to the remote player service, and gets information with a combination of AIDL to fetch data
* and BroadcastReceivers to be notified of automatic changes to the player state.
*
* This class is responsible for all communication to the remote service, including starting,
* binding, unbinding and restarting the service if it crashes.
*/
public class ServicePlayerController implements PlayerController {
private static final int POSITION_TICK_MS = 200;
private static final int SERVICE_RESTART_THRESHOLD_MS = 500;
private Context mContext;
private IPlayerService mBinding;
private long mServiceStartRequestTime;
private PublishSubject<String> mErrorStream = PublishSubject.create();
private PublishSubject<String> mInfoStream = PublishSubject.create();
private final Prop<Boolean> mPlaying = new Prop<>("playing");
private final Prop<Song> mNowPlaying = new Prop<>("now playing");
private final Prop<List<Song>> mQueue = new Prop<>("queue", Collections.emptyList());
private final Prop<Integer> mQueuePosition = new Prop<>("queue index");
private final Prop<Integer> mCurrentPosition = new Prop<>("seek position");
private final Prop<Integer> mDuration = new Prop<>("duration");
private final Prop<Integer> mMultiRepeatCount = new Prop<>("multi-repeat");
private final Prop<Long> mSleepTimerEndTime = new Prop<>("sleep timer");
private BehaviorSubject<Boolean> mShuffled;
private BehaviorSubject<Integer> mRepeatMode;
private BehaviorSubject<Bitmap> mArtwork;
private Subscription mCurrentPositionClock;
private ObservableQueue<Runnable> mRequestQueue;
private Subscription mRequestQueueSubscription;
public ServicePlayerController(Context context, PreferenceStore preferenceStore) {
mContext = context;
mShuffled = BehaviorSubject.create(preferenceStore.isShuffled());
mRepeatMode = BehaviorSubject.create(preferenceStore.getRepeatMode());
mRequestQueue = new ObservableQueue<>();
startService();
isPlaying().subscribe(
isPlaying -> {
if (isPlaying) {
startCurrentPositionClock();
} else {
stopCurrentPositionClock();
}
}, throwable -> {
Timber.e(throwable, "Failed to update current position clock");
});
}
private void startService() {
MediaStoreUtil.getPermission(mContext)
.subscribe(this::bindService, t -> Timber.i(t, "Failed to get Storage permission"));
}
private void bindService(boolean hasMediaStorePermission) {
long timeSinceLastStartRequest = SystemClock.uptimeMillis() - mServiceStartRequestTime;
if (!hasMediaStorePermission || mBinding != null
|| timeSinceLastStartRequest < SERVICE_RESTART_THRESHOLD_MS) {
return;
}
Intent serviceIntent = PlayerService.newIntent(mContext, true);
// Manually start the service to ensure that it is associated with this task and can
// appropriately set its dismiss behavior
mContext.startService(serviceIntent);
mServiceStartRequestTime = SystemClock.uptimeMillis();
mContext.bindService(serviceIntent, new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mBinding = IPlayerService.Stub.asInterface(service);
initAllProperties();
bindRequestQueue();
}
@Override
public void onServiceDisconnected(ComponentName name) {
mContext.unbindService(this);
releaseAllProperties();
mServiceStartRequestTime = 0;
mBinding = null;
if (mRequestQueueSubscription != null) {
mRequestQueueSubscription.unsubscribe();
mRequestQueueSubscription = null;
}
}
}, Context.BIND_WAIVE_PRIORITY);
}
private void ensureServiceStarted() {
if (mBinding == null) {
startService();
}
}
private void bindRequestQueue() {
mRequestQueueSubscription = mRequestQueue.toObservable()
.subscribe(Runnable::run, throwable -> {
Timber.e(throwable, "Failed to process request");
// Make sure to restart the request queue, otherwise all future commands will
// be dropped
bindRequestQueue();
});
}
private void execute(Runnable command) {
ensureServiceStarted();
mRequestQueue.enqueue(command);
}
private void startCurrentPositionClock() {
if (mCurrentPositionClock != null && !mCurrentPositionClock.isUnsubscribed()) {
return;
}
mCurrentPositionClock = Observable.interval(POSITION_TICK_MS, TimeUnit.MILLISECONDS)
.observeOn(Schedulers.computation())
.subscribe(tick -> {
if (!mCurrentPosition.isSubscribedTo()) {
stopCurrentPositionClock();
} else {
mCurrentPosition.invalidate();
}
}, throwable -> {
Timber.e(throwable, "Failed to perform position tick");
});
}
private void stopCurrentPositionClock() {
if (mCurrentPositionClock != null) {
mCurrentPositionClock.unsubscribe();
mCurrentPositionClock = null;
}
}
private void releaseAllProperties() {
mPlaying.setFunction(null);
mNowPlaying.setFunction(null);
mQueue.setFunction(null);
mQueuePosition.setFunction(null);
mCurrentPosition.setFunction(null);
mDuration.setFunction(null);
mMultiRepeatCount.setFunction(null);
mSleepTimerEndTime.setFunction(null);
}
private void initAllProperties() {
mPlaying.setFunction(mBinding::isPlaying);
mNowPlaying.setFunction(mBinding::getNowPlaying);
mQueue.setFunction(mBinding::getQueue);
mQueuePosition.setFunction(mBinding::getQueuePosition);
mCurrentPosition.setFunction(mBinding::getCurrentPosition);
mDuration.setFunction(mBinding::getDuration);
mMultiRepeatCount.setFunction(mBinding::getMultiRepeatCount);
mSleepTimerEndTime.setFunction(mBinding::getSleepTimerEndTime);
invalidateAll();
}
private void invalidateAll() {
mPlaying.invalidate();
mNowPlaying.invalidate();
mQueue.invalidate();
mQueuePosition.invalidate();
mCurrentPosition.invalidate();
mDuration.invalidate();
mMultiRepeatCount.invalidate();
mSleepTimerEndTime.invalidate();
}
@Override
public Observable<String> getError() {
return mErrorStream.asObservable();
}
@Override
public Observable<String> getInfo() {
return mInfoStream.asObservable();
}
@Override
public Single<PlayerState> getPlayerState() {
return Observable.fromCallable(mBinding::getPlayerState).toSingle();
}
@Override
public void restorePlayerState(PlayerState restoreState) {
execute(() -> {
try {
mBinding.restorePlayerState(restoreState);
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to restore player state");
}
});
}
@Override
public void stop() {
execute(() -> {
try {
mBinding.stop();
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to stop service");
}
});
}
@Override
public void skip() {
execute(() -> {
try {
mBinding.skip();
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to skip current track");
}
});
}
@Override
public void previous() {
execute(() -> {
try {
mBinding.previous();
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to skip backward");
}
});
}
@Override
public void togglePlay() {
execute(() -> {
try {
mBinding.togglePlay();
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to toggle playback");
}
});
}
@Override
public void play() {
execute(() -> {
try {
mBinding.play();
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to resume playback");
}
});
}
@Override
public void pause() {
execute(() -> {
try {
mBinding.pause();
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to pause playback");
}
});
}
@Override
public void updatePlayerPreferences(ReadOnlyPreferenceStore preferenceStore) {
execute(() -> {
try {
mBinding.setPreferences(new ImmutablePreferenceStore(preferenceStore));
mShuffled.onNext(preferenceStore.isShuffled());
mRepeatMode.onNext(preferenceStore.getRepeatMode());
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to update remote player preferences");
}
});
}
@Override
public void setQueue(List<Song> newQueue, int newPosition) {
execute(() -> {
try {
mBinding.setQueue(newQueue, newPosition);
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to set queue");
}
});
}
@Override
public void clearQueue() {
setQueue(Collections.emptyList(), 0);
}
@Override
public void changeSong(int newPosition) {
execute(() -> {
try {
mBinding.changeSong(newPosition);
mNowPlaying.invalidate();
mQueuePosition.invalidate();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to change song");
}
});
}
@Override
public void editQueue(List<Song> queue, int newPosition) {
execute(() -> {
try {
mBinding.editQueue(queue, newPosition);
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to edit queue");
}
});
}
@Override
public void queueNext(Song song) {
execute(() -> {
try {
mBinding.queueNext(song);
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to queue next song");
}
});
}
@Override
public void queueNext(List<Song> songs) {
execute(() -> {
try {
mBinding.queueNextList(songs);
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to queue next songs");
}
});
}
@Override
public void queueLast(Song song) {
execute(() -> {
try {
mBinding.queueLast(song);
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to queue last song");
}
});
}
@Override
public void queueLast(List<Song> songs) {
execute(() -> {
try {
mBinding.queueLastList(songs);
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to queue last songs");
}
});
}
@Override
public void seek(int position) {
execute(() -> {
try {
mBinding.seekTo(position);
invalidateAll();
} catch (RemoteException exception) {
Timber.e(exception, "Failed to seek");
}
});
}
@Override
public Observable<Boolean> isPlaying() {
ensureServiceStarted();
return mPlaying.getObservable();
}
@Override
public Observable<Song> getNowPlaying() {
ensureServiceStarted();
return mNowPlaying.getObservable();
}
@Override
public Observable<List<Song>> getQueue() {
ensureServiceStarted();
return mQueue.getObservable();
}
@Override
public Observable<Integer> getQueuePosition() {
ensureServiceStarted();
return mQueuePosition.getObservable();
}
@Override
public Observable<Integer> getCurrentPosition() {
ensureServiceStarted();
startCurrentPositionClock();
return mCurrentPosition.getObservable();
}
@Override
public Observable<Integer> getDuration() {
ensureServiceStarted();
return mDuration.getObservable();
}
@Override
public Observable<Boolean> isShuffleEnabled() {
ensureServiceStarted();
return mShuffled.asObservable().distinctUntilChanged();
}
@Override
public Observable<Integer> getRepeatMode() {
ensureServiceStarted();
return mMultiRepeatCount.getObservable()
.flatMap(multiRepeatCount -> {
if (multiRepeatCount > 1) {
return Observable.just(multiRepeatCount);
} else {
return mRepeatMode.asObservable();
}
})
.distinctUntilChanged();
}
@Override
public void setMultiRepeatCount(int count) {
execute(() -> {
try {
mBinding.setMultiRepeatCount(count);
mMultiRepeatCount.setValue(count);
} catch (RemoteException exception) {
Timber.e(exception, "Failed to set multi-repeat count");
}
});
}
@Override
public Observable<Long> getSleepTimerEndTime() {
ensureServiceStarted();
return mSleepTimerEndTime.getObservable();
}
@Override
public void setSleepTimerEndTime(long timestampInMillis) {
execute(() -> {
try {
mBinding.setSleepTimerEndTime(timestampInMillis);
mSleepTimerEndTime.setValue(timestampInMillis);
} catch (RemoteException exception) {
Timber.e(exception, "Failed to set sleep-timer end time");
}
});
}
@Override
public void disableSleepTimer() {
setSleepTimerEndTime(0L);
}
@Override
public Observable<Bitmap> getArtwork() {
if (mArtwork == null) {
mArtwork = BehaviorSubject.create();
getNowPlaying()
.observeOn(Schedulers.io())
.map((Song song) -> {
if (song == null) {
return null;
}
return Util.fetchFullArt(mContext, song);
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(mArtwork::onNext, throwable -> {
Timber.e(throwable, "Failed to fetch artwork");
mArtwork.onNext(null);
});
}
return mNowPlaying.getSubject()
.map(Optional::isPresent)
.switchMap(current -> {
if (current) {
return mArtwork;
} else {
return Observable.empty();
}
});
}
/**
* A {@link BroadcastReceiver} class listening for intents with an
* {@link MusicPlayer#UPDATE_BROADCAST} action. This broadcast must be sent ordered with this
* receiver being the highest priority so that the UI can access this class for accurate
* information from the player service
*/
public static class Listener extends BroadcastReceiver {
@Inject
PlayerController mController;
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getBooleanExtra(MusicPlayer.UPDATE_EXTRA_MINOR, false)) {
// Ignore minor updates – we already handle them without being notified
return;
}
if (mController == null) {
JockeyApplication.getComponent(context).inject(this);
}
if (mController instanceof ServicePlayerController) {
ServicePlayerController playerController = (ServicePlayerController) mController;
if (intent.getAction().equals(MusicPlayer.UPDATE_BROADCAST)) {
playerController.invalidateAll();
} else if (intent.getAction().equals(MusicPlayer.INFO_BROADCAST)) {
String error = intent.getExtras().getString(MusicPlayer.INFO_EXTRA_MESSAGE);
playerController.mInfoStream.onNext(error);
} else if (intent.getAction().equals(MusicPlayer.ERROR_BROADCAST)) {
String info = intent.getExtras().getString(MusicPlayer.ERROR_EXTRA_MSG);
playerController.mErrorStream.onNext(info);
}
}
}
}
private static final class Prop<T> {
private final String mName;
private final T mNullValue;
private final BehaviorSubject<Optional<T>> mSubject;
private final Observable<T> mObservable;
private Retriever<T> mRetriever;
public Prop(String propertyName) {
this(propertyName, null);
}
public Prop(String propertyName, T nullValue) {
mName = propertyName;
mNullValue = nullValue;
mSubject = BehaviorSubject.create();
mObservable = mSubject.filter(Optional::isPresent)
.map(Optional::getValue)
.distinctUntilChanged();
}
public void setFunction(Retriever<T> retriever) {
mRetriever = retriever;
}
public void invalidate() {
mSubject.onNext(Optional.empty());
if (mRetriever != null) {
Observable.fromCallable(mRetriever::retrieve)
.map(data -> (data == null) ? mNullValue : data)
.map(Optional::ofNullable)
.subscribe(mSubject::onNext, throwable -> {
Timber.e(throwable, "Failed to fetch " + mName + " property.");
});
}
}
public boolean isSubscribedTo() {
return mSubject.hasObservers();
}
public void setValue(T value) {
mSubject.onNext(Optional.ofNullable(value));
}
protected BehaviorSubject<Optional<T>> getSubject() {
return mSubject;
}
public Observable<T> getObservable() {
return mObservable;
}
interface Retriever<T> {
T retrieve() throws Exception;
}
}
}