diff options
Diffstat (limited to 'src/com/android/tv/guide')
-rw-r--r-- | src/com/android/tv/guide/GenreListAdapter.java | 23 | ||||
-rw-r--r-- | src/com/android/tv/guide/GuideUtils.java | 110 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramGrid.java | 270 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramGuide.java | 265 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramItemView.java | 33 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramListAdapter.java | 36 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramManager.java | 656 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramRow.java | 33 | ||||
-rw-r--r-- | src/com/android/tv/guide/ProgramTableAdapter.java | 172 | ||||
-rw-r--r-- | src/com/android/tv/guide/TimeListAdapter.java | 35 |
10 files changed, 871 insertions, 762 deletions
diff --git a/src/com/android/tv/guide/GenreListAdapter.java b/src/com/android/tv/guide/GenreListAdapter.java index 2913599c..ce19eb2d 100644 --- a/src/com/android/tv/guide/GenreListAdapter.java +++ b/src/com/android/tv/guide/GenreListAdapter.java @@ -17,6 +17,7 @@ package com.android.tv.guide; import android.content.Context; +import android.support.annotation.MainThread; import android.support.v7.widget.RecyclerView; import android.util.Log; import android.view.LayoutInflater; @@ -32,7 +33,7 @@ import java.util.List; /** * Adapts the genre items obtained from {@link GenreItems} to the program guide side panel. */ -public class GenreListAdapter extends RecyclerView.Adapter<GenreListAdapter.GenreRowHolder> { +class GenreListAdapter extends RecyclerView.Adapter<GenreListAdapter.GenreRowHolder> { private static final String TAG = "GenreListAdapter"; private static final boolean DEBUG = false; @@ -41,7 +42,7 @@ public class GenreListAdapter extends RecyclerView.Adapter<GenreListAdapter.Genr private final ProgramGuide mProgramGuide; private String[] mGenreLabels; - public GenreListAdapter(Context context, ProgramManager programManager, ProgramGuide guide) { + GenreListAdapter(Context context, ProgramManager programManager, ProgramGuide guide) { mContext = context; mProgramManager = programManager; mProgramManager.addListener(new ProgramManager.ListenerAdapter() { @@ -79,16 +80,28 @@ public class GenreListAdapter extends RecyclerView.Adapter<GenreListAdapter.Genr @Override public GenreRowHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); + itemView.addOnAttachStateChangeListener(new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View view) { + // Animation is not meaningful now, skip it. + view.getStateListAnimator().jumpToCurrentState(); + } + + @Override + public void onViewDetachedFromWindow(View view) { + // Do nothing + } + }); return new GenreRowHolder(itemView, mProgramGuide); } - public static class GenreRowHolder extends RecyclerView.ViewHolder implements + static class GenreRowHolder extends RecyclerView.ViewHolder implements View.OnFocusChangeListener { private final ProgramGuide mProgramGuide; private int mGenreId; - // Should be called from main thread. - public GenreRowHolder(View itemView, ProgramGuide programGuide) { + @MainThread + GenreRowHolder(View itemView, ProgramGuide programGuide) { super(itemView); mProgramGuide = programGuide; } diff --git a/src/com/android/tv/guide/GuideUtils.java b/src/com/android/tv/guide/GuideUtils.java index 5d11f061..403d00b5 100644 --- a/src/com/android/tv/guide/GuideUtils.java +++ b/src/com/android/tv/guide/GuideUtils.java @@ -16,30 +16,38 @@ package com.android.tv.guide; +import android.graphics.Rect; +import android.support.annotation.NonNull; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewParent; + +import java.util.ArrayList; import java.util.concurrent.TimeUnit; -public class GuideUtils { +class GuideUtils { + private static final int INVALID_INDEX = -1; private static int sWidthPerHour = 0; /** * Sets the width in pixels that corresponds to an hour in program guide. * Assume that this is called from main thread only, so, no synchronization. */ - public static void setWidthPerHour(int widthPerHour) { + static void setWidthPerHour(int widthPerHour) { sWidthPerHour = widthPerHour; } /** * Gets the number of pixels in program guide table that corresponds to the given milliseconds. */ - public static int convertMillisToPixel(long millis) { + static int convertMillisToPixel(long millis) { return (int) (millis * sWidthPerHour / TimeUnit.HOURS.toMillis(1)); } /** * Gets the number of pixels in program guide table that corresponds to the given range. */ - public static int convertMillisToPixel(long startMillis, long endMillis) { + static int convertMillisToPixel(long startMillis, long endMillis) { // Convert to pixels first to avoid accumulation of rounding errors. return GuideUtils.convertMillisToPixel(endMillis) - GuideUtils.convertMillisToPixel(startMillis); @@ -48,9 +56,101 @@ public class GuideUtils { /** * Gets the time in millis that corresponds to the given pixels in the program guide. */ - public static long convertPixelToMillis(int pixel) { + static long convertPixelToMillis(int pixel) { return pixel * TimeUnit.HOURS.toMillis(1) / sWidthPerHour; } + /** + * Return the view should be focused in the given program row according to the focus range. + + * @param keepCurrentProgramFocused If {@code true}, focuses on the current program if possible, + * else falls back the general logic. + */ + static View findNextFocusedProgram(View programRow, int focusRangeLeft, + int focusRangeRight, boolean keepCurrentProgramFocused) { + ArrayList<View> focusables = new ArrayList<>(); + findFocusables(programRow, focusables); + + if (keepCurrentProgramFocused) { + // Select the current program if possible. + for (int i = 0; i < focusables.size(); ++i) { + View focusable = focusables.get(i); + if (focusable instanceof ProgramItemView + && isCurrentProgram((ProgramItemView) focusable)) { + return focusable; + } + } + } + + // Find the largest focusable among fully overlapped focusables. + int maxFullyOverlappedWidth = Integer.MIN_VALUE; + int maxPartiallyOverlappedWidth = Integer.MIN_VALUE; + int nextFocusIndex = INVALID_INDEX; + for (int i = 0; i < focusables.size(); ++i) { + View focusable = focusables.get(i); + Rect focusableRect = new Rect(); + focusable.getGlobalVisibleRect(focusableRect); + if (focusableRect.left <= focusRangeLeft && focusRangeRight <= focusableRect.right) { + // the old focused range is fully inside the focusable, return directly. + return focusable; + } else if (focusRangeLeft <= focusableRect.left + && focusableRect.right <= focusRangeRight) { + // the focusable is fully inside the old focused range, choose the widest one. + int width = focusableRect.width(); + if (width > maxFullyOverlappedWidth) { + nextFocusIndex = i; + maxFullyOverlappedWidth = width; + } + } else if (maxFullyOverlappedWidth == Integer.MIN_VALUE) { + int overlappedWidth = (focusRangeLeft <= focusableRect.left) ? + focusRangeRight - focusableRect.left + : focusableRect.right - focusRangeLeft; + if (overlappedWidth > maxPartiallyOverlappedWidth) { + nextFocusIndex = i; + maxPartiallyOverlappedWidth = overlappedWidth; + } + } + } + if (nextFocusIndex != INVALID_INDEX) { + return focusables.get(nextFocusIndex); + } + return null; + } + + /** + * Returns {@code true} if the program displayed in the give + * {@link com.android.tv.guide.ProgramItemView} is a current program. + */ + static boolean isCurrentProgram(ProgramItemView view) { + return view.getTableEntry().isCurrentProgram(); + } + + /** + * Returns {@code true} if the given view is a descendant of the give container. + */ + static boolean isDescendant(ViewGroup container, View view) { + if (view == null) { + return false; + } + for (ViewParent p = view.getParent(); p != null; p = p.getParent()) { + if (p == container) { + return true; + } + } + return false; + } + + private static void findFocusables(View v, ArrayList<View> outFocusable) { + if (v.isFocusable()) { + outFocusable.add(v); + } + if (v instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) v; + for (int i = 0; i < viewGroup.getChildCount(); ++i) { + findFocusables(viewGroup.getChildAt(i), outFocusable); + } + } + } + private GuideUtils() { } } diff --git a/src/com/android/tv/guide/ProgramGrid.java b/src/com/android/tv/guide/ProgramGrid.java index 77de5827..58436425 100644 --- a/src/com/android/tv/guide/ProgramGrid.java +++ b/src/com/android/tv/guide/ProgramGrid.java @@ -20,17 +20,15 @@ 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.util.Range; import android.view.View; -import android.view.ViewGroup; import android.view.ViewTreeObserver; import com.android.tv.R; import com.android.tv.ui.OnRepeatedKeyInterceptListener; -import java.util.ArrayList; import java.util.concurrent.TimeUnit; /** @@ -52,7 +50,7 @@ public class ProgramGrid extends VerticalGridView { clearUpDownFocusState(newFocus); } mNextFocusByUpDown = null; - if (newFocus != ProgramGrid.this && contains(newFocus)) { + if (GuideUtils.isDescendant(ProgramGrid.this, newFocus)) { mLastFocusedView = newFocus; } } @@ -90,8 +88,9 @@ public class ProgramGrid extends VerticalGridView { private View mLastFocusedView; private final Rect mTempRect = new Rect(); + private int mLastUpDownDirection; - private boolean mKeepCurrentProgram; + private boolean mKeepCurrentProgramFocused; private ChildFocusListener mChildFocusListener; private final OnRepeatedKeyInterceptListener mOnRepeatedKeyInterceptListener; @@ -132,21 +131,6 @@ public class ProgramGrid extends VerticalGridView { setOnKeyInterceptListener(mOnRepeatedKeyInterceptListener); } - /** - * Initializes ProgramGrid. It should be called before the view is actually attached to - * Window. - */ - public void initialize(ProgramManager programManager) { - mProgramManager = programManager; - } - - /** - * Registers a listener focus events occurring on children to the {@code ProgramGrid}. - */ - public void setChildFocusListener(ChildFocusListener childFocusListener) { - mChildFocusListener = childFocusListener; - } - @Override public void requestChildFocus(View child, View focused) { if (mChildFocusListener != null) { @@ -173,11 +157,11 @@ public class ProgramGrid extends VerticalGridView { @Override public View focusSearch(View focused, int direction) { mNextFocusByUpDown = null; - if (focused == null || !contains(focused)) { + if (focused == null || (focused != this && !GuideUtils.isDescendant(this, focused))) { return super.focusSearch(focused, direction); } if (direction == View.FOCUS_UP || direction == View.FOCUS_DOWN) { - updateUpDownFocusState(focused); + updateUpDownFocusState(focused, direction); View nextFocus = focusFind(focused, direction); if (nextFocus != null) { return nextFocus; @@ -186,15 +170,85 @@ public class ProgramGrid extends VerticalGridView { return super.focusSearch(focused, direction); } + @Override + public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { + if (mLastFocusedView != null && mLastFocusedView.isShown()) { + if (mLastFocusedView.requestFocus()) { + return true; + } + } + return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); + } + + @Override + protected void onScrollChanged(int l, int t, int oldl, int oldt) { + // It is required to properly handle OnRepeatedKeyInterceptListener. If the focused + // item's are at the almost end of screen, focus change to the next item doesn't work. + // It restricts that a focus item's position cannot be too far from the desired position. + View focusedView = findFocus(); + if (focusedView != null && mOnRepeatedKeyInterceptListener.isFocusAccelerated()) { + int[] location = new int[2]; + getLocationOnScreen(location); + int[] focusedLocation = new int[2]; + focusedView.getLocationOnScreen(focusedLocation); + int y = focusedLocation[1] - location[1]; + int minY = (mSelectionRow - 1) * mRowHeight; + if (y < minY) scrollBy(0, y - minY); + 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(); + } + + /** + * Initializes ProgramGrid. It should be called before the view is actually attached to + * Window. + */ + void initialize(ProgramManager programManager) { + mProgramManager = programManager; + } + + /** + * Registers a listener focus events occurring on children to the {@code ProgramGrid}. + */ + void setChildFocusListener(ChildFocusListener childFocusListener) { + mChildFocusListener = childFocusListener; + } + + void onItemSelectionReset() { + getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); + } + /** * Resets focus states. If the logic to keep the last focus needs to be cleared, it should * be called. */ - public void resetFocusState() { + void resetFocusState() { mLastFocusedView = null; clearUpDownFocusState(null); } + /** Returns the currently focused item's horizontal range. */ + Range<Integer> getFocusRange() { + return new Range<>(mFocusRangeLeft, mFocusRangeRight); + } + + /** Returns if the next focused item should be the current program if possible. */ + boolean isKeepCurrentProgramFocused() { + return mKeepCurrentProgramFocused; + } + + /** Returns the last up/down move direction of browsing */ + int getLastUpDownDirection() { + return mLastUpDownDirection; + } + private View focusFind(View focused, int direction) { int focusedChildIndex = getFocusedChildIndex(); if (focusedChildIndex == INVALID_INDEX) { @@ -204,85 +258,26 @@ public class ProgramGrid extends VerticalGridView { int nextChildIndex = direction == View.FOCUS_UP ? focusedChildIndex - 1 : focusedChildIndex + 1; if (nextChildIndex < 0 || nextChildIndex >= getChildCount()) { - return focused; - } - View nextChild = getChildAt(nextChildIndex); - ArrayList<View> focusables = new ArrayList<>(); - findFocusables(nextChild, focusables); - - int index = INVALID_INDEX; - if (mKeepCurrentProgram) { - // Select the current program if possible. - for (int i = 0; i < focusables.size(); ++i) { - View focusable = focusables.get(i); - if (!(focusable instanceof ProgramItemView)) { - continue; - } - if (((ProgramItemView) focusable).getTableEntry().isCurrentProgram()) { - index = i; - break; - } - } - if (index != INVALID_INDEX) { - mNextFocusByUpDown = focusables.get(index); - return mNextFocusByUpDown; - } else { - mKeepCurrentProgram = false; - } - } - - // Find the largest focusable among fully overlapped focusables. - int maxWidth = Integer.MIN_VALUE; - for (int i = 0; i < focusables.size(); ++i) { - View focusable = focusables.get(i); - Rect focusableRect = mTempRect; - focusable.getGlobalVisibleRect(focusableRect); - if (mFocusRangeLeft <= focusableRect.left && focusableRect.right <= mFocusRangeRight) { - int width = focusableRect.width(); - if (width > maxWidth) { - index = i; - maxWidth = width; - } - } else if (focusableRect.left <= mFocusRangeLeft - && mFocusRangeRight <= focusableRect.right) { - // focusableRect contains [mLeft, mRight]. - index = i; - break; - } - } - if (index != INVALID_INDEX) { - mNextFocusByUpDown = focusables.get(index); - return mNextFocusByUpDown; - } - - // Find the largest overlapped view among partially overlapped focusables. - maxWidth = Integer.MIN_VALUE; - for (int i = 0; i < focusables.size(); ++i) { - View focusable = focusables.get(i); - Rect focusableRect = mTempRect; - focusable.getGlobalVisibleRect(focusableRect); - if (mFocusRangeLeft <= focusableRect.left && focusableRect.left <= mFocusRangeRight) { - int overlappedWidth = mFocusRangeRight - focusableRect.left; - if (overlappedWidth > maxWidth) { - index = i; - maxWidth = overlappedWidth; - } - } else if (mFocusRangeLeft <= focusableRect.right - && focusableRect.right <= mFocusRangeRight) { - int overlappedWidth = focusableRect.right - mFocusRangeLeft; - if (overlappedWidth > maxWidth) { - index = i; - maxWidth = overlappedWidth; - } + // Wraparound if reached head or end + if (getSelectedPosition() == 0) { + scrollToPosition(getAdapter().getItemCount() - 1); + return null; + } else if (getSelectedPosition() == getAdapter().getItemCount() - 1) { + scrollToPosition(0); + return null; } + return focused; } - if (index != INVALID_INDEX) { - mNextFocusByUpDown = focusables.get(index); - return mNextFocusByUpDown; + View nextFocusedProgram = GuideUtils.findNextFocusedProgram(getChildAt(nextChildIndex), + mFocusRangeLeft, mFocusRangeRight, mKeepCurrentProgramFocused); + if (nextFocusedProgram != null) { + nextFocusedProgram.getGlobalVisibleRect(mTempRect); + mNextFocusByUpDown = nextFocusedProgram; + + } else { + Log.w(TAG, "focusFind doesn't find proper focusable"); } - - Log.w(TAG, "focusFind doesn't find proper focusable"); - return null; + return nextFocusedProgram; } // Returned value is not the position of VerticalGridView. But it's the index of ViewGroup @@ -296,7 +291,8 @@ public class ProgramGrid extends VerticalGridView { return INVALID_INDEX; } - private void updateUpDownFocusState(View focused) { + private void updateUpDownFocusState(View focused, int direction) { + mLastUpDownDirection = direction; int rightMostFocusablePosition = getRightMostFocusablePosition(); Rect focusedRect = mTempRect; @@ -319,11 +315,13 @@ public class ProgramGrid extends VerticalGridView { } private void clearUpDownFocusState(View focus) { + mLastUpDownDirection = 0; mFocusRangeLeft = 0; mFocusRangeRight = getRightMostFocusablePosition(); mNextFocusByUpDown = null; - mKeepCurrentProgram = focus != null && focus instanceof ProgramItemView - && ((ProgramItemView) focus).getTableEntry().isCurrentProgram(); + // If focus is not a program item, drop focus to the current program when back to the grid + mKeepCurrentProgramFocused = !(focus instanceof ProgramItemView) + || GuideUtils.isCurrentProgram((ProgramItemView) focus); } private int getRightMostFocusablePosition() { @@ -333,56 +331,6 @@ public class ProgramGrid extends VerticalGridView { return mTempRect.right - GuideUtils.convertMillisToPixel(FOCUS_AREA_RIGHT_MARGIN_MILLIS); } - private boolean contains(View v) { - if (v == this) { - return true; - } - if (v == null || v == v.getRootView()) { - return false; - } - return contains((View) v.getParent()); - } - - public void onItemSelectionReset() { - getViewTreeObserver().addOnPreDrawListener(mPreDrawListener); - } - - @Override - public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { - if (mLastFocusedView != null && mLastFocusedView.isShown()) { - if (mLastFocusedView.requestFocus()) { - return true; - } - } - return super.onRequestFocusInDescendants(direction, previouslyFocusedRect); - } - - @Override - protected void onScrollChanged(int l, int t, int oldl, int oldt) { - // It is required to properly handle OnRepeatedKeyInterceptListener. If the focused - // item's are at the almost end of screen, focus change to the next item doesn't work. - // It restricts that a focus item's position cannot be too far from the desired position. - View focusedView = findFocus(); - if (focusedView != null && mOnRepeatedKeyInterceptListener.isFocusAccelerated()) { - int[] location = new int[2]; - getLocationOnScreen(location); - int[] focusedLocation = new int[2]; - focusedView.getLocationOnScreen(focusedLocation); - int y = focusedLocation[1] - location[1]; - int minY = (mSelectionRow - 1) * mRowHeight; - if (y < minY) scrollBy(0, y - minY); - 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(); @@ -398,7 +346,7 @@ public class ProgramGrid extends VerticalGridView { return -1; } - public void updateInputLogo() { + private void updateInputLogo() { int childCount = getChildCount(); if (childCount == 0) { return; @@ -409,25 +357,13 @@ public class ProgramGrid extends VerticalGridView { } View childView = getChildAt(firstVisibleChildIndex); int childAdapterPosition = getChildAdapterPosition(childView); - ((ProgramTableAdapter.ProgramRowHolder) getChildViewHolder(childView)) + ((ProgramTableAdapter.ProgramRowViewHolder) getChildViewHolder(childView)) .updateInputLogo(childAdapterPosition, true); for (int i = firstVisibleChildIndex + 1; i < childCount; i++) { childView = getChildAt(i); - ((ProgramTableAdapter.ProgramRowHolder) getChildViewHolder(childView)) + ((ProgramTableAdapter.ProgramRowViewHolder) getChildViewHolder(childView)) .updateInputLogo(childAdapterPosition, false); childAdapterPosition = getChildAdapterPosition(childView); } } - - private static void findFocusables(View v, ArrayList<View> outFocusable) { - if (v.isFocusable()) { - outFocusable.add(v); - } - if (v instanceof ViewGroup) { - ViewGroup viewGroup = (ViewGroup) v; - for (int i = 0; i < viewGroup.getChildCount(); ++i) { - findFocusables(viewGroup.getChildAt(i), outFocusable); - } - } - } } diff --git a/src/com/android/tv/guide/ProgramGuide.java b/src/com/android/tv/guide/ProgramGuide.java index 120b3dba..dd5444e2 100644 --- a/src/com/android/tv/guide/ProgramGuide.java +++ b/src/com/android/tv/guide/ProgramGuide.java @@ -48,7 +48,7 @@ import com.android.tv.ChannelTuner; import com.android.tv.Features; import com.android.tv.MainActivity; import com.android.tv.R; -import com.android.tv.analytics.DurationTimer; +import com.android.tv.util.DurationTimer; import com.android.tv.analytics.Tracker; import com.android.tv.common.WeakHandler; import com.android.tv.data.ChannelDataManager; @@ -143,6 +143,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { private int mLastRequestedGenreId = GenreItems.ID_ALL_CHANNELS; private boolean mIsDuringResetRowSelection; private final Handler mHandler = new ProgramGuideHandler(this); + private boolean mActive; private final Runnable mHideRunnable = new Runnable() { @Override @@ -217,7 +218,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { .getDimensionPixelOffset(R.dimen.program_guide_side_panel_alignment_y)); mSidePanelGridView.setWindowAlignmentOffsetPercent( VerticalGridView.WINDOW_ALIGN_OFFSET_PERCENT_DISABLED); - // TODO: Remove this check when we ship TV with epg search enabled. + if (Features.EPG_SEARCH.isEnabled(mActivity)) { mSearchOrb = (SearchOrbView) mContainer.findViewById( R.id.program_guide_side_panel_search_orb); @@ -250,8 +251,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { res.getInteger(R.integer.max_recycled_view_pool_epg_header_row_item)); mTimelineRow.setAdapter(mTimeListAdapter); - ProgramTableAdapter programTableAdapter = new ProgramTableAdapter(mActivity, - mProgramManager, this); + ProgramTableAdapter programTableAdapter = new ProgramTableAdapter(mActivity, this); programTableAdapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() { @Override public void onChanged() { @@ -304,13 +304,6 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { R.animator.program_guide_side_panel_enter_full, 0, R.animator.program_guide_table_enter_full); - mShowAnimatorFull.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - ((ViewGroup) mSidePanel).setDescendantFocusability( - ViewGroup.FOCUS_AFTER_DESCENDANTS); - } - }); mShowAnimatorPartial = createAnimator( R.animator.program_guide_side_panel_enter_partial, @@ -383,34 +376,6 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { || mSharedPreference.getBoolean(KEY_SHOW_GUIDE_PARTIAL, true); } - private void updateGuidePosition() { - // Align EPG at vertical center, if EPG table height is less than the screen size. - Resources res = mActivity.getResources(); - int screenHeight = mContainer.getHeight(); - if (screenHeight <= 0) { - // mContainer is not initialized yet. - return; - } - int startPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start); - int topPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_top); - int bottomPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_bottom); - int tableHeight = res.getDimensionPixelOffset(R.dimen.program_guide_table_header_row_height) - + mDetailHeight + mRowHeight * mGrid.getAdapter().getItemCount() + topPadding - + bottomPadding; - if (tableHeight > screenHeight) { - // EPG height is longer that the screen height. - mTable.setPaddingRelative(startPadding, topPadding, 0, 0); - LayoutParams layoutParams = mTable.getLayoutParams(); - layoutParams.height = LayoutParams.WRAP_CONTENT; - mTable.setLayoutParams(layoutParams); - } else { - mTable.setPaddingRelative(startPadding, topPadding, 0, bottomPadding); - LayoutParams layoutParams = mTable.getLayoutParams(); - layoutParams.height = tableHeight; - mTable.setLayoutParams(layoutParams); - } - } - @Override public void onRequestChildFocus(View oldFocus, View newFocus) { if (oldFocus != null && newFocus != null) { @@ -431,40 +396,6 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } } - private Animator createAnimator(int sidePanelAnimResId, int sidePanelGridAnimResId, - int tableAnimResId) { - List<Animator> animatorList = new ArrayList<>(); - - Animator sidePanelAnimator = AnimatorInflater.loadAnimator(mActivity, sidePanelAnimResId); - sidePanelAnimator.setTarget(mSidePanel); - animatorList.add(sidePanelAnimator); - - if (sidePanelGridAnimResId != 0) { - Animator sidePanelGridAnimator = AnimatorInflater.loadAnimator(mActivity, - sidePanelGridAnimResId); - sidePanelGridAnimator.setTarget(mSidePanelGridView); - sidePanelGridAnimator.addListener( - new HardwareLayerAnimatorListenerAdapter(mSidePanelGridView)); - animatorList.add(sidePanelGridAnimator); - } - Animator tableAnimator = AnimatorInflater.loadAnimator(mActivity, tableAnimResId); - tableAnimator.setTarget(mTable); - tableAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable)); - animatorList.add(tableAnimator); - - AnimatorSet set = new AnimatorSet(); - set.playTogether(animatorList); - return set; - } - - /** - * Returns {@code true} if the program guide should process the input events. - */ - public boolean isActive() { - return mContainer.getVisibility() == View.VISIBLE && !mHideAnimatorFull.isStarted() - && !mHideAnimatorPartial.isStarted(); - } - /** * Show the program guide. This reveals the side panel, and the program guide table is shown * partially. @@ -494,14 +425,11 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mTimeListAdapter.update(mStartUtcTime); mTimelineRow.resetScroll(); + mContainer.setVisibility(View.VISIBLE); + mActive = true; if (!mShowGuidePartial) { - // Avoid changing focus from the genre side panel to the grid during animation. - // The descendant focus is changed to FOCUS_AFTER_DESCENDANTS after the animation. - ((ViewGroup) mSidePanel).setDescendantFocusability( - ViewGroup.FOCUS_BLOCK_DESCENDANTS); + mTable.requestFocus(); } - - mContainer.setVisibility(View.VISIBLE); positionCurrentTimeIndicator(); mSidePanelGridView.setSelectedPosition(0); if (DEBUG) { @@ -536,13 +464,13 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } }); } + updateGuidePosition(); runnableAfterAnimatorReady.run(); if (mShowGuidePartial) { mShowAnimatorPartial.start(); } else { mShowAnimatorFull.start(); } - updateGuidePosition(); } }; mContainer.getViewTreeObserver().addOnGlobalLayoutListener(mOnLayoutListenerForShow); @@ -564,7 +492,8 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { cancelHide(); mProgramManager.programGuideVisibilityChanged(false); mProgramManager.removeListener(mProgramManagerListener); - if (isFull()) { + mActive = false; + if (!mShowGuidePartial) { mHideAnimatorFull.start(); } else { mHideAnimatorPartial.start(); @@ -587,50 +516,21 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } } + /** + * Schedules hiding the program guide. + */ public void scheduleHide() { cancelHide(); mHandler.postDelayed(mHideRunnable, mShowDurationMillis); } /** - * Returns the scroll offset of the time line row in pixels. - */ - public int getTimelineRowScrollOffset() { - return mTimelineRow.getScrollOffset(); - } - - /** - * Cancel hiding the program guide. + * Cancels hiding the program guide. */ public void cancelHide() { mHandler.removeCallbacks(mHideRunnable); } - // Returns if program table is full screen mode. - private boolean isFull() { - return mPartialToFullAnimator.isStarted() || mTable.getTranslationX() == 0; - } - - private void startFull() { - if (isFull() || mAccessibilityManager.isEnabled()) { - // If accessibility service is enabled, focus cannot be moved to side panel due to it's - // hidden. Therefore, we don't hide side panel when accessibility service is enabled. - return; - } - mShowGuidePartial = false; - mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply(); - mPartialToFullAnimator.start(); - } - - private void startPartial() { - if (!isFull()) { - return; - } - mShowGuidePartial = true; - mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply(); - mFullToPartialAnimator.start(); - } - /** * Process the {@code KEYCODE_BACK} key event. */ @@ -639,16 +539,30 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } /** - * Gets {@link VerticalGridView} for "genre select" side panel. + * Returns {@code true} if the program guide should process the input events. */ - public VerticalGridView getSidePanel() { - return mSidePanelGridView; + public boolean isActive() { + return mActive; + } + + /** + * Returns {@code true} if the program guide is shown, i.e. showing animation is done and + * hiding animation is not started yet. + */ + public boolean isRunningAnimation() { + return mShowAnimatorPartial.isStarted() || mShowAnimatorFull.isStarted() + || mHideAnimatorPartial.isStarted() || mHideAnimatorFull.isStarted(); + } + + /** Returns if program table is in full screen mode. **/ + boolean isFull() { + return !mShowGuidePartial; } /** * Requests change genre to {@code genreId}. */ - public void requestGenreChange(int genreId) { + void requestGenreChange(int genreId) { if (mLastRequestedGenreId == genreId) { // When Recycler.onLayout() removes its children to recycle, // View tries to find next focus candidate immediately @@ -679,6 +593,104 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mProgramTableFadeOutAnimator.start(); } + /** + * Returns the scroll offset of the time line row in pixels. + */ + int getTimelineRowScrollOffset() { + return mTimelineRow.getScrollOffset(); + } + + /** Returns the program grid view that hold all component views. */ + ProgramGrid getProgramGrid() { + return mGrid; + } + + /** + * Gets {@link VerticalGridView} for "genre select" side panel. + */ + VerticalGridView getSidePanel() { + return mSidePanelGridView; + } + + /** Returns the program manager the program guide is using to provide program information. */ + ProgramManager getProgramManager() { + return mProgramManager; + } + + private void updateGuidePosition() { + // Align EPG at vertical center, if EPG table height is less than the screen size. + Resources res = mActivity.getResources(); + int screenHeight = mContainer.getHeight(); + if (screenHeight <= 0) { + // mContainer is not initialized yet. + return; + } + int startPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_start); + int topPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_top); + int bottomPadding = res.getDimensionPixelOffset(R.dimen.program_guide_table_margin_bottom); + int tableHeight = res.getDimensionPixelOffset(R.dimen.program_guide_table_header_row_height) + + mDetailHeight + mRowHeight * mGrid.getAdapter().getItemCount() + topPadding + + bottomPadding; + if (tableHeight > screenHeight) { + // EPG height is longer that the screen height. + mTable.setPaddingRelative(startPadding, topPadding, 0, 0); + LayoutParams layoutParams = mTable.getLayoutParams(); + layoutParams.height = LayoutParams.WRAP_CONTENT; + mTable.setLayoutParams(layoutParams); + } else { + mTable.setPaddingRelative(startPadding, topPadding, 0, bottomPadding); + LayoutParams layoutParams = mTable.getLayoutParams(); + layoutParams.height = tableHeight; + mTable.setLayoutParams(layoutParams); + } + } + + private Animator createAnimator(int sidePanelAnimResId, int sidePanelGridAnimResId, + int tableAnimResId) { + List<Animator> animatorList = new ArrayList<>(); + + Animator sidePanelAnimator = AnimatorInflater.loadAnimator(mActivity, sidePanelAnimResId); + sidePanelAnimator.setTarget(mSidePanel); + animatorList.add(sidePanelAnimator); + + if (sidePanelGridAnimResId != 0) { + Animator sidePanelGridAnimator = AnimatorInflater.loadAnimator(mActivity, + sidePanelGridAnimResId); + sidePanelGridAnimator.setTarget(mSidePanelGridView); + sidePanelGridAnimator.addListener( + new HardwareLayerAnimatorListenerAdapter(mSidePanelGridView)); + animatorList.add(sidePanelGridAnimator); + } + Animator tableAnimator = AnimatorInflater.loadAnimator(mActivity, tableAnimResId); + tableAnimator.setTarget(mTable); + tableAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(mTable)); + animatorList.add(tableAnimator); + + AnimatorSet set = new AnimatorSet(); + set.playTogether(animatorList); + return set; + } + + private void startFull() { + if (!mShowGuidePartial || mAccessibilityManager.isEnabled()) { + // If accessibility service is enabled, focus cannot be moved to side panel due to it's + // hidden. Therefore, we don't hide side panel when accessibility service is enabled. + return; + } + mShowGuidePartial = false; + mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply(); + mPartialToFullAnimator.start(); + } + + private void startPartial() { + if (mShowGuidePartial) { + return; + } + mShowGuidePartial = true; + mSharedPreference.edit().putBoolean(KEY_SHOW_GUIDE_PARTIAL, mShowGuidePartial).apply(); + mFullToPartialAnimator.start(); + } + private void startCurrentTimeIndicator(long initialDelay) { mHandler.postDelayed(mUpdateTimeIndicator, initialDelay); } @@ -775,10 +787,12 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { mDetailInAnimator.cancel(); } - int direction = 0; - if (outRow != null && inRow != null) { - // -1 means the selection goes downwards and 1 goes upwards - direction = outRow.getTop() < inRow.getTop() ? -1 : 1; + int operationDirection = mGrid.getLastUpDownDirection(); + int animationPadding = 0; + if (operationDirection == View.FOCUS_UP) { + animationPadding = mDetailPadding; + } else if (operationDirection == View.FOCUS_DOWN) { + animationPadding = -mDetailPadding; } View outDetail = outRow != null ? outRow.findViewById(R.id.detail) : null; @@ -788,7 +802,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { Animator fadeOutAnimator = ObjectAnimator.ofPropertyValuesHolder(outDetailContent, PropertyValuesHolder.ofFloat(View.ALPHA, outDetail.getAlpha(), 0f), PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, - outDetailContent.getTranslationY(), direction * mDetailPadding)); + outDetailContent.getTranslationY(), animationPadding)); fadeOutAnimator.setStartDelay(0); fadeOutAnimator.setDuration(mAnimationDuration); fadeOutAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(outDetailContent)); @@ -842,8 +856,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { }); Animator fadeInAnimator = ObjectAnimator.ofPropertyValuesHolder(inDetailContent, PropertyValuesHolder.ofFloat(View.ALPHA, 0f, 1f), - PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, - direction * -mDetailPadding, 0f)); + PropertyValuesHolder.ofFloat(View.TRANSLATION_Y, -animationPadding, 0f)); fadeInAnimator.setDuration(mAnimationDuration); fadeInAnimator.addListener(new HardwareLayerAnimatorListenerAdapter(inDetailContent)); @@ -910,7 +923,7 @@ public class ProgramGuide implements ProgramGrid.ChildFocusListener { } private static class ProgramGuideHandler extends WeakHandler<ProgramGuide> { - public ProgramGuideHandler(ProgramGuide ref) { + ProgramGuideHandler(ProgramGuide ref) { super(ref); } diff --git a/src/com/android/tv/guide/ProgramItemView.java b/src/com/android/tv/guide/ProgramItemView.java index 4c7a4404..b23d578c 100644 --- a/src/com/android/tv/guide/ProgramItemView.java +++ b/src/com/android/tv/guide/ProgramItemView.java @@ -44,8 +44,8 @@ import com.android.tv.analytics.Tracker; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.DvrUiHelper; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; +import com.android.tv.dvr.ui.DvrUiHelper; import com.android.tv.guide.ProgramManager.TableEntry; import com.android.tv.util.ToastUtils; import com.android.tv.util.Utils; @@ -73,6 +73,7 @@ public class ProgramItemView extends TextView { private static TextAppearanceSpan sEpisodeTitleStyle; private static TextAppearanceSpan sGrayedOutEpisodeTitleStyle; + private ProgramGuide mProgramGuide; private DvrManager mDvrManager; private TableEntry mTableEntry; private int mMaxWidthForRipple; @@ -106,18 +107,19 @@ 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 (entry.program != null && CommonFeatures.DVR.isEnabled(view.getContext())) { DvrManager dvrManager = singletons.getDvrManager(); if (entry.entryStartUtcMillis > System.currentTimeMillis() && dvrManager.isProgramRecordable(entry.program)) { if (entry.scheduledRecording == null) { - if (DvrUiHelper.checkStorageStatusAndShowErrorMessage(tvActivity, - channel.getInputId()) - && DvrUiHelper.handleCreateSchedule(tvActivity, entry.program)) { - String msg = view.getContext().getString( - R.string.dvr_msg_program_scheduled, entry.program.getTitle()); - ToastUtils.show(view.getContext(), msg, Toast.LENGTH_SHORT); - } + DvrUiHelper.checkStorageStatusAndShowErrorMessage(tvActivity, + channel.getInputId(), new Runnable() { + @Override + public void run() { + DvrUiHelper.requestRecordingFutureProgram(tvActivity, + entry.program, false); + } + }); } else { dvrManager.removeScheduledRecording(entry.scheduledRecording); String msg = view.getResources().getString( @@ -158,6 +160,11 @@ public class ProgramItemView extends TextView { } if (entry.isCurrentProgram()) { Drawable background = getBackground(); + if (!mProgramGuide.isActive() || mProgramGuide.isRunningAnimation()) { + // If program guide is not active or is during showing/hiding, + // the animation is unnecessary, skip it. + background.jumpToCurrentState(); + } int progress = getProgress(entry.entryStartUtcMillis, entry.entryEndUtcMillis); setProgress(background, R.id.reverse_progress, MAX_PROGRESS - progress); } @@ -247,8 +254,9 @@ public class ProgramItemView extends TextView { } @SuppressLint("SwitchIntDef") - public void setValues(TableEntry entry, int selectedGenreId, long fromUtcMillis, - long toUtcMillis, String gapTitle) { + public void setValues(ProgramGuide programGuide, TableEntry entry, int selectedGenreId, + long fromUtcMillis, long toUtcMillis, String gapTitle) { + mProgramGuide = programGuide; mTableEntry = entry; ViewGroup.LayoutParams layoutParams = getLayoutParams(); @@ -376,6 +384,7 @@ public class ProgramItemView extends TextView { } setTag(null); + mProgramGuide = null; mTableEntry = null; } diff --git a/src/com/android/tv/guide/ProgramListAdapter.java b/src/com/android/tv/guide/ProgramListAdapter.java index 03aea5ad..c1fcdd40 100644 --- a/src/com/android/tv/guide/ProgramListAdapter.java +++ b/src/com/android/tv/guide/ProgramListAdapter.java @@ -32,11 +32,12 @@ 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> +class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter.ProgramItemViewHolder> implements TableEntriesUpdatedListener { private static final String TAG = "ProgramListAdapter"; private static final boolean DEBUG = false; + private final ProgramGuide mProgramGuide; private final ProgramManager mProgramManager; private final int mChannelIndex; private final String mNoInfoProgramTitle; @@ -44,9 +45,10 @@ public class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter. private long mChannelId; - public ProgramListAdapter(Resources res, ProgramManager programManager, int channelIndex) { + ProgramListAdapter(Resources res, ProgramGuide programGuide, int channelIndex) { setHasStableIds(true); - mProgramManager = programManager; + mProgramGuide = programGuide; + mProgramManager = programGuide.getProgramManager(); mChannelIndex = channelIndex; mNoInfoProgramTitle = res.getString(R.string.program_title_for_no_information); mBlockedProgramTitle = res.getString(R.string.program_title_for_blocked_channel); @@ -65,10 +67,6 @@ public class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter. } } - public ProgramManager getProgramManager() { - return mProgramManager; - } - @Override public int getItemCount() { return mProgramManager.getTableEntryCount(mChannelId); @@ -85,38 +83,40 @@ public class ProgramListAdapter extends RecyclerView.Adapter<ProgramListAdapter. } @Override - public void onBindViewHolder(ProgramViewHolder holder, int position) { + public void onBindViewHolder(ProgramItemViewHolder holder, int position) { TableEntry tableEntry = mProgramManager.getTableEntry(mChannelId, position); String gapTitle = tableEntry.isBlocked() ? mBlockedProgramTitle : mNoInfoProgramTitle; - holder.onBind(tableEntry, this.getProgramManager(), gapTitle); + holder.onBind(tableEntry, mProgramGuide, gapTitle); } @Override - public void onViewRecycled(ProgramViewHolder holder) { + public void onViewRecycled(ProgramItemViewHolder holder) { holder.onUnbind(); } @Override - public ProgramViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + public ProgramItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); - return new ProgramViewHolder(itemView); + return new ProgramItemViewHolder(itemView); } - public static class ProgramViewHolder extends RecyclerView.ViewHolder { + static class ProgramItemViewHolder extends RecyclerView.ViewHolder { // Should be called from main thread. - public ProgramViewHolder(View itemView) { + ProgramItemViewHolder(View itemView) { super(itemView); } - public void onBind(TableEntry entry, ProgramManager programManager, String gapTitle) { + void onBind(TableEntry entry, ProgramGuide programGuide, String gapTitle) { if (DEBUG) { Log.d(TAG, "onBind. View = " + itemView + ", Entry = " + entry); } - ((ProgramItemView) itemView).setValues(entry, programManager.getSelectedGenreId(), - programManager.getFromUtcMillis(), programManager.getToUtcMillis(), gapTitle); + ProgramManager programManager = programGuide.getProgramManager(); + ((ProgramItemView) itemView).setValues(programGuide, entry, + programManager.getSelectedGenreId(), programManager.getFromUtcMillis(), + programManager.getToUtcMillis(), gapTitle); } - public void onUnbind() { + void onUnbind() { ((ProgramItemView) itemView).clearValues(); } } diff --git a/src/com/android/tv/guide/ProgramManager.java b/src/com/android/tv/guide/ProgramManager.java index e3d919df..4ec3f77e 100644 --- a/src/com/android/tv/guide/ProgramManager.java +++ b/src/com/android/tv/guide/ProgramManager.java @@ -29,7 +29,7 @@ import com.android.tv.data.ProgramDataManager; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrScheduleManager; import com.android.tv.dvr.DvrScheduleManager.OnConflictStateChangeListener; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.util.TvInputManagerHelper; import com.android.tv.util.Utils; @@ -68,107 +68,6 @@ public class ProgramManager { private long mFromUtcMillis; private long mToUtcMillis; - /** - * Entry for program guide table. An "entry" can be either an actual program or a gap between - * programs. This is needed for {@link ProgramListAdapter} because - * {@link android.support.v17.leanback.widget.HorizontalGridView} ignores margins between items. - */ - public static class TableEntry { - /** Channel ID which this entry is included. */ - public final long channelId; - - /** 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; - - /** End time of entry in UTC milliseconds */ - public final long entryEndUtcMillis; - - private final boolean mIsBlocked; - - 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, null, startUtcMillis, endUtcMillis, blocked); - } - - 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, 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() { - return !Program.isValid(program); - } - - /** - * Returns true if this channel is blocked. - */ - public boolean isBlocked() { - return mIsBlocked; - } - - /** - * Returns true if this program is on the air. - */ - public boolean isCurrentProgram() { - long current = System.currentTimeMillis(); - return entryStartUtcMillis <= current && entryEndUtcMillis > current; - } - - /** - * Returns if this program has the genre. - */ - public boolean hasGenre(int genreId) { - return !isGap() && program.hasGenre(genreId); - } - - /** - * Returns the width of table entry, in pixels. - */ - public int getWidth() { - return GuideUtils.convertMillisToPixel(entryStartUtcMillis, entryEndUtcMillis); - } - - @Override - public String toString() { - return "TableEntry{" - + "hashCode=" + hashCode() - + ", channelId=" + channelId - + ", program=" + program - + ", startTime=" + Utils.toTimeString(entryStartUtcMillis) - + ", endTimeTime=" + Utils.toTimeString(entryEndUtcMillis) + "}"; - } - } - private List<Channel> mChannels = new ArrayList<>(); private final Map<Long, List<TableEntry>> mChannelIdEntriesMap = new HashMap<>(); private final List<List<Channel>> mGenreChannelList = new ArrayList<>(); @@ -293,7 +192,7 @@ public class ProgramManager { mDvrScheduleManager = dvrScheduleManager; } - public void programGuideVisibilityChanged(boolean visible) { + void programGuideVisibilityChanged(boolean visible) { mProgramDataManager.setPauseProgramUpdate(visible); if (visible) { mChannelDataManager.addListener(mChannelDataManagerListener); @@ -325,87 +224,51 @@ public class ProgramManager { /** * Adds a {@link Listener}. */ - public void addListener(Listener listener) { + void addListener(Listener listener) { mListeners.add(listener); } /** * Registers a listener to be invoked when table entries are updated. */ - public void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { + void addTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { mTableEntriesUpdatedListeners.add(listener); } /** * Registers a listener to be invoked when a table entry is changed. */ - public void addTableEntryChangedListener(TableEntryChangedListener listener) { + void addTableEntryChangedListener(TableEntryChangedListener listener) { mTableEntryChangedListeners.add(listener); } /** * Removes a {@link Listener}. */ - public void removeListener(Listener listener) { + void removeListener(Listener listener) { mListeners.remove(listener); } /** * Removes a previously installed table entries update listener. */ - public void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { + void removeTableEntriesUpdatedListener(TableEntriesUpdatedListener listener) { mTableEntriesUpdatedListeners.remove(listener); } /** * Removes a previously installed table entry changed listener. */ - public void removeTableEntryChangedListener(TableEntryChangedListener listener) { + 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 - * with built channel list. - * This is expected to be called whenever program guide is shown. - */ - public void buildGenreFilters() { - if (DEBUG) Log.d(TAG, "buildGenreFilters"); - - mGenreChannelList.clear(); - for (int i = 0; i < GenreItems.getGenreCount(); i++) { - mGenreChannelList.add(new ArrayList<>()); - } - for (Channel channel : mChannels) { - // TODO: Use programs in visible area instead of using current programs only. - Program currentProgram = mProgramDataManager.getCurrentProgram(channel.getId()); - if (currentProgram != null && currentProgram.getCanonicalGenres() != null) { - for (String genre : currentProgram.getCanonicalGenres()) { - mGenreChannelList.get(GenreItems.getId(genre)).add(channel); - } - } - } - mGenreChannelList.set(GenreItems.ID_ALL_CHANNELS, mChannels); - mFilteredGenreIds.clear(); - mFilteredGenreIds.add(0); - for (int i = 1; i < GenreItems.getGenreCount(); i++) { - if (mGenreChannelList.get(i).size() > 0) { - mFilteredGenreIds.add(i); - } - } - mSelectedGenreId = GenreItems.ID_ALL_CHANNELS; - mFilteredChannels = mChannels; - notifyGenresUpdated(); - } - - /** * Resets channel list with given genre. * Caller should call {@link #buildGenreFilters()} prior to call this API to make * This notifies channel updates to listeners. */ - public void resetChannelListWithGenre(int genreId) { + void resetChannelListWithGenre(int genreId) { if (genreId == mSelectedGenreId) { return; } @@ -422,13 +285,154 @@ public class ProgramManager { } /** + * Update the initial time range to manage. It updates program entries and genre as well. + */ + void updateInitialTimeRange(long startUtcMillis, long endUtcMillis) { + mStartUtcMillis = startUtcMillis; + if (endUtcMillis > mEndUtcMillis) { + mEndUtcMillis = endUtcMillis; + } + + mProgramDataManager.setPrefetchTimeRange(mStartUtcMillis); + updateChannels(true); + setTimeRange(startUtcMillis, endUtcMillis); + } + + + /** + * Shifts the time range by the given time. Also makes ProgramGuide scroll the views. + */ + void shiftTime(long timeMillisToScroll) { + long fromUtcMillis = mFromUtcMillis + timeMillisToScroll; + long toUtcMillis = mToUtcMillis + timeMillisToScroll; + if (fromUtcMillis < mStartUtcMillis) { + fromUtcMillis = mStartUtcMillis; + toUtcMillis += mStartUtcMillis - fromUtcMillis; + } + if (toUtcMillis > mEndUtcMillis) { + fromUtcMillis -= toUtcMillis - mEndUtcMillis; + toUtcMillis = mEndUtcMillis; + } + setTimeRange(fromUtcMillis, toUtcMillis); + } + + /** + * Returned the scrolled(shifted) time in milliseconds. + */ + long getShiftedTime() { + return mFromUtcMillis - mStartUtcMillis; + } + + /** + * Returns the start time set by {@link #updateInitialTimeRange}. + */ + long getStartTime() { + return mStartUtcMillis; + } + + /** + * Returns the program index of the program with {@code entryId} or -1 if not found. + */ + 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. + */ + 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); + if (entry.entryStartUtcMillis <= time + && time < entry.entryEndUtcMillis) { + return i; + } + } + return -1; + } + + /** + * Returns the start time of currently managed time range, in UTC millisecond. + */ + long getFromUtcMillis() { + return mFromUtcMillis; + } + + /** + * Returns the end time of currently managed time range, in UTC millisecond. + */ + long getToUtcMillis() { + return mToUtcMillis; + } + + /** + * Returns the number of the currently managed channels. + */ + int getChannelCount() { + return mFilteredChannels.size(); + } + + /** + * Returns a {@link Channel} at a given {@code channelIndex} of the currently managed channels. + * Returns {@code null} if such a channel is not found. + */ + Channel getChannel(int channelIndex) { + if (channelIndex < 0 || channelIndex >= getChannelCount()) { + return null; + } + return mFilteredChannels.get(channelIndex); + } + + /** + * Returns the index of provided {@link Channel} within the currently managed channels. + * Returns -1 if such a channel is not found. + */ + int getChannelIndex(Channel channel) { + return mFilteredChannels.indexOf(channel); + } + + /** + * Returns the index of channel with {@code channelId} within the currently managed channels. + * Returns -1 if such a channel is not found. + */ + 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}. + */ + int getTableEntryCount(long channelId) { + return mChannelIdEntriesMap.get(channelId).size(); + } + + /** + * Returns an entry as {@link Program} for a given {@code channelId} and {@code index} of + * entries within the currently managed time range. Returned {@link Program} can be a dummy one + * (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between programs. + */ + TableEntry getTableEntry(long channelId, int index) { + return mChannelIdEntriesMap.get(channelId).get(index); + } + + /** * Returns list genre ID's which has a channel. */ - public List<Integer> getFilteredGenreIds() { + List<Integer> getFilteredGenreIds() { return mFilteredGenreIds; } - public int getSelectedGenreId() { + int getSelectedGenreId() { return mSelectedGenreId; } @@ -439,11 +443,24 @@ public class ProgramManager { mChannels = mChannelDataManager.getBrowsableChannelList(); mSelectedGenreId = GenreItems.ID_ALL_CHANNELS; mFilteredChannels = mChannels; + updateTableEntriesWithoutNotification(clearPreviousTableEntries); + // Channel update notification should be called after updating table entries, so that + // the listener can get the entries. notifyChannelsUpdated(); - updateTableEntries(clearPreviousTableEntries); + notifyTableEntriesUpdated(); + buildGenreFilters(); } private void updateTableEntries(boolean clear) { + updateTableEntriesWithoutNotification(clear); + notifyTableEntriesUpdated(); + buildGenreFilters(); + } + + /** + * Updates the table entries without notifying the change. + */ + private void updateTableEntriesWithoutNotification(boolean clear) { if (clear) { mChannelIdEntriesMap.clear(); } @@ -491,46 +508,41 @@ public class ProgramManager { } } } - - notifyTableEntriesUpdated(); - buildGenreFilters(); - } - - private void notifyGenresUpdated() { - for (Listener listener : mListeners) { - listener.onGenresUpdated(); - } } - private void notifyChannelsUpdated() { - for (Listener listener : mListeners) { - listener.onChannelsUpdated(); - } - } + /** + * 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 + * with built channel list. + * This is expected to be called whenever program guide is shown. + */ + private void buildGenreFilters() { + if (DEBUG) Log.d(TAG, "buildGenreFilters"); - private void notifyTimeRangeUpdated() { - for (Listener listener : mListeners) { - listener.onTimeRangeUpdated(); + mGenreChannelList.clear(); + for (int i = 0; i < GenreItems.getGenreCount(); i++) { + mGenreChannelList.add(new ArrayList<>()); } - } - - private void notifyTableEntriesUpdated() { - for (TableEntriesUpdatedListener listener : mTableEntriesUpdatedListeners) { - listener.onTableEntriesUpdated(); + for (Channel channel : mChannels) { + Program currentProgram = mProgramDataManager.getCurrentProgram(channel.getId()); + if (currentProgram != null && currentProgram.getCanonicalGenres() != null) { + for (String genre : currentProgram.getCanonicalGenres()) { + mGenreChannelList.get(GenreItems.getId(genre)).add(channel); + } + } } - } - - private void notifyTableEntryUpdated(TableEntry entry) { - for (TableEntryChangedListener listener : mTableEntryChangedListeners) { - listener.onTableEntryChanged(entry); + mGenreChannelList.set(GenreItems.ID_ALL_CHANNELS, mChannels); + mFilteredGenreIds.clear(); + mFilteredGenreIds.add(0); + for (int i = 1; i < GenreItems.getGenreCount(); i++) { + if (mGenreChannelList.get(i).size() > 0) { + mFilteredGenreIds.add(i); + } } - } - - 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); + mSelectedGenreId = GenreItems.ID_ALL_CHANNELS; + mFilteredChannels = mChannels; + notifyGenresUpdated(); } @Nullable @@ -551,32 +563,11 @@ public class ProgramManager { return null; } - /** - * Returns the start time of currently managed time range, in UTC millisecond. - */ - public long getFromUtcMillis() { - return mFromUtcMillis; - } - - /** - * Returns the end time of currently managed time range, in UTC millisecond. - */ - public long getToUtcMillis() { - return mToUtcMillis; - } - - /** - * Update the initial time range to manage. It updates program entries and genre as well. - */ - public void updateInitialTimeRange(long startUtcMillis, long endUtcMillis) { - mStartUtcMillis = startUtcMillis; - if (endUtcMillis > mEndUtcMillis) { - mEndUtcMillis = endUtcMillis; - } - - mProgramDataManager.setPrefetchTimeRange(mStartUtcMillis); - updateChannels(true); - setTimeRange(startUtcMillis, endUtcMillis); + 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); } private void setTimeRange(long fromUtcMillis, long toUtcMillis) { @@ -592,57 +583,6 @@ public class ProgramManager { } } - /** - * Returns the number of the currently managed channels. - */ - public int getChannelCount() { - return mFilteredChannels.size(); - } - - /** - * Returns a {@link Channel} at a given {@code channelIndex} of the currently managed channels. - * Returns {@code null} if such a channel is not found. - */ - public Channel getChannel(int channelIndex) { - if (channelIndex < 0 || channelIndex >= getChannelCount()) { - return null; - } - return mFilteredChannels.get(channelIndex); - } - - /** - * Returns the index of provided {@link Channel} within the currently managed channels. - * Returns -1 if such a channel is not found. - */ - public int getChannelIndex(Channel channel) { - return mFilteredChannels.indexOf(channel); - } - - /** - * 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}. - */ - public int getTableEntryCount(long channelId) { - return mChannelIdEntriesMap.get(channelId).size(); - } - - /** - * Returns an entry as {@link Program} for a given {@code channelId} and {@code index} of - * entries within the currently managed time range. Returned {@link Program} can be a dummy one - * (e.g., whose channelId is INVALID_ID), when it corresponds to a gap between programs. - */ - public TableEntry getTableEntry(long channelId, int index) { - return mChannelIdEntriesMap.get(channelId).get(index); - } - private List<TableEntry> createProgramEntries(long channelId, boolean parentalControlsEnabled) { List<TableEntry> entries = new ArrayList<>(); boolean channelLocked = parentalControlsEnabled @@ -690,89 +630,159 @@ public class ProgramManager { return entries; } - public interface Listener { - void onGenresUpdated(); - void onChannelsUpdated(); - void onTimeRangeUpdated(); + private void notifyGenresUpdated() { + for (Listener listener : mListeners) { + listener.onGenresUpdated(); + } } - public interface TableEntriesUpdatedListener { - void onTableEntriesUpdated(); + private void notifyChannelsUpdated() { + for (Listener listener : mListeners) { + listener.onChannelsUpdated(); + } } - public interface TableEntryChangedListener { - void onTableEntryChanged(TableEntry entry); + private void notifyTimeRangeUpdated() { + for (Listener listener : mListeners) { + listener.onTimeRangeUpdated(); + } } - public static class ListenerAdapter implements Listener { - @Override - public void onGenresUpdated() { } - - @Override - public void onChannelsUpdated() { } + private void notifyTableEntriesUpdated() { + for (TableEntriesUpdatedListener listener : mTableEntriesUpdatedListeners) { + listener.onTableEntriesUpdated(); + } + } - @Override - public void onTimeRangeUpdated() { } + private void notifyTableEntryUpdated(TableEntry entry) { + for (TableEntryChangedListener listener : mTableEntryChangedListeners) { + listener.onTableEntryChanged(entry); + } } /** - * Shifts the time range by the given time. Also makes ProgramGuide scroll the views. + * Entry for program guide table. An "entry" can be either an actual program or a gap between + * programs. This is needed for {@link ProgramListAdapter} because + * {@link android.support.v17.leanback.widget.HorizontalGridView} ignores margins between items. */ - public void shiftTime(long timeMillisToScroll) { - long fromUtcMillis = mFromUtcMillis + timeMillisToScroll; - long toUtcMillis = mToUtcMillis + timeMillisToScroll; - if (fromUtcMillis < mStartUtcMillis) { - fromUtcMillis = mStartUtcMillis; - toUtcMillis += mStartUtcMillis - fromUtcMillis; + static class TableEntry { + /** Channel ID which this entry is included. */ + final long channelId; + + /** Program corresponding to the entry. {@code null} means that this entry is a gap. */ + final Program program; + + final ScheduledRecording scheduledRecording; + + /** Start time of entry in UTC milliseconds. */ + final long entryStartUtcMillis; + + /** End time of entry in UTC milliseconds */ + final long entryEndUtcMillis; + + private final boolean mIsBlocked; + + private TableEntry(long channelId, long startUtcMillis, long endUtcMillis) { + this(channelId, null, startUtcMillis, endUtcMillis, false); } - if (toUtcMillis > mEndUtcMillis) { - fromUtcMillis -= toUtcMillis - mEndUtcMillis; - toUtcMillis = mEndUtcMillis; + + private TableEntry(long channelId, long startUtcMillis, long endUtcMillis, + boolean blocked) { + this(channelId, null, null, startUtcMillis, endUtcMillis, blocked); + } + + 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, 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}. + */ + 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. + */ + boolean isGap() { + return !Program.isValid(program); + } + + /** + * Returns true if this channel is blocked. + */ + boolean isBlocked() { + return mIsBlocked; + } + + /** + * Returns true if this program is on the air. + */ + boolean isCurrentProgram() { + long current = System.currentTimeMillis(); + return entryStartUtcMillis <= current && entryEndUtcMillis > current; + } + + /** + * Returns if this program has the genre. + */ + boolean hasGenre(int genreId) { + return !isGap() && program.hasGenre(genreId); + } + + /** + * Returns the width of table entry, in pixels. + */ + int getWidth() { + return GuideUtils.convertMillisToPixel(entryStartUtcMillis, entryEndUtcMillis); + } + + @Override + public String toString() { + return "TableEntry{" + + "hashCode=" + hashCode() + + ", channelId=" + channelId + + ", program=" + program + + ", startTime=" + Utils.toTimeString(entryStartUtcMillis) + + ", endTimeTime=" + Utils.toTimeString(entryEndUtcMillis) + "}"; } - setTimeRange(fromUtcMillis, toUtcMillis); } - /** - * Returned the scrolled(shifted) time in milliseconds. - */ - public long getShiftedTime() { - return mFromUtcMillis - mStartUtcMillis; + interface Listener { + void onGenresUpdated(); + void onChannelsUpdated(); + void onTimeRangeUpdated(); } - /** - * Returns the start time set by {@link #updateInitialTimeRange}. - */ - public long getStartTime() { - return mStartUtcMillis; + interface TableEntriesUpdatedListener { + void onTableEntriesUpdated(); } - /** - * 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; + interface TableEntryChangedListener { + void onTableEntryChanged(TableEntry entry); } - /** - * Returns the program index of the program at {@code time} or -1 if not found. - */ - 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); - if (entry.entryStartUtcMillis <= time - && time < entry.entryEndUtcMillis) { - return i; - } - } - return -1; + static class ListenerAdapter implements Listener { + @Override + public void onGenresUpdated() { } + + @Override + public void onChannelsUpdated() { } + + @Override + public void onTimeRangeUpdated() { } } } diff --git a/src/com/android/tv/guide/ProgramRow.java b/src/com/android/tv/guide/ProgramRow.java index 2c98ab2d..fefc724c 100644 --- a/src/com/android/tv/guide/ProgramRow.java +++ b/src/com/android/tv/guide/ProgramRow.java @@ -21,9 +21,11 @@ import android.graphics.Rect; import android.support.v7.widget.LinearLayoutManager; import android.util.AttributeSet; import android.util.Log; +import android.util.Range; import android.view.View; import android.view.ViewTreeObserver.OnGlobalLayoutListener; +import com.android.tv.MainActivity; import com.android.tv.data.Channel; import com.android.tv.guide.ProgramManager.TableEntry; import com.android.tv.util.Utils; @@ -37,6 +39,7 @@ public class ProgramRow extends TimelineGridView { private static final long ONE_HOUR_MILLIS = TimeUnit.HOURS.toMillis(1); private static final long HALF_HOUR_MILLIS = ONE_HOUR_MILLIS / 2; + private ProgramGuide mProgramGuide; private ProgramManager mProgramManager; private boolean mKeepFocusToCurrentProgram; @@ -44,8 +47,8 @@ public class ProgramRow extends TimelineGridView { interface ChildFocusListener { /** - * Is called after focus is moved. It used {@link ChildFocusListener#isChild} to decide if - * old and new focuses are listener's children. + * Is called after focus is moved. Caller should check if old and new focuses are + * listener's children. * See {@code ProgramRow#setChildFocusListener(ChildFocusListener)}. */ void onChildFocus(View oldFocus, View newFocus); @@ -213,7 +216,6 @@ public class ProgramRow extends TimelineGridView { // so give focus back in onChildAttachedToWindow(). mKeepFocusToCurrentProgram = true; } - // TODO: Try to keep focus for non-current program. } super.onChildDetachedFromWindow(child); } @@ -237,16 +239,18 @@ public class ProgramRow extends TimelineGridView { @Override public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) { - // Give focus to the current program by default. - // Note that this logic is used only if requestFocus() is called to the ProgramRow, - // so focus finding logic will not be blocked by this. - View currentProgram = getCurrentProgramView(); - if (currentProgram != null) { - return currentProgram.requestFocus(); + ProgramGrid programGrid = mProgramGuide.getProgramGrid(); + + // Give focus according to the previous focused range + Range<Integer> focusRange = programGrid.getFocusRange(); + View nextFocus = GuideUtils.findNextFocusedProgram(this, focusRange.getLower(), + focusRange.getUpper(), programGrid.isKeepCurrentProgramFocused()); + + if (nextFocus != null) { + return nextFocus.requestFocus(); } if (DEBUG) Log.d(TAG, "onRequestFocusInDescendants"); - boolean result = super.onRequestFocusInDescendants(direction, previouslyFocusedRect); if (!result) { // The default focus search logic of LeanbackLibrary is sometimes failed. @@ -276,10 +280,11 @@ public class ProgramRow extends TimelineGridView { } /** - * Sets the instance of {@link ProgramManager} + * Sets the instance of {@link ProgramGuide} */ - public void setProgramManager(ProgramManager programManager) { - mProgramManager = programManager; + public void setProgramGuide(ProgramGuide programGuide) { + mProgramGuide = programGuide; + mProgramManager = programGuide.getProgramManager(); } /** @@ -300,7 +305,7 @@ public class ProgramRow extends TimelineGridView { .scrollToPositionWithOffset(position, offset); // Workaround to b/31598505. When a program's duration is too long, // RecyclerView.onScrolled() will not be called after scrollToPositionWithOffset(). - // Therefore we have to update children's visible areas by ourselves in theis case. + // Therefore we have to update children's visible areas by ourselves in this case. // Since scrollToPositionWithOffset() will call requestLayout(), we can listen to this // behavior to ensure program items' visible areas are correctly updated after layouts // are adjusted, i.e., scrolling is over. diff --git a/src/com/android/tv/guide/ProgramTableAdapter.java b/src/com/android/tv/guide/ProgramTableAdapter.java index e4a67972..99f853b1 100644 --- a/src/com/android/tv/guide/ProgramTableAdapter.java +++ b/src/com/android/tv/guide/ProgramTableAdapter.java @@ -45,19 +45,21 @@ import android.view.ViewGroup; import android.view.ViewParent; import android.view.ViewTreeObserver; import android.view.accessibility.AccessibilityManager; +import android.view.accessibility.AccessibilityManager.AccessibilityStateChangeListener; import android.widget.ImageView; import android.widget.LinearLayout; import android.widget.TextView; import com.android.tv.R; import com.android.tv.TvApplication; +import com.android.tv.common.TvCommonUtils; import com.android.tv.common.feature.CommonFeatures; import com.android.tv.data.Channel; import com.android.tv.data.Program; import com.android.tv.data.Program.CriticScore; import com.android.tv.dvr.DvrDataManager; import com.android.tv.dvr.DvrManager; -import com.android.tv.dvr.ScheduledRecording; +import com.android.tv.dvr.data.ScheduledRecording; import com.android.tv.guide.ProgramManager.TableEntriesUpdatedListener; import com.android.tv.parental.ParentalControlSettings; import com.android.tv.ui.HardwareLayerAnimatorListenerAdapter; @@ -73,7 +75,7 @@ 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> +class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapter.ProgramRowViewHolder> implements ProgramManager.TableEntryChangedListener { private static final String TAG = "ProgramTableAdapter"; private static final boolean DEBUG = false; @@ -112,8 +114,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte private final int mDvrPaddingStartWithTrack; private final int mDvrPaddingStartWithOutTrack; - public ProgramTableAdapter(Context context, ProgramManager programManager, - ProgramGuide programGuide) { + ProgramTableAdapter(Context context, ProgramGuide programGuide) { mContext = context; mAccessibilityManager = (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); @@ -125,8 +126,8 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte mDvrManager = null; mDvrDataManager = null; } - mProgramManager = programManager; mProgramGuide = programGuide; + mProgramManager = programGuide.getProgramManager(); Resources res = context.getResources(); mChannelLogoWidth = res.getDimensionPixelSize( @@ -193,7 +194,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte mProgramListAdapters.clear(); for (int i = 0; i < mProgramManager.getChannelCount(); i++) { ProgramListAdapter listAdapter = new ProgramListAdapter(mContext.getResources(), - mProgramManager, i); + mProgramGuide, i); mProgramManager.addTableEntriesUpdatedListener(listAdapter); mProgramListAdapters.add(listAdapter); } @@ -211,12 +212,12 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte } @Override - public void onBindViewHolder(ProgramRowHolder holder, int position) { + public void onBindViewHolder(ProgramRowViewHolder holder, int position) { holder.onBind(position); } @Override - public void onBindViewHolder(ProgramRowHolder holder, int position, List<Object> payloads) { + public void onBindViewHolder(ProgramRowViewHolder holder, int position, List<Object> payloads) { if (!payloads.isEmpty()) { holder.updateDetailView(); } else { @@ -225,11 +226,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte } @Override - public ProgramRowHolder onCreateViewHolder(ViewGroup parent, int viewType) { + public ProgramRowViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View itemView = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false); ProgramRow programRow = (ProgramRow) itemView.findViewById(R.id.row); programRow.setRecycledViewPool(mRecycledViewPool); - return new ProgramRowHolder(itemView); + return new ProgramRowViewHolder(itemView); } @Override @@ -241,18 +242,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte notifyItemChanged(channelIndex, true); } - @Override - public void onViewAttachedToWindow(ProgramRowHolder holder) { - holder.onAttachedToWindow(); - } - - @Override - public void onViewDetachedFromWindow(ProgramRowHolder holder) { - holder.onDetachedFromWindow(); - } - - // TODO: make it static - public class ProgramRowHolder extends RecyclerView.ViewHolder + class ProgramRowViewHolder extends RecyclerView.ViewHolder implements ProgramRow.ChildFocusListener { private final ViewGroup mContainer; @@ -269,6 +259,12 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte } } }; + private final Runnable mUpdateDetailViewRunnable = new Runnable() { + @Override + public void run() { + updateDetailView(); + } + }; private final RecyclerView.OnScrollListener mOnScrollListener = new RecyclerView.OnScrollListener() { @@ -282,8 +278,9 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte new ViewTreeObserver.OnGlobalFocusChangeListener() { @Override public void onGlobalFocusChanged(View oldFocus, View newFocus) { - onChildFocus(isChild(oldFocus) ? oldFocus : null, - isChild(newFocus) ? newFocus : null); + onChildFocus( + GuideUtils.isDescendant(mContainer, oldFocus) ? oldFocus : null, + GuideUtils.isDescendant(mContainer, newFocus) ? newFocus : null); } }; @@ -312,11 +309,38 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte private final ImageView mInputLogoView; private boolean mIsInputLogoVisible; + private AccessibilityStateChangeListener mAccessibilityStateChangeListener = + new AccessibilityManager.AccessibilityStateChangeListener() { + @Override + public void onAccessibilityStateChanged(boolean enable) { + enable &= !TvCommonUtils.isRunningInTest(); + mDetailView.setFocusable(enable); + mChannelHeaderView.setFocusable(enable); + } + }; - public ProgramRowHolder(View itemView) { + ProgramRowViewHolder(View itemView) { super(itemView); mContainer = (ViewGroup) itemView; + mContainer.addOnAttachStateChangeListener( + new View.OnAttachStateChangeListener() { + @Override + public void onViewAttachedToWindow(View v) { + mContainer.getViewTreeObserver() + .addOnGlobalFocusChangeListener(mGlobalFocusChangeListener); + mAccessibilityManager.addAccessibilityStateChangeListener( + mAccessibilityStateChangeListener); + } + + @Override + public void onViewDetachedFromWindow(View v) { + mContainer.getViewTreeObserver() + .removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener); + mAccessibilityManager.removeAccessibilityStateChangeListener( + mAccessibilityStateChangeListener); + } + }); mProgramRow = (ProgramRow) mContainer.findViewById(R.id.row); mDetailView = (ViewGroup) mContainer.findViewById(R.id.detail); @@ -339,23 +363,18 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte mChannelLogoView = (ImageView) mContainer.findViewById(R.id.channel_logo); mChannelBlockView = (ImageView) mContainer.findViewById(R.id.channel_block); mInputLogoView = (ImageView) mContainer.findViewById(R.id.input_logo); - mDetailView.setFocusable(mAccessibilityManager.isEnabled()); - mChannelHeaderView.setFocusable(mAccessibilityManager.isEnabled()); - mAccessibilityManager.addAccessibilityStateChangeListener( - new AccessibilityManager.AccessibilityStateChangeListener() { - @Override - public void onAccessibilityStateChanged(boolean enable) { - mDetailView.setFocusable(enable); - mChannelHeaderView.setFocusable(enable); - } - }); + + boolean accessibilityEnabled = mAccessibilityManager.isEnabled() + && !TvCommonUtils.isRunningInTest(); + mDetailView.setFocusable(accessibilityEnabled); + mChannelHeaderView.setFocusable(accessibilityEnabled); } public void onBind(int position) { onBindChannel(mProgramManager.getChannel(position)); mProgramRow.swapAdapter(mProgramListAdapters.get(position), true); - mProgramRow.setProgramManager(mProgramManager); + mProgramRow.setProgramGuide(mProgramGuide); mProgramRow.setChannel(mProgramManager.getChannel(position)); mProgramRow.setChildFocusListener(this); mProgramRow.resetScroll(mProgramGuide.getTimelineRowScrollOffset()); @@ -416,24 +435,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte } } - public boolean isChild(View view) { - if (view == null) { - return false; - } - for (ViewParent p = view.getParent(); p != null; p = p.getParent()) { - if (p == mContainer) { - return true; - } - } - return false; - } - @Override public void onChildFocus(View oldFocus, View newFocus) { if (newFocus == null) { return; - } - // When the accessibility service is enabled, focus might be put on channel's header or + } // When the accessibility service is enabled, focus might be put on channel's header or // detail view, besides program items. if (newFocus == mChannelHeaderView) { mSelectedEntry = ((ProgramItemView) mProgramRow.getChildAt(0)).getTableEntry(); @@ -443,7 +449,15 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte mSelectedEntry = ((ProgramItemView) newFocus).getTableEntry(); } if (oldFocus == null) { - updateDetailView(); + // Focus moved from other row. + if (mProgramGuide.getProgramGrid().isInLayout()) { + // We need to post runnable to avoid updating detail view when + // the recycler view is in layout, which may cause detail view not + // laid out according to the updated contents. + mHandler.post(mUpdateDetailViewRunnable); + } else { + updateDetailView(); + } return; } @@ -508,16 +522,6 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte }); } - private void onAttachedToWindow() { - mContainer.getViewTreeObserver() - .addOnGlobalFocusChangeListener(mGlobalFocusChangeListener); - } - - private void onDetachedFromWindow() { - mContainer.getViewTreeObserver() - .removeOnGlobalFocusChangeListener(mGlobalFocusChangeListener); - } - private void updateDetailView() { if (mSelectedEntry == null) { // The view holder is never on focus before. @@ -556,10 +560,8 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte program.getStartTimeUtcMillis(), program.getEndTimeUtcMillis(), false)); - boolean trackMetaDataVisible = false; - trackMetaDataVisible |= - updateTextView(mAspectRatioView, Utils.getAspectRatioString( - program.getVideoWidth(), program.getVideoHeight())); + boolean trackMetaDataVisible = updateTextView(mAspectRatioView, Utils + .getAspectRatioString(program.getVideoWidth(), program.getVideoHeight())); int videoDefinitionLevel = Utils.getVideoDefinitionLevelFromSize( program.getVideoWidth(), program.getVideoHeight()); @@ -658,7 +660,9 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte if (TextUtils.isEmpty(name)) { return mContext.getString(R.string.program_guide_content_locked); } else { - return mContext.getString(R.string.program_guide_content_locked_format, name); + return TvContentRating.UNRATED.equals(blockedRating) + ? mContext.getString(R.string.program_guide_content_locked_unrated) + : mContext.getString(R.string.program_guide_content_locked_format, name); } } @@ -666,7 +670,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte * Update tv input logo. It should be called when the visible child item in ProgramGrid * changed. */ - public void updateInputLogo(int lastPosition, boolean forceShow) { + void updateInputLogo(int lastPosition, boolean forceShow) { if (mChannel == null) { mInputLogoView.setVisibility(View.GONE); mIsInputLogoVisible = false; @@ -731,7 +735,7 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte mInputLogoView.setVisibility(View.VISIBLE); } - private void updateCriticScoreView(ProgramRowHolder holder, final long programId, + private void updateCriticScoreView(ProgramRowViewHolder holder, final long programId, CriticScore criticScore, View view) { TextView criticScoreSource = (TextView) view.findViewById(R.id.critic_score_source); TextView criticScoreText = (TextView) view.findViewById(R.id.critic_score_score); @@ -759,11 +763,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte } } - private static ImageLoaderCallback<ProgramRowHolder> createCriticScoreLogoCallback( - ProgramRowHolder holder, final long programId, ImageView logoView) { - return new ImageLoaderCallback<ProgramRowHolder>(holder) { + private static ImageLoaderCallback<ProgramRowViewHolder> createCriticScoreLogoCallback( + ProgramRowViewHolder holder, final long programId, ImageView logoView) { + return new ImageLoaderCallback<ProgramRowViewHolder>(holder) { @Override - public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap logoImage) { + public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logoImage) { if (logoImage == null || holder.mSelectedEntry == null || holder.mSelectedEntry.program == null || holder.mSelectedEntry.program.getId() != programId) { @@ -776,11 +780,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte }; } - private static ImageLoaderCallback<ProgramRowHolder> createProgramPosterArtCallback( - ProgramRowHolder holder, final Program program) { - return new ImageLoaderCallback<ProgramRowHolder>(holder) { + private static ImageLoaderCallback<ProgramRowViewHolder> createProgramPosterArtCallback( + ProgramRowViewHolder holder, final Program program) { + return new ImageLoaderCallback<ProgramRowViewHolder>(holder) { @Override - public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap posterArt) { + public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap posterArt) { if (posterArt == null || holder.mSelectedEntry == null || holder.mSelectedEntry.program == null) { return; @@ -794,11 +798,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte }; } - private static ImageLoaderCallback<ProgramRowHolder> createChannelLogoLoadedCallback( - ProgramRowHolder holder, final long channelId) { - return new ImageLoaderCallback<ProgramRowHolder>(holder) { + private static ImageLoaderCallback<ProgramRowViewHolder> createChannelLogoLoadedCallback( + ProgramRowViewHolder holder, final long channelId) { + return new ImageLoaderCallback<ProgramRowViewHolder>(holder) { @Override - public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap logo) { + public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logo) { if (logo == null || holder.mChannel == null || holder.mChannel.getId() != channelId) { return; @@ -808,11 +812,11 @@ public class ProgramTableAdapter extends RecyclerView.Adapter<ProgramTableAdapte }; } - private static ImageLoaderCallback<ProgramRowHolder> createTvInputLogoLoadedCallback( - final TvInputInfo info, ProgramRowHolder holder) { - return new ImageLoaderCallback<ProgramRowHolder>(holder) { + private static ImageLoaderCallback<ProgramRowViewHolder> createTvInputLogoLoadedCallback( + final TvInputInfo info, ProgramRowViewHolder holder) { + return new ImageLoaderCallback<ProgramRowViewHolder>(holder) { @Override - public void onBitmapLoaded(ProgramRowHolder holder, @Nullable Bitmap logo) { + public void onBitmapLoaded(ProgramRowViewHolder holder, @Nullable Bitmap logo) { if (logo != null && holder.mChannel != null && info.getId() .equals(holder.mChannel.getInputId())) { holder.updateInputLogoInternal(logo); diff --git a/src/com/android/tv/guide/TimeListAdapter.java b/src/com/android/tv/guide/TimeListAdapter.java index 868fed46..d9e96a40 100644 --- a/src/com/android/tv/guide/TimeListAdapter.java +++ b/src/com/android/tv/guide/TimeListAdapter.java @@ -16,6 +16,7 @@ package com.android.tv.guide; +import android.content.Context; import android.content.res.Resources; import android.support.v7.widget.RecyclerView; import android.text.format.DateFormat; @@ -25,26 +26,40 @@ import android.view.ViewGroup; import android.widget.TextView; import com.android.tv.R; +import com.android.tv.util.Utils; import java.util.Date; +import java.util.Locale; import java.util.concurrent.TimeUnit; /** * Adapts the time range from {@link ProgramManager} to the timeline header row of the program * guide table. */ -public class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeViewHolder> { +class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeViewHolder> { private static final long TIME_UNIT_MS = TimeUnit.MINUTES.toMillis(30); + + // Ex. 3:00 AM + private static final String TIME_PATTERN_SAME_DAY = "h:mm a"; + // Ex. Oct 21, 3:00 AM + private static final String TIME_PATTERN_DIFFERENT_DAY = "MMM d, h:mm a"; + private static int sRowHeaderOverlapping; // Nearest half hour at or before the start time. private long mStartUtcMs; + private final String mTimePatternSameDay; + private final String mTimePatternDifferentDay; - public TimeListAdapter(Resources res) { + TimeListAdapter(Resources res) { if (sRowHeaderOverlapping == 0) { sRowHeaderOverlapping = Math.abs(res.getDimensionPixelOffset( R.dimen.program_guide_table_header_row_overlap)); } + Locale locale = res.getConfiguration().locale; + mTimePatternSameDay = DateFormat.getBestDateTimePattern(locale, TIME_PATTERN_SAME_DAY); + mTimePatternDifferentDay = + DateFormat.getBestDateTimePattern(locale, TIME_PATTERN_DIFFERENT_DAY); } public void update(long startTimeMs) { @@ -68,10 +83,14 @@ public class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeVi long endTime = startTime + TIME_UNIT_MS; View itemView = holder.itemView; - - TextView textView = (TextView) itemView.findViewById(R.id.time); - String time = DateFormat.getTimeFormat(itemView.getContext()).format(new Date(startTime)); - textView.setText(time); + Date timeDate = new Date(startTime); + String timeString; + if (Utils.isInGivenDay(System.currentTimeMillis(), startTime)) { + timeString = DateFormat.format(mTimePatternSameDay, timeDate).toString(); + } else { + timeString = DateFormat.format(mTimePatternDifferentDay, timeDate).toString(); + } + ((TextView) itemView.findViewById(R.id.time)).setText(timeString); RecyclerView.LayoutParams lp = (RecyclerView.LayoutParams) itemView.getLayoutParams(); lp.width = GuideUtils.convertMillisToPixel(startTime, endTime); @@ -90,8 +109,8 @@ public class TimeListAdapter extends RecyclerView.Adapter<TimeListAdapter.TimeVi return new TimeViewHolder(itemView); } - public static class TimeViewHolder extends RecyclerView.ViewHolder { - public TimeViewHolder(View itemView) { + static class TimeViewHolder extends RecyclerView.ViewHolder { + TimeViewHolder(View itemView) { super(itemView); } } |