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.graphics.Rect;
20 import android.view.View;
21 import android.view.ViewGroup;
22 import android.view.ViewParent;
23 import java.util.ArrayList;
24 import java.util.concurrent.TimeUnit;
25 
26 class GuideUtils {
27     private static final int INVALID_INDEX = -1;
28     private static int sWidthPerHour = 0;
29 
30     /**
31      * Sets the width in pixels that corresponds to an hour in program guide. Assume that this is
32      * called from main thread only, so, no synchronization.
33      */
setWidthPerHour(int widthPerHour)34     static void setWidthPerHour(int widthPerHour) {
35         sWidthPerHour = widthPerHour;
36     }
37 
38     /**
39      * Gets the number of pixels in program guide table that corresponds to the given milliseconds.
40      */
convertMillisToPixel(long millis)41     static int convertMillisToPixel(long millis) {
42         return (int) (millis * sWidthPerHour / TimeUnit.HOURS.toMillis(1));
43     }
44 
45     /** Gets the number of pixels in program guide table that corresponds to the given range. */
convertMillisToPixel(long startMillis, long endMillis)46     static int convertMillisToPixel(long startMillis, long endMillis) {
47         // Convert to pixels first to avoid accumulation of rounding errors.
48         return GuideUtils.convertMillisToPixel(endMillis)
49                 - GuideUtils.convertMillisToPixel(startMillis);
50     }
51 
52     /** Gets the time in millis that corresponds to the given pixels in the program guide. */
convertPixelToMillis(int pixel)53     static long convertPixelToMillis(int pixel) {
54         return pixel * TimeUnit.HOURS.toMillis(1) / sWidthPerHour;
55     }
56 
57     /**
58      * Return the view should be focused in the given program row according to the focus range.
59      *
60      * @param keepCurrentProgramFocused If {@code true}, focuses on the current program if possible,
61      *     else falls back the general logic.
62      */
findNextFocusedProgram( View programRow, int focusRangeLeft, int focusRangeRight, boolean keepCurrentProgramFocused)63     static View findNextFocusedProgram(
64             View programRow,
65             int focusRangeLeft,
66             int focusRangeRight,
67             boolean keepCurrentProgramFocused) {
68         ArrayList<View> focusables = new ArrayList<>();
69         findFocusables(programRow, focusables);
70 
71         if (keepCurrentProgramFocused) {
72             // Select the current program if possible.
73             for (int i = 0; i < focusables.size(); ++i) {
74                 View focusable = focusables.get(i);
75                 if (focusable instanceof ProgramItemView
76                         && isCurrentProgram((ProgramItemView) focusable)) {
77                     return focusable;
78                 }
79             }
80         }
81 
82         // Find the largest focusable among fully overlapped focusables.
83         int maxFullyOverlappedWidth = Integer.MIN_VALUE;
84         int maxPartiallyOverlappedWidth = Integer.MIN_VALUE;
85         int nextFocusIndex = INVALID_INDEX;
86         for (int i = 0; i < focusables.size(); ++i) {
87             View focusable = focusables.get(i);
88             Rect focusableRect = new Rect();
89             focusable.getGlobalVisibleRect(focusableRect);
90             if (focusableRect.left <= focusRangeLeft && focusRangeRight <= focusableRect.right) {
91                 // the old focused range is fully inside the focusable, return directly.
92                 return focusable;
93             } else if (focusRangeLeft <= focusableRect.left
94                     && focusableRect.right <= focusRangeRight) {
95                 // the focusable is fully inside the old focused range, choose the widest one.
96                 int width = focusableRect.width();
97                 if (width > maxFullyOverlappedWidth) {
98                     nextFocusIndex = i;
99                     maxFullyOverlappedWidth = width;
100                 }
101             } else if (maxFullyOverlappedWidth == Integer.MIN_VALUE) {
102                 int overlappedWidth =
103                         (focusRangeLeft <= focusableRect.left)
104                                 ? focusRangeRight - focusableRect.left
105                                 : focusableRect.right - focusRangeLeft;
106                 if (overlappedWidth > maxPartiallyOverlappedWidth) {
107                     nextFocusIndex = i;
108                     maxPartiallyOverlappedWidth = overlappedWidth;
109                 }
110             }
111         }
112         if (nextFocusIndex != INVALID_INDEX) {
113             return focusables.get(nextFocusIndex);
114         }
115         return null;
116     }
117 
118     /**
119      * Returns {@code true} if the program displayed in the give {@link
120      * com.android.tv.guide.ProgramItemView} is a current program.
121      */
isCurrentProgram(ProgramItemView view)122     static boolean isCurrentProgram(ProgramItemView view) {
123         return view.getTableEntry().isCurrentProgram();
124     }
125 
126     /** Returns {@code true} if the given view is a descendant of the give container. */
isDescendant(ViewGroup container, View view)127     static boolean isDescendant(ViewGroup container, View view) {
128         if (view == null) {
129             return false;
130         }
131         for (ViewParent p = view.getParent(); p != null; p = p.getParent()) {
132             if (p == container) {
133                 return true;
134             }
135         }
136         return false;
137     }
138 
findFocusables(View v, ArrayList<View> outFocusable)139     private static void findFocusables(View v, ArrayList<View> outFocusable) {
140         if (v.isFocusable()) {
141             outFocusable.add(v);
142         }
143         if (v instanceof ViewGroup) {
144             ViewGroup viewGroup = (ViewGroup) v;
145             for (int i = 0; i < viewGroup.getChildCount(); ++i) {
146                 findFocusables(viewGroup.getChildAt(i), outFocusable);
147             }
148         }
149     }
150 
GuideUtils()151     private GuideUtils() {}
152 }
153