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