package tv.emby.embyatv.livetv;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.Typeface;
import android.graphics.drawable.BitmapDrawable;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Handler;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.HorizontalScrollView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.ScrollView;
import android.widget.TextClock;
import android.widget.TextView;
import com.squareup.picasso.Picasso;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import mediabrowser.apiinteraction.EmptyResponse;
import mediabrowser.apiinteraction.Response;
import mediabrowser.model.dto.BaseItemDto;
import mediabrowser.model.livetv.ChannelInfoDto;
import mediabrowser.model.livetv.SeriesTimerInfoDto;
import tv.emby.embyatv.R;
import tv.emby.embyatv.TvApp;
import tv.emby.embyatv.base.BaseActivity;
import tv.emby.embyatv.base.CustomMessage;
import tv.emby.embyatv.base.IMessageListener;
import tv.emby.embyatv.ui.GuideChannelHeader;
import tv.emby.embyatv.ui.GuidePagingButton;
import tv.emby.embyatv.ui.HorizontalScrollViewListener;
import tv.emby.embyatv.ui.LiveProgramDetailPopup;
import tv.emby.embyatv.ui.ObservableHorizontalScrollView;
import tv.emby.embyatv.ui.ObservableScrollView;
import tv.emby.embyatv.ui.ProgramGridCell;
import tv.emby.embyatv.ui.RecordPopup;
import tv.emby.embyatv.ui.ScrollViewListener;
import tv.emby.embyatv.util.InfoLayoutHelper;
import tv.emby.embyatv.util.Utils;
/**
* Created by Eric on 5/3/2015.
*/
public class LiveTvGuideActivity extends BaseActivity implements ILiveTvGuide {
public static final int ROW_HEIGHT = Utils.convertDpToPixel(TvApp.getApplication(),55);
public static final int PIXELS_PER_MINUTE = Utils.convertDpToPixel(TvApp.getApplication(),6);
private static final int IMAGE_SIZE = Utils.convertDpToPixel(TvApp.getApplication(), 150);
public static final int PAGEBUTTON_HEIGHT = Utils.convertDpToPixel(TvApp.getApplication(), 20);
public static final int PAGEBUTTON_WIDTH = 120 * PIXELS_PER_MINUTE;
public static final int PAGE_SIZE = 75;
public static final int NORMAL_HOURS = 9;
public static final int FILTERED_HOURS = 3;
private LiveTvGuideActivity mActivity;
private TextView mDisplayDate;
private TextView mTitle;
private TextView mChannelStatus;
private TextView mFilterStatus;
private TextView mSummary;
private ImageView mImage;
private ImageView mBackdrop;
private LinearLayout mInfoRow;
private LinearLayout mChannels;
private LinearLayout mTimeline;
private LinearLayout mProgramRows;
private ScrollView mChannelScroller;
private HorizontalScrollView mTimelineScroller;
private View mSpinner;
private BaseItemDto mSelectedProgram;
private ProgramGridCell mSelectedProgramView;
private long mLastLoad = 0;
private List<ChannelInfoDto> mAllChannels;
private String mFirstFocusChannelId;
private GuideFilters mFilters = new GuideFilters();
private Calendar mCurrentGuideStart;
private Calendar mCurrentGuideEnd;
private long mCurrentLocalGuideStart;
private long mCurrentLocalGuideEnd;
private int mCurrentDisplayChannelStartNdx = 0;
private int mCurrentDisplayChannelEndNdx = 0;
private Handler mHandler = new Handler();
private Typeface roboto;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mActivity = this;
roboto = TvApp.getApplication().getDefaultFont();
setContentView(R.layout.live_tv_guide);
mDisplayDate = (TextView) findViewById(R.id.displayDate);
mTitle = (TextView) findViewById(R.id.title);
mTitle.setTypeface(roboto);
mSummary = (TextView) findViewById(R.id.summary);
mSummary.setTypeface(roboto);
mChannelStatus = (TextView) findViewById(R.id.channelsStatus);
mFilterStatus = (TextView) findViewById(R.id.filterStatus);
mChannelStatus.setTypeface(roboto);
mFilterStatus.setTypeface(roboto);
mChannelStatus.setTextColor(Color.GRAY);
mFilterStatus.setTextColor(Color.GRAY);
mInfoRow = (LinearLayout) findViewById(R.id.infoRow);
mImage = (ImageView) findViewById(R.id.programImage);
mBackdrop = (ImageView) findViewById(R.id.backdrop);
mChannels = (LinearLayout) findViewById(R.id.channels);
mTimeline = (LinearLayout) findViewById(R.id.timeline);
mProgramRows = (LinearLayout) findViewById(R.id.programRows);
mSpinner = findViewById(R.id.spinner);
mSpinner.setVisibility(View.VISIBLE);
findViewById(R.id.filterButton).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
showFilterOptions();
}
});
mProgramRows.setFocusable(false);
mChannelScroller = (ScrollView) findViewById(R.id.channelScroller);
ObservableScrollView programVScroller = (ObservableScrollView) findViewById(R.id.programVScroller);
programVScroller.setScrollViewListener(new ScrollViewListener() {
@Override
public void onScrollChanged(ObservableScrollView scrollView, int x, int y, int oldx, int oldy) {
mChannelScroller.scrollTo(x, y);
}
});
mTimelineScroller = (HorizontalScrollView) findViewById(R.id.timelineHScroller);
mTimelineScroller.setFocusable(false);
mTimelineScroller.setFocusableInTouchMode(false);
mTimeline.setFocusable(false);
mTimeline.setFocusableInTouchMode(false);
mChannelScroller.setFocusable(false);
mChannelScroller.setFocusableInTouchMode(false);
ObservableHorizontalScrollView programHScroller = (ObservableHorizontalScrollView) findViewById(R.id.programHScroller);
programHScroller.setScrollViewListener(new HorizontalScrollViewListener() {
@Override
public void onScrollChanged(ObservableHorizontalScrollView scrollView, int x, int y, int oldx, int oldy) {
mTimelineScroller.scrollTo(x, y);
}
});
programHScroller.setFocusable(false);
programHScroller.setFocusableInTouchMode(false);
mChannels.setFocusable(false);
mChannelScroller.setFocusable(false);
//Register to receive message from popup
registerMessageListener(new IMessageListener() {
@Override
public void onMessageReceived(CustomMessage message) {
if (message.equals(CustomMessage.ActionComplete)) dismissProgramOptions();
}
});
}
private int getGuideHours() {
return mFilters.any() ? FILTERED_HOURS : NORMAL_HOURS;
}
private void load() {
fillTimeLine(getGuideHours());
TvManager.loadAllChannels(new Response<Integer>() {
@Override
public void onResponse(Integer ndx) {
if (ndx >= PAGE_SIZE) {
// last channel is not in first page so grab a set where it will be in the middle
ndx = ndx - (PAGE_SIZE / 2);
} else {
ndx = 0; // just start at beginning
}
mLastLoad = System.currentTimeMillis();
mAllChannels = TvManager.getAllChannels();
if (mAllChannels.size() > 0) {
displayChannels(ndx, PAGE_SIZE);
} else {
mSpinner.setVisibility(View.GONE);
}
}
});
}
private void reload() {
fillTimeLine(getGuideHours());
displayChannels(mCurrentDisplayChannelStartNdx, PAGE_SIZE);
mLastLoad = System.currentTimeMillis();
}
@Override
protected void onResume() {
super.onResume();
if (System.currentTimeMillis() > mLastLoad + 3600000) {
if (mAllChannels == null) {
mAllChannels = TvManager.getAllChannels();
if (mAllChannels == null) load();
else reload();
} else reload();
mFirstFocusChannelId = TvManager.getLastLiveTvChannel();
}
}
@Override
protected void onPause() {
super.onPause();
if (mDisplayProgramsTask != null) mDisplayProgramsTask.cancel(true);
if (mDetailPopup != null) mDetailPopup.dismiss();
}
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_MENU:
// bring up filter selection
showFilterOptions();
break;
case KeyEvent.KEYCODE_MEDIA_PLAY:
case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE:
if ((mDetailPopup == null || !mDetailPopup.isShowing()) && (mFilterPopup == null || !mFilterPopup.isShowing())
&& mSelectedProgram != null && mSelectedProgram.getChannelId() != null) {
// tune to the current channel
Utils.Beep();
Utils.retrieveAndPlay(mSelectedProgram.getChannelId(), false, this);
return true;
}
}
return super.onKeyUp(keyCode, event);
}
private LiveProgramDetailPopup mDetailPopup;
private FilterPopup mFilterPopup;
class FilterPopup {
final int WIDTH = Utils.convertDpToPixel(TvApp.getApplication(), 250);
final int HEIGHT = Utils.convertDpToPixel(TvApp.getApplication(), 400);
PopupWindow mPopup;
LiveTvGuideActivity mActivity;
CheckBox mMovies;
CheckBox mNews;
CheckBox mSeries;
CheckBox mKids;
CheckBox mSports;
CheckBox mPremiere;
Button mFilterButton;
Button mClearButton;
FilterPopup(LiveTvGuideActivity activity) {
mActivity = activity;
LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.guide_filter_popup, null);
mPopup = new PopupWindow(layout, WIDTH, HEIGHT);
mPopup.setFocusable(true);
mPopup.setOutsideTouchable(true);
mPopup.setBackgroundDrawable(new BitmapDrawable()); // necessary for popup to dismiss
mPopup.setAnimationStyle(R.style.PopupSlideInRight);
mMovies = (CheckBox) layout.findViewById(R.id.movies);
mSeries = (CheckBox) layout.findViewById(R.id.series);
mNews = (CheckBox) layout.findViewById(R.id.news);
mKids = (CheckBox) layout.findViewById(R.id.kids);
mSports = (CheckBox) layout.findViewById(R.id.sports);
mPremiere = (CheckBox) layout.findViewById(R.id.premiere);
mFilterButton = (Button) layout.findViewById(R.id.okButton);
mFilterButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mFilters.setMovies(mMovies.isChecked());
mFilters.setSeries(mSeries.isChecked());
mFilters.setNews(mNews.isChecked());
mFilters.setKids(mKids.isChecked());
mFilters.setSports(mSports.isChecked());
mFilters.setPremiere(mPremiere.isChecked());
load();
mPopup.dismiss();
}
});
mClearButton = (Button) layout.findViewById(R.id.clearButton);
mClearButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
mFilters.clear();
load();
mPopup.dismiss();
}
});
}
public boolean isShowing() {
return (mPopup != null && mPopup.isShowing());
}
public void show() {
mMovies.setChecked(mFilters.isMovies());
mSeries.setChecked(mFilters.isSeries());
mNews.setChecked(mFilters.isNews());
mKids.setChecked(mFilters.isKids());
mSports.setChecked(mFilters.isSports());
mPremiere.setChecked(mFilters.isPremiere());
mPopup.showAtLocation(mTimelineScroller, Gravity.NO_GRAVITY, mTimelineScroller.getRight(), mSummary.getTop());
}
public void dismiss() {
if (mPopup != null && mPopup.isShowing()) {
mPopup.dismiss();
}
}
}
public void dismissProgramOptions() {
if (mDetailPopup != null) mDetailPopup.dismiss();
}
public void showProgramOptions() {
if (mSelectedProgram == null) return;
if (mDetailPopup == null) mDetailPopup = new LiveProgramDetailPopup(this, mSummary.getWidth(), new EmptyResponse() {
@Override
public void onResponse() {
Utils.retrieveAndPlay(mSelectedProgram.getChannelId(), false, mActivity);
}
});
mDetailPopup.setContent(mSelectedProgram, mSelectedProgramView);
mDetailPopup.show(mImage, mTitle.getLeft(), mTitle.getTop() - 10);
}
public void showFilterOptions() {
if (mFilterPopup == null) mFilterPopup = new FilterPopup(this);
mFilterPopup.show();
}
public void displayChannels(int start, int max) {
int end = start + max;
if (end > mAllChannels.size()) end = mAllChannels.size();
if (mFilters.any()) {
// if we are filtered, then we need to get programs for all channels
mCurrentDisplayChannelStartNdx = 0;
mCurrentDisplayChannelEndNdx = mAllChannels.size()-1;
} else {
mCurrentDisplayChannelStartNdx = start;
mCurrentDisplayChannelEndNdx = end - 1;
}
TvApp.getApplication().getLogger().Debug("*** Display channels pre-execute");
mSpinner.setVisibility(View.VISIBLE);
mChannels.removeAllViews();
mProgramRows.removeAllViews();
mChannelStatus.setText("");
mFilterStatus.setText("");
TvManager.getProgramsAsync(mCurrentDisplayChannelStartNdx, mCurrentDisplayChannelEndNdx, mCurrentGuideEnd, new EmptyResponse() {
@Override
public void onResponse() {
TvApp.getApplication().getLogger().Debug("*** Programs response");
if (mDisplayProgramsTask != null) mDisplayProgramsTask.cancel(true);
mDisplayProgramsTask = new DisplayProgramsTask();
mDisplayProgramsTask.execute(mCurrentDisplayChannelStartNdx, mCurrentDisplayChannelEndNdx);
}
});
}
DisplayProgramsTask mDisplayProgramsTask;
class DisplayProgramsTask extends AsyncTask<Integer, Integer, Void> {
View firstRow;
int displayedChannels = 0;
@Override
protected void onPreExecute() {
TvApp.getApplication().getLogger().Debug("*** Display programs pre-execute");
mChannels.removeAllViews();
mProgramRows.removeAllViews();
if (mCurrentDisplayChannelStartNdx > 0) {
// Show a paging row for channels above
int pageUpStart = mCurrentDisplayChannelStartNdx - PAGE_SIZE;
if (pageUpStart < 0) pageUpStart = 0;
TextView placeHolder = new TextView(mActivity);
placeHolder.setHeight(LiveTvGuideActivity.PAGEBUTTON_HEIGHT);
mChannels.addView(placeHolder);
displayedChannels = 0;
mProgramRows.addView(new GuidePagingButton(mActivity, mActivity, pageUpStart, getString(R.string.lbl_load_channels)+mAllChannels.get(pageUpStart).getNumber() + " - "+mAllChannels.get(mCurrentDisplayChannelStartNdx-1).getNumber()));
}
}
@Override
protected Void doInBackground(Integer... params) {
int start = params[0];
int end = params[1];
boolean first = true;
TvApp.getApplication().getLogger().Debug("*** About to iterate programs");
LinearLayout prevRow = null;
for (int i = start; i <= end; i++) {
if (isCancelled()) return null;
final ChannelInfoDto channel = TvManager.getChannel(i);
List<BaseItemDto> programs = TvManager.getProgramsForChannel(channel.getId(), mFilters);
final LinearLayout row = getProgramRow(programs, channel.getId());
if (first) {
first = false;
firstRow = row;
}
// put focus on the last tuned channel
if (channel.getId().equals(mFirstFocusChannelId)) {
firstRow = row;
mFirstFocusChannelId = null; // only do this first time in not while paging around
}
// set focus parameters if we are not on first row
// this makes focus movements more predictable for the grid view
if (prevRow != null) {
TvManager.setFocusParms(row, prevRow, true);
TvManager.setFocusParms(prevRow, row, false);
}
prevRow = row;
runOnUiThread(new Runnable() {
@Override
public void run() {
GuideChannelHeader header = new GuideChannelHeader(mActivity, channel);
mChannels.addView(header);
header.loadImage();
mProgramRows.addView(row);
}
});
displayedChannels++;
}
return null;
}
@Override
protected void onPostExecute(Void aVoid) {
TvApp.getApplication().getLogger().Debug("*** Display programs post execute");
if (mCurrentDisplayChannelEndNdx < mAllChannels.size()-1 && !mFilters.any()) {
// Show a paging row for channels below
int pageDnEnd = mCurrentDisplayChannelEndNdx + PAGE_SIZE;
if (pageDnEnd >= mAllChannels.size()) pageDnEnd = mAllChannels.size()-1;
TextView placeHolder = new TextView(mActivity);
placeHolder.setHeight(PAGEBUTTON_HEIGHT);
mChannels.addView(placeHolder);
mProgramRows.addView(new GuidePagingButton(mActivity, mActivity, mCurrentDisplayChannelEndNdx + 1, getString(R.string.lbl_load_channels)+mAllChannels.get(mCurrentDisplayChannelEndNdx+1).getNumber() + " - "+mAllChannels.get(pageDnEnd).getNumber()));
}
mChannelStatus.setText(displayedChannels+" of "+mAllChannels.size()+" channels");
mFilterStatus.setText(mFilters.toString() + " for next "+getGuideHours()+" hours");
mFilterStatus.setTextColor(mFilters.any() ? Color.WHITE : Color.GRAY);
mSpinner.setVisibility(View.GONE);
if (firstRow != null) firstRow.requestFocus();
}
}
private int currentCellId = 0;
private LinearLayout getProgramRow(List<BaseItemDto> programs, String channelId) {
LinearLayout programRow = new LinearLayout(this);
if (programs.size() == 0) {
BaseItemDto empty = new BaseItemDto();
empty.setName(" <No Program Data Available>");
empty.setChannelId(channelId);
empty.setStartDate(Utils.convertToUtcDate(new Date(mCurrentLocalGuideStart)));
empty.setEndDate(Utils.convertToUtcDate(new Date(mCurrentLocalGuideStart+(150*60000))));
ProgramGridCell cell = new ProgramGridCell(this, this, empty);
cell.setId(currentCellId++);
cell.setLayoutParams(new ViewGroup.LayoutParams(150 * PIXELS_PER_MINUTE, ROW_HEIGHT));
cell.setFocusable(true);
programRow.addView(cell);
return programRow;
}
long prevEnd = getCurrentLocalStartDate();
for (BaseItemDto item : programs) {
long start = item.getStartDate() != null ? Utils.convertToLocalDate(item.getStartDate()).getTime() : getCurrentLocalStartDate();
if (start < getCurrentLocalStartDate()) start = getCurrentLocalStartDate();
if (start > prevEnd) {
// fill empty time slot
BaseItemDto empty = new BaseItemDto();
empty.setName(" <No Program Data Available>");
empty.setChannelId(channelId);
empty.setStartDate(Utils.convertToUtcDate(new Date(prevEnd)));
Long duration = (start - prevEnd);
empty.setEndDate(Utils.convertToUtcDate(new Date(prevEnd+duration)));
ProgramGridCell cell = new ProgramGridCell(this, this, empty);
cell.setId(currentCellId++);
cell.setLayoutParams(new ViewGroup.LayoutParams(((Long)(duration / 60000)).intValue() * PIXELS_PER_MINUTE, ROW_HEIGHT));
cell.setFocusable(true);
programRow.addView(cell);
}
long end = item.getEndDate() != null ? Utils.convertToLocalDate(item.getEndDate()).getTime() : getCurrentLocalEndDate();
if (end > getCurrentLocalEndDate()) end = getCurrentLocalEndDate();
prevEnd = end;
Long duration = (end - start) / 60000;
//TvApp.getApplication().getLogger().Debug("Duration for "+item.getName()+" is "+duration.intValue());
if (duration > 0) {
ProgramGridCell program = new ProgramGridCell(this, this, item);
program.setId(currentCellId++);
program.setLayoutParams(new ViewGroup.LayoutParams(duration.intValue() * PIXELS_PER_MINUTE, ROW_HEIGHT));
program.setFocusable(true);
programRow.addView(program);
}
}
return programRow;
}
private void fillTimeLine(int hours) {
mCurrentGuideStart = Calendar.getInstance();
mCurrentGuideStart.set(Calendar.MINUTE, mCurrentGuideStart.get(Calendar.MINUTE) >= 30 ? 30 : 0);
mCurrentGuideStart.set(Calendar.SECOND, 0);
mCurrentGuideStart.set(Calendar.MILLISECOND, 0);
mCurrentLocalGuideStart = mCurrentGuideStart.getTimeInMillis();
mDisplayDate.setText(Utils.getFriendlyDate(mCurrentGuideStart.getTime()));
Calendar current = (Calendar) mCurrentGuideStart.clone();
mCurrentGuideEnd = (Calendar) mCurrentGuideStart.clone();
int oneHour = 60 * PIXELS_PER_MINUTE;
int halfHour = 30 * PIXELS_PER_MINUTE;
int interval = current.get(Calendar.MINUTE) >= 30 ? 30 : 60;
mCurrentGuideEnd.add(Calendar.HOUR, hours);
mCurrentLocalGuideEnd = mCurrentGuideEnd.getTimeInMillis();
mTimeline.removeAllViews();
while (current.before(mCurrentGuideEnd)) {
TextView time = new TextView(this);
time.setText(android.text.format.DateFormat.getTimeFormat(this).format(current.getTime()));
time.setWidth(interval == 30 ? halfHour : oneHour);
mTimeline.addView(time);
current.add(Calendar.MINUTE, interval);
//after first one, we always go on hours
interval = 60;
}
}
public long getCurrentLocalStartDate() { return mCurrentLocalGuideStart; }
public long getCurrentLocalEndDate() { return mCurrentLocalGuideEnd; }
private Runnable detailUpdateTask = new Runnable() {
@Override
public void run() {
if (mSelectedProgram.getOverview() == null && mSelectedProgram.getId() != null) {
TvApp.getApplication().getApiClient().GetItemAsync(mSelectedProgram.getId(), TvApp.getApplication().getCurrentUser().getId(), new Response<BaseItemDto>() {
@Override
public void onResponse(BaseItemDto response) {
mSelectedProgram = response;
detailUpdateInternal();
}
@Override
public void onError(Exception exception) {
TvApp.getApplication().getLogger().ErrorException("Unable to get program details", exception);
detailUpdateInternal();
}
});
} else {
detailUpdateInternal();
}
}
};
private void detailUpdateInternal() {
mTitle.setText(mSelectedProgram.getName());
mSummary.setText(mSelectedProgram.getOverview());
if (mSelectedProgram.getId() != null) {
mDisplayDate.setText(Utils.getFriendlyDate(Utils.convertToLocalDate(mSelectedProgram.getStartDate())));
String url = Utils.getPrimaryImageUrl(mSelectedProgram, TvApp.getApplication().getApiClient());
Picasso.with(mActivity).load(url).resize(IMAGE_SIZE, IMAGE_SIZE).centerInside().into(mImage);
//info row
InfoLayoutHelper.addInfoRow(mActivity, mSelectedProgram, mInfoRow, false, false);
if (Utils.isTrue(mSelectedProgram.getIsNews())) {
mBackdrop.setImageResource(R.drawable.newsbanner);
} else if (Utils.isTrue(mSelectedProgram.getIsKids())) {
mBackdrop.setImageResource(R.drawable.kidsbanner);
} else if (Utils.isTrue(mSelectedProgram.getIsSports())) {
mBackdrop.setImageResource(R.drawable.sportsbanner);
} else if (Utils.isTrue(mSelectedProgram.getIsMovie())) {
mBackdrop.setImageResource(R.drawable.moviebanner);
} else {
mBackdrop.setImageResource(R.drawable.tvbanner);
}
} else {
mInfoRow.removeAllViews();
mBackdrop.setImageResource(R.drawable.tvbanner);
mImage.setImageResource(R.drawable.blank10x10);
}
}
public void setSelectedProgram(ProgramGridCell programView) {
mSelectedProgramView = programView;
mSelectedProgram = programView.getProgram();
mHandler.removeCallbacks(detailUpdateTask);
mHandler.postDelayed(detailUpdateTask, 500);
}
}