1 /*
2  * Copyright (C) 2022 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 
17 package android.inputmethodservice.navigationbar;
18 
19 import static android.inputmethodservice.navigationbar.NavigationBarConstants.DARK_MODE_ICON_COLOR_SINGLE_TONE;
20 import static android.inputmethodservice.navigationbar.NavigationBarConstants.LIGHT_MODE_ICON_COLOR_SINGLE_TONE;
21 import static android.inputmethodservice.navigationbar.NavigationBarConstants.NAVBAR_BACK_BUTTON_IME_OFFSET;
22 import static android.inputmethodservice.navigationbar.NavigationBarUtils.dpToPx;
23 import static android.view.WindowManagerPolicyConstants.NAV_BAR_MODE_GESTURAL;
24 
25 import android.animation.ObjectAnimator;
26 import android.animation.PropertyValuesHolder;
27 import android.annotation.DrawableRes;
28 import android.annotation.FloatRange;
29 import android.app.StatusBarManager;
30 import android.content.Context;
31 import android.content.res.Configuration;
32 import android.graphics.Canvas;
33 import android.util.AttributeSet;
34 import android.util.Log;
35 import android.util.SparseArray;
36 import android.view.Display;
37 import android.view.MotionEvent;
38 import android.view.Surface;
39 import android.view.View;
40 import android.view.animation.Interpolator;
41 import android.view.animation.PathInterpolator;
42 import android.view.inputmethod.InputMethodManager;
43 import android.widget.FrameLayout;
44 
45 import java.util.function.Consumer;
46 
47 /**
48  * @hide
49  */
50 public final class NavigationBarView extends FrameLayout {
51     private static final boolean DEBUG = false;
52     private static final String TAG = "NavBarView";
53 
54     // Copied from com.android.systemui.animation.Interpolators#FAST_OUT_SLOW_IN
55     private static final Interpolator FAST_OUT_SLOW_IN = new PathInterpolator(0.4f, 0f, 0.2f, 1f);
56 
57     // The current view is always mHorizontal.
58     View mCurrentView = null;
59     private View mHorizontal;
60 
61     private int mCurrentRotation = -1;
62 
63     int mDisabledFlags = 0;
64     int mNavigationIconHints = StatusBarManager.NAVIGATION_HINT_BACK_ALT;
65     private final int mNavBarMode = NAV_BAR_MODE_GESTURAL;
66 
67     private KeyButtonDrawable mBackIcon;
68     private KeyButtonDrawable mImeSwitcherIcon;
69     private Context mLightContext;
70     private final int mLightIconColor;
71     private final int mDarkIconColor;
72 
73     private final android.inputmethodservice.navigationbar.DeadZone mDeadZone;
74     private boolean mDeadZoneConsuming = false;
75 
76     private final SparseArray<ButtonDispatcher> mButtonDispatchers = new SparseArray<>();
77     private Configuration mConfiguration;
78     private Configuration mTmpLastConfiguration;
79 
80     private NavigationBarInflaterView mNavigationInflaterView;
81 
NavigationBarView(Context context, AttributeSet attrs)82     public NavigationBarView(Context context, AttributeSet attrs) {
83         super(context, attrs);
84 
85         mLightContext = context;
86         mLightIconColor = LIGHT_MODE_ICON_COLOR_SINGLE_TONE;
87         mDarkIconColor = DARK_MODE_ICON_COLOR_SINGLE_TONE;
88 
89         mConfiguration = new Configuration();
90         mTmpLastConfiguration = new Configuration();
91         mConfiguration.updateFrom(context.getResources().getConfiguration());
92 
93         mButtonDispatchers.put(com.android.internal.R.id.input_method_nav_back,
94                 new ButtonDispatcher(com.android.internal.R.id.input_method_nav_back));
95         mButtonDispatchers.put(com.android.internal.R.id.input_method_nav_ime_switcher,
96                 new ButtonDispatcher(com.android.internal.R.id.input_method_nav_ime_switcher));
97         mButtonDispatchers.put(com.android.internal.R.id.input_method_nav_home_handle,
98                 new ButtonDispatcher(com.android.internal.R.id.input_method_nav_home_handle));
99 
100         mDeadZone = new android.inputmethodservice.navigationbar.DeadZone(this);
101 
102         getImeSwitchButton().setOnClickListener(view -> view.getContext()
103                 .getSystemService(InputMethodManager.class).showInputMethodPicker());
104     }
105 
106     @Override
onInterceptTouchEvent(MotionEvent event)107     public boolean onInterceptTouchEvent(MotionEvent event) {
108         return shouldDeadZoneConsumeTouchEvents(event) || super.onInterceptTouchEvent(event);
109     }
110 
111     @Override
onTouchEvent(MotionEvent event)112     public boolean onTouchEvent(MotionEvent event) {
113         shouldDeadZoneConsumeTouchEvents(event);
114         return super.onTouchEvent(event);
115     }
116 
shouldDeadZoneConsumeTouchEvents(MotionEvent event)117     private boolean shouldDeadZoneConsumeTouchEvents(MotionEvent event) {
118         int action = event.getActionMasked();
119         if (action == MotionEvent.ACTION_DOWN) {
120             mDeadZoneConsuming = false;
121         }
122         if (mDeadZone.onTouchEvent(event) || mDeadZoneConsuming) {
123             switch (action) {
124                 case MotionEvent.ACTION_DOWN:
125                     mDeadZoneConsuming = true;
126                     break;
127                 case MotionEvent.ACTION_CANCEL:
128                 case MotionEvent.ACTION_UP:
129                     mDeadZoneConsuming = false;
130                     break;
131             }
132             return true;
133         }
134         return false;
135     }
136 
getCurrentView()137     public View getCurrentView() {
138         return mCurrentView;
139     }
140 
141     /**
142      * Applies {@code consumer} to each of the nav bar views.
143      */
forEachView(Consumer<View> consumer)144     public void forEachView(Consumer<View> consumer) {
145         if (mHorizontal != null) {
146             consumer.accept(mHorizontal);
147         }
148     }
149 
getBackButton()150     public ButtonDispatcher getBackButton() {
151         return mButtonDispatchers.get(com.android.internal.R.id.input_method_nav_back);
152     }
153 
getImeSwitchButton()154     public ButtonDispatcher getImeSwitchButton() {
155         return mButtonDispatchers.get(com.android.internal.R.id.input_method_nav_ime_switcher);
156     }
157 
getHomeHandle()158     public ButtonDispatcher getHomeHandle() {
159         return mButtonDispatchers.get(com.android.internal.R.id.input_method_nav_home_handle);
160     }
161 
getButtonDispatchers()162     public SparseArray<ButtonDispatcher> getButtonDispatchers() {
163         return mButtonDispatchers;
164     }
165 
reloadNavIcons()166     private void reloadNavIcons() {
167         updateIcons(Configuration.EMPTY);
168     }
169 
updateIcons(Configuration oldConfig)170     private void updateIcons(Configuration oldConfig) {
171         final boolean orientationChange = oldConfig.orientation != mConfiguration.orientation;
172         final boolean densityChange = oldConfig.densityDpi != mConfiguration.densityDpi;
173         final boolean dirChange =
174                 oldConfig.getLayoutDirection() != mConfiguration.getLayoutDirection();
175 
176         if (densityChange || dirChange) {
177             mImeSwitcherIcon = getDrawable(com.android.internal.R.drawable.ic_ime_switcher);
178         }
179         if (orientationChange || densityChange || dirChange) {
180             mBackIcon = getBackDrawable();
181         }
182     }
183 
getBackDrawable()184     private KeyButtonDrawable getBackDrawable() {
185         KeyButtonDrawable drawable = getDrawable(com.android.internal.R.drawable.ic_ime_nav_back);
186         orientBackButton(drawable);
187         return drawable;
188     }
189 
190     /**
191      * @return whether this nav bar mode is edge to edge
192      */
isGesturalMode(int mode)193     public static boolean isGesturalMode(int mode) {
194         return mode == NAV_BAR_MODE_GESTURAL;
195     }
196 
orientBackButton(KeyButtonDrawable drawable)197     private void orientBackButton(KeyButtonDrawable drawable) {
198         final boolean useAltBack =
199                 (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
200         final boolean isRtl = mConfiguration.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
201         float degrees = useAltBack ? (isRtl ? 90 : -90) : 0;
202         if (drawable.getRotation() == degrees) {
203             return;
204         }
205 
206         if (isGesturalMode(mNavBarMode)) {
207             drawable.setRotation(degrees);
208             return;
209         }
210 
211         // Animate the back button's rotation to the new degrees and only in portrait move up the
212         // back button to line up with the other buttons
213         float targetY = useAltBack
214                 ? -dpToPx(NAVBAR_BACK_BUTTON_IME_OFFSET, getResources())
215                 : 0;
216         ObjectAnimator navBarAnimator = ObjectAnimator.ofPropertyValuesHolder(drawable,
217                 PropertyValuesHolder.ofFloat(KeyButtonDrawable.KEY_DRAWABLE_ROTATE, degrees),
218                 PropertyValuesHolder.ofFloat(KeyButtonDrawable.KEY_DRAWABLE_TRANSLATE_Y, targetY));
219         navBarAnimator.setInterpolator(FAST_OUT_SLOW_IN);
220         navBarAnimator.setDuration(200);
221         navBarAnimator.start();
222     }
223 
getDrawable(@rawableRes int icon)224     private KeyButtonDrawable getDrawable(@DrawableRes int icon) {
225         return KeyButtonDrawable.create(mLightContext, mLightIconColor, mDarkIconColor, icon,
226                 true /* hasShadow */, null /* ovalBackgroundColor */);
227     }
228 
229     @Override
setLayoutDirection(int layoutDirection)230     public void setLayoutDirection(int layoutDirection) {
231         reloadNavIcons();
232 
233         super.setLayoutDirection(layoutDirection);
234     }
235 
236     /**
237      * Updates the navigation icons based on {@code hints}.
238      *
239      * @param hints bit flags defined in {@link StatusBarManager}.
240      */
setNavigationIconHints(int hints)241     public void setNavigationIconHints(int hints) {
242         if (hints == mNavigationIconHints) return;
243         final boolean newBackAlt = (hints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
244         final boolean oldBackAlt =
245                 (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_BACK_ALT) != 0;
246         if (newBackAlt != oldBackAlt) {
247             //onImeVisibilityChanged(newBackAlt);
248         }
249 
250         if (DEBUG) {
251             android.widget.Toast.makeText(getContext(), "Navigation icon hints = " + hints, 500)
252                     .show();
253         }
254         mNavigationIconHints = hints;
255         updateNavButtonIcons();
256     }
257 
updateNavButtonIcons()258     private void updateNavButtonIcons() {
259         // We have to replace or restore the back and home button icons when exiting or entering
260         // carmode, respectively. Recents are not available in CarMode in nav bar so change
261         // to recent icon is not required.
262         KeyButtonDrawable backIcon = mBackIcon;
263         orientBackButton(backIcon);
264         getBackButton().setImageDrawable(backIcon);
265 
266         getImeSwitchButton().setImageDrawable(mImeSwitcherIcon);
267 
268         // Update IME button visibility, a11y and rotate button always overrides the appearance
269         final boolean imeSwitcherVisible =
270                 (mNavigationIconHints & StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN) != 0;
271         getImeSwitchButton().setVisibility(imeSwitcherVisible ? View.VISIBLE : View.INVISIBLE);
272 
273         getBackButton().setVisibility(View.VISIBLE);
274         getHomeHandle().setVisibility(View.INVISIBLE);
275 
276         // We used to be reporting the touch regions via notifyActiveTouchRegions() here.
277         // TODO(b/215593010): Consider taking care of this in the Launcher side.
278     }
279 
getContextDisplay()280     private Display getContextDisplay() {
281         return getContext().getDisplay();
282     }
283 
284     @Override
onFinishInflate()285     public void onFinishInflate() {
286         super.onFinishInflate();
287         mNavigationInflaterView = findViewById(com.android.internal.R.id.input_method_nav_inflater);
288         mNavigationInflaterView.setButtonDispatchers(mButtonDispatchers);
289 
290         updateOrientationViews();
291         reloadNavIcons();
292     }
293 
294     @Override
onDraw(Canvas canvas)295     protected void onDraw(Canvas canvas) {
296         mDeadZone.onDraw(canvas);
297         super.onDraw(canvas);
298     }
299 
updateOrientationViews()300     private void updateOrientationViews() {
301         mHorizontal = findViewById(com.android.internal.R.id.input_method_nav_horizontal);
302 
303         updateCurrentView();
304     }
305 
updateCurrentView()306     private void updateCurrentView() {
307         resetViews();
308         mCurrentView = mHorizontal;
309         mCurrentView.setVisibility(View.VISIBLE);
310         mCurrentRotation = getContextDisplay().getRotation();
311         mNavigationInflaterView.setAlternativeOrder(mCurrentRotation == Surface.ROTATION_90);
312         mNavigationInflaterView.updateButtonDispatchersCurrentView();
313     }
314 
resetViews()315     private void resetViews() {
316         mHorizontal.setVisibility(View.GONE);
317     }
318 
reorient()319     private void reorient() {
320         updateCurrentView();
321 
322         final android.inputmethodservice.navigationbar.NavigationBarFrame frame =
323                 getRootView().findViewByPredicate(view -> view instanceof NavigationBarFrame);
324         frame.setDeadZone(mDeadZone);
325         mDeadZone.onConfigurationChanged(mCurrentRotation);
326 
327         if (DEBUG) {
328             Log.d(TAG, "reorient(): rot=" + mCurrentRotation);
329         }
330 
331         // Resolve layout direction if not resolved since components changing layout direction such
332         // as changing languages will recreate this view and the direction will be resolved later
333         if (!isLayoutDirectionResolved()) {
334             resolveLayoutDirection();
335         }
336         updateNavButtonIcons();
337     }
338 
339     @Override
onConfigurationChanged(Configuration newConfig)340     protected void onConfigurationChanged(Configuration newConfig) {
341         super.onConfigurationChanged(newConfig);
342         mTmpLastConfiguration.updateFrom(mConfiguration);
343         final int changes = mConfiguration.updateFrom(newConfig);
344 
345         updateIcons(mTmpLastConfiguration);
346         if (mTmpLastConfiguration.densityDpi != mConfiguration.densityDpi
347                 || mTmpLastConfiguration.getLayoutDirection()
348                         != mConfiguration.getLayoutDirection()) {
349             // If car mode or density changes, we need to reset the icons.
350             updateNavButtonIcons();
351         }
352     }
353 
354     @Override
onAttachedToWindow()355     protected void onAttachedToWindow() {
356         super.onAttachedToWindow();
357         // This needs to happen first as it can changed the enabled state which can affect whether
358         // the back button is visible
359         requestApplyInsets();
360         reorient();
361         updateNavButtonIcons();
362     }
363 
364     @Override
onDetachedFromWindow()365     protected void onDetachedFromWindow() {
366         super.onDetachedFromWindow();
367         for (int i = 0; i < mButtonDispatchers.size(); ++i) {
368             mButtonDispatchers.valueAt(i).onDestroy();
369         }
370     }
371 
372     /**
373      * Updates the dark intensity.
374      *
375      * @param intensity The intensity of darkness from {@code 0.0f} to {@code 1.0f}.
376      */
setDarkIntensity(@loatRangefrom = 0.0f, to = 1.0f) float intensity)377     public void setDarkIntensity(@FloatRange(from = 0.0f, to = 1.0f) float intensity) {
378         for (int i = 0; i < mButtonDispatchers.size(); ++i) {
379             mButtonDispatchers.valueAt(i).setDarkIntensity(intensity);
380         }
381     }
382 }
383