SoftwareKeyboardControllerCompat.java
/*
* Copyright 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.core.view;
import static android.os.Build.VERSION.SDK_INT;
import android.content.Context;
import android.view.View;
import android.view.WindowInsets;
import android.view.WindowInsetsController;
import android.view.inputmethod.InputMethodManager;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Provide controls for showing and hiding the IME.
* <p>
* This class provides the implementation for {@link WindowInsetsControllerCompat#show(int)} and
* {@link WindowInsetsControllerCompat#hide(int)} for the {@link WindowInsetsCompat.Type#ime()}.
* <p>
* This class only requires a View as a dependency, whereas {@link WindowInsetsControllerCompat}
* requires a Window for all of its behavior.
*/
public final class SoftwareKeyboardControllerCompat {
private final Impl mImpl;
public SoftwareKeyboardControllerCompat(@NonNull View view) {
if (SDK_INT >= 30) {
mImpl = new Impl30(view);
} else if (SDK_INT >= 20) {
mImpl = new Impl20(view);
} else {
mImpl = new Impl();
}
}
@RequiresApi(30)
@Deprecated
SoftwareKeyboardControllerCompat(@NonNull WindowInsetsController windowInsetsController) {
mImpl = new Impl30(windowInsetsController);
}
/**
* Request that the system show a software keyboard.
* <p>
* This request is best effort. If the system can currently show a software keyboard, it
* will be shown. However, there is no guarantee that the system will be able to show a
* software keyboard. If the system cannot show a software keyboard currently,
* this call will be silently ignored.
*/
public void show() {
mImpl.show();
}
/**
* Hide the software keyboard.
* <p>
* This request is best effort, if the system cannot hide the software keyboard this call
* will silently be ignored.
*/
public void hide() {
mImpl.hide();
}
private static class Impl {
Impl() {
//private
}
void show() {
}
void hide() {
}
}
@RequiresApi(20)
private static class Impl20 extends Impl {
@Nullable
private final View mView;
Impl20(@Nullable View view) {
mView = view;
}
@Override
void show() {
// We'll try to find an available textView to focus to show the IME
View view = mView;
if (view == null) {
return;
}
if (view.isInEditMode() || view.onCheckIsTextEditor()) {
// The IME needs a text view to be focused to be shown
// The view given to retrieve this controller is a textView so we can assume
// that we can focus it in order to show the IME
view.requestFocus();
} else {
view = view.getRootView().findFocus();
}
// Fallback on the container view
if (view == null) {
view = mView.getRootView().findViewById(android.R.id.content);
}
if (view != null && view.hasWindowFocus()) {
final View finalView = view;
finalView.post(() -> {
InputMethodManager imm =
(InputMethodManager) finalView.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
imm.showSoftInput(finalView, 0);
});
}
}
@Override
void hide() {
if (mView != null) {
((InputMethodManager) mView.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE))
.hideSoftInputFromWindow(mView.getWindowToken(),
0);
}
}
}
@RequiresApi(30)
private static class Impl30 extends Impl20 {
@Nullable
private View mView;
@Nullable
private WindowInsetsController mWindowInsetsController;
Impl30(@NonNull View view) {
super(view);
mView = view;
}
Impl30(@Nullable WindowInsetsController windowInsetsController) {
super(null);
mWindowInsetsController = windowInsetsController;
}
@Override
void show() {
if (mView != null && SDK_INT < 33) {
InputMethodManager imm =
(InputMethodManager) mView.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
// This is a strange-looking workaround by making a call and ignoring the result.
// We don't use the return value here, but isActive() has the side-effect of
// calling a hidden method checkFocus(), which ensures that the IME state has the
// correct view in some situations (especially when the focused view changes).
// This is essentially a backport, since an equivalent checkFocus() call was
// added in API 32 to improve behavior and an additional change in API 33:
// https://issuetracker.google.com/issues/189858204
imm.isActive();
}
WindowInsetsController insetsController = null;
if (mWindowInsetsController != null) {
insetsController = mWindowInsetsController;
} else if (mView != null) {
insetsController = mView.getWindowInsetsController();
}
if (insetsController != null) {
insetsController.show(WindowInsets.Type.ime());
} else {
// Couldn't find an insets controller, fallback to old implementation
super.show();
}
}
@Override
void hide() {
WindowInsetsController insetsController = null;
if (mWindowInsetsController != null) {
insetsController = mWindowInsetsController;
} else if (mView != null) {
insetsController = mView.getWindowInsetsController();
}
if (insetsController != null) {
if (SDK_INT <= 33) {
final AtomicBoolean isImeInsetsControllable = new AtomicBoolean(false);
final WindowInsetsController.OnControllableInsetsChangedListener listener =
(windowInsetsController, typeMask) -> isImeInsetsControllable.set(
(typeMask & WindowInsetsCompat.Type.IME) != 0);
// Register the OnControllableInsetsChangedListener would synchronously
// callback current controllable insets. Adding the listener here to check if
// ime inset is controllable.
insetsController.addOnControllableInsetsChangedListener(listener);
if (!isImeInsetsControllable.get()) {
final InputMethodManager imm = (InputMethodManager) mView.getContext()
.getSystemService(Context.INPUT_METHOD_SERVICE);
// This is a backport when the app is in multi-windowing mode, it cannot
// control the ime insets. Use the InputMethodManager instead.
imm.hideSoftInputFromWindow(mView.getWindowToken(), 0);
}
insetsController.removeOnControllableInsetsChangedListener(listener);
}
insetsController.hide(WindowInsets.Type.ime());
} else {
// Couldn't find an insets controller, fallback to old implementation
super.hide();
}
}
}
}