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; 18 19 import static android.inputmethodservice.InputMethodService.ENABLE_HIDE_IME_CAPTION_BAR; 20 import static android.view.WindowInsets.Type.captionBar; 21 import static android.view.WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS; 22 23 import android.animation.ValueAnimator; 24 import android.annotation.FloatRange; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.app.StatusBarManager; 28 import android.graphics.Color; 29 import android.graphics.Insets; 30 import android.graphics.Rect; 31 import android.graphics.Region; 32 import android.inputmethodservice.navigationbar.NavigationBarFrame; 33 import android.inputmethodservice.navigationbar.NavigationBarView; 34 import android.view.Gravity; 35 import android.view.LayoutInflater; 36 import android.view.View; 37 import android.view.ViewGroup; 38 import android.view.ViewParent; 39 import android.view.ViewTreeObserver; 40 import android.view.Window; 41 import android.view.WindowInsets; 42 import android.view.WindowInsetsController.Appearance; 43 import android.view.animation.Interpolator; 44 import android.view.animation.PathInterpolator; 45 import android.widget.FrameLayout; 46 47 import com.android.internal.inputmethod.InputMethodNavButtonFlags; 48 49 import java.util.Objects; 50 51 /** 52 * This class hides details behind {@link InputMethodService#canImeRenderGesturalNavButtons()} from 53 * {@link InputMethodService}. 54 * 55 * <p>All the package-private methods are no-op when 56 * {@link InputMethodService#canImeRenderGesturalNavButtons()} returns {@code false}.</p> 57 */ 58 final class NavigationBarController { 59 60 private interface Callback { updateTouchableInsets(@onNull InputMethodService.Insets originalInsets, @NonNull ViewTreeObserver.InternalInsetsInfo dest)61 default void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets, 62 @NonNull ViewTreeObserver.InternalInsetsInfo dest) { 63 } 64 onSoftInputWindowCreated(@onNull SoftInputWindow softInputWindow)65 default void onSoftInputWindowCreated(@NonNull SoftInputWindow softInputWindow) { 66 } 67 onViewInitialized()68 default void onViewInitialized() { 69 } 70 onWindowShown()71 default void onWindowShown() { 72 } 73 onDestroy()74 default void onDestroy() { 75 } 76 onNavButtonFlagsChanged(@nputMethodNavButtonFlags int navButtonFlags)77 default void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { 78 } 79 toDebugString()80 default String toDebugString() { 81 return "No-op implementation"; 82 } 83 84 Callback NOOP = new Callback() { 85 }; 86 } 87 88 private final Callback mImpl; 89 NavigationBarController(@onNull InputMethodService inputMethodService)90 NavigationBarController(@NonNull InputMethodService inputMethodService) { 91 mImpl = InputMethodService.canImeRenderGesturalNavButtons() 92 ? new Impl(inputMethodService) : Callback.NOOP; 93 } 94 updateTouchableInsets(@onNull InputMethodService.Insets originalInsets, @NonNull ViewTreeObserver.InternalInsetsInfo dest)95 void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets, 96 @NonNull ViewTreeObserver.InternalInsetsInfo dest) { 97 mImpl.updateTouchableInsets(originalInsets, dest); 98 } 99 onSoftInputWindowCreated(@onNull SoftInputWindow softInputWindow)100 void onSoftInputWindowCreated(@NonNull SoftInputWindow softInputWindow) { 101 mImpl.onSoftInputWindowCreated(softInputWindow); 102 } 103 onViewInitialized()104 void onViewInitialized() { 105 mImpl.onViewInitialized(); 106 } 107 onWindowShown()108 void onWindowShown() { 109 mImpl.onWindowShown(); 110 } 111 onDestroy()112 void onDestroy() { 113 mImpl.onDestroy(); 114 } 115 onNavButtonFlagsChanged(@nputMethodNavButtonFlags int navButtonFlags)116 void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { 117 mImpl.onNavButtonFlagsChanged(navButtonFlags); 118 } 119 toDebugString()120 String toDebugString() { 121 return mImpl.toDebugString(); 122 } 123 124 private static final class Impl implements Callback, Window.DecorCallback { 125 private static final int DEFAULT_COLOR_ADAPT_TRANSITION_TIME = 1700; 126 127 // Copied from com.android.systemui.animation.Interpolators#LEGACY_DECELERATE 128 private static final Interpolator LEGACY_DECELERATE = 129 new PathInterpolator(0f, 0f, 0.2f, 1f); 130 131 @NonNull 132 private final InputMethodService mService; 133 134 private boolean mDestroyed = false; 135 136 private boolean mImeDrawsImeNavBar; 137 138 @Nullable 139 private NavigationBarFrame mNavigationBarFrame; 140 @Nullable 141 Insets mLastInsets; 142 143 private boolean mShouldShowImeSwitcherWhenImeIsShown; 144 145 @Appearance 146 private int mAppearance; 147 148 @FloatRange(from = 0.0f, to = 1.0f) 149 private float mDarkIntensity; 150 151 @Nullable 152 private ValueAnimator mTintAnimator; 153 154 private boolean mDrawLegacyNavigationBarBackground; 155 156 private final Rect mTempRect = new Rect(); 157 private final int[] mTempPos = new int[2]; 158 Impl(@onNull InputMethodService inputMethodService)159 Impl(@NonNull InputMethodService inputMethodService) { 160 mService = inputMethodService; 161 } 162 163 @Nullable getSystemInsets()164 private Insets getSystemInsets() { 165 if (mService.mWindow == null) { 166 return null; 167 } 168 final View decorView = mService.mWindow.getWindow().getDecorView(); 169 if (decorView == null) { 170 return null; 171 } 172 final WindowInsets windowInsets = decorView.getRootWindowInsets(); 173 if (windowInsets == null) { 174 return null; 175 } 176 final Insets stableBarInsets = 177 windowInsets.getInsetsIgnoringVisibility(WindowInsets.Type.systemBars()); 178 return Insets.min(windowInsets.getInsets(WindowInsets.Type.systemBars() 179 | WindowInsets.Type.displayCutout()), stableBarInsets); 180 } 181 installNavigationBarFrameIfNecessary()182 private void installNavigationBarFrameIfNecessary() { 183 if (!mImeDrawsImeNavBar) { 184 return; 185 } 186 if (mNavigationBarFrame != null) { 187 return; 188 } 189 final View rawDecorView = mService.mWindow.getWindow().getDecorView(); 190 if (!(rawDecorView instanceof ViewGroup)) { 191 return; 192 } 193 final ViewGroup decorView = (ViewGroup) rawDecorView; 194 mNavigationBarFrame = decorView.findViewByPredicate( 195 NavigationBarFrame.class::isInstance); 196 final Insets systemInsets = getSystemInsets(); 197 if (mNavigationBarFrame == null) { 198 mNavigationBarFrame = new NavigationBarFrame(mService); 199 LayoutInflater.from(mService).inflate( 200 com.android.internal.R.layout.input_method_navigation_bar, 201 mNavigationBarFrame); 202 if (systemInsets != null) { 203 decorView.addView(mNavigationBarFrame, new FrameLayout.LayoutParams( 204 ViewGroup.LayoutParams.MATCH_PARENT, 205 systemInsets.bottom, Gravity.BOTTOM)); 206 mLastInsets = systemInsets; 207 } else { 208 decorView.addView(mNavigationBarFrame); 209 } 210 final NavigationBarView navigationBarView = mNavigationBarFrame.findViewByPredicate( 211 NavigationBarView.class::isInstance); 212 if (navigationBarView != null) { 213 // TODO(b/213337792): Support InputMethodService#setBackDisposition(). 214 // TODO(b/213337792): Set NAVIGATION_HINT_IME_SHOWN only when necessary. 215 final int hints = StatusBarManager.NAVIGATION_HINT_BACK_ALT 216 | (mShouldShowImeSwitcherWhenImeIsShown 217 ? StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN 218 : 0); 219 navigationBarView.setNavigationIconHints(hints); 220 } 221 } else { 222 mNavigationBarFrame.setLayoutParams(new FrameLayout.LayoutParams( 223 ViewGroup.LayoutParams.MATCH_PARENT, systemInsets.bottom, Gravity.BOTTOM)); 224 mLastInsets = systemInsets; 225 } 226 227 if (mDrawLegacyNavigationBarBackground) { 228 mNavigationBarFrame.setBackgroundColor(Color.BLACK); 229 } else { 230 mNavigationBarFrame.setBackground(null); 231 } 232 233 setIconTintInternal(calculateTargetDarkIntensity(mAppearance, 234 mDrawLegacyNavigationBarBackground)); 235 236 if (ENABLE_HIDE_IME_CAPTION_BAR) { 237 mNavigationBarFrame.setOnApplyWindowInsetsListener((view, insets) -> { 238 if (mNavigationBarFrame != null) { 239 boolean visible = insets.isVisible(captionBar()); 240 mNavigationBarFrame.setVisibility(visible ? View.VISIBLE : View.GONE); 241 } 242 return view.onApplyWindowInsets(insets); 243 }); 244 } 245 } 246 uninstallNavigationBarFrameIfNecessary()247 private void uninstallNavigationBarFrameIfNecessary() { 248 if (mNavigationBarFrame == null) { 249 return; 250 } 251 final ViewParent parent = mNavigationBarFrame.getParent(); 252 if (parent instanceof ViewGroup) { 253 ((ViewGroup) parent).removeView(mNavigationBarFrame); 254 } 255 if (ENABLE_HIDE_IME_CAPTION_BAR) { 256 mNavigationBarFrame.setOnApplyWindowInsetsListener(null); 257 } 258 mNavigationBarFrame = null; 259 } 260 261 @Override updateTouchableInsets(@onNull InputMethodService.Insets originalInsets, @NonNull ViewTreeObserver.InternalInsetsInfo dest)262 public void updateTouchableInsets(@NonNull InputMethodService.Insets originalInsets, 263 @NonNull ViewTreeObserver.InternalInsetsInfo dest) { 264 if (!mImeDrawsImeNavBar || mNavigationBarFrame == null) { 265 return; 266 } 267 268 final Insets systemInsets = getSystemInsets(); 269 if (systemInsets != null) { 270 final Window window = mService.mWindow.getWindow(); 271 final View decor = window.getDecorView(); 272 273 // If the extract view is shown, everything is touchable, so no need to update 274 // touchable insets, but we still update normal insets below. 275 if (!mService.isExtractViewShown()) { 276 Region touchableRegion = null; 277 final View inputFrame = mService.mInputFrame; 278 switch (originalInsets.touchableInsets) { 279 case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_FRAME: 280 if (inputFrame.getVisibility() == View.VISIBLE) { 281 inputFrame.getLocationInWindow(mTempPos); 282 mTempRect.set(mTempPos[0], mTempPos[1], 283 mTempPos[0] + inputFrame.getWidth(), 284 mTempPos[1] + inputFrame.getHeight()); 285 touchableRegion = new Region(mTempRect); 286 } 287 break; 288 case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_CONTENT: 289 if (inputFrame.getVisibility() == View.VISIBLE) { 290 inputFrame.getLocationInWindow(mTempPos); 291 mTempRect.set(mTempPos[0], originalInsets.contentTopInsets, 292 mTempPos[0] + inputFrame.getWidth(), 293 mTempPos[1] + inputFrame.getHeight()); 294 touchableRegion = new Region(mTempRect); 295 } 296 break; 297 case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_VISIBLE: 298 if (inputFrame.getVisibility() == View.VISIBLE) { 299 inputFrame.getLocationInWindow(mTempPos); 300 mTempRect.set(mTempPos[0], originalInsets.visibleTopInsets, 301 mTempPos[0] + inputFrame.getWidth(), 302 mTempPos[1] + inputFrame.getHeight()); 303 touchableRegion = new Region(mTempRect); 304 } 305 break; 306 case ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION: 307 touchableRegion = new Region(); 308 touchableRegion.set(originalInsets.touchableRegion); 309 break; 310 } 311 // Hereafter "mTempRect" means a navigation bar rect. 312 mTempRect.set(decor.getLeft(), decor.getBottom() - systemInsets.bottom, 313 decor.getRight(), decor.getBottom()); 314 if (touchableRegion == null) { 315 touchableRegion = new Region(mTempRect); 316 } else { 317 touchableRegion.union(mTempRect); 318 } 319 320 dest.touchableRegion.set(touchableRegion); 321 dest.setTouchableInsets( 322 ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 323 } 324 325 // TODO(b/215443343): See if we can use View#OnLayoutChangeListener(). 326 // TODO(b/215443343): See if we can replace DecorView#mNavigationColorViewState.view 327 boolean zOrderChanged = false; 328 if (decor instanceof ViewGroup) { 329 ViewGroup decorGroup = (ViewGroup) decor; 330 final View navbarBackgroundView = window.getNavigationBarBackgroundView(); 331 zOrderChanged = navbarBackgroundView != null 332 && decorGroup.indexOfChild(navbarBackgroundView) 333 > decorGroup.indexOfChild(mNavigationBarFrame); 334 } 335 final boolean insetChanged = !Objects.equals(systemInsets, mLastInsets); 336 if (zOrderChanged || insetChanged) { 337 scheduleRelayout(); 338 } 339 } 340 } 341 scheduleRelayout()342 private void scheduleRelayout() { 343 // Capture the current frame object in case the object is replaced or cleared later. 344 final NavigationBarFrame frame = mNavigationBarFrame; 345 frame.post(() -> { 346 if (mDestroyed) { 347 return; 348 } 349 if (!frame.isAttachedToWindow()) { 350 return; 351 } 352 final Window window = mService.mWindow.getWindow(); 353 if (window == null) { 354 return; 355 } 356 final View decor = window.peekDecorView(); 357 if (decor == null) { 358 return; 359 } 360 if (!(decor instanceof ViewGroup)) { 361 return; 362 } 363 final ViewGroup decorGroup = (ViewGroup) decor; 364 final Insets currentSystemInsets = getSystemInsets(); 365 if (!Objects.equals(currentSystemInsets, mLastInsets)) { 366 frame.setLayoutParams(new FrameLayout.LayoutParams( 367 ViewGroup.LayoutParams.MATCH_PARENT, 368 currentSystemInsets.bottom, Gravity.BOTTOM)); 369 mLastInsets = currentSystemInsets; 370 } 371 final View navbarBackgroundView = 372 window.getNavigationBarBackgroundView(); 373 if (navbarBackgroundView != null 374 && decorGroup.indexOfChild(navbarBackgroundView) 375 > decorGroup.indexOfChild(frame)) { 376 decorGroup.bringChildToFront(frame); 377 } 378 }); 379 } 380 381 @Override onSoftInputWindowCreated(@onNull SoftInputWindow softInputWindow)382 public void onSoftInputWindowCreated(@NonNull SoftInputWindow softInputWindow) { 383 final Window window = softInputWindow.getWindow(); 384 mAppearance = window.getSystemBarAppearance(); 385 window.setDecorCallback(this); 386 } 387 388 @Override onViewInitialized()389 public void onViewInitialized() { 390 if (mDestroyed) { 391 return; 392 } 393 installNavigationBarFrameIfNecessary(); 394 } 395 396 @Override onDestroy()397 public void onDestroy() { 398 if (mDestroyed) { 399 return; 400 } 401 if (mTintAnimator != null) { 402 mTintAnimator.cancel(); 403 mTintAnimator = null; 404 } 405 mDestroyed = true; 406 } 407 408 @Override onWindowShown()409 public void onWindowShown() { 410 if (mDestroyed || !mImeDrawsImeNavBar || mNavigationBarFrame == null) { 411 return; 412 } 413 final Insets systemInsets = getSystemInsets(); 414 if (systemInsets != null) { 415 if (!Objects.equals(systemInsets, mLastInsets)) { 416 mNavigationBarFrame.setLayoutParams(new NavigationBarFrame.LayoutParams( 417 ViewGroup.LayoutParams.MATCH_PARENT, 418 systemInsets.bottom, Gravity.BOTTOM)); 419 mLastInsets = systemInsets; 420 } 421 final Window window = mService.mWindow.getWindow(); 422 View rawDecorView = window.getDecorView(); 423 if (rawDecorView instanceof ViewGroup) { 424 final ViewGroup decor = (ViewGroup) rawDecorView; 425 final View navbarBackgroundView = window.getNavigationBarBackgroundView(); 426 if (navbarBackgroundView != null 427 && decor.indexOfChild(navbarBackgroundView) 428 > decor.indexOfChild(mNavigationBarFrame)) { 429 decor.bringChildToFront(mNavigationBarFrame); 430 } 431 } 432 if (!ENABLE_HIDE_IME_CAPTION_BAR) { 433 mNavigationBarFrame.setVisibility(View.VISIBLE); 434 } 435 } 436 } 437 438 @Override onNavButtonFlagsChanged(@nputMethodNavButtonFlags int navButtonFlags)439 public void onNavButtonFlagsChanged(@InputMethodNavButtonFlags int navButtonFlags) { 440 if (mDestroyed) { 441 return; 442 } 443 444 final boolean imeDrawsImeNavBar = 445 (navButtonFlags & InputMethodNavButtonFlags.IME_DRAWS_IME_NAV_BAR) != 0; 446 final boolean shouldShowImeSwitcherWhenImeIsShown = 447 (navButtonFlags & InputMethodNavButtonFlags.SHOW_IME_SWITCHER_WHEN_IME_IS_SHOWN) 448 != 0; 449 450 mImeDrawsImeNavBar = imeDrawsImeNavBar; 451 final boolean prevShouldShowImeSwitcherWhenImeIsShown = 452 mShouldShowImeSwitcherWhenImeIsShown; 453 mShouldShowImeSwitcherWhenImeIsShown = shouldShowImeSwitcherWhenImeIsShown; 454 455 if (ENABLE_HIDE_IME_CAPTION_BAR) { 456 mService.mWindow.getWindow().getDecorView().getWindowInsetsController() 457 .setImeCaptionBarInsetsHeight(getImeCaptionBarHeight()); 458 } 459 460 if (imeDrawsImeNavBar) { 461 installNavigationBarFrameIfNecessary(); 462 if (mNavigationBarFrame == null) { 463 return; 464 } 465 if (mShouldShowImeSwitcherWhenImeIsShown 466 == prevShouldShowImeSwitcherWhenImeIsShown) { 467 return; 468 } 469 final NavigationBarView navigationBarView = mNavigationBarFrame.findViewByPredicate( 470 NavigationBarView.class::isInstance); 471 if (navigationBarView == null) { 472 return; 473 } 474 final int hints = StatusBarManager.NAVIGATION_HINT_BACK_ALT 475 | (shouldShowImeSwitcherWhenImeIsShown 476 ? StatusBarManager.NAVIGATION_HINT_IME_SWITCHER_SHOWN : 0); 477 navigationBarView.setNavigationIconHints(hints); 478 } else { 479 uninstallNavigationBarFrameIfNecessary(); 480 } 481 } 482 483 @Override onSystemBarAppearanceChanged(@ppearance int appearance)484 public void onSystemBarAppearanceChanged(@Appearance int appearance) { 485 if (mDestroyed) { 486 return; 487 } 488 489 mAppearance = appearance; 490 491 if (mNavigationBarFrame == null) { 492 return; 493 } 494 495 final float targetDarkIntensity = calculateTargetDarkIntensity(mAppearance, 496 mDrawLegacyNavigationBarBackground); 497 498 if (mTintAnimator != null) { 499 mTintAnimator.cancel(); 500 } 501 mTintAnimator = ValueAnimator.ofFloat(mDarkIntensity, targetDarkIntensity); 502 mTintAnimator.addUpdateListener( 503 animation -> setIconTintInternal((Float) animation.getAnimatedValue())); 504 mTintAnimator.setDuration(DEFAULT_COLOR_ADAPT_TRANSITION_TIME); 505 mTintAnimator.setStartDelay(0); 506 mTintAnimator.setInterpolator(LEGACY_DECELERATE); 507 mTintAnimator.start(); 508 } 509 setIconTintInternal(float darkIntensity)510 private void setIconTintInternal(float darkIntensity) { 511 mDarkIntensity = darkIntensity; 512 if (mNavigationBarFrame == null) { 513 return; 514 } 515 final NavigationBarView navigationBarView = 516 mNavigationBarFrame.findViewByPredicate(NavigationBarView.class::isInstance); 517 if (navigationBarView == null) { 518 return; 519 } 520 navigationBarView.setDarkIntensity(darkIntensity); 521 } 522 523 @FloatRange(from = 0.0f, to = 1.0f) calculateTargetDarkIntensity(@ppearance int appearance, boolean drawLegacyNavigationBarBackground)524 private static float calculateTargetDarkIntensity(@Appearance int appearance, 525 boolean drawLegacyNavigationBarBackground) { 526 final boolean lightNavBar = !drawLegacyNavigationBarBackground 527 && (appearance & APPEARANCE_LIGHT_NAVIGATION_BARS) != 0; 528 return lightNavBar ? 1.0f : 0.0f; 529 } 530 531 @Override onDrawLegacyNavigationBarBackgroundChanged( boolean drawLegacyNavigationBarBackground)532 public boolean onDrawLegacyNavigationBarBackgroundChanged( 533 boolean drawLegacyNavigationBarBackground) { 534 if (mDestroyed) { 535 return false; 536 } 537 538 if (drawLegacyNavigationBarBackground != mDrawLegacyNavigationBarBackground) { 539 mDrawLegacyNavigationBarBackground = drawLegacyNavigationBarBackground; 540 if (mNavigationBarFrame != null) { 541 if (mDrawLegacyNavigationBarBackground) { 542 mNavigationBarFrame.setBackgroundColor(Color.BLACK); 543 } else { 544 mNavigationBarFrame.setBackground(null); 545 } 546 scheduleRelayout(); 547 } 548 onSystemBarAppearanceChanged(mAppearance); 549 } 550 return drawLegacyNavigationBarBackground; 551 } 552 553 /** 554 * Returns the height of the IME caption bar if this should be shown, or {@code 0} instead. 555 */ getImeCaptionBarHeight()556 private int getImeCaptionBarHeight() { 557 return mImeDrawsImeNavBar 558 ? mService.getResources().getDimensionPixelSize( 559 com.android.internal.R.dimen.navigation_bar_frame_height) 560 : 0; 561 } 562 563 @Override toDebugString()564 public String toDebugString() { 565 return "{mImeDrawsImeNavBar=" + mImeDrawsImeNavBar 566 + " mNavigationBarFrame=" + mNavigationBarFrame 567 + " mShouldShowImeSwitcherWhenImeIsShown=" 568 + mShouldShowImeSwitcherWhenImeIsShown 569 + " mAppearance=0x" + Integer.toHexString(mAppearance) 570 + " mDarkIntensity=" + mDarkIntensity 571 + " mDrawLegacyNavigationBarBackground=" + mDrawLegacyNavigationBarBackground 572 + "}"; 573 } 574 } 575 } 576