package com.atomjack.vcfp.fragments;
import android.app.PendingIntent;
import android.content.Context;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.speech.RecognizerIntent;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.view.GestureDetectorCompat;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.SeekBar;
import android.widget.TextView;
import com.atomjack.shared.Intent;
import com.atomjack.shared.NewLogger;
import com.atomjack.shared.PlayerState;
import com.atomjack.shared.Preferences;
import com.atomjack.shared.WearConstants;
import com.atomjack.vcfp.Feedback;
import com.atomjack.vcfp.MediaOptionsDialog;
import com.atomjack.vcfp.R;
import com.atomjack.vcfp.VoiceControlForPlexApplication;
import com.atomjack.vcfp.activities.MainActivity;
import com.atomjack.vcfp.interfaces.ActiveConnectionHandler;
import com.atomjack.vcfp.interfaces.ActivityListener;
import com.atomjack.vcfp.interfaces.InputStreamHandler;
import com.atomjack.vcfp.model.Connection;
import com.atomjack.vcfp.model.PlexClient;
import com.atomjack.vcfp.model.PlexMedia;
import com.atomjack.vcfp.model.PlexTrack;
import com.atomjack.vcfp.model.PlexVideo;
import com.atomjack.vcfp.model.Stream;
import com.atomjack.vcfp.net.PlexHttpClient;
import com.atomjack.vcfp.services.PlexSearchService;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.ArrayList;
import butterknife.ButterKnife;
import butterknife.OnClick;
import cz.fhucho.android.util.SimpleDiskCache;
public abstract class PlayerFragment extends Fragment
implements SeekBar.OnSeekBarChangeListener {
protected NewLogger logger;
protected PlexMedia nowPlayingMedia;
protected ArrayList<? extends PlexMedia> nowPlayingPlaylist = new ArrayList<>();
protected int currentMediaIndex = 0; // the index of the currently playing media in the playlist
protected PlexClient client;
private View mainView;
protected Feedback feedback;
private boolean doingMic = false;
protected PlayerState currentState = PlayerState.STOPPED;
protected int position = -1;
int screenWidth = -1;
int screenHeight = -1;
SimpleDiskCache simpleDiskCache;
// UI Elements
protected boolean resumePlayback;
// @Bind(R.id.playButton)
protected ImageButton playButton;
// @Bind(R.id.pauseButton)
protected ImageButton pauseButton;
// @Bind(R.id.playPauseSpinner)
protected ProgressBar playPauseSpinner;
protected boolean isSeeking = false;
protected SeekBar seekBar;
protected TextView currentTimeDisplay;
protected TextView durationDisplay;
protected ImageView nowPlayingPoster;
protected boolean fromWear = false;
protected GestureDetectorCompat mDetector;
protected MediaOptionsDialog mediaOptionsDialog;
private int layout = -1;
private LayoutInflater inflater;
protected ActivityListener activityListener;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
if(savedInstanceState != null) {
logger.d("onSavedInstanceState is not null");
nowPlayingMedia = savedInstanceState.getParcelable(Intent.EXTRA_MEDIA);
nowPlayingPlaylist = savedInstanceState.getParcelableArrayList(Intent.EXTRA_PLAYLIST);
// mediaContainer = savedInstanceState.getParcelable(Intent.EXTRA_ALBUM);
fromWear = savedInstanceState.getBoolean(WearConstants.FROM_WEAR, false);
layout = savedInstanceState.getInt(Intent.EXTRA_LAYOUT);
client = savedInstanceState.getParcelable(Intent.EXTRA_CLIENT);
currentState = (PlayerState) savedInstanceState.getSerializable(Intent.EXTRA_CURRENT_STATE);
logger.d("got current state: %s", currentState);
}
logger.d("layout: %d", layout);
if(layout == -1) { // Layout can't be found, so alert activity something went wrong so it closes fragment out
mainView = inflater.inflate(R.layout.player_fragment, container, false);
activityListener.onLayoutNotFound();
} else {
mainView = inflater.inflate(layout, container, false);
ButterKnife.bind(getActivity(), mainView);
this.inflater = inflater;
showNowPlaying();
setState(currentState); // Call this right away so that the play/pause spinner gets changed to the appropriate button, when orientation is changing while playing or paused
seekBar = (SeekBar) mainView.findViewById(R.id.seekBar);
seekBar.setOnSeekBarChangeListener(this);
seekBar.setMax(nowPlayingMedia.duration / 1000);
if(VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.RESUME, false))
seekBar.setProgress(Integer.parseInt(nowPlayingMedia.viewOffset) / 1000);
else
seekBar.setProgress(0);
setCurrentTimeDisplay(getOffset(nowPlayingMedia));
durationDisplay.setText(VoiceControlForPlexApplication.secondsToTimecode(nowPlayingMedia.duration / 1000));
}
return mainView;
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(Intent.EXTRA_MEDIA, nowPlayingMedia);
outState.putParcelableArrayList(Intent.EXTRA_PLAYLIST, nowPlayingPlaylist);
outState.putInt(Intent.EXTRA_LAYOUT, layout);
outState.putParcelable(Intent.EXTRA_CLIENT, client);
outState.putSerializable(Intent.EXTRA_CURRENT_STATE, currentState);
}
public PlayerFragment() {
simpleDiskCache = VoiceControlForPlexApplication.getInstance().mSimpleDiskCache;
logger = new NewLogger(this);
}
public void init(int layout, PlexClient client, PlexMedia media, ArrayList<? extends PlexMedia> playlist, boolean fromWear) {
this.layout = layout;
this.client = client;
nowPlayingMedia = media;
nowPlayingPlaylist = playlist == null ? new ArrayList<PlexMedia>() : playlist;
this.fromWear = fromWear;
currentMediaIndex = 0;
}
public void mediaChanged(PlexMedia media) {
nowPlayingMedia = media;
if(mainView != null) { // Can't do anything with UI elements until mainView is defined and set
showNowPlaying();
setCurrentTimeDisplay(getOffset(nowPlayingMedia));
seekBar.setMax(nowPlayingMedia.duration / 1000);
seekBar.setProgress(Integer.parseInt(nowPlayingMedia.viewOffset) / 1000);
durationDisplay.setText(VoiceControlForPlexApplication.secondsToTimecode(nowPlayingMedia.duration / 1000));
}
}
@Override
public void onAttach(Context context) {
super.onAttach(context);
feedback = ((MainActivity)context).feedback;
try {
activityListener = (ActivityListener) context;
} catch (ClassCastException e) {
throw new ClassCastException(context.toString()
+ " must implement ActivityListener");
}
}
public PlexMedia getNowPlayingMedia() {
return nowPlayingMedia;
}
public void showNowPlaying() {
if(mainView == null)
return;
if (nowPlayingMedia instanceof PlexVideo) {
PlexVideo video = (PlexVideo)nowPlayingMedia;
if(video.isMovie() || video.isClip()) {
TextView title = (TextView) mainView.findViewById(R.id.nowPlayingTitle);
title.setText(video.title);
} else {
TextView showTitle = (TextView)mainView.findViewById(R.id.nowPlayingShowTitle);
showTitle.setText(video.grandparentTitle);
showTitle.setSelected(true);
TextView episodeTitle = (TextView)mainView.findViewById(R.id.nowPlayingEpisodeTitle);
if(video.parentIndex != null && video.index != null) {
episodeTitle.setText(String.format("%s (s%02de%02d)", video.title, Integer.parseInt(video.parentIndex), Integer.parseInt(video.index)));
} else {
episodeTitle.setText(video.title);
}
episodeTitle.setSelected(true);
}
} else if (nowPlayingMedia instanceof PlexTrack) {
PlexTrack track = (PlexTrack)nowPlayingMedia;
TextView artist = (TextView)mainView.findViewById(R.id.nowPlayingArtist);
logger.d("Setting artist to %s", track.grandparentTitle);
artist.setText(track.grandparentTitle);
TextView album = (TextView)mainView.findViewById(R.id.nowPlayingAlbum);
album.setText(track.parentTitle);
TextView title = (TextView)mainView.findViewById(R.id.nowPlayingTitle);
title.setText(track.title);
}
TextView nowPlayingOnClient = (TextView)mainView.findViewById(R.id.nowPlayingOnClient);
nowPlayingOnClient.setText(getResources().getString(R.string.now_playing_on) + " " + client.name);
// Hide stream options on chromecast, for now
if (mainView.findViewById(R.id.mediaOptionsButton) != null && nowPlayingMedia.getStreams(Stream.SUBTITLE).size() == 0 && nowPlayingMedia.getStreams(Stream.AUDIO).size() == 0) {
mainView.findViewById(R.id.mediaOptionsButton).setVisibility(View.GONE);
}
logger.d("Setting thumb in showNowPlaying");
attachUIElements();
final FrameLayout nowPlayingPosterContainer = (FrameLayout)mainView.findViewById(R.id.nowPlayingPosterContainer);
logger.d("nowPlayingPosterContainer: %s", nowPlayingPosterContainer);
if(nowPlayingPosterContainer != null) {
if(screenWidth != -1) {
setThumb(screenWidth, screenHeight);
} else {
ViewTreeObserver vto = nowPlayingPosterContainer.getViewTreeObserver();
vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
nowPlayingPosterContainer.getViewTreeObserver().removeOnGlobalLayoutListener(this);
screenWidth = nowPlayingPosterContainer.getMeasuredWidth();
screenHeight = nowPlayingPosterContainer.getMeasuredHeight();
String[] prefs = VoiceControlForPlexApplication.getMediaPosterPrefs(nowPlayingMedia);
if (VoiceControlForPlexApplication.getInstance().prefs.get(prefs[0], -1) == -1) {
VoiceControlForPlexApplication.getInstance().prefs.put(prefs[0], screenWidth);
VoiceControlForPlexApplication.getInstance().prefs.put(prefs[1], screenHeight);
}
logger.d("Found dimensions: %d/%d", screenWidth, screenHeight);
setThumb(screenWidth, screenHeight);
}
});
}
}
}
@Override
public void onResume() {
super.onResume();
logger.d("onResume");
if(doingMic) {
doPlay();
doingMic = false;
}
}
private void setThumb(final byte[] bytes) {
logger.d("Setting thumb with %d bytes", bytes.length);
if(getActivity() != null) {
getActivity().runOnUiThread(new Runnable() {
@Override
public void run() {
Bitmap posterBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
nowPlayingPoster.setImageBitmap(posterBitmap);
}
});
}
}
protected void setThumb(int maxWidth, int maxHeight) {
String thumb = nowPlayingMedia.thumb;
logger.d("setThumb: %s", thumb);
if(nowPlayingMedia instanceof PlexVideo) {
PlexVideo video = (PlexVideo)nowPlayingMedia;
thumb = video.isMovie() || video.isClip() ? video.thumb : video.grandparentThumb;
logger.d("orientation: %s, type: %s", VoiceControlForPlexApplication.getOrientation(), video.type);
if(video.isClip()) {
}
if(VoiceControlForPlexApplication.getOrientation() == Configuration.ORIENTATION_LANDSCAPE) {
thumb = video.art;
}
} else if(nowPlayingMedia instanceof PlexTrack) {
PlexTrack track = (PlexTrack)nowPlayingMedia;
if(VoiceControlForPlexApplication.getOrientation() == Configuration.ORIENTATION_LANDSCAPE)
thumb = track.art;
else
thumb = track.thumb;
}
if(thumb != null && thumb.equals("")) {
thumb = null;
}
logger.d("thumb: %s", thumb);
SimpleDiskCache.InputStreamEntry thumbEntry = null;
try {
thumbEntry = simpleDiskCache.getInputStream(nowPlayingMedia.getCacheKey(thumb != null ? thumb : nowPlayingMedia.key));
} catch (Exception ex) {
ex.printStackTrace();
}
if (thumbEntry != null) {
logger.d("Using cached thumb: %s", nowPlayingMedia.getCacheKey(thumb));
try {
setThumb(IOUtils.toByteArray(thumbEntry.getInputStream()));
} catch (Exception e) { e.printStackTrace(); }
} else {
logger.d("Downloading thumb");
getThumb(maxWidth, maxHeight, thumb, nowPlayingMedia);
}
}
private void getThumb(final int maxWidth, final int maxHeight, final String thumb, final PlexMedia media) {
if(thumb == null) {
InputStream is = getResources().openRawResource(+ R.drawable.ic_launcher);
try {
InputStream iss = new ByteArrayInputStream(IOUtils.toByteArray(is));
iss.reset();
simpleDiskCache.put(media.getCacheKey(media.key), iss);
setThumb(IOUtils.toByteArray(iss));
} catch (IOException e) {
logger.d("Exception getting/saving thumb");
e.printStackTrace();
}
} else {
media.server.findServerConnection(new ActiveConnectionHandler() {
@Override
public void onSuccess(Connection connection) {
String path = String.format("/photo/:/transcode?width=%d&height=%d&url=%s", maxWidth, maxHeight, Uri.encode(String.format("http://127.0.0.1:32400%s", thumb)));
String url = media.server.buildURL(connection, path);
logger.d("thumb url: %s", url);
PlexHttpClient.getThumb(url, new InputStreamHandler() {
@Override
public void onSuccess(InputStream is) {
try {
simpleDiskCache.put(media.getCacheKey(thumb), is);
setThumb(maxWidth, maxHeight);
} catch (Exception e) {
logger.d("Exception getting/saving thumb");
e.printStackTrace();
}
}
});
}
@Override
public void onFailure(int statusCode) {
}
});
}
}
private void attachUIElements() {
nowPlayingPoster = (ImageView) mainView.findViewById(R.id.nowPlayingPoster);
ImageButton rewindButton = (ImageButton)mainView.findViewById(R.id.rewindButton);
if(rewindButton != null)
rewindButton.setOnClickListener(v -> doRewind());
ImageButton forwardButton = (ImageButton)mainView.findViewById(R.id.forwardButton);
if(forwardButton != null)
forwardButton.setOnClickListener(v -> doForward());
ImageButton previousButton = (ImageButton)mainView.findViewById(R.id.previousButton);
if(previousButton != null) {
if(nowPlayingPlaylist == null || nowPlayingPlaylist.size() == 1) {
previousButton.setVisibility(View.GONE);
} else {
previousButton.setAlpha(1.0f);
previousButton.setOnClickListener(v -> doPrevious());
}
}
playButton = (ImageButton)mainView.findViewById(R.id.playButton);
pauseButton = (ImageButton)mainView.findViewById(R.id.pauseButton);
playPauseSpinner = (ProgressBar)mainView.findViewById(R.id.playPauseSpinner);
ImageButton nextButton = (ImageButton)mainView.findViewById(R.id.nextButton);
if(nextButton != null) {
if(nowPlayingPlaylist == null || nowPlayingPlaylist.size() == 1) {
nextButton.setVisibility(View.GONE);
} else {
nextButton.setAlpha(1.0f);
nextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doNext();
}
});
}
}
// List<? extends PlexMedia> list = null;
// if(mediaContainer.videos.size() > 0) {
// list = mediaContainer.videos;
// } else if(mediaContainer.tracks.size() > 0) {
// list = mediaContainer.tracks;
// }
if(nowPlayingPlaylist != null) {
int index = 0;
for(PlexMedia m : nowPlayingPlaylist) {
if(m.key.equals(nowPlayingMedia.key))
break;
index++;
}
logger.d("Index: %d", index);
if(index == 0)
previousButton.setAlpha(0.4f);
else if(index+1 == nowPlayingPlaylist.size())
nextButton.setAlpha(0.4f);
}
ImageButton mediaOptionsButton = (ImageButton)mainView.findViewById(R.id.mediaOptionsButton);
if(mediaOptionsButton != null) {
mediaOptionsButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doMediaOptions();
}
});
}
ImageButton micButton = (ImageButton)mainView.findViewById(R.id.micButton);
if(micButton != null) {
micButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doMic();
}
});
}
ImageButton stopButton = (ImageButton)mainView.findViewById(R.id.stopButton);
stopButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
doStop();
}
});
currentTimeDisplay = (TextView)mainView.findViewById(R.id.currentTimeView);
durationDisplay = (TextView)mainView.findViewById(R.id.durationView);
mDetector = new GestureDetectorCompat(getActivity(), new TouchGestureListener());
LinearLayout target = (LinearLayout)mainView.findViewById(R.id.nowPlayingTapTarget);
if(target != null) {
target.setOnTouchListener(new View.OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
return mDetector.onTouchEvent(event);
}
});
}
}
class TouchGestureListener extends GestureDetector.SimpleOnGestureListener {
@Override
public boolean onDown(MotionEvent e) {
return true;
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
logger.d("Single tap.");
if(currentState == PlayerState.PLAYING)
doPause();
else
doPlay();
return true;
}
@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
float SWIPE_SPEED_THRESHOLD = 2000;
try {
float diffY = e2.getY() - e1.getY();
float diffX = e2.getX() - e1.getX();
if (Math.abs(diffX) > Math.abs(diffY) && Math.abs(velocityX) >= SWIPE_SPEED_THRESHOLD) {
if (diffX > 0) {
logger.d("Doing forward via fling right");
doForward();
} else {
logger.d("Doing back via fling left");
doRewind();
}
}
} catch (Exception e) {
e.printStackTrace();
}
return true;
}
}
// The follow methods are defined in the PlexPlayerFragment and CastPlayerFragment subclasses
protected abstract void doRewind();
protected abstract void doForward();
@OnClick(R.id.playButton)
protected abstract void doPlay();
@OnClick(R.id.pauseButton)
protected abstract void doPause();
protected abstract void doStop();
protected abstract void doNext();
protected abstract void doPrevious();
protected void doMic() {
if(nowPlayingMedia.server != null) {
if(currentState == PlayerState.PLAYING) {
doingMic = true;
doPause();
}
android.content.Intent serviceIntent = new android.content.Intent(getActivity(), PlexSearchService.class);
serviceIntent.putExtra(com.atomjack.shared.Intent.EXTRA_SERVER, VoiceControlForPlexApplication.gsonWrite.toJson(nowPlayingMedia.server));
serviceIntent.putExtra(com.atomjack.shared.Intent.EXTRA_CLIENT, VoiceControlForPlexApplication.gsonWrite.toJson(client));
serviceIntent.putExtra(com.atomjack.shared.Intent.EXTRA_RESUME, resumePlayback);
serviceIntent.putExtra(com.atomjack.shared.Intent.EXTRA_FROM_MIC, true);
SecureRandom random = new SecureRandom();
serviceIntent.setData(Uri.parse(new BigInteger(130, random).toString(32)));
PendingIntent resultsPendingIntent = PendingIntent.getService(getActivity(), 0, serviceIntent, PendingIntent.FLAG_ONE_SHOT);
android.content.Intent listenerIntent = new android.content.Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
listenerIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
listenerIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, "voice.recognition.test");
listenerIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 5);
listenerIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, resultsPendingIntent);
listenerIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, getResources().getString(R.string.voice_prompt));
startActivity(listenerIntent);
}
}
protected void doMediaOptions() {
logger.d("doMediaOptions!!!");
if(nowPlayingMedia == null) {
return;
}
mediaOptionsDialog = new MediaOptionsDialog(getActivity(), nowPlayingMedia, client);
mediaOptionsDialog.setStreamChangeListener(stream -> activityListener.setStream(stream));
mediaOptionsDialog.show();
}
protected void setCurrentTimeDisplay(long seconds) {
currentTimeDisplay.setText(VoiceControlForPlexApplication.secondsToTimecode(seconds));
}
protected int getOffset(PlexMedia media) {
logger.d("getting offset, mediaoffset: %s", media.viewOffset);
if((VoiceControlForPlexApplication.getInstance().prefs.get(Preferences.RESUME, false) || resumePlayback) && media.viewOffset != null)
return Integer.parseInt(media.viewOffset) / 1000;
else
return 0;
}
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
setCurrentTimeDisplay(progress);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
isSeeking = true;
}
public void setState(PlayerState newState) {
// logger.d("setState: %s, current state: %s", newState, currentState);
currentState = newState;
if (playPauseSpinner != null && playButton != null && pauseButton != null) {
playPauseSpinner.setVisibility(currentState == PlayerState.BUFFERING ? View.VISIBLE : View.INVISIBLE);
playButton.setVisibility(currentState == PlayerState.PAUSED ? View.VISIBLE : View.INVISIBLE);
pauseButton.setVisibility(currentState == PlayerState.PLAYING ? View.VISIBLE : View.INVISIBLE);
}
}
public void setPosition(int position) {
if(!isSeeking) {
this.position = position;
if (seekBar != null)
seekBar.setProgress(position);
else
logger.d("Seekbar is null");
if (currentTimeDisplay != null)
setCurrentTimeDisplay(position);
}
}
}