package com.marverenic.music.fragments;
import android.databinding.DataBindingUtil;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.design.widget.Snackbar;
import android.support.v7.widget.PopupMenu;
import android.support.v7.widget.Toolbar;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import com.marverenic.music.BR;
import com.marverenic.music.JockeyApplication;
import com.marverenic.music.R;
import com.marverenic.music.data.store.PreferenceStore;
import com.marverenic.music.databinding.FragmentNowPlayingBinding;
import com.marverenic.music.dialog.AppendPlaylistDialogFragment;
import com.marverenic.music.dialog.CreatePlaylistDialogFragment;
import com.marverenic.music.dialog.DurationPickerDialogFragment;
import com.marverenic.music.dialog.NumberPickerDialogFragment;
import com.marverenic.music.model.Song;
import com.marverenic.music.player.MusicPlayer;
import com.marverenic.music.player.PlayerController;
import com.marverenic.music.player.PlayerState;
import com.marverenic.music.view.TimeView;
import com.marverenic.music.viewmodel.NowPlayingArtworkViewModel;
import com.trello.rxlifecycle.FragmentEvent;
import java.util.List;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import timber.log.Timber;
import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
import static android.support.design.widget.Snackbar.LENGTH_LONG;
import static android.support.design.widget.Snackbar.LENGTH_SHORT;
public class NowPlayingFragment extends BaseFragment implements Toolbar.OnMenuItemClickListener,
NumberPickerDialogFragment.OnNumberPickedListener,
DurationPickerDialogFragment.OnDurationPickedListener {
private static final String TAG_MAKE_PLAYLIST = "CreatePlaylistDialog";
private static final String TAG_APPEND_PLAYLIST = "AppendPlaylistDialog";
private static final String TAG_MULTI_REPEAT_PICKER = "MultiRepeatPickerDialog";
private static final String TAG_SLEEP_TIMER_PICKER = "SleepTimerPickerDialog";
private static final int DEFAULT_MULTI_REPEAT_VALUE = 3;
@Inject PreferenceStore mPrefStore;
@Inject PlayerController mPlayerController;
private FragmentNowPlayingBinding mBinding;
private NowPlayingArtworkViewModel mArtworkViewModel;
private MenuItem mCreatePlaylistMenuItem;
private MenuItem mAppendToPlaylistMenuItem;
private MenuItem mRepeatMenuItem;
private MenuItem mShuffleMenuItem;
private Subscription mSleepTimerSubscription;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
JockeyApplication.getComponent(this).inject(this);
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
mBinding = DataBindingUtil.inflate(inflater, R.layout.fragment_now_playing,
container, false);
mArtworkViewModel = new NowPlayingArtworkViewModel(this);
mBinding.setArtworkViewModel(mArtworkViewModel);
setupToolbar(mBinding.nowPlayingToolbar);
mPlayerController.getSleepTimerEndTime()
.compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW))
.subscribe(this::updateSleepTimerCounter, throwable -> {
Timber.e(throwable, "Failed to update sleep timer end timestamp");
});
mPlayerController.getInfo()
.compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW))
.subscribe(this::showSnackbar, throwable -> {
Timber.e(throwable, "Failed to display info message");
});
mPlayerController.getError()
.compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW))
.subscribe(this::showSnackbar, throwable -> {
Timber.e(throwable, "Failed to display error message");
});
return mBinding.getRoot();
}
@Override
public void onResume() {
mArtworkViewModel.notifyPropertyChanged(BR.gesturesEnabled);
super.onResume();
}
private void setupToolbar(Toolbar toolbar) {
if (getResources().getConfiguration().orientation != ORIENTATION_LANDSCAPE) {
toolbar.setBackground(new ColorDrawable(Color.TRANSPARENT));
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
toolbar.setElevation(getResources().getDimension(R.dimen.header_elevation));
}
toolbar.setTitle("");
toolbar.setNavigationIcon(R.drawable.ic_clear_24dp);
toolbar.inflateMenu(R.menu.activity_now_playing);
toolbar.setOnMenuItemClickListener(this);
toolbar.setNavigationOnClickListener(v -> {
getActivity().onBackPressed();
});
mCreatePlaylistMenuItem = toolbar.getMenu().findItem(R.id.menu_now_playing_save);
mAppendToPlaylistMenuItem = toolbar.getMenu().findItem(R.id.menu_now_playing_append);
mShuffleMenuItem = toolbar.getMenu().findItem(R.id.menu_now_playing_shuffle);
mRepeatMenuItem = toolbar.getMenu().findItem(R.id.menu_now_playing_repeat);
mPlayerController.getQueue()
.compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW))
.map(this::queueContainsLocalSongs)
.subscribe(this::updatePlaylistActionEnabled, throwable -> {
Timber.e(throwable, "Failed to update playlist enabled state");
});
mPlayerController.isShuffleEnabled()
.compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW))
.subscribe(this::updateShuffleIcon, throwable -> {
Timber.e(throwable, "Failed to update shuffle icon");
});
mPlayerController.getRepeatMode()
.compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW))
.subscribe(this::updateRepeatIcon, throwable -> {
Timber.e(throwable, "Failed to update repeat icon");
});
}
private boolean queueContainsLocalSongs(List<Song> queue) {
for (Song song : queue) {
if (song.isInLibrary()) {
return true;
}
}
return false;
}
private void updatePlaylistActionEnabled(boolean canCreatePlaylist) {
mCreatePlaylistMenuItem.setEnabled(canCreatePlaylist);
mAppendToPlaylistMenuItem.setEnabled(canCreatePlaylist);
}
private void updateShuffleIcon(boolean shuffled) {
if (shuffled) {
mShuffleMenuItem.getIcon().setAlpha(255);
mShuffleMenuItem.setTitle(getResources().getString(R.string.action_disable_shuffle));
} else {
mShuffleMenuItem.getIcon().setAlpha(128);
mShuffleMenuItem.setTitle(getResources().getString(R.string.action_enable_shuffle));
}
}
private void updateRepeatIcon(int repeatMode) {
@DrawableRes int icon;
boolean active = true;
if (repeatMode > 1) {
switch (repeatMode) {
case 2:
icon = R.drawable.ic_repeat_two_24dp;
break;
case 3:
icon = R.drawable.ic_repeat_three_24dp;
break;
case 4:
icon = R.drawable.ic_repeat_four_24dp;
break;
case 5:
icon = R.drawable.ic_repeat_five_24dp;
break;
case 6:
icon = R.drawable.ic_repeat_six_24dp;
break;
case 7:
icon = R.drawable.ic_repeat_seven_24dp;
break;
case 8:
icon = R.drawable.ic_repeat_eight_24dp;
break;
case 9:
default:
icon = R.drawable.ic_repeat_nine_24dp;
break;
}
} else if (mPrefStore.getRepeatMode() == MusicPlayer.REPEAT_ALL) {
icon = R.drawable.ic_repeat_24dp;
} else if (mPrefStore.getRepeatMode() == MusicPlayer.REPEAT_ONE) {
icon = R.drawable.ic_repeat_one_24dp;
} else {
icon = R.drawable.ic_repeat_24dp;
active = false;
}
mRepeatMenuItem.setIcon(icon);
mRepeatMenuItem.getIcon().setAlpha(active ? 255 : 128);
}
@Override
public boolean onMenuItemClick(MenuItem item) {
switch (item.getItemId()) {
case R.id.menu_now_playing_shuffle:
toggleShuffle();
return true;
case R.id.menu_now_playing_repeat:
showRepeatMenu();
return true;
case R.id.menu_now_playing_sleep_timer:
mPlayerController.getSleepTimerEndTime()
.compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW))
.take(1)
.subscribe(this::showSleepTimerDialog, throwable -> {
Timber.e(throwable, "Failed to show sleep timer dialog");
});
return true;
case R.id.menu_now_playing_save:
saveQueueAsPlaylist();
return true;
case R.id.menu_now_playing_append:
addQueueToPlaylist();
return true;
case R.id.menu_now_playing_clear:
clearQueue();
return true;
}
return false;
}
private void toggleShuffle() {
mPrefStore.toggleShuffle();
mPlayerController.updatePlayerPreferences(mPrefStore);
if (mPrefStore.isShuffled()) {
showSnackbar(R.string.confirm_enable_shuffle);
} else {
showSnackbar(R.string.confirm_disable_shuffle);
}
}
private void showRepeatMenu() {
View anchor = mBinding.getRoot().findViewById(R.id.menu_now_playing_repeat);
PopupMenu menu = new PopupMenu(getContext(), anchor, Gravity.END);
menu.inflate(R.menu.activity_now_playing_repeat);
menu.setOnMenuItemClickListener(item -> {
switch (item.getItemId()) {
case R.id.menu_item_repeat_all:
changeRepeatMode(MusicPlayer.REPEAT_ALL, R.string.confirm_enable_repeat);
return true;
case R.id.menu_item_repeat_none:
changeRepeatMode(MusicPlayer.REPEAT_NONE, R.string.confirm_disable_repeat);
return true;
case R.id.menu_item_repeat_one:
changeRepeatMode(MusicPlayer.REPEAT_ONE, R.string.confirm_enable_repeat_one);
return true;
case R.id.menu_item_repeat_multi:
showMultiRepeatDialog();
return true;
default:
return false;
}
});
menu.show();
}
private void changeRepeatMode(int repeatMode, @StringRes int confirmationMessage) {
mPrefStore.setRepeatMode(repeatMode);
mPlayerController.setMultiRepeatCount(0);
mPlayerController.updatePlayerPreferences(mPrefStore);
showSnackbar(confirmationMessage);
}
private void showSleepTimerDialog(long currentSleepTimerEndTime) {
long timeLeftInMs = currentSleepTimerEndTime - System.currentTimeMillis();
int defaultValue;
if (timeLeftInMs > 0) {
long minutes = TimeUnit.MINUTES.convert(timeLeftInMs, TimeUnit.MILLISECONDS);
long seconds = TimeUnit.SECONDS.convert(timeLeftInMs, TimeUnit.MILLISECONDS) % 60;
defaultValue = (int) minutes + ((seconds >= 30) ? 1 : 0);
defaultValue = Math.max(defaultValue, 1);
} else {
long prevTimeInMillis = mPrefStore.getLastSleepTimerDuration();
defaultValue = (int) TimeUnit.MINUTES.convert(prevTimeInMillis, TimeUnit.MILLISECONDS);
}
new DurationPickerDialogFragment.Builder(this)
.setMinValue(1)
.setDefaultValue(defaultValue)
.setMaxValue(120)
.setTitle(getString(R.string.enable_sleep_timer))
.setDisableButtonText((timeLeftInMs > 0)
? getString(R.string.action_disable_sleep_timer)
: null)
.show(TAG_SLEEP_TIMER_PICKER);
}
@Override
public void onDurationPicked(int durationInMinutes) {
// Callback for when a sleep timer value is chosen
if (durationInMinutes == DurationPickerDialogFragment.NO_VALUE) {
mPlayerController.disableSleepTimer();
showSnackbar(R.string.confirm_disable_sleep_timer);
return;
}
long durationInMillis = TimeUnit.MILLISECONDS.convert(durationInMinutes, TimeUnit.MINUTES);
long endTimestamp = System.currentTimeMillis() + durationInMillis;
mPlayerController.setSleepTimerEndTime(endTimestamp);
String confirmationMessage = getResources().getQuantityString(
R.plurals.confirm_enable_sleep_timer, durationInMinutes, durationInMinutes);
showSnackbar(confirmationMessage);
mPrefStore.setLastSleepTimerDuration(durationInMillis);
}
private void updateSleepTimerCounter(long endTimestamp) {
TimeView sleepTimerCounter = mBinding.nowPlayingSleepTimer;
long sleepTimerValue = endTimestamp - System.currentTimeMillis();
if (mSleepTimerSubscription != null) {
mSleepTimerSubscription.unsubscribe();
}
if (sleepTimerValue <= 0) {
sleepTimerCounter.setVisibility(View.GONE);
} else {
sleepTimerCounter.setVisibility(View.VISIBLE);
sleepTimerCounter.setTime((int) sleepTimerValue);
mSleepTimerSubscription = Observable.interval(500, TimeUnit.MILLISECONDS)
.subscribeOn(Schedulers.computation())
.map(tick -> (int) (sleepTimerValue - 500 * tick))
.observeOn(AndroidSchedulers.mainThread())
.compose(bindUntilEvent(FragmentEvent.DESTROY_VIEW))
.subscribe(time -> {
sleepTimerCounter.setTime(time);
if (time <= 0) {
mSleepTimerSubscription.unsubscribe();
animateOutSleepTimerCounter();
}
}, throwable -> {
Timber.e(throwable, "Failed to update sleep timer value");
});
}
}
private void animateOutSleepTimerCounter() {
TimeView sleepTimerCounter = mBinding.nowPlayingSleepTimer;
Animation transition = AnimationUtils.loadAnimation(getContext(), R.anim.tooltip_out_down);
transition.setStartOffset(250);
transition.setDuration(300);
transition.setInterpolator(getContext(), android.R.interpolator.accelerate_quint);
sleepTimerCounter.startAnimation(transition);
new Handler().postDelayed(() -> sleepTimerCounter.setVisibility(View.GONE), 550);
}
private void showMultiRepeatDialog() {
mPlayerController.getRepeatMode().take(1).subscribe(currentCount -> {
new NumberPickerDialogFragment.Builder(this)
.setMinValue(2)
.setMaxValue(10)
.setDefaultValue((currentCount > 1)
? currentCount
: DEFAULT_MULTI_REPEAT_VALUE)
.setWrapSelectorWheel(false)
.setTitle(getString(R.string.enable_multi_repeat_title))
.setMessage(getString(R.string.multi_repeat_description))
.show(TAG_MULTI_REPEAT_PICKER);
}, throwable -> {
Timber.e(throwable, "Failed to show multi repeat dialog");
});
}
@Override
public void onNumberPicked(int chosen) {
// Callback for when a Multi-Repeat value is chosen
mPlayerController.setMultiRepeatCount(chosen);
showSnackbar(getString(R.string.confirm_enable_multi_repeat, chosen));
}
private void saveQueueAsPlaylist() {
mPlayerController.getQueue().take(1).subscribe(queue -> {
new CreatePlaylistDialogFragment.Builder(getFragmentManager())
.setSongs(queue)
.showSnackbarIn(R.id.now_playing_artwork)
.show(TAG_MAKE_PLAYLIST);
}, throwable -> {
Timber.e(throwable, "Failed to save queue as playlist");
});
}
private void addQueueToPlaylist() {
mPlayerController.getQueue()
.compose(bindToLifecycle())
.take(1)
.subscribe(queue -> {
new AppendPlaylistDialogFragment.Builder(getContext(), getFragmentManager())
.setTitle(getString(R.string.header_add_queue_to_playlist))
.setSongs(queue)
.showSnackbarIn(R.id.now_playing_artwork)
.show(TAG_APPEND_PLAYLIST);
}, throwable -> {
Timber.e(throwable, "Failed to add queue to playlist");
});
}
private void clearQueue() {
mPlayerController.getPlayerState()
.compose(this.<PlayerState>bindToLifecycle().forSingle())
.subscribe(playerState -> {
mPlayerController.clearQueue();
Snackbar.make(mBinding.getRoot(), R.string.confirm_clear_queue, LENGTH_LONG)
.setAction(R.string.action_undo, view -> {
mPlayerController.restorePlayerState(playerState);
})
.show();
}, throwable -> {
Timber.e(throwable, "Failed to clear queue");
});
}
private void showSnackbar(@StringRes int stringId) {
showSnackbar(getString(stringId));
}
private void showSnackbar(String message) {
if (((View) mBinding.getRoot().getParent()).getVisibility() == View.VISIBLE) {
Snackbar.make(mBinding.getRoot(), message, LENGTH_SHORT).show();
}
}
}