1 /*
2  * Copyright (C) 2020 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 package com.android.car.ui;
17 
18 import static android.view.accessibility.AccessibilityNodeInfo.ACTION_DISMISS;
19 
20 import android.content.Context;
21 import android.os.Bundle;
22 import android.util.AttributeSet;
23 import android.view.View;
24 
25 import androidx.annotation.Nullable;
26 
27 /**
28  * A transparent {@link View} that can take focus. It's used by {@link
29  * com.android.car.rotary.RotaryService} to support rotary controller navigation. Each {@link
30  * android.view.Window} must have at least one FocusParkingView. The {@link FocusParkingView} must
31  * be the first in Tab order, and outside of all {@link FocusArea}s.
32  *
33  * <p>
34  * Android doesn't clear focus automatically when focus is set in another window. If we try to clear
35  * focus in the previous window, Android will re-focus a view in that window, resulting in two
36  * windows being focused simultaneously. Adding this view to each window can fix this issue. This
37  * view is transparent and its default focus highlight is disabled, so it's invisible to the user no
38  * matter whether it's focused or not. It can take focus so that RotaryService can "park" the focus
39  * on it to remove the focus highlight.
40  * <p>
41  * If the focused view is scrolled off the screen, Android will refocus the first focusable view in
42  * the window. The FocusParkingView should be the first view so that it gets focus. The
43  * RotaryService detects this and moves focus to the scrolling container.
44  * <p>
45  * If there is only one focus area in the current window, rotating the controller within the focus
46  * area will cause RotaryService to move the focus around from the view on the right to the view on
47  * the left or vice versa. Adding this view to each window can fix this issue. When RotaryService
48  * finds out the focus target is a FocusParkingView, it will know a wrap-around is going to happen.
49  * Then it will avoid the wrap-around by not moving focus.
50  */
51 public class FocusParkingView extends View {
52 
FocusParkingView(Context context)53     public FocusParkingView(Context context) {
54         super(context);
55         init();
56     }
57 
FocusParkingView(Context context, @Nullable AttributeSet attrs)58     public FocusParkingView(Context context, @Nullable AttributeSet attrs) {
59         super(context, attrs);
60         init();
61     }
62 
FocusParkingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr)63     public FocusParkingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
64         super(context, attrs, defStyleAttr);
65         init();
66     }
67 
FocusParkingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes)68     public FocusParkingView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
69             int defStyleRes) {
70         super(context, attrs, defStyleAttr, defStyleRes);
71         init();
72     }
73 
init()74     private void init() {
75         // This view is focusable, visible and enabled so it can take focus.
76         setFocusable(View.FOCUSABLE);
77         setVisibility(VISIBLE);
78         setEnabled(true);
79 
80         // This view is not clickable so it won't affect the app's behavior when the user clicks on
81         // it by accident.
82         setClickable(false);
83 
84         // This view is always transparent.
85         setAlpha(0f);
86 
87         // Prevent Android from drawing the default focus highlight for this view when it's focused.
88         setDefaultFocusHighlightEnabled(false);
89     }
90 
91     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)92     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
93         // This size of the view is always 1 x 1 pixel, no matter what value is set in the layout
94         // file (match_parent, wrap_content, 100dp, 0dp, etc). Small size is to ensure it has little
95         // impact on the layout, non-zero size is to ensure it can take focus.
96         setMeasuredDimension(1, 1);
97     }
98 
99     @Override
onWindowFocusChanged(boolean hasWindowFocus)100     public void onWindowFocusChanged(boolean hasWindowFocus) {
101         if (!hasWindowFocus) {
102             // We need to clear the focus (by parking the focus on the FocusParkingView) once the
103             // current window goes to background. This can't be done by RotaryService because
104             // RotaryService sees the window as removed, thus can't perform any action (such as
105             // focus, clear focus) on the nodes in the window. So FocusParkingView has to grab the
106             // focus proactively.
107             requestFocus();
108         }
109         super.onWindowFocusChanged(hasWindowFocus);
110     }
111 
112     @Override
getAccessibilityClassName()113     public CharSequence getAccessibilityClassName() {
114         return FocusParkingView.class.getName();
115     }
116 
117     @Override
performAccessibilityAction(int action, Bundle arguments)118     public boolean performAccessibilityAction(int action, Bundle arguments) {
119         if (action == ACTION_DISMISS) {
120             // Try to move focus to the default focus.
121             getRootView().restoreDefaultFocus();
122             // The action failed if the FocusParkingView is still focused.
123             return !isFocused();
124         }
125         return super.performAccessibilityAction(action, arguments);
126     }
127 }
128