blob: 20a6d136457be3cce3271e777d3b87a7513a3d21 [file] [log] [blame]
package org.robolectric.shadows;
import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
import static android.os.Build.VERSION_CODES.LOLLIPOP_MR1;
import static android.os.Build.VERSION_CODES.M;
import static android.os.Build.VERSION_CODES.N;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;
import static android.os.Build.VERSION_CODES.S;
import static android.os.Build.VERSION_CODES.TIRAMISU;
import static org.robolectric.util.reflector.Reflector.reflector;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Looper;
import android.os.ResultReceiver;
import android.util.SparseArray;
import android.view.View;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InputMethodSubtype;
import com.google.common.base.Optional;
import com.google.common.collect.ImmutableList;
import java.util.List;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
import org.robolectric.util.ReflectionHelpers;
import org.robolectric.util.ReflectionHelpers.ClassParameter;
import org.robolectric.util.reflector.Accessor;
import org.robolectric.util.reflector.Direct;
import org.robolectric.util.reflector.ForType;
import org.robolectric.util.reflector.Static;
@Implements(value = InputMethodManager.class)
public class ShadowInputMethodManager {
/**
* Handler for receiving soft input visibility changed event.
*
* <p>Since Android does not have any API for retrieving soft input status, most application
* relies on GUI layout changes to detect the soft input change event. Currently, Robolectric are
* not able to simulate the GUI change when application changes the soft input through {@code
* InputMethodManager}, this handler can be used by application to simulate GUI change in response
* of the soft input change.
*/
public interface SoftInputVisibilityChangeHandler {
void handleSoftInputVisibilityChange(boolean softInputVisible);
}
/** Handler for receiving PrivateCommands. */
public interface PrivateCommandListener {
void onPrivateCommand(View view, String action, Bundle data);
}
private boolean softInputVisible;
private Optional<SoftInputVisibilityChangeHandler> visibilityChangeHandler = Optional.absent();
private Optional<PrivateCommandListener> privateCommandListener = Optional.absent();
private List<InputMethodInfo> inputMethodInfoList = ImmutableList.of();
private List<InputMethodInfo> enabledInputMethodInfoList = ImmutableList.of();
private Optional<InputMethodSubtype> inputMethodSubtype = Optional.absent();
@Implementation
protected boolean showSoftInput(View view, int flags) {
return showSoftInput(view, flags, null);
}
@Implementation(maxSdk = R)
protected boolean showSoftInput(View view, int flags, ResultReceiver resultReceiver) {
setSoftInputVisibility(true);
return true;
}
@Implementation(minSdk = S, maxSdk = TIRAMISU)
protected boolean showSoftInput(
View view, int flags, ResultReceiver resultReceiver, int ignoredReason) {
return showSoftInput(view, flags, resultReceiver);
}
@Implementation(minSdk = S)
protected boolean hideSoftInputFromWindow(
IBinder windowToken, int flags, ResultReceiver resultReceiver, int ignoredReason) {
return hideSoftInputFromWindow(windowToken, flags, resultReceiver);
}
@Implementation(maxSdk = R)
protected boolean hideSoftInputFromWindow(IBinder windowToken, int flags) {
return hideSoftInputFromWindow(windowToken, flags, null);
}
@Implementation
protected boolean hideSoftInputFromWindow(
IBinder windowToken, int flags, ResultReceiver resultReceiver) {
int resultCode;
if (isSoftInputVisible()) {
setSoftInputVisibility(false);
resultCode = InputMethodManager.RESULT_HIDDEN;
} else {
resultCode = InputMethodManager.RESULT_UNCHANGED_HIDDEN;
}
if (resultReceiver != null) {
resultReceiver.send(resultCode, null);
}
return true;
}
@Implementation
protected void toggleSoftInput(int showFlags, int hideFlags) {
setSoftInputVisibility(!isSoftInputVisible());
}
public boolean isSoftInputVisible() {
return softInputVisible;
}
public void setSoftInputVisibilityHandler(
SoftInputVisibilityChangeHandler visibilityChangeHandler) {
this.visibilityChangeHandler =
Optional.<SoftInputVisibilityChangeHandler>of(visibilityChangeHandler);
}
private void setSoftInputVisibility(boolean visible) {
if (visible == softInputVisible) {
return;
}
softInputVisible = visible;
if (visibilityChangeHandler.isPresent()) {
visibilityChangeHandler.get().handleSoftInputVisibilityChange(softInputVisible);
}
}
/**
* The framework implementation does a blocking call to system server. This will deadlock on
* Robolectric, so just stub out the method.
*/
@Implementation(minSdk = S)
protected void closeCurrentInput() {}
/**
* Returns the list of {@link InputMethodInfo} that are installed.
*
* <p>This method differs from Android implementation by allowing the list to be set using {@link
* #setInputMethodInfoList(List)}.
*/
@Implementation
protected List<InputMethodInfo> getInputMethodList() {
return inputMethodInfoList;
}
/**
* Sets the list of {@link InputMethodInfo} that are marked as installed. See {@link
* #getInputMethodList()}.
*/
public void setInputMethodInfoList(List<InputMethodInfo> inputMethodInfoList) {
this.inputMethodInfoList = inputMethodInfoList;
}
/**
* Returns the {@link InputMethodSubtype} that is installed.
*
* <p>This method differs from Android implementation by allowing the list to be set using {@link
* #setCurrentInputMethodSubtype(InputMethodSubtype)}.
*/
@Implementation
protected InputMethodSubtype getCurrentInputMethodSubtype() {
return inputMethodSubtype.orNull();
}
/**
* Sets the current {@link InputMethodSubtype} that will be returned by {@link
* #getCurrentInputMethodSubtype()}.
*/
public void setCurrentInputMethodSubtype(InputMethodSubtype inputMethodSubtype) {
this.inputMethodSubtype = Optional.of(inputMethodSubtype);
}
/**
* Returns the list of {@link InputMethodInfo} that are enabled.
*
* <p>This method differs from Android implementation by allowing the list to be set using {@link
* #setEnabledInputMethodInfoList(List)}.
*/
@Implementation
protected List<InputMethodInfo> getEnabledInputMethodList() {
return enabledInputMethodInfoList;
}
/**
* Sets the list of {@link InputMethodInfo} that are marked as enabled. See {@link
* #getEnabledInputMethodList()}.
*/
public void setEnabledInputMethodInfoList(List<InputMethodInfo> inputMethodInfoList) {
this.enabledInputMethodInfoList = inputMethodInfoList;
}
@Implementation
protected void restartInput(View view) {}
@Implementation
protected boolean isActive(View view) {
return false;
}
@Implementation
protected boolean isActive() {
return false;
}
@Implementation
protected boolean isFullscreenMode() {
return false;
}
@Implementation(maxSdk = Q)
protected void focusIn(View view) {}
@Implementation(minSdk = M, maxSdk = Q)
protected void onViewDetachedFromWindow(View view) {}
@Implementation
protected void displayCompletions(View view, CompletionInfo[] completions) {}
@Implementation(maxSdk = LOLLIPOP_MR1)
protected static InputMethodManager peekInstance() {
// Android has a bug pre M where peekInstance was dereferenced without a null check:-
// https://github.com/aosp-mirror/platform_frameworks_base/commit/a046faaf38ad818e6b5e981a39fd7394cf7cee03
// So for earlier versions, just call through directly to getInstance()
if (RuntimeEnvironment.getApiLevel() <= JELLY_BEAN_MR1) {
return ReflectionHelpers.callStaticMethod(
InputMethodManager.class,
"getInstance",
ClassParameter.from(Looper.class, Looper.getMainLooper()));
} else if (RuntimeEnvironment.getApiLevel() <= LOLLIPOP_MR1) {
return InputMethodManager.getInstance();
}
return reflector(_InputMethodManager_.class).peekInstance();
}
@Implementation(minSdk = N)
protected boolean startInputInner(
int startInputReason,
IBinder windowGainingFocus,
int startInputFlags,
int softInputMode,
int windowFlags) {
return true;
}
@Implementation(minSdk = M)
protected void sendAppPrivateCommand(View view, String action, Bundle data) {
if (privateCommandListener.isPresent()) {
privateCommandListener.get().onPrivateCommand(view, action, data);
}
}
public void setAppPrivateCommandListener(PrivateCommandListener listener) {
privateCommandListener = Optional.of(listener);
}
@Resetter
public static void reset() {
int apiLevel = RuntimeEnvironment.getApiLevel();
_InputMethodManager_ _reflector = reflector(_InputMethodManager_.class);
if (apiLevel <= JELLY_BEAN_MR1) {
_reflector.setMInstance(null);
} else {
_reflector.setInstance(null);
}
if (apiLevel > P) {
_reflector.getInstanceMap().clear();
}
}
@ForType(InputMethodManager.class)
interface _InputMethodManager_ {
@Static
@Direct
InputMethodManager peekInstance();
@Static
@Accessor("mInstance")
void setMInstance(InputMethodManager instance);
@Static
@Accessor("sInstance")
void setInstance(InputMethodManager instance);
@Static
@Accessor("sInstanceMap")
SparseArray<InputMethodManager> getInstanceMap();
}
}