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