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