Verified Commit 622e7a77 authored by Torsten Grote's avatar Torsten Grote Committed by akwizgran
Browse files

[android] Soft keyboard fixes

1. Manually request focus for input fields and show keyboard

This is needed when targetting API 28 which doesn't give focus anymore
automatically like it used to be.

Closes #1505

2. Remember keyboard states across screen rotations

This also upgrades the emoji library and gets rid of the
KeyboardAwareLinearLayout that is still a relict from the time when we
were using Signal's emoji implementation.

3. Move soft keyboard showing/hiding into UiUtils
parent 103e8482
......@@ -117,7 +117,7 @@ dependencies {
implementation 'de.hdodenhof:circleimageview:2.2.0'
implementation 'com.google.zxing:core:3.3.3'
implementation 'uk.co.samuelwall:material-tap-target-prompt:2.14.0'
implementation 'com.vanniktech:emoji-google:0.5.1'
implementation 'com.vanniktech:emoji-google:0.6.0' // later versions already use androidx
implementation 'com.github.kobakei:MaterialFabSpeedDial:1.2.1' // later versions already use androidx
def glideVersion = '4.9.0'
implementation("com.github.bumptech.glide:glide:$glideVersion") {
......
......@@ -89,7 +89,7 @@
<activity
android:name="org.briarproject.briar.android.account.SetupActivity"
android:label="@string/setup_title"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="stateVisible|adjustResize">
</activity>
<activity
......@@ -126,7 +126,7 @@
android:label="@string/app_name"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="stateHidden|adjustResize">
android:windowSoftInputMode="stateUnchanged|adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
......@@ -145,7 +145,7 @@
android:name="org.briarproject.briar.android.privategroup.creation.CreateGroupActivity"
android:label="@string/groups_create_group_title"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="stateVisible|adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
......@@ -156,7 +156,7 @@
android:label="@string/app_name"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="adjustResize|stateHidden">
android:windowSoftInputMode="adjustResize|stateUnchanged">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
......@@ -223,7 +223,7 @@
android:name="org.briarproject.briar.android.forum.CreateForumActivity"
android:label="@string/create_forum_title"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:windowSoftInputMode="adjustResize">
android:windowSoftInputMode="stateVisible|adjustResize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
......@@ -234,7 +234,7 @@
android:label="@string/app_name"
android:parentActivityName="org.briarproject.briar.android.navdrawer.NavDrawerActivity"
android:theme="@style/BriarTheme.NoActionBar"
android:windowSoftInputMode="adjustResize|stateHidden">
android:windowSoftInputMode="adjustResize|stateUnchanged">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.navdrawer.NavDrawerActivity"/>
......@@ -302,7 +302,7 @@
android:name="org.briarproject.briar.android.blog.ReblogActivity"
android:label="@string/blogs_reblog_button"
android:parentActivityName="org.briarproject.briar.android.blog.BlogActivity"
android:windowSoftInputMode="stateHidden">
android:windowSoftInputMode="stateUnchanged">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.briarproject.briar.android.blog.BlogActivity"/>
......
......@@ -2,7 +2,6 @@ package org.briarproject.briar.android.activity;
import android.content.Context;
import android.os.Bundle;
import android.os.IBinder;
import android.support.annotation.LayoutRes;
import android.support.annotation.UiThread;
import android.support.v4.app.Fragment;
......@@ -12,7 +11,6 @@ import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.view.inputmethod.InputMethodManager;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
......@@ -44,7 +42,6 @@ import javax.inject.Inject;
import static android.arch.lifecycle.Lifecycle.State.STARTED;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.view.WindowManager.LayoutParams.FLAG_SECURE;
import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
import static java.util.logging.Level.INFO;
import static java.util.logging.Logger.getLogger;
import static org.briarproject.briar.android.TestingConstants.PREVENT_SCREENSHOTS;
......@@ -218,14 +215,11 @@ public abstract class BaseActivity extends AppCompatActivity
}
public void showSoftKeyboard(View view) {
Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).showSoftInput(view, SHOW_IMPLICIT);
UiUtils.showSoftKeyboard(view);
}
public void hideSoftKeyboard(View view) {
IBinder token = view.getWindowToken();
Object o = getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).hideSoftInputFromWindow(token, 0);
UiUtils.hideSoftKeyboard(view);
}
@UiThread
......
......@@ -89,6 +89,12 @@ public class RssFeedImportActivity extends BriarActivity {
progressBar = findViewById(R.id.progressBar);
}
@Override
public void onStart() {
super.onStart();
if (urlInput.requestFocus()) showSoftKeyboard(urlInput);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return super.onCreateOptionsMenu(menu);
......
......@@ -89,6 +89,7 @@ public class WriteBlogPostActivity extends BriarActivity
public void onStart() {
super.onStart();
notificationManager.blockNotification(groupId);
if (input.requestFocus()) showSoftKeyboard(input);
}
@Override
......
......@@ -21,9 +21,11 @@ import org.briarproject.briar.android.activity.BaseActivity;
import javax.inject.Inject;
import static android.view.WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.api.identity.AuthorConstants.MAX_AUTHOR_NAME_LENGTH;
import static org.briarproject.bramble.util.StringUtils.toUtf8;
import static org.briarproject.briar.android.util.UiUtils.showSoftKeyboard;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
......@@ -91,4 +93,14 @@ public class AliasDialogFragment extends AppCompatDialogFragment {
}
}
@Override
public void onStart() {
super.onStart();
if (aliasEditText.requestFocus()) {
requireNonNull(getDialog().getWindow())
.setSoftInputMode(SOFT_INPUT_STATE_ALWAYS_VISIBLE);
showSoftKeyboard(aliasEditText);
}
}
}
......@@ -276,7 +276,7 @@ public class ConversationActivity extends BriarActivity
textInputView.setSendController(sendController);
textInputView.setMaxTextLength(MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
textInputView.setReady(false);
textInputView.addOnKeyboardShownListener(this::scrollToBottom);
textInputView.setOnKeyboardShownListener(this::scrollToBottom);
}
private void scrollToBottom() {
......
......@@ -94,7 +94,7 @@ public class CreateForumActivity extends BriarActivity {
@Override
public void onStart() {
super.onStart();
showSoftKeyboard(nameEntry);
if (nameEntry.requestFocus()) showSoftKeyboard(nameEntry);
}
@Override
......
......@@ -83,6 +83,10 @@ public class ChangePasswordActivity extends BriarActivity
newPasswordConfirmation.addTextChangedListener(tw);
newPasswordConfirmation.setOnEditorActionListener(this);
changePasswordButton.setOnClickListener(this);
if (state == null && currentPassword.requestFocus()) {
showSoftKeyboard(currentPassword);
}
}
@Override
......
......@@ -94,7 +94,7 @@ public class CreateGroupFragment extends BaseFragment {
@Override
public void onStart() {
super.onStart();
listener.showSoftKeyboard(nameEntry);
if (nameEntry.requestFocus()) listener.showSoftKeyboard(nameEntry);
}
@Override
......
......@@ -27,7 +27,6 @@ import org.briarproject.briar.android.threaded.ThreadListController.ThreadListDa
import org.briarproject.briar.android.threaded.ThreadListController.ThreadListListener;
import org.briarproject.briar.android.util.BriarSnackbarBuilder;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.KeyboardAwareLinearLayout;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextSendController;
import org.briarproject.briar.android.view.TextSendController.SendListener;
......@@ -284,14 +283,10 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
scrollToItemAtTop(item);
} else {
// wait with scrolling until keyboard opened
textInput.addOnKeyboardShownListener(
new KeyboardAwareLinearLayout.OnKeyboardShownListener() {
@Override
public void onKeyboardShown() {
scrollToItemAtTop(item);
textInput.removeOnKeyboardShownListener(this);
}
});
textInput.setOnKeyboardShownListener(() -> {
scrollToItemAtTop(item);
textInput.setOnKeyboardShownListener(null);
});
}
}
......
......@@ -13,8 +13,8 @@ import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.LinearLayout;
import com.vanniktech.emoji.EmojiEditText;
import com.vanniktech.emoji.EmojiPopup;
import com.vanniktech.emoji.RecentEmoji;
......@@ -31,8 +31,9 @@ import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
import static java.lang.Character.isWhitespace;
import static java.util.Objects.requireNonNull;
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong;
import static org.briarproject.briar.android.util.UiUtils.resolveColorAttribute;
public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
public class EmojiTextInputView extends LinearLayout implements
TextWatcher {
@Inject
......@@ -41,12 +42,16 @@ public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
private final AppCompatImageButton emojiToggle;
private final EmojiPopup emojiPopup;
private final EditText editText;
private final InputMethodManager imm;
@Nullable
private TextInputListener listener;
@Nullable
private OnKeyboardShownListener keyboardShownListener;
private int maxLength = Integer.MAX_VALUE;
private boolean emptyTextAllowed = false;
private boolean isEmpty = true;
private boolean keyboardOpen = false;
public EmojiTextInputView(Context context) {
this(context, null);
......@@ -104,17 +109,26 @@ public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
// stuff we can't do in edit mode goes below
if (isInEditMode()) {
emojiPopup = null;
imm = null;
return;
}
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
imm = (InputMethodManager) requireNonNull(o);
BriarApplication app =
(BriarApplication) context.getApplicationContext();
app.getApplicationComponent().inject(this);
emojiPopup = EmojiPopup.Builder
.fromRootView(this)
.fromRootView(getRootView())
.setRecentEmoji(recentEmoji)
.setOnEmojiPopupShownListener(this::showKeyboardIcon)
.setOnEmojiPopupDismissListener(this::showEmojiIcon)
.build((EmojiEditText) editText);
.setKeyboardAnimationStyle(R.style.emoji_fade_animation_style)
.setOnSoftKeyboardOpenListener(this::onKeyboardOpened)
.setOnSoftKeyboardCloseListener(this::onKeyboardClosed)
.setIconColor(resolveColorAttribute(getContext(),
R.attr.colorControlNormal))
.build(editText);
emojiToggle.setOnClickListener(v -> emojiPopup.toggle());
}
......@@ -231,6 +245,10 @@ public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
editText.setHint(hint);
}
boolean isKeyboardOpen() {
return keyboardOpen || imm.isFullscreenMode();
}
private void showEmojiIcon() {
emojiToggle.setImageResource(R.drawable.ic_emoji_toggle);
}
......@@ -240,23 +258,43 @@ public class EmojiTextInputView extends KeyboardAwareLinearLayout implements
}
void showSoftKeyboard() {
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
InputMethodManager imm = (InputMethodManager) requireNonNull(o);
imm.showSoftInput(editText, SHOW_IMPLICIT);
}
void hideSoftKeyboard() {
if (emojiPopup.isShowing()) emojiPopup.dismiss();
IBinder token = editText.getWindowToken();
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
InputMethodManager imm = (InputMethodManager) requireNonNull(o);
imm.hideSoftInputFromWindow(token, 0);
}
private void onKeyboardOpened(
@SuppressWarnings("unused") int keyboardHeight) {
keyboardOpen = true;
if (keyboardShownListener != null)
keyboardShownListener.onKeyboardShown();
}
private void onKeyboardClosed() {
if (imm.isFullscreenMode()) {
onKeyboardOpened(0);
return;
}
keyboardOpen = false;
}
void setOnKeyboardShownListener(
@Nullable OnKeyboardShownListener listener) {
keyboardShownListener = listener;
}
interface TextInputListener {
void onTextIsEmptyChanged(boolean isEmpty);
void onSendEvent();
}
public interface OnKeyboardShownListener {
void onKeyboardShown();
}
}
/*
Taken from Signal, licences under GPLv3
*/
package org.briarproject.briar.android.view;
import android.annotation.TargetApi;
import android.content.Context;
import android.content.SharedPreferences;
import android.graphics.Rect;
import android.os.Build;
import android.preference.PreferenceManager;
import android.support.annotation.UiThread;
import android.util.AttributeSet;
import android.view.View;
import android.view.WindowManager;
import android.widget.LinearLayout;
import org.briarproject.briar.R;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import static android.content.Context.WINDOW_SERVICE;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
import static java.util.Objects.requireNonNull;
import static java.util.logging.Level.INFO;
import static java.util.logging.Level.WARNING;
/**
* RelativeLayout that, when a view container, will report back when it thinks
* a soft keyboard has been opened and what its height would be.
*/
@UiThread
public class KeyboardAwareLinearLayout extends LinearLayout {
private static final Logger LOG =
Logger.getLogger(KeyboardAwareLinearLayout.class.getName());
private final Rect rect = new Rect();
private final Set<OnKeyboardShownListener> shownListeners = new HashSet<>();
private final int minKeyboardSize;
private final int minCustomKeyboardSize;
private final int defaultCustomKeyboardSize;
private final int minCustomKeyboardTopMargin;
private final int statusBarHeight;
private int viewInset;
private boolean keyboardOpen = false;
private int rotation = -1;
public KeyboardAwareLinearLayout(Context context) {
this(context, null);
}
public KeyboardAwareLinearLayout(Context context,
@Nullable AttributeSet attrs) {
this(context, attrs, 0);
}
public KeyboardAwareLinearLayout(Context context,
@Nullable AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
rotation = getDeviceRotation();
int statusBarRes = getResources()
.getIdentifier("status_bar_height", "dimen", "android");
minKeyboardSize =
getResources().getDimensionPixelSize(R.dimen.min_keyboard_size);
minCustomKeyboardSize = getResources()
.getDimensionPixelSize(R.dimen.min_custom_keyboard_size);
defaultCustomKeyboardSize = getResources()
.getDimensionPixelSize(R.dimen.default_custom_keyboard_size);
minCustomKeyboardTopMargin = getResources()
.getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin);
statusBarHeight = statusBarRes > 0 ?
getResources().getDimensionPixelSize(statusBarRes) : 0;
viewInset = getViewInset();
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
updateRotation();
updateKeyboardState();
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
private void updateRotation() {
int oldRotation = rotation;
rotation = getDeviceRotation();
if (oldRotation != rotation) {
LOG.info("Rotation changed");
onKeyboardClose();
}
}
private void updateKeyboardState() {
if (isLandscape()) {
if (keyboardOpen) onKeyboardClose();
return;
}
if (viewInset == 0 && Build.VERSION.SDK_INT >= 21)
viewInset = getViewInset();
int availableHeight =
getRootView().getHeight() - statusBarHeight - viewInset;
getWindowVisibleDisplayFrame(rect);
int keyboardHeight = availableHeight - (rect.bottom - rect.top);
if (keyboardHeight > minKeyboardSize) {
if (getKeyboardHeight() != keyboardHeight)
setKeyboardPortraitHeight(keyboardHeight);
if (!keyboardOpen) onKeyboardOpen(keyboardHeight);
} else if (keyboardOpen) {
onKeyboardClose();
}
}
@TargetApi(21)
private int getViewInset() {
try {
Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
attachInfoField.setAccessible(true);
Object attachInfo = attachInfoField.get(this);
if (attachInfo != null) {
Field stableInsetsField =
attachInfo.getClass().getDeclaredField("mStableInsets");
stableInsetsField.setAccessible(true);
Rect insets = (Rect) stableInsetsField.get(attachInfo);
return insets.bottom;
}
} catch (NoSuchFieldException e) {
LOG.log(WARNING,
"field reflection error when measuring view inset", e);
} catch (IllegalAccessException e) {
LOG.log(WARNING,
"access reflection error when measuring view inset", e);
}
return 0;
}
protected void onKeyboardOpen(int keyboardHeight) {
if (LOG.isLoggable(INFO))
LOG.info("onKeyboardOpen(" + keyboardHeight + ")");
keyboardOpen = true;
notifyShownListeners();
}
protected void onKeyboardClose() {
LOG.info("onKeyboardClose()");
keyboardOpen = false;
}
public boolean isKeyboardOpen() {
return keyboardOpen;
}
public int getKeyboardHeight() {
return isLandscape() ? getKeyboardLandscapeHeight() :
getKeyboardPortraitHeight();
}
public boolean isLandscape() {
int rotation = getDeviceRotation();
return rotation == ROTATION_90 || rotation == ROTATION_270;
}
private int getDeviceRotation() {
WindowManager windowManager =
(WindowManager) getContext().getSystemService(WINDOW_SERVICE);
return requireNonNull(windowManager).getDefaultDisplay().getRotation();
}
private int getKeyboardLandscapeHeight() {
return Math.max(getHeight(), getRootView().getHeight()) / 2;
}
private int getKeyboardPortraitHeight() {
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(getContext());
int keyboardHeight = prefs.getInt("keyboard_height_portrait",
defaultCustomKeyboardSize);
return clamp(keyboardHeight, minCustomKeyboardSize,
getRootView().getHeight() - minCustomKeyboardTopMargin);
}
private int clamp(int value, int min, int max) {
return Math.min(Math.max(value, min), max);
}
private void setKeyboardPortraitHeight(int height) {
SharedPreferences prefs =
PreferenceManager.getDefaultSharedPreferences(getContext());
prefs.edit().putInt("keyboard_height_portrait", height).apply();
}
public void addOnKeyboardShownListener(OnKeyboardShownListener listener) {
shownListeners.add(listener);
}
public void removeOnKeyboardShownListener(
OnKeyboardShownListener listener) {
shownListeners.remove(listener);
}
private void notifyShownListeners() {
// Make a copy as listeners may remove themselves when called
Set<OnKeyboardShownListener> listeners = new HashSet<>(shownListeners);
for (OnKeyboardShownListener listener : listeners) {
listener.onKeyboardShown();
}