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 */
16package com.android.tv.ui;
17
18import android.os.Message;
19import android.support.annotation.NonNull;
20import android.support.v17.leanback.widget.VerticalGridView;
21import android.util.Log;
22import android.view.KeyEvent;
23import android.view.View;
24
25import com.android.tv.common.WeakHandler;
26
27/**
28 * Listener to make focus change faster over time.
29 */
30public class OnRepeatedKeyInterceptListener implements VerticalGridView.OnKeyInterceptListener {
31    private static final String TAG = "OnRepeatedKeyListener";
32    private static final boolean DEBUG = false;
33
34    private static final int[] THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS = { 2000, 5000 };
35    private static final int[] MAX_SKIPPED_VIEW_COUNT = { 1, 4 };
36    private static final int MSG_MOVE_FOCUS = 1000;
37
38    private final VerticalGridView mView;
39    private final MyHandler mHandler = new MyHandler(this);
40    private int mDirection;
41    private boolean mFocusAccelerated;
42    private long mRepeatedKeyInterval;
43
44    public OnRepeatedKeyInterceptListener(VerticalGridView view) {
45        mView = view;
46    }
47
48    public boolean isFocusAccelerated() {
49        return mFocusAccelerated;
50    }
51
52    @Override
53    public boolean onInterceptKeyEvent(KeyEvent event) {
54        mHandler.removeMessages(MSG_MOVE_FOCUS);
55        if (event.getKeyCode() != KeyEvent.KEYCODE_DPAD_UP &&
56                event.getKeyCode() != KeyEvent.KEYCODE_DPAD_DOWN) {
57            return false;
58        }
59
60        long duration = event.getEventTime() - event.getDownTime();
61        if (duration < THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS[0]
62                || event.isCanceled()) {
63            mFocusAccelerated = false;
64            return false;
65        }
66        mDirection = event.getKeyCode() == KeyEvent.KEYCODE_DPAD_UP ? View.FOCUS_UP
67                : View.FOCUS_DOWN;
68        int skippedViewCount = MAX_SKIPPED_VIEW_COUNT[0];
69        for (int i = 1 ;i < THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS.length; ++i) {
70            if (THRESHOLD_FAST_FOCUS_CHANGE_TIME_MS[i] < duration) {
71                skippedViewCount = MAX_SKIPPED_VIEW_COUNT[i];
72            } else {
73                break;
74            }
75        }
76        if (event.getAction() == KeyEvent.ACTION_DOWN) {
77            mRepeatedKeyInterval = duration / event.getRepeatCount();
78            mFocusAccelerated = true;
79        } else {
80            // HACK: we move focus skippedViewCount times more even after ACTION_UP. Without this
81            // hack, a focused view's position doesn't reach to the desired position
82            // in ProgramGrid.
83            mFocusAccelerated = false;
84        }
85        for (int i = 0; i < skippedViewCount; ++i) {
86            mHandler.sendEmptyMessageDelayed(MSG_MOVE_FOCUS,
87                    mRepeatedKeyInterval * i / (skippedViewCount + 1));
88        }
89        if (DEBUG) Log.d(TAG, "onInterceptKeyEvent: focused view " + mView.findFocus());
90        return false;
91    }
92
93    private static class MyHandler extends WeakHandler<OnRepeatedKeyInterceptListener> {
94        private MyHandler(OnRepeatedKeyInterceptListener listener) {
95            super(listener);
96        }
97
98        @Override
99        public void handleMessage(Message msg, @NonNull OnRepeatedKeyInterceptListener listener) {
100            if (msg.what == MSG_MOVE_FOCUS) {
101                View focused = listener.mView.findFocus();
102                if (DEBUG) Log.d(TAG, "MSG_MOVE_FOCUS: focused view " + focused);
103                if (focused != null) {
104                    View v = focused.focusSearch(listener.mDirection);
105                    if (v != null && v != focused) {
106                        v.requestFocus(listener.mDirection);
107                    }
108                }
109            }
110        }
111    }
112}
113