aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tv/guide
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/android/tv/guide')
-rw-r--r--src/com/android/tv/guide/GenreListAdapter.java23
-rw-r--r--src/com/android/tv/guide/GuideUtils.java110
-rw-r--r--src/com/android/tv/guide/ProgramGrid.java270
-rw-r--r--src/com/android/tv/guide/ProgramGuide.java265
-rw-r--r--src/com/android/tv/guide/ProgramItemView.java33
-rw-r--r--src/com/android/tv/guide/ProgramListAdapter.java36
-rw-r--r--src/com/android/tv/guide/ProgramManager.java656
-rw-r--r--src/com/android/tv/guide/ProgramRow.java33
-rw-r--r--src/com/android/tv/guide/ProgramTableAdapter.java172
-rw-r--r--src/com/android/tv/guide/TimeListAdapter.java35
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);
}
}