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