diff options
Diffstat (limited to 'src/com/android/tv/guide')
-rw-r--r-- | src/com/android/tv/guide/ProgramGrid.java | 60 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramGuide.java | 19 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramItemView.java | 136 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramListAdapter.java | 48 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramManager.java | 179 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramRow.java | 2 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramTableAdapter.java | 92 | ||||
-rw-r--r-- | src/com/android/tv/guide/TimelineRow.java | 4 |
8 files changed, 386 insertions, 154 deletions
diff --git a/src/com/android/tv/guide/ProgramGrid.java b/src/com/android/tv/guide/ProgramGrid.java index 1339ddf8..77de5827 100644 --- a/src/com/android/tv/guide/ProgramGrid.java +++ b/src/com/android/tv/guide/ProgramGrid.java @@ -20,6 +20,7 @@ import android.content.Context; import android.content.res.Resources; import android.graphics.Rect; import android.support.v17.leanback.widget.VerticalGridView; +import android.support.v7.widget.RecyclerView.LayoutManager; import android.util.AttributeSet; import android.util.Log; import android.view.View; @@ -66,6 +67,16 @@ public class ProgramGrid extends VerticalGridView { } }; + private final ViewTreeObserver.OnPreDrawListener mPreDrawListener = + new ViewTreeObserver.OnPreDrawListener() { + @Override + public boolean onPreDraw() { + getViewTreeObserver().removeOnPreDrawListener(this); + updateInputLogo(); + return true; + } + }; + private ProgramManager mProgramManager; private View mNextFocusByUpDown; @@ -83,7 +94,7 @@ public class ProgramGrid extends VerticalGridView { private boolean mKeepCurrentProgram; private ChildFocusListener mChildFocusListener; - private OnRepeatedKeyInterceptListener mOnRepeatedKeyInterceptListener; + private final OnRepeatedKeyInterceptListener mOnRepeatedKeyInterceptListener; interface ChildFocusListener { /** @@ -332,6 +343,10 @@ public class ProgramGrid extends VerticalGridView { return contains((View) v.getParent()); } + public void onItemSelectionReset() { + getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); + } + @Override public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { if (mLastFocusedView != null && mLastFocusedView.isShown()) { @@ -359,6 +374,49 @@ public class ProgramGrid extends VerticalGridView { int maxY = (mSelectionRow + 1) * mRowHeight + mDetailHeight; if (y > maxY) scrollBy(0, y - maxY); } + updateInputLogo(); + } + + @Override + public void onViewRemoved(View view) { + // It is required to ensure input logo showing when the scroll is moved to most bottom. + updateInputLogo(); + } + + private int getFirstVisibleChildIndex() { + final LayoutManager mLayoutManager = getLayoutManager(); + int top = mLayoutManager.getPaddingTop(); + int childCount = getChildCount(); + for (int i = 0; i < childCount; i++) { + View childView = getChildAt(i); + int childTop = mLayoutManager.getDecoratedTop(childView); + int childBottom = mLayoutManager.getDecoratedBottom(childView); + if ((childTop + childBottom) / 2 > top) { + return i; + } + } + return -1; + } + + public void updateInputLogo() { + int childCount = getChildCount(); + if (childCount == 0) { + return; + } + int firstVisibleChildIndex = getFirstVisibleChildIndex(); + if (firstVisibleChildIndex == -1) { + return; + } + View childView = getChildAt(firstVisibleChildIndex); + int childAdapterPosition = getChildAdapterPosition(childView); + ((ProgramTableAdapter.ProgramRowHolder) getChildViewHolder(childView)) + .updateInputLogo(childAdapterPosition, true); + for (int i = firstVisibleChildIndex + 1; i < childCount; i++) { + childView = getChildAt(i); + ((ProgramTableAdapter.ProgramRowHolder) getChildViewHolder(childView)) + .updateInputLogo(childAdapterPosition, false); + childAdapterPosition = getChildAdapterPosition(childView); + } } private static void findFocusables(View v, ArrayList<View> outFocusable) { diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java index 77a1146b..bfcb8b0d 100644 --- a/src/com/android/tv/guide/ProgramGuide.java +++ b/src/com/android/tv/guide/ProgramGuide.java @@ -31,6 +31,7 @@ import android.os.Message; import android.os.SystemClock; import android.preference.PreferenceManager; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v17.leanback.widget.OnChildSelectedListener; import android.support.v17.leanback.widget.SearchOrbView; import android.support.v17.leanback.widget.VerticalGridView; @@ -52,6 +53,7 @@ import com.android.tv.common.WeakHandler; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.GenreItems; import com.android.tv.data.ProgramDataManager; +import com.android.tv.dvr.DvrDataManager; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -160,12 +162,11 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { public ProgramGuide(MainActivity activity, ChannelTuner channelTuner, TvInputManagerHelper tvInputManagerHelper, ChannelDataManager channelDataManager, - ProgramDataManager programDataManager, Tracker tracker, Runnable preShowRunnable, - Runnable postHideRunnable) { + ProgramDataManager programDataManager, @Nullable DvrDataManager dvrDataManager, + Tracker tracker, Runnable preShowRunnable, Runnable postHideRunnable) { mActivity = activity; - mProgramManager = new ProgramManager(tvInputManagerHelper, - channelDataManager, - programDataManager); + mProgramManager = new ProgramManager(tvInputManagerHelper, channelDataManager, + programDataManager, dvrDataManager); mChannelTuner = channelTuner; mTracker = tracker; mPreShowRunnable = preShowRunnable; @@ -245,7 +246,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mTimelineRow.setAdapter(mTimeListAdapter); ProgramTableAdapter programTableAdapter = new ProgramTableAdapter(mActivity, - tvInputManagerHelper, mProgramManager, this); + mProgramManager, this); programTableAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { @@ -590,7 +591,10 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { return mTimelineRow.getScrollOffset(); } - private void cancelHide() { + /** + * Cancel hiding the program guide. + */ + public void cancelHide() { mHandler.removeCallbacks(mHideRunnable); } @@ -720,6 +724,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { Math.max(mProgramManager.getChannelIndex(mChannelTuner.getCurrentChannel()), 0)); mGrid.resetFocusState(); + mGrid.onItemSelectionReset(); mIsDuringResetRowSelection = false; } diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java index 09a93037..172ee070 100644 --- a/src/com/android/tv/guide/ProgramItemView.java +++ b/src/com/android/tv/guide/ProgramItemView.java @@ -16,9 +16,7 @@ package com.android.tv.guide; -import android.app.AlertDialog; import android.content.Context; -import android.content.DialogInterface; import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.drawable.Drawable; @@ -26,6 +24,7 @@ import android.graphics.drawable.LayerDrawable; import android.graphics.drawable.StateListDrawable; import android.os.Handler; import android.os.SystemClock; +import android.support.v4.os.BuildCompat; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; @@ -43,15 +42,14 @@ import com.android.tv.TvApplication; import com.android.tv.analytics.Tracker; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; -import com.android.tv.data.Program; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.Recording; +import com.android.tv.dvr.ui.DvrDialogFragment; +import com.android.tv.dvr.ui.DvrRecordDeleteFragment; +import com.android.tv.dvr.ui.DvrRecordScheduleFragment; import com.android.tv.guide.ProgramManager.TableEntry; import com.android.tv.util.Utils; import java.lang.reflect.InvocationTargetException; -import java.util.ArrayList; -import java.util.List; import java.util.concurrent.TimeUnit; public class ProgramItemView extends TextView { @@ -60,9 +58,6 @@ public class ProgramItemView extends TextView { private static final long FOCUS_UPDATE_FREQUENCY = TimeUnit.SECONDS.toMillis(1); private static final int MAX_PROGRESS = 10000; // From android.widget.ProgressBar.MAX_VALUE - private static final int ACTION_RECORD_PROGRAM = 100; - private static final int ACTION_RECORD_SEASON = 101; - // State indicating the focused program is the current program private static final int[] STATE_CURRENT_PROGRAM = { R.attr.state_current_program }; @@ -89,6 +84,10 @@ public class ProgramItemView extends TextView { @Override public void onClick(final View view) { TableEntry entry = ((ProgramItemView) view).mTableEntry; + if (entry == null) { + //do nothing + return; + } ApplicationSingletons singletons = TvApplication.getSingletons(view.getContext()); Tracker tracker = singletons.getTracker(); tracker.sendEpgItemClicked(); @@ -105,78 +104,38 @@ public class ProgramItemView extends TextView { }, entry.getWidth() > ((ProgramItemView) view).mMaxWidthForRipple ? 0 : view.getResources() .getInteger(R.integer.program_guide_ripple_anim_duration)); - } else if (CommonFeatures.DVR.isEnabled(view.getContext())) { + } else if (CommonFeatures.DVR.isEnabled(view.getContext()) && BuildCompat + .isAtLeastN()) { final MainActivity tvActivity = (MainActivity) view.getContext(); final DvrManager dvrManager = singletons.getDvrManager(); final Channel channel = tvActivity.getChannelDataManager() .getChannel(entry.channelId); - if (dvrManager.canRecord(channel.getInputId())) { - showDvrDialog(view, entry, dvrManager); + if (dvrManager.canRecord(channel.getInputId()) && entry.program != null) { + if (entry.scheduledRecording == null) { + showDvrDialog(view, entry); + } else { + showRecordDeleteDialog(view, entry); + } } } } - private void showDvrDialog(final View view, TableEntry entry, final DvrManager dvrManager) { - List<CharSequence> items = new ArrayList<>(); - final List<Integer> actions = new ArrayList<>(); - // TODO: the items can be changed by the state of the program. For example, - // if the program is already added in scheduler, we need to show an item to - // delete the recording schedule. - items.add(view.getResources().getString(R.string.epg_dvr_record_program)); - actions.add(ACTION_RECORD_PROGRAM); - items.add(view.getResources().getString(R.string.epg_dvr_record_season)); - actions.add(ACTION_RECORD_SEASON); - - final Program program = entry.program; - final List<Recording> conflicts = dvrManager - .getScheduledRecordingsThatConflict(program); - // TODO: it is a tentative UI. Don't publish the UI. - DialogInterface.OnClickListener onClickListener - = new DialogInterface.OnClickListener() { - @Override - public void onClick(final DialogInterface dialog, int which) { - if (actions.get(which) == ACTION_RECORD_PROGRAM) { - if (conflicts.isEmpty()) { - dvrManager.addSchedule(program, conflicts); - } else { - showConflictDialog(view, dvrManager, program, conflicts); - } - } else if (actions.get(which) == ACTION_RECORD_SEASON) { - dvrManager.addSeasonSchedule(program); - } - dialog.dismiss(); - } - }; - new AlertDialog.Builder(view.getContext()) - .setItems(items.toArray(new CharSequence[items.size()]), onClickListener) - .create() - .show(); + private void showDvrDialog(final View view, TableEntry entry) { + Utils.showToastMessageForDeveloperFeature(view.getContext()); + DvrRecordScheduleFragment dvrRecordScheduleFragment = + new DvrRecordScheduleFragment(entry); + DvrDialogFragment dvrDialogFragment = new DvrDialogFragment(dvrRecordScheduleFragment); + ((MainActivity) view.getContext()).getOverlayManager().showDialogFragment( + DvrDialogFragment.DIALOG_TAG, dvrDialogFragment, true, true); } - }; - private static void showConflictDialog(final View view, final DvrManager dvrManager, - final Program program, final List<Recording> conflicts) { - DialogInterface.OnClickListener conflictClickListener - = new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - if (which == AlertDialog.BUTTON_POSITIVE) { - dvrManager.addSchedule(program, conflicts); - dialog.dismiss(); - } - } - }; - StringBuilder sb = new StringBuilder(); - for (Recording r : conflicts) { - sb.append(r.toString()).append('\n'); + private void showRecordDeleteDialog(final View view, final TableEntry entry) { + DvrRecordDeleteFragment recordDeleteDialogFragment = new DvrRecordDeleteFragment(entry); + DvrDialogFragment dvrDialogFragment = new DvrDialogFragment(recordDeleteDialogFragment); + ((MainActivity) view.getContext()).getOverlayManager().showDialogFragment( + DvrDialogFragment.DIALOG_TAG, dvrDialogFragment, true, true); } - new AlertDialog.Builder(view.getContext()).setTitle(R.string.dvr_epg_conflict_dialog_title) - .setMessage(sb.toString()) - .setPositiveButton(R.string.dvr_epg_record, conflictClickListener) - .setNegativeButton(R.string.dvr_epg_do_not_record, conflictClickListener) - .create() - .show(); - } + }; private static final View.OnFocusChangeListener ON_FOCUS_CHANGED = new View.OnFocusChangeListener() { @@ -198,6 +157,10 @@ public class ProgramItemView extends TextView { public void run() { refreshDrawableState(); TableEntry entry = mTableEntry; + if (entry == null) { + //do nothing + return; + } if (entry.isCurrentProgram()) { Drawable background = getBackground(); int progress = getProgress(entry.entryStartUtcMillis, entry.entryEndUtcMillis); @@ -220,6 +183,8 @@ public class ProgramItemView extends TextView { public ProgramItemView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); + setOnClickListener(ON_CLICKED); + setOnFocusChangeListener(ON_FOCUS_CHANGED); } private void initIfNeeded() { @@ -282,11 +247,9 @@ public class ProgramItemView extends TextView { return mTableEntry; } - public void onBind(TableEntry entry, ProgramListAdapter adapter) { + public void setValues(TableEntry entry, int selectedGenreId, long fromUtcMillis, + long toUtcMillis, String gapTitle) { mTableEntry = entry; - setOnClickListener(ON_CLICKED); - setOnFocusChangeListener(ON_FOCUS_CHANGED); - ProgramManager programManager = adapter.getProgramManager(); ViewGroup.LayoutParams layoutParams = getLayoutParams(); layoutParams.width = entry.getWidth(); @@ -303,16 +266,19 @@ public class ProgramItemView extends TextView { setText(null); } else { if (entry.isGap()) { - if (entry.isBlocked()) { - title = adapter.getBlockedProgramTitle(); - } else { - title = adapter.getNoInfoProgramTitle(); - } + title = gapTitle; episode = null; - } else if (entry.hasGenre(programManager.getSelectedGenreId())) { + } else if (entry.hasGenre(selectedGenreId)) { titleStyle = sProgramTitleStyle; episodeStyle = sEpisodeTitleStyle; } + if (TextUtils.isEmpty(title)) { + title = getResources().getString(R.string.program_title_for_no_information); + } + if (mTableEntry.scheduledRecording != null) { + //TODO(dvr): use a proper icon for UI status. + title = "®" + title; + } SpannableStringBuilder description = new SpannableStringBuilder(); description.append(title); @@ -340,12 +306,11 @@ public class ProgramItemView extends TextView { measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED); mTextWidth = getMeasuredWidth() - getPaddingStart() - getPaddingEnd(); int start = GuideUtils.convertMillisToPixel(entry.entryStartUtcMillis); - int guideStart = GuideUtils.convertMillisToPixel(programManager.getFromUtcMillis()); + int guideStart = GuideUtils.convertMillisToPixel(fromUtcMillis); layoutVisibleArea(guideStart - start); // Maximum width for us to use a ripple - mMaxWidthForRipple = GuideUtils.convertMillisToPixel( - programManager.getFromUtcMillis(), programManager.getToUtcMillis()); + mMaxWidthForRipple = GuideUtils.convertMillisToPixel(fromUtcMillis, toUtcMillis); } /** @@ -374,14 +339,13 @@ public class ProgramItemView extends TextView { } } - public void onUnbind() { + public void clearValues() { if (getHandler() != null) { getHandler().removeCallbacks(mUpdateFocus); } setTag(null); - setOnFocusChangeListener(null); - setOnClickListener(null); + mTableEntry = null; } private static int getProgress(long start, long end) { diff --git a/src/com/android/tv/guide/ProgramListAdapter.java b/src/com/android/tv/guide/ProgramListAdapter.java index 88ba435e..03aea5ad 100644 --- a/src/com/android/tv/guide/ProgramListAdapter.java +++ b/src/com/android/tv/guide/ProgramListAdapter.java @@ -16,7 +16,6 @@ package com.android.tv.guide; -import android.content.Context; import android.content.res.Resources; import android.support.v7.widget.RecyclerView; import android.util.Log; @@ -33,30 +32,24 @@ import com.android.tv.guide.ProgramManager.TableEntry; * Adapts a program list for a specific channel from {@link ProgramManager} to a row of the program * guide table. */ -public class ProgramListAdapter extends - RecyclerView.Adapter<ProgramListAdapter.ProgramViewHolder> implements - TableEntriesUpdatedListener { +public class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter.ProgramViewHolder> + implements TableEntriesUpdatedListener { private static final String TAG = "ProgramListAdapter"; private static final boolean DEBUG = false; - private final String mNoInfoProgramTitle; - private final String mBlockedProgramTitle; - private final ProgramManager mProgramManager; private final int mChannelIndex; + private final String mNoInfoProgramTitle; + private final String mBlockedProgramTitle; private long mChannelId; - public ProgramListAdapter(Context context, ProgramManager programManager, - int channelIndex) { - Resources res = context.getResources(); - mNoInfoProgramTitle = res.getString( - R.string.program_title_for_no_information); - mBlockedProgramTitle = res.getString( - R.string.program_title_for_blocked_channel); - + public ProgramListAdapter(Resources res, ProgramManager programManager, int channelIndex) { + setHasStableIds(true); mProgramManager = programManager; mChannelIndex = channelIndex; + mNoInfoProgramTitle = res.getString(R.string.program_title_for_no_information); + mBlockedProgramTitle = res.getString(R.string.program_title_for_blocked_channel); onTableEntriesUpdated(); } @@ -76,14 +69,6 @@ public class ProgramListAdapter extends return mProgramManager; } - public String getNoInfoProgramTitle() { - return mNoInfoProgramTitle; - } - - public String getBlockedProgramTitle() { - return mBlockedProgramTitle; - } - @Override public int getItemCount() { return mProgramManager.getTableEntryCount(mChannelId); @@ -95,8 +80,15 @@ public class ProgramListAdapter extends } @Override + public long getItemId(int position) { + return mProgramManager.getTableEntry(mChannelId, position).getId(); + } + + @Override public void onBindViewHolder(ProgramViewHolder holder, int position) { - holder.onBind(mProgramManager.getTableEntry(mChannelId, position), this); + TableEntry tableEntry = mProgramManager.getTableEntry(mChannelId, position); + String gapTitle = tableEntry.isBlocked() ? mBlockedProgramTitle : mNoInfoProgramTitle; + holder.onBind(tableEntry, this.getProgramManager(), gapTitle); } @Override @@ -116,16 +108,16 @@ public class ProgramListAdapter extends super(itemView); } - public void onBind(TableEntry entry, ProgramListAdapter adapter) { + public void onBind(TableEntry entry, ProgramManager programManager, String gapTitle) { if (DEBUG) { Log.d(TAG, "onBind. View = " + itemView + ", Entry = " + entry); } - - ((ProgramItemView) itemView).onBind(entry, adapter); + ((ProgramItemView) itemView).setValues(entry, programManager.getSelectedGenreId(), + programManager.getFromUtcMillis(), programManager.getToUtcMillis(), gapTitle); } public void onUnbind() { - ((ProgramItemView) itemView).onUnbind(); + ((ProgramItemView) itemView).clearValues(); } } } diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java index df52abbe..fe1a981f 100644 --- a/src/com/android/tv/guide/ProgramManager.java +++ b/src/com/android/tv/guide/ProgramManager.java @@ -17,14 +17,17 @@ package com.android.tv.guide; import android.support.annotation.MainThread; +import android.support.annotation.Nullable; +import android.util.ArraySet; import android.util.Log; -import com.android.tv.common.CollectionUtils; import com.android.tv.data.Channel; import com.android.tv.data.ChannelDataManager; import com.android.tv.data.GenreItems; import com.android.tv.data.Program; import com.android.tv.data.ProgramDataManager; +import com.android.tv.dvr.DvrDataManager; +import com.android.tv.dvr.ScheduledRecording; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -55,6 +58,7 @@ public class ProgramManager { private final TvInputManagerHelper mTvInputManagerHelper; private final ChannelDataManager mChannelDataManager; private final ProgramDataManager mProgramDataManager; + private final DvrDataManager mDvrDataManager; // Only set if DVR is enabled private long mStartUtcMillis; private long mEndUtcMillis; @@ -74,6 +78,8 @@ public class ProgramManager { /** Program corresponding to the entry. {@code null} means that this entry is a gap. */ public final Program program; + public final ScheduledRecording scheduledRecording; + /** Start time of entry in UTC milliseconds. */ public final long entryStartUtcMillis; @@ -82,34 +88,39 @@ public class ProgramManager { private final boolean mIsBlocked; - private TableEntry(long startUtcMillis, long endUtcMillis) { - this(INVALID_ID, null, startUtcMillis, endUtcMillis, false); - } - private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) { this(channelId, null, startUtcMillis, endUtcMillis, false); } private TableEntry(long channelId, long startUtcMillis, long endUtcMillis, boolean blocked) { - this(channelId, null, startUtcMillis, endUtcMillis, blocked); + this(channelId, null, null, startUtcMillis, endUtcMillis, blocked); } - private TableEntry(long channelId, Program program, - long entryStartUtcMillis, long entryEndUtcMillis) { - this(channelId, program, entryStartUtcMillis, entryEndUtcMillis, false); + private TableEntry(long channelId, Program program, long entryStartUtcMillis, + long entryEndUtcMillis, boolean isBlocked) { + this(channelId, program, null, entryStartUtcMillis, entryEndUtcMillis, isBlocked); } - private TableEntry(long channelId, Program program, + private TableEntry(long channelId, Program program, ScheduledRecording scheduledRecording, long entryStartUtcMillis, long entryEndUtcMillis, boolean isBlocked) { this.channelId = channelId; this.program = program; + this.scheduledRecording = scheduledRecording; this.entryStartUtcMillis = entryStartUtcMillis; this.entryEndUtcMillis = entryEndUtcMillis; mIsBlocked = isBlocked; } /** + * A stable id useful for {@link android.support.v7.widget.RecyclerView.Adapter}. + */ + public long getId() { + // using a negative entryEndUtcMillis keeps it from conflicting with program Id + return program != null ? program.getId() : -entryEndUtcMillis; + } + + /** * Returns true if this is a gap. */ public boolean isGap() { @@ -167,9 +178,10 @@ public class ProgramManager { // Should be matched with mSelectedGenreId always. private List<Channel> mFilteredChannels = mChannels; - private final Set<Listener> mListeners = CollectionUtils.createSmallSet(); - private final Set<TableEntriesUpdatedListener> mTableEntriesUpdatedListeners = CollectionUtils - .createSmallSet(); + private final Set<Listener> mListeners = new ArraySet<>(); + private final Set<TableEntriesUpdatedListener> mTableEntriesUpdatedListeners = new ArraySet<>(); + + private final Set<TableEntryChangedListener> mTableEntryChangedListeners = new ArraySet<>(); private final ChannelDataManager.Listener mChannelDataManagerListener = new ChannelDataManager.Listener() { @@ -197,12 +209,49 @@ public class ProgramManager { } }; + private final DvrDataManager.ScheduledRecordingListener mScheduledRecordingListener = + new DvrDataManager.ScheduledRecordingListener() { + @Override + public void onScheduledRecordingAdded(ScheduledRecording scheduledRecording) { + TableEntry oldEntry = getTableEntry(scheduledRecording); + if (oldEntry != null) { + TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program, + scheduledRecording, oldEntry.entryStartUtcMillis, + oldEntry.entryEndUtcMillis, oldEntry.isBlocked()); + updateEntry(oldEntry, newEntry); + } + } + + @Override + public void onScheduledRecordingRemoved(ScheduledRecording scheduledRecording) { + TableEntry oldEntry = getTableEntry(scheduledRecording); + if (oldEntry != null) { + TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program, null, + oldEntry.entryStartUtcMillis, oldEntry.entryEndUtcMillis, + oldEntry.isBlocked()); + updateEntry(oldEntry, newEntry); + } + } + + @Override + public void onScheduledRecordingStatusChanged(ScheduledRecording scheduledRecording) { + TableEntry oldEntry = getTableEntry(scheduledRecording); + if (oldEntry != null) { + TableEntry newEntry = new TableEntry(oldEntry.channelId, oldEntry.program, + scheduledRecording, oldEntry.entryStartUtcMillis, + oldEntry.entryEndUtcMillis, oldEntry.isBlocked()); + updateEntry(oldEntry, newEntry); + } + } + }; + public ProgramManager(TvInputManagerHelper tvInputManagerHelper, - ChannelDataManager channelDataManager, - ProgramDataManager programDataManager) { + ChannelDataManager channelDataManager, ProgramDataManager programDataManager, + @Nullable DvrDataManager dvrDataManager) { mTvInputManagerHelper = tvInputManagerHelper; mChannelDataManager = channelDataManager; mProgramDataManager = programDataManager; + mDvrDataManager = dvrDataManager; } public void programGuideVisibilityChanged(boolean visible) { @@ -210,41 +259,61 @@ public class ProgramManager { if (visible) { mChannelDataManager.addListener(mChannelDataManagerListener); mProgramDataManager.addListener(mProgramDataManagerListener); + if (mDvrDataManager != null) { + mDvrDataManager.addScheduledRecordingListener(mScheduledRecordingListener); + } } else { mChannelDataManager.removeListener(mChannelDataManagerListener); mProgramDataManager.removeListener(mProgramDataManagerListener); + if (mDvrDataManager != null) { + mDvrDataManager.removeScheduledRecordingListener(mScheduledRecordingListener); + } } } /** - * Add a {@link Listener}. + * Adds a {@link Listener}. */ public void addListener(Listener listener) { mListeners.add(listener); } /** - * Register a listener to be invoked when table entries are updated. + * Registers a listener to be invoked when table entries are updated. */ public void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { mTableEntriesUpdatedListeners.add(listener); } /** - * Remove a {@link Listener}. + * Registers a listener to be invoked when a table entry is changed. + */ + public void addTableEntryChangedListener(TableEntryChangedListener listener) { + mTableEntryChangedListeners.add(listener); + } + + /** + * Removes a {@link Listener}. */ public void removeListener(Listener listener) { mListeners.remove(listener); } /** - * Remove a previously installed table entries update listener. + * Removes a previously installed table entries update listener. */ public void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { mTableEntriesUpdatedListeners.remove(listener); } /** + * Removes a previously installed table entry changed listener. + */ + public void removeTableEntryChangedListener(TableEntryChangedListener listener) { + mTableEntryChangedListeners.remove(listener); + } + + /** * Build genre filters based on the current programs. * This categories channels by its current program's canonical genres * and subsequent @{link resetChannelListWithGenre(int)} calls will reset channel list @@ -366,6 +435,7 @@ public class ProgramManager { } else if (lastEntry.entryEndUtcMillis == Long.MAX_VALUE) { entries.remove(entries.size() - 1); entries.add(new TableEntry(lastEntry.channelId, lastEntry.program, + lastEntry.scheduledRecording, lastEntry.entryStartUtcMillis, mEndUtcMillis, lastEntry.mIsBlocked)); } @@ -403,6 +473,37 @@ public class ProgramManager { } } + private void notifyTableEntryUpdated(TableEntry entry) { + for (TableEntryChangedListener listener : mTableEntryChangedListeners) { + listener.onTableEntryChanged(entry); + } + } + + private void updateEntry(TableEntry old, TableEntry newEntry) { + List<TableEntry> entries = mChannelIdEntriesMap.get(old.channelId); + int index = entries.indexOf(old); + entries.set(index, newEntry); + notifyTableEntryUpdated(newEntry); + } + + @Nullable + private TableEntry getTableEntry(ScheduledRecording scheduledRecording) { + return getTableEntry(scheduledRecording.getChannelId(), scheduledRecording.getProgramId()); + } + + @Nullable + private TableEntry getTableEntry(long channelId, long entryId) { + List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); + if (entries != null) { + for (TableEntry entry : entries) { + if (entry.getId() == entryId) { + return entry; + } + } + } + return null; + } + /** * Returns the start time of currently managed time range, in UTC millisecond. */ @@ -471,6 +572,14 @@ public class ProgramManager { } /** + * Returns the index of channel with {@code channelId} within the currently managed channels. + * Returns -1 if such a channel is not found. + */ + public int getChannelIndex(long channelId) { + return getChannelIndex(mChannelDataManager.getChannel(channelId)); + } + + /** * Returns the number of "entries", which lies within the currently managed time range, for a * given {@code channelId}. */ @@ -511,8 +620,10 @@ public class ProgramManager { lastProgramEndTime = programStartTime; } if (programEndTime > lastProgramEndTime) { - entries.add(new TableEntry(channelId, program, lastProgramEndTime, - programEndTime)); + ScheduledRecording scheduledRecording = mDvrDataManager == null ? null + : mDvrDataManager.getScheduledRecordingForProgramId(program.getId()); + entries.add(new TableEntry(channelId, program, scheduledRecording, + lastProgramEndTime, programEndTime, false)); lastProgramEndTime = programEndTime; } } @@ -525,7 +636,8 @@ public class ProgramManager { // the first entry from UI perspective. So we clip it out. entries.remove(0); entries.set(0, new TableEntry(secondEntry.channelId, secondEntry.program, - mStartUtcMillis, secondEntry.entryEndUtcMillis)); + secondEntry.scheduledRecording, mStartUtcMillis, + secondEntry.entryEndUtcMillis, secondEntry.mIsBlocked)); } } return entries; @@ -555,6 +667,10 @@ public class ProgramManager { void onTableEntriesUpdated(); } + public interface TableEntryChangedListener { + void onTableEntryChanged(TableEntry entry); + } + public static class ListenerAdapter implements Listener { @Override public void onGenresUpdated() { } @@ -598,9 +714,24 @@ public class ProgramManager { } /** - * Returns the program index of the program at {@code time}. + * Returns the program index of the program with {@code entryId} or -1 if not found. + */ + public int getProgramIdIndex(long channelId, long entryId) { + List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); + if (entries != null) { + for (int i = 0; i < entries.size(); i++) { + if (entries.get(i).getId() == entryId) { + return i; + } + } + } + return -1; + } + + /** + * Returns the program index of the program at {@code time} or -1 if not found. */ - public int getProgramIndex(long channelId, long time) { + public int getProgramIndexAtTime(long channelId, long time) { List<TableEntry> entries = mChannelIdEntriesMap.get(channelId); for (int i = 0; i < entries.size(); ++i) { TableEntry entry = entries.get(i); diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java index 4f38b879..54b864db 100644 --- a/src/com/android/tv/guide/ProgramRow.java +++ b/src/com/android/tv/guide/ProgramRow.java @@ -306,7 +306,7 @@ public class ProgramRow extends TimelineGridView { public void resetScroll(int scrollOffset) { long startTime = GuideUtils.convertPixelToMillis(scrollOffset) + mProgramManager.getStartTime(); - int position = mChannel == null ? -1 : mProgramManager.getProgramIndex( + int position = mChannel == null ? -1 : mProgramManager.getProgramIndexAtTime( mChannel.getId(), startTime); if (position < 0) { getLayoutManager().scrollToPosition(0); diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java index a86c1332..83755b5f 100644 --- a/src/com/android/tv/guide/ProgramTableAdapter.java +++ b/src/com/android/tv/guide/ProgramTableAdapter.java @@ -26,7 +26,9 @@ import android.content.res.ColorStateList; import android.content.res.Resources; import android.graphics.Bitmap; import android.media.tv.TvContentRating; +import android.media.tv.TvInputInfo; import android.os.Handler; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.RecycledViewPool; @@ -43,11 +45,15 @@ import android.widget.ImageView; import android.widget.TextView; import com.android.tv.R; +import com.android.tv.TvApplication; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; +import com.android.tv.util.ImageCache; +import com.android.tv.util.ImageLoader; +import com.android.tv.util.ImageLoader.LoadTvInputLogoTask; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -57,8 +63,8 @@ import java.util.List; /** * Adapts the {@link ProgramListAdapter} list to the body of the program guide table. */ -public class ProgramTableAdapter extends - RecyclerView.Adapter<ProgramTableAdapter.ProgramRowHolder> { +public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.ProgramRowHolder> + implements ProgramManager.TableEntryChangedListener { private static final String TAG = "ProgramTableAdapter"; private static final boolean DEBUG = false; @@ -84,10 +90,10 @@ public class ProgramTableAdapter extends private final int mDetailPadding; private final TextAppearanceSpan mEpisodeTitleStyle; - public ProgramTableAdapter(Context context, TvInputManagerHelper tvInputManagerHelper, - ProgramManager programManager, ProgramGuide programGuide) { + public ProgramTableAdapter(Context context, ProgramManager programManager, + ProgramGuide programGuide) { mContext = context; - mTvInputManagerHelper = tvInputManagerHelper; + mTvInputManagerHelper = TvApplication.getSingletons(context).getTvInputManagerHelper(); mProgramManager = programManager; mProgramGuide = programGuide; @@ -140,6 +146,7 @@ public class ProgramTableAdapter extends } }); update(); + mProgramManager.addTableEntryChangedListener(this); } private void update() { @@ -149,7 +156,8 @@ public class ProgramTableAdapter extends } mProgramListAdapters.clear(); for (int i = 0; i < mProgramManager.getChannelCount(); i++) { - ProgramListAdapter listAdapter = new ProgramListAdapter(mContext, mProgramManager, i); + ProgramListAdapter listAdapter = new ProgramListAdapter(mContext.getResources(), + mProgramManager, i); mProgramManager.addTableEntriesUpdatedListener(listAdapter); mProgramListAdapters.add(listAdapter); } @@ -179,6 +187,14 @@ public class ProgramTableAdapter extends return new ProgramRowHolder(itemView); } + @Override + public void onTableEntryChanged(ProgramManager.TableEntry tableEntry) { + int channelIndex = mProgramManager.getChannelIndex(tableEntry.channelId); + int pos = mProgramManager.getProgramIdIndex(tableEntry.channelId, tableEntry.getId()); + if (DEBUG) Log.d(TAG, "update(" + channelIndex + ", " + pos + ")"); + mProgramListAdapters.get(channelIndex).notifyItemChanged(pos, tableEntry); + } + // TODO: make it static public class ProgramRowHolder extends RecyclerView.ViewHolder implements ProgramRow.ChildFocusListener { @@ -223,6 +239,9 @@ public class ProgramTableAdapter extends private final TextView mChannelNameView; private final ImageView mChannelLogoView; private final ImageView mChannelBlockView; + private final ImageView mInputLogoView; + + private boolean mIsInputLogoVisible; public ProgramRowHolder(View itemView) { super(itemView); @@ -244,6 +263,7 @@ public class ProgramTableAdapter extends mChannelNameView = (TextView) mContainer.findViewById(R.id.channel_name); mChannelLogoView = (ImageView) mContainer.findViewById(R.id.channel_logo); mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block); + mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo); } public void onBind(int position) { @@ -267,6 +287,8 @@ public class ProgramTableAdapter extends if (DEBUG) Log.d(TAG, "onBindChannel " + channel); mChannel = channel; + mInputLogoView.setVisibility(View.GONE); + mIsInputLogoVisible = false; if (channel == null) { mChannelNumberView.setVisibility(View.GONE); mChannelNameView.setVisibility(View.GONE); @@ -467,6 +489,43 @@ public class ProgramTableAdapter extends } } + /** + * Update tv input logo. It should be called when the visible child item in ProgramGrid + * changed. + */ + public void updateInputLogo(int lastPosition, boolean forceShow) { + if (mChannel == null) { + mInputLogoView.setVisibility(View.GONE); + mIsInputLogoVisible = false; + return; + } + + boolean showLogo = forceShow; + if (!showLogo) { + Channel lastChannel = mProgramManager.getChannel(lastPosition); + if (lastChannel == null + || !mChannel.getInputId().equals(lastChannel.getInputId())) { + showLogo = true; + } + } + + if (showLogo) { + if (!mIsInputLogoVisible) { + mIsInputLogoVisible = true; + TvInputInfo info = mTvInputManagerHelper.getTvInputInfo(mChannel.getInputId()); + if (info != null) { + LoadTvInputLogoTask task = new LoadTvInputLogoTask( + itemView.getContext(), ImageCache.getInstance(), info); + ImageLoader.loadBitmap(createTvInputLogoLoadedCallback(info, this), task); + } + } + } else { + mInputLogoView.setVisibility(View.GONE); + mInputLogoView.setImageDrawable(null); + mIsInputLogoVisible = false; + } + } + private void updateTextView(TextView textView, String text) { if (!TextUtils.isEmpty(text)) { textView.setVisibility(View.VISIBLE); @@ -487,6 +546,14 @@ public class ProgramTableAdapter extends mChannelLogoView.setVisibility(View.VISIBLE); } + private void updateInputLogoInternal(@NonNull Bitmap tvInputLogo) { + if (!mIsInputLogoVisible) { + return; + } + mInputLogoView.setImageBitmap(tvInputLogo); + mInputLogoView.setVisibility(View.VISIBLE); + } + private void onHorizontalScrolled() { if (mDetailInAnimator != null) { mHandler.removeCallbacks(mDetailInStarter); @@ -526,4 +593,17 @@ public class ProgramTableAdapter extends } }; } + + private static ImageLoaderCallback<ProgramRowHolder> createTvInputLogoLoadedCallback( + final TvInputInfo info, ProgramRowHolder holder) { + return new ImageLoaderCallback<ProgramRowHolder>(holder) { + @Override + public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap logo) { + if (logo != null && info.getId() + .equals(holder.mChannel.getInputId())) { + holder.updateInputLogoInternal(logo); + } + } + }; + } } diff --git a/src/com/android/tv/guide/TimelineRow.java b/src/com/android/tv/guide/TimelineRow.java index 891b14cd..3f0c8678 100644 --- a/src/com/android/tv/guide/TimelineRow.java +++ b/src/com/android/tv/guide/TimelineRow.java @@ -64,7 +64,9 @@ public class TimelineRow extends TimelineGridView { public void onRtlPropertiesChanged(int layoutDirection) { super.onRtlPropertiesChanged(layoutDirection); // Reset scroll - scrollTo(getScrollOffset(), false); + if (isAttachedToWindow()) { + scrollTo(getScrollOffset(), false); + } } @Override |