• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.tv.guide;
18 
19 import android.content.Context;
20 import android.graphics.Rect;
21 import android.support.v7.widget.LinearLayoutManager;
22 import android.util.AttributeSet;
23 import android.util.Log;
24 import android.util.Range;
25 import android.view.View;
26 import android.view.ViewTreeObserver.OnGlobalLayoutListener;
27 
28 import com.android.tv.MainActivity;
29 import com.android.tv.data.Channel;
30 import com.android.tv.guide.ProgramManager.TableEntry;
31 import com.android.tv.util.Utils;
32 
33 import java.util.concurrent.TimeUnit;
34 
35 public class ProgramRow extends TimelineGridView {
36     private static final String TAG = "ProgramRow";
37     private static final boolean DEBUG = false;
38 
39     private static final long ONE_HOUR_MILLIS = TimeUnit.HOURS.toMillis(1);
40     private static final long HALF_HOUR_MILLIS = ONE_HOUR_MILLIS / 2;
41 
42     private ProgramGuide mProgramGuide;
43     private ProgramManager mProgramManager;
44 
45     private boolean mKeepFocusToCurrentProgram;
46     private ChildFocusListener mChildFocusListener;
47 
48     interface ChildFocusListener {
49         /**
50          * Is called after focus is moved. Caller should check if old and new focuses are
51          * listener's children.
52          * See {@code ProgramRow#setChildFocusListener(ChildFocusListener)}.
53          */
onChildFocus(View oldFocus, View newFocus)54         void onChildFocus(View oldFocus, View newFocus);
55     }
56 
57     /**
58      * Used only for debugging.
59      */
60     private Channel mChannel;
61 
62     private final OnGlobalLayoutListener mLayoutListener = new OnGlobalLayoutListener() {
63         @Override
64         public void onGlobalLayout() {
65             getViewTreeObserver().removeOnGlobalLayoutListener(this);
66             updateChildVisibleArea();
67         }
68     };
69 
ProgramRow(Context context)70     public ProgramRow(Context context) {
71         this(context, null);
72     }
73 
ProgramRow(Context context, AttributeSet attrs)74     public ProgramRow(Context context, AttributeSet attrs) {
75         this(context, attrs, 0);
76     }
77 
ProgramRow(Context context, AttributeSet attrs, int defStyle)78     public ProgramRow(Context context, AttributeSet attrs, int defStyle) {
79         super(context, attrs, defStyle);
80     }
81 
82     /**
83      * Registers a listener focus events occurring on children to the {@code ProgramRow}.
84      */
setChildFocusListener(ChildFocusListener childFocusListener)85     public void setChildFocusListener(ChildFocusListener childFocusListener) {
86         mChildFocusListener = childFocusListener;
87     }
88 
89     @Override
onViewAdded(View child)90     public void onViewAdded(View child) {
91         super.onViewAdded(child);
92         ProgramItemView itemView = (ProgramItemView) child;
93         if (getLeft() <= itemView.getRight() && itemView.getLeft() <= getRight()) {
94             itemView.updateVisibleArea();
95         }
96     }
97 
98     @Override
onScrolled(int dx, int dy)99     public void onScrolled(int dx, int dy) {
100         // Remove callback to prevent updateChildVisibleArea being called twice.
101         getViewTreeObserver().removeOnGlobalLayoutListener(mLayoutListener);
102         super.onScrolled(dx, dy);
103         if (DEBUG) {
104             Log.d(TAG, "onScrolled by " + dx);
105             Log.d(TAG, "channelId=" + mChannel.getId() + ", childCount=" + getChildCount());
106             Log.d(TAG, "ProgramRow {" + Utils.toRectString(this) + "}");
107         }
108         updateChildVisibleArea();
109     }
110 
111     /**
112      * Moves focus to the current program.
113      */
focusCurrentProgram()114     public void focusCurrentProgram() {
115         View currentProgram = getCurrentProgramView();
116         if (currentProgram == null) {
117             currentProgram = getChildAt(0);
118         }
119         if (mChildFocusListener != null) {
120             mChildFocusListener.onChildFocus(null, currentProgram);
121         }
122     }
123 
124     // Call this API after RTL is resolved. (i.e. View is measured.)
isDirectionStart(int direction)125     private boolean isDirectionStart(int direction) {
126         return getLayoutDirection() == LAYOUT_DIRECTION_LTR
127                 ? direction == View.FOCUS_LEFT : direction == View.FOCUS_RIGHT;
128     }
129 
130     // Call this API after RTL is resolved. (i.e. View is measured.)
isDirectionEnd(int direction)131     private boolean isDirectionEnd(int direction) {
132         return getLayoutDirection() == LAYOUT_DIRECTION_LTR
133                 ? direction == View.FOCUS_RIGHT : direction == View.FOCUS_LEFT;
134     }
135 
136     @Override
focusSearch(View focused, int direction)137     public View focusSearch(View focused, int direction) {
138         TableEntry focusedEntry = ((ProgramItemView) focused).getTableEntry();
139         long fromMillis = mProgramManager.getFromUtcMillis();
140         long toMillis = mProgramManager.getToUtcMillis();
141 
142         if (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD) {
143             if (focusedEntry.entryStartUtcMillis < fromMillis) {
144                 // The current entry starts outside of the view; Align or scroll to the left.
145                 scrollByTime(Math.max(-ONE_HOUR_MILLIS,
146                         focusedEntry.entryStartUtcMillis - fromMillis));
147                 return focused;
148             }
149         } else if (isDirectionEnd(direction) || direction == View.FOCUS_FORWARD) {
150             if (focusedEntry.entryEndUtcMillis >= toMillis + ONE_HOUR_MILLIS) {
151                 // The current entry ends outside of the view; Scroll to the right.
152                 scrollByTime(ONE_HOUR_MILLIS);
153                 return focused;
154             }
155         }
156 
157         View target = super.focusSearch(focused, direction);
158         if (!(target instanceof ProgramItemView)) {
159             if (isDirectionEnd(direction) || direction == View.FOCUS_FORWARD) {
160                 if (focusedEntry.entryEndUtcMillis != toMillis) {
161                     // The focused entry is the last entry; Align to the right edge.
162                     scrollByTime(focusedEntry.entryEndUtcMillis - toMillis);
163                     return focused;
164                 }
165             }
166             return target;
167         }
168 
169         TableEntry targetEntry = ((ProgramItemView) target).getTableEntry();
170 
171         if (isDirectionStart(direction) || direction == View.FOCUS_BACKWARD) {
172             if (targetEntry.entryStartUtcMillis < fromMillis &&
173                     targetEntry.entryEndUtcMillis < fromMillis + HALF_HOUR_MILLIS) {
174                 // The target entry starts outside the view; Align or scroll to the left.
175                 scrollByTime(Math.max(-ONE_HOUR_MILLIS,
176                         targetEntry.entryStartUtcMillis - fromMillis));
177             }
178         } else if (isDirectionEnd(direction) || direction == View.FOCUS_FORWARD) {
179             if (targetEntry.entryStartUtcMillis > fromMillis + ONE_HOUR_MILLIS + HALF_HOUR_MILLIS) {
180                 // The target entry starts outside the view; Align or scroll to the right.
181                 scrollByTime(Math.min(ONE_HOUR_MILLIS,
182                         targetEntry.entryStartUtcMillis - fromMillis - ONE_HOUR_MILLIS));
183             }
184         }
185 
186         return target;
187     }
188 
scrollByTime(long timeToScroll)189     private void scrollByTime(long timeToScroll) {
190         if (DEBUG) {
191             Log.d(TAG, "scrollByTime(timeToScroll="
192                     + TimeUnit.MILLISECONDS.toMinutes(timeToScroll) + "min)");
193         }
194         mProgramManager.shiftTime(timeToScroll);
195     }
196 
197     @Override
onChildDetachedFromWindow(View child)198     public void onChildDetachedFromWindow(View child) {
199         if (child.hasFocus()) {
200             // Focused view can be detached only if it's updated.
201             TableEntry entry = ((ProgramItemView) child).getTableEntry();
202             if (entry.program == null) {
203                 // The focus is lost due to information loaded. Requests focus immediately.
204                 // (Because this entry is detached after real entries attached, we can't take
205                 // the below approach to resume focus on entry being attached.)
206                 post(new Runnable() {
207                     @Override
208                     public void run() {
209                         requestFocus();
210                     }
211                 });
212             } else if (entry.isCurrentProgram()) {
213                 if (DEBUG) Log.d(TAG, "Keep focus to the current program");
214                 // Current program is visible in the guide.
215                 // Updated entries including current program's will be attached again soon
216                 // so give focus back in onChildAttachedToWindow().
217                 mKeepFocusToCurrentProgram = true;
218             }
219         }
220         super.onChildDetachedFromWindow(child);
221     }
222 
223     @Override
onChildAttachedToWindow(View child)224     public void onChildAttachedToWindow(View child) {
225         super.onChildAttachedToWindow(child);
226         if (mKeepFocusToCurrentProgram) {
227             TableEntry entry = ((ProgramItemView) child).getTableEntry();
228             if (entry.isCurrentProgram()) {
229                 mKeepFocusToCurrentProgram = false;
230                 post(new Runnable() {
231                     @Override
232                     public void run() {
233                         requestFocus();
234                     }
235                 });
236             }
237         }
238     }
239 
240     @Override
onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect)241     public boolean onRequestFocusInDescendants(int direction, Rect previouslyFocusedRect) {
242         ProgramGrid programGrid = mProgramGuide.getProgramGrid();
243 
244         // Give focus according to the previous focused range
245         Range<Integer> focusRange = programGrid.getFocusRange();
246         View nextFocus = GuideUtils.findNextFocusedProgram(this, focusRange.getLower(),
247                 focusRange.getUpper(), programGrid.isKeepCurrentProgramFocused());
248 
249         if (nextFocus != null) {
250             return nextFocus.requestFocus();
251         }
252 
253         if (DEBUG) Log.d(TAG, "onRequestFocusInDescendants");
254         boolean result = super.onRequestFocusInDescendants(direction, previouslyFocusedRect);
255         if (!result) {
256             // The default focus search logic of LeanbackLibrary is sometimes failed.
257             // As a fallback solution, we request focus to the first focusable view.
258             for (int i = 0; i < getChildCount(); ++i) {
259                 View child = getChildAt(i);
260                 if (child.isShown() && child.hasFocusable()) {
261                     return child.requestFocus();
262                 }
263             }
264         }
265         return result;
266     }
267 
getCurrentProgramView()268     private View getCurrentProgramView() {
269         for (int i = 0; i < getChildCount(); ++i) {
270             TableEntry entry = ((ProgramItemView) getChildAt(i)).getTableEntry();
271             if (entry.isCurrentProgram()) {
272                 return getChildAt(i);
273             }
274         }
275         return null;
276     }
277 
setChannel(Channel channel)278     public void setChannel(Channel channel) {
279         mChannel = channel;
280     }
281 
282     /**
283      * Sets the instance of {@link ProgramGuide}
284      */
setProgramGuide(ProgramGuide programGuide)285     public void setProgramGuide(ProgramGuide programGuide) {
286         mProgramGuide = programGuide;
287         mProgramManager = programGuide.getProgramManager();
288     }
289 
290     /**
291      * Resets the scroll with the initial offset {@code scrollOffset}.
292      */
resetScroll(int scrollOffset)293     public void resetScroll(int scrollOffset) {
294         long startTime = GuideUtils.convertPixelToMillis(scrollOffset)
295                 + mProgramManager.getStartTime();
296         int position = mChannel == null ? -1 : mProgramManager.getProgramIndexAtTime(
297                 mChannel.getId(), startTime);
298         if (position < 0) {
299             getLayoutManager().scrollToPosition(0);
300         } else {
301             TableEntry entry = mProgramManager.getTableEntry(mChannel.getId(), position);
302             int offset = GuideUtils.convertMillisToPixel(
303                     mProgramManager.getStartTime(), entry.entryStartUtcMillis) - scrollOffset;
304             ((LinearLayoutManager) getLayoutManager())
305                     .scrollToPositionWithOffset(position, offset);
306             // Workaround to b/31598505. When a program's duration is too long,
307             // RecyclerView.onScrolled() will not be called after scrollToPositionWithOffset().
308             // Therefore we have to update children's visible areas by ourselves in this case.
309             // Since scrollToPositionWithOffset() will call requestLayout(), we can listen to this
310             // behavior to ensure program items' visible areas are correctly updated after layouts
311             // are adjusted, i.e., scrolling is over.
312             getViewTreeObserver().addOnGlobalLayoutListener(mLayoutListener);
313         }
314     }
315 
updateChildVisibleArea()316     private void updateChildVisibleArea() {
317         for (int i = 0; i < getChildCount(); ++i) {
318             ProgramItemView child = (ProgramItemView) getChildAt(i);
319             if (getLeft() < child.getRight() && child.getLeft() < getRight()) {
320                 child.updateVisibleArea();
321             }
322         }
323     }
324 }
325