1816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko/*
2816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Copyright (C) 2015 The Android Open Source Project
3816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko *
4816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Licensed under the Apache License, Version 2.0 (the "License");
5816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * you may not use this file except in compliance with the License.
6816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * You may obtain a copy of the License at
7816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko *
8816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko *      http://www.apache.org/licenses/LICENSE-2.0
9816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko *
10816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * Unless required by applicable law or agreed to in writing, software
11816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * distributed under the License is distributed on an "AS IS" BASIS,
12816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * See the License for the specific language governing permissions and
14816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko * limitations under the License.
15816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko */
16816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
17816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkopackage com.android.tv.guide;
18816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
196ebde20b03db4c0d57f67acaac11832b610b966bNick Chalkoimport android.graphics.Rect;
206ebde20b03db4c0d57f67acaac11832b610b966bNick Chalkoimport android.support.annotation.NonNull;
216ebde20b03db4c0d57f67acaac11832b610b966bNick Chalkoimport android.view.View;
226ebde20b03db4c0d57f67acaac11832b610b966bNick Chalkoimport android.view.ViewGroup;
236ebde20b03db4c0d57f67acaac11832b610b966bNick Chalkoimport android.view.ViewParent;
246ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko
256ebde20b03db4c0d57f67acaac11832b610b966bNick Chalkoimport java.util.ArrayList;
26816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalkoimport java.util.concurrent.TimeUnit;
27816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
286ebde20b03db4c0d57f67acaac11832b610b966bNick Chalkoclass GuideUtils {
296ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    private static final int INVALID_INDEX = -1;
30816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    private static int sWidthPerHour = 0;
31816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
32816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    /**
33816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko     * Sets the width in pixels that corresponds to an hour in program guide.
34816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko     * Assume that this is called from main thread only, so, no synchronization.
35816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko     */
366ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    static void setWidthPerHour(int widthPerHour) {
37816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        sWidthPerHour = widthPerHour;
38816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
39816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
40816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    /**
41816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko     * Gets the number of pixels in program guide table that corresponds to the given milliseconds.
42816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko     */
436ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    static int convertMillisToPixel(long millis) {
44816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return (int) (millis * sWidthPerHour / TimeUnit.HOURS.toMillis(1));
45816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
46816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
47816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    /**
48816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko     * Gets the number of pixels in program guide table that corresponds to the given range.
49816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko     */
506ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    static int convertMillisToPixel(long startMillis, long endMillis) {
51816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        // Convert to pixels first to avoid accumulation of rounding errors.
52816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return GuideUtils.convertMillisToPixel(endMillis)
53816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko                - GuideUtils.convertMillisToPixel(startMillis);
54816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
55816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
56816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    /**
57816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko     * Gets the time in millis that corresponds to the given pixels in the program guide.
58816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko     */
596ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    static long convertPixelToMillis(int pixel) {
60816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko        return pixel * TimeUnit.HOURS.toMillis(1) / sWidthPerHour;
61816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    }
62816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko
636ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    /**
646ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko     * Return the view should be focused in the given program row according to the focus range.
656ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko
666ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko     * @param keepCurrentProgramFocused If {@code true}, focuses on the current program if possible,
676ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko     *                                  else falls back the general logic.
686ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko     */
696ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    static View findNextFocusedProgram(View programRow, int focusRangeLeft,
706ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            int focusRangeRight, boolean keepCurrentProgramFocused) {
716ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        ArrayList<View> focusables = new ArrayList<>();
726ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        findFocusables(programRow, focusables);
736ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko
746ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        if (keepCurrentProgramFocused) {
756ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            // Select the current program if possible.
766ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            for (int i = 0; i < focusables.size(); ++i) {
776ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                View focusable = focusables.get(i);
786ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                if (focusable instanceof ProgramItemView
796ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                        && isCurrentProgram((ProgramItemView) focusable)) {
806ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                    return focusable;
816ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                }
826ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            }
836ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        }
846ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko
856ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        // Find the largest focusable among fully overlapped focusables.
866ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        int maxFullyOverlappedWidth = Integer.MIN_VALUE;
876ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        int maxPartiallyOverlappedWidth = Integer.MIN_VALUE;
886ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        int nextFocusIndex = INVALID_INDEX;
896ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        for (int i = 0; i < focusables.size(); ++i) {
906ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            View focusable = focusables.get(i);
916ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            Rect focusableRect = new Rect();
926ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            focusable.getGlobalVisibleRect(focusableRect);
936ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            if (focusableRect.left <= focusRangeLeft && focusRangeRight <= focusableRect.right) {
946ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                // the old focused range is fully inside the focusable, return directly.
956ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                return focusable;
966ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            } else if (focusRangeLeft <= focusableRect.left
976ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                    && focusableRect.right <= focusRangeRight) {
986ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                // the focusable is fully inside the old focused range, choose the widest one.
996ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                int width = focusableRect.width();
1006ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                if (width > maxFullyOverlappedWidth) {
1016ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                    nextFocusIndex = i;
1026ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                    maxFullyOverlappedWidth = width;
1036ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                }
1046ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            } else if (maxFullyOverlappedWidth == Integer.MIN_VALUE) {
1056ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                int overlappedWidth = (focusRangeLeft <= focusableRect.left) ?
1066ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                        focusRangeRight - focusableRect.left
1076ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                        : focusableRect.right - focusRangeLeft;
1086ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                if (overlappedWidth > maxPartiallyOverlappedWidth) {
1096ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                    nextFocusIndex = i;
1106ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                    maxPartiallyOverlappedWidth = overlappedWidth;
1116ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                }
1126ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            }
1136ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        }
1146ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        if (nextFocusIndex != INVALID_INDEX) {
1156ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            return focusables.get(nextFocusIndex);
1166ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        }
1176ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        return null;
1186ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    }
1196ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko
1206ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    /**
1216ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko     *  Returns {@code true} if the program displayed in the give
1226ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko     *  {@link com.android.tv.guide.ProgramItemView} is a current program.
1236ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko     */
1246ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    static boolean isCurrentProgram(ProgramItemView view) {
1256ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        return view.getTableEntry().isCurrentProgram();
1266ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    }
1276ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko
1286ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    /**
1296ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko     * Returns {@code true} if the given view is a descendant of the give container.
1306ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko     */
1316ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    static boolean isDescendant(ViewGroup container, View view) {
1326ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        if (view == null) {
1336ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            return false;
1346ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        }
1356ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        for (ViewParent p = view.getParent(); p != null; p = p.getParent()) {
1366ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            if (p == container) {
1376ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                return true;
1386ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            }
1396ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        }
1406ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        return false;
1416ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    }
1426ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko
1436ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    private static void findFocusables(View v, ArrayList<View> outFocusable) {
1446ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        if (v.isFocusable()) {
1456ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            outFocusable.add(v);
1466ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        }
1476ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        if (v instanceof ViewGroup) {
1486ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            ViewGroup viewGroup = (ViewGroup) v;
1496ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            for (int i = 0; i < viewGroup.getChildCount(); ++i) {
1506ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko                findFocusables(viewGroup.getChildAt(i), outFocusable);
1516ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko            }
1526ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko        }
1536ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko    }
1546ebde20b03db4c0d57f67acaac11832b610b966bNick Chalko
155816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko    private GuideUtils() { }
156816a4be1a0f34f6a48877c8afd3dbbca19eac435Nick Chalko}
157