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.view.View;
21import android.view.ViewGroup;
22import android.view.ViewParent;
23import java.util.ArrayList;
24import java.util.concurrent.TimeUnit;
25
26class 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     */
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     */
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. */
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. */
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     */
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     */
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. */
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
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
151    private GuideUtils() {}
152}
153