[android] support adding image attachments to private messages

This commit is contained in:
Torsten Grote
2018-11-20 19:30:33 -02:00
parent 52ec56d690
commit 800dfed5c1
17 changed files with 438 additions and 66 deletions

View File

@@ -30,4 +30,10 @@ public interface TestingConstants {
long EXPIRY_DATE = IS_DEBUG_BUILD || IS_BETA_BUILD ?
BuildConfig.BuildTimestamp + 90 * 24 * 60 * 60 * 1000L :
Long.MAX_VALUE;
/**
* Feature flag for enabling image attachments.
*/
boolean FEATURE_FLAG_IMAGE_ATTACHMENTS = IS_DEBUG_BUILD;
}

View File

@@ -14,5 +14,6 @@ public interface RequestCodes {
int REQUEST_BLUETOOTH_DISCOVERABLE = 10;
int REQUEST_UNLOCK = 11;
int REQUEST_KEYGUARD_UNLOCK = 12;
int REQUEST_ATTACH_IMAGE = 13;
}

View File

@@ -1,5 +1,6 @@
package org.briarproject.briar.android.blog;
import android.net.Uri;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
@@ -20,6 +21,8 @@ import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
import java.util.List;
import javax.annotation.Nullable;
import javax.inject.Inject;
@@ -118,10 +121,9 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
}
@Override
public void onSendClick(String text) {
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
ui.input.hideSoftKeyboard();
String comment = getComment();
feedController.repeatPost(item, comment,
feedController.repeatPost(item, text,
new UiExceptionHandler<DbException>(this) {
@Override
public void onExceptionUi(DbException exception) {
@@ -131,12 +133,6 @@ public class ReblogFragment extends BaseFragment implements TextInputListener {
finish();
}
@Nullable
private String getComment() {
if (ui.input.getText().length() == 0) return null;
return ui.input.getText().toString();
}
private void showProgressBar() {
ui.progressBar.setVisibility(VISIBLE);
ui.input.setVisibility(GONE);

View File

@@ -1,7 +1,9 @@
package org.briarproject.briar.android.blog;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.KeyEvent;
@@ -14,8 +16,9 @@ import org.briarproject.bramble.api.FormatException;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.api.identity.IdentityManager;
import org.briarproject.bramble.api.identity.LocalAuthor;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.bramble.api.sync.GroupId;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
@@ -27,6 +30,7 @@ import org.briarproject.briar.api.blog.BlogPost;
import org.briarproject.briar.api.blog.BlogPostFactory;
import java.security.GeneralSecurityException;
import java.util.List;
import java.util.logging.Logger;
import javax.inject.Inject;
@@ -35,8 +39,12 @@ import static android.view.View.GONE;
import static android.view.View.VISIBLE;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.bramble.util.StringUtils.truncateUtf8;
import static org.briarproject.briar.api.blog.BlogConstants.MAX_BLOG_POST_TEXT_LENGTH;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class WriteBlogPostActivity extends BriarActivity
implements OnEditorActionListener, TextInputListener {
@@ -58,9 +66,8 @@ public class WriteBlogPostActivity extends BriarActivity
@Inject
volatile BlogManager blogManager;
@SuppressWarnings("ConstantConditions")
@Override
public void onCreate(Bundle state) {
public void onCreate(@Nullable Bundle state) {
super.onCreate(state);
Intent i = getIntent();
@@ -128,17 +135,19 @@ public class WriteBlogPostActivity extends BriarActivity
}
private void enableOrDisablePublishButton() {
input.setSendButtonEnabled(input.getText().length() > 0);
input.setSendButtonEnabled(!input.isEmpty());
}
@Override
public void onSendClick(String text) {
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
if (isNullOrEmpty(text)) return;
// hide publish button, show progress bar
input.hideSoftKeyboard();
input.setVisibility(GONE);
progressBar.setVisibility(VISIBLE);
text = StringUtils.truncateUtf8(text, MAX_BLOG_POST_TEXT_LENGTH);
text = truncateUtf8(text, MAX_BLOG_POST_TEXT_LENGTH);
storePost(text);
}

View File

@@ -6,6 +6,7 @@ import android.arch.lifecycle.ViewModelProvider;
import android.arch.lifecycle.ViewModelProviders;
import android.content.DialogInterface;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
@@ -52,7 +53,6 @@ import org.briarproject.bramble.api.sync.Message;
import org.briarproject.bramble.api.sync.MessageId;
import org.briarproject.bramble.api.sync.event.MessagesAckedEvent;
import org.briarproject.bramble.api.sync.event.MessagesSentEvent;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.activity.BriarActivity;
@@ -64,6 +64,7 @@ import org.briarproject.briar.android.introduction.IntroductionActivity;
import org.briarproject.briar.android.privategroup.conversation.GroupActivity;
import org.briarproject.briar.android.view.BriarRecyclerView;
import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextInputView.AttachImageListener;
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
import org.briarproject.briar.api.android.AndroidNotificationManager;
import org.briarproject.briar.api.blog.BlogSharingManager;
@@ -106,6 +107,7 @@ import static android.support.v4.app.ActivityOptionsCompat.makeSceneTransitionAn
import static android.support.v4.view.ViewCompat.setTransitionName;
import static android.support.v7.util.SortedList.INVALID_POSITION;
import static android.view.Gravity.RIGHT;
import static android.widget.Toast.LENGTH_LONG;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.Collections.emptyList;
import static java.util.Collections.sort;
@@ -115,6 +117,10 @@ import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logDuration;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.LogUtils.now;
import static org.briarproject.bramble.util.StringUtils.isNullOrEmpty;
import static org.briarproject.bramble.util.StringUtils.truncateUtf8;
import static org.briarproject.briar.android.TestingConstants.FEATURE_FLAG_IMAGE_ATTACHMENTS;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_ATTACH_IMAGE;
import static org.briarproject.briar.android.activity.RequestCodes.REQUEST_INTRODUCTION;
import static org.briarproject.briar.android.conversation.ImageActivity.ATTACHMENT;
import static org.briarproject.briar.android.conversation.ImageActivity.DATE;
@@ -131,7 +137,7 @@ import static uk.co.samuelwall.materialtaptargetprompt.MaterialTapTargetPrompt.S
@ParametersNotNullByDefault
public class ConversationActivity extends BriarActivity
implements EventListener, ConversationListener, TextInputListener,
TextCache, AttachmentCache {
TextCache, AttachmentCache, AttachImageListener {
public static final String CONTACT_ID = "briar.CONTACT_ID";
@@ -251,6 +257,9 @@ public class ConversationActivity extends BriarActivity
textInputView = findViewById(R.id.text_input_container);
textInputView.setListener(this);
if (FEATURE_FLAG_IMAGE_ATTACHMENTS) {
textInputView.setAttachImageListener(this);
}
}
@Override
@@ -267,6 +276,8 @@ public class ConversationActivity extends BriarActivity
Snackbar.LENGTH_SHORT);
snackbar.getView().setBackgroundResource(R.color.briar_primary);
snackbar.show();
} else if (request == REQUEST_ATTACH_IMAGE && result == RESULT_OK) {
textInputView.onImageReceived(data);
}
}
@@ -573,9 +584,19 @@ public class ConversationActivity extends BriarActivity
}
@Override
public void onSendClick(String text) {
if (text.isEmpty()) return;
text = StringUtils.truncateUtf8(text, MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
public void onAttachImage(Intent intent) {
startActivityForResult(intent, REQUEST_ATTACH_IMAGE);
}
@Override
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
if (!imageUris.isEmpty()) {
Toast.makeText(this, "Not yet implemented.", LENGTH_LONG).show();
textInputView.setText("");
return;
}
if (isNullOrEmpty(text)) return;
text = truncateUtf8(text, MAX_PRIVATE_MESSAGE_TEXT_LENGTH);
long timestamp = System.currentTimeMillis();
timestamp = Math.max(timestamp, getMinTimestampForNewMessage());
if (messagingGroupId == null) loadGroupId(text, timestamp);

View File

@@ -1,8 +1,8 @@
package org.briarproject.briar.android.introduction;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.app.ActionBar;
import android.view.LayoutInflater;
@@ -17,7 +17,8 @@ import org.briarproject.bramble.api.contact.Contact;
import org.briarproject.bramble.api.contact.ContactId;
import org.briarproject.bramble.api.contact.ContactManager;
import org.briarproject.bramble.api.db.DbException;
import org.briarproject.bramble.util.StringUtils;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.activity.ActivityComponent;
import org.briarproject.briar.android.fragment.BaseFragment;
@@ -25,6 +26,7 @@ import org.briarproject.briar.android.view.TextInputView;
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
import org.briarproject.briar.api.introduction.IntroductionManager;
import java.util.List;
import java.util.logging.Logger;
import javax.inject.Inject;
@@ -38,9 +40,12 @@ import static android.view.View.VISIBLE;
import static android.widget.Toast.LENGTH_SHORT;
import static java.util.logging.Level.WARNING;
import static org.briarproject.bramble.util.LogUtils.logException;
import static org.briarproject.bramble.util.StringUtils.truncateUtf8;
import static org.briarproject.briar.android.util.UiUtils.getContactDisplayName;
import static org.briarproject.briar.api.introduction.IntroductionConstants.MAX_INTRODUCTION_TEXT_LENGTH;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public class IntroductionMessageFragment extends BaseFragment
implements TextInputListener {
@@ -84,8 +89,9 @@ public class IntroductionMessageFragment extends BaseFragment
}
@Override
public View onCreateView(@NonNull LayoutInflater inflater,
ViewGroup container, Bundle savedInstanceState) {
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// change toolbar text
ActionBar actionBar = introductionActivity.getSupportActionBar();
@@ -184,14 +190,14 @@ public class IntroductionMessageFragment extends BaseFragment
}
@Override
public void onSendClick(@NonNull String text) {
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
// disable button to prevent accidental double invitations
ui.message.setSendButtonEnabled(false);
String txt = ui.message.getText().toString();
if (txt.isEmpty()) txt = null;
else txt = StringUtils.truncateUtf8(txt, MAX_INTRODUCTION_TEXT_LENGTH);
makeIntroduction(contact1, contact2, txt);
if (text != null) {
text = truncateUtf8(text, MAX_INTRODUCTION_TEXT_LENGTH);
}
makeIntroduction(contact1, contact2, text);
// don't wait for the introduction to be made before finishing activity
introductionActivity.hideSoftKeyboard(ui.message);

View File

@@ -1,7 +1,9 @@
package org.briarproject.briar.android.sharing;
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.UiThread;
import android.support.design.widget.Snackbar;
@@ -10,17 +12,23 @@ import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import org.briarproject.bramble.api.nullsafety.MethodsNotNullByDefault;
import org.briarproject.bramble.api.nullsafety.NotNullByDefault;
import org.briarproject.bramble.api.nullsafety.ParametersNotNullByDefault;
import org.briarproject.briar.R;
import org.briarproject.briar.android.fragment.BaseFragment;
import org.briarproject.briar.android.view.LargeTextInputView;
import org.briarproject.briar.android.view.TextInputView.TextInputListener;
import java.util.List;
import static android.support.design.widget.Snackbar.LENGTH_SHORT;
import static org.briarproject.bramble.util.StringUtils.truncateUtf8;
import static org.briarproject.bramble.util.StringUtils.utf8IsTooLong;
import static org.briarproject.briar.api.sharing.SharingConstants.MAX_INVITATION_TEXT_LENGTH;
@MethodsNotNullByDefault
@ParametersNotNullByDefault
public abstract class BaseMessageFragment extends BaseFragment
implements TextInputListener {
@@ -34,8 +42,9 @@ public abstract class BaseMessageFragment extends BaseFragment
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
public View onCreateView(@Nullable LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
// inflate view
View v = inflater.inflate(R.layout.fragment_message, container,
@@ -76,7 +85,8 @@ public abstract class BaseMessageFragment extends BaseFragment
}
@Override
public void onSendClick(String text) {
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
if (text == null) return;
if (utf8IsTooLong(text, listener.getMaximumTextLength())) {
Snackbar.make(message, R.string.text_too_long, LENGTH_SHORT).show();
return;

View File

@@ -1,6 +1,7 @@
package org.briarproject.briar.android.threaded;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.CallSuper;
import android.support.annotation.StringRes;
@@ -33,6 +34,7 @@ import org.briarproject.briar.api.client.NamedGroup;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;
import javax.annotation.Nullable;
@@ -348,8 +350,8 @@ public abstract class ThreadListActivity<G extends NamedGroup, I extends ThreadI
}
@Override
public void onSendClick(String text) {
if (text.trim().length() == 0)
public void onSendClick(@Nullable String text, List<Uri> imageUris) {
if (text == null || text.trim().length() == 0)
return;
if (utf8IsTooLong(text, getMaxTextLength())) {
displaySnackbar(R.string.text_too_long);

View File

@@ -0,0 +1,211 @@
package org.briarproject.briar.android.view;
import android.content.ClipData;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.support.annotation.Nullable;
import android.support.annotation.UiThread;
import android.support.design.widget.FloatingActionButton;
import android.support.v7.graphics.Palette;
import android.support.v7.widget.AppCompatImageButton;
import android.text.Editable;
import android.text.TextWatcher;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.engine.GlideException;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.target.Target;
import com.vanniktech.emoji.EmojiEditText;
import org.briarproject.briar.R;
import org.briarproject.briar.android.conversation.glide.GlideApp;
import org.briarproject.briar.android.view.TextInputView.AttachImageListener;
import java.util.ArrayList;
import java.util.List;
import static android.content.Intent.ACTION_GET_CONTENT;
import static android.content.Intent.ACTION_OPEN_DOCUMENT;
import static android.content.Intent.CATEGORY_OPENABLE;
import static android.content.Intent.EXTRA_ALLOW_MULTIPLE;
import static android.graphics.Color.BLACK;
import static android.graphics.Color.WHITE;
import static android.os.Build.VERSION.SDK_INT;
import static android.support.v7.app.AppCompatDelegate.MODE_NIGHT_YES;
import static android.support.v7.app.AppCompatDelegate.getDefaultNightMode;
import static android.view.View.GONE;
import static android.view.View.INVISIBLE;
import static android.view.View.VISIBLE;
import static com.bumptech.glide.load.engine.DiskCacheStrategy.NONE;
import static com.bumptech.glide.load.resource.bitmap.DownsampleStrategy.FIT_CENTER;
import static java.util.Collections.emptyList;
import static java.util.Collections.singletonList;
class TextInputAttachmentController implements TextWatcher {
private final EmojiEditText editText;
private final View sendButton;
private final AppCompatImageButton imageButton;
private final ViewGroup imageLayout;
private final ImageView imageView;
private final AttachImageListener listener;
private String textHint;
private List<Uri> imageUris = emptyList();
public TextInputAttachmentController(View v, EmojiEditText editText,
View sendButton, AttachImageListener listener) {
imageLayout = v.findViewById(R.id.imageLayout);
imageView = v.findViewById(R.id.imageView);
FloatingActionButton imageCancelButton =
v.findViewById(R.id.imageCancelButton);
imageButton = v.findViewById(R.id.imageButton);
this.listener = listener;
this.sendButton = sendButton;
this.editText = editText;
this.textHint = editText.getHint().toString();
editText.addTextChangedListener(this);
imageButton.setOnClickListener(view -> onImageButtonClicked());
imageCancelButton.setOnClickListener(view -> afterSendButtonClicked());
showImageButton(true);
}
private void onImageButtonClicked() {
Intent intent = new Intent(SDK_INT >= 19 ?
ACTION_OPEN_DOCUMENT : ACTION_GET_CONTENT);
intent.addCategory(CATEGORY_OPENABLE);
intent.setType("image/*");
if (SDK_INT >= 18)
intent.putExtra(EXTRA_ALLOW_MULTIPLE, false);
listener.onAttachImage(intent);
}
void onImageReceived(@Nullable Intent resultData) {
if (resultData == null) return;
if (resultData.getData() != null) {
imageUris = singletonList(resultData.getData());
} else if (SDK_INT >= 18 && resultData.getClipData() != null) {
ClipData clipData = resultData.getClipData();
imageUris = new ArrayList<>(clipData.getItemCount());
for (int i = 0; i < clipData.getItemCount(); i++) {
imageUris.add(clipData.getItemAt(i).getUri());
}
} else {
return;
}
showImageButton(false);
editText.setHint(R.string.image_caption_hint);
imageLayout.setVisibility(VISIBLE);
GlideApp.with(imageView)
.asBitmap()
.load(imageUris.get(0)) // TODO show more than the first
.diskCacheStrategy(NONE)
.downsample(FIT_CENTER)
.addListener(new RequestListener<Bitmap>() {
@Override
public boolean onLoadFailed(@Nullable GlideException e,
Object model, Target<Bitmap> target,
boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(Bitmap resource,
Object model, Target<Bitmap> target,
DataSource dataSource, boolean isFirstResource) {
Palette.from(resource).generate(
TextInputAttachmentController.this::onPaletteGenerated);
return false;
}
})
.into(imageView);
}
@UiThread
private void onPaletteGenerated(@Nullable Palette palette) {
int color;
if (palette == null) {
color = getDefaultNightMode() == MODE_NIGHT_YES ? BLACK : WHITE;
} else {
color = getDefaultNightMode() == MODE_NIGHT_YES ?
palette.getDarkMutedColor(BLACK) :
palette.getLightMutedColor(WHITE);
}
imageView.setBackgroundColor(color);
}
private void showImageButton(boolean showImageButton) {
if (showImageButton) {
imageButton.setVisibility(VISIBLE);
if (SDK_INT <= 15) {
sendButton.setVisibility(INVISIBLE);
} else {
sendButton.clearAnimation();
sendButton.animate().alpha(0f).withEndAction(
() -> sendButton.setVisibility(INVISIBLE)
).start();
imageButton.clearAnimation();
imageButton.animate().alpha(1f).start();
}
} else {
sendButton.setVisibility(VISIBLE);
if (SDK_INT <= 15) {
imageButton.setVisibility(INVISIBLE);
} else {
sendButton.clearAnimation();
sendButton.animate().alpha(1f).start();
imageButton.clearAnimation();
imageButton.animate().alpha(0f).withEndAction(
() -> imageButton.setVisibility(INVISIBLE)
).start();
}
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,
int after) {
// noop
}
@Override
public void onTextChanged(CharSequence s, int start, int before,
int count) {
if (start != 0 || !imageUris.isEmpty()) return;
if (s.length() > 0) showImageButton(false);
else if (s.length() == 0) showImageButton(true);
}
@Override
public void afterTextChanged(Editable s) {
// noop
}
public List<Uri> getUris() {
return imageUris;
}
public void saveHint(String hint) {
textHint = hint;
}
void afterSendButtonClicked() {
// restore hint
editText.setHint(textHint);
// hide image layout
imageLayout.setVisibility(GONE);
// reset image URIs
imageUris = emptyList();
// show the image button again, so images can get attached
showImageButton(true);
}
}

View File

@@ -2,10 +2,13 @@ package org.briarproject.briar.android.view;
import android.animation.LayoutTransition;
import android.content.Context;
import android.content.Intent;
import android.content.res.TypedArray;
import android.graphics.Rect;
import android.net.Uri;
import android.os.IBinder;
import android.support.annotation.CallSuper;
import android.support.annotation.Nullable;
import android.support.annotation.StringRes;
import android.support.annotation.UiThread;
import android.support.v7.widget.AppCompatImageButton;
@@ -26,13 +29,16 @@ import org.briarproject.briar.R;
import org.briarproject.briar.android.BriarApplication;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout;
import javax.annotation.Nullable;
import java.util.List;
import javax.inject.Inject;
import static android.content.Context.INPUT_METHOD_SERVICE;
import static android.content.Context.LAYOUT_INFLATER_SERVICE;
import static android.view.KeyEvent.KEYCODE_ENTER;
import static android.view.inputmethod.InputMethodManager.SHOW_IMPLICIT;
import static java.util.Collections.emptyList;
import static java.util.Objects.requireNonNull;
@UiThread
@MethodsNotNullByDefault
@@ -44,6 +50,8 @@ public class TextInputView extends KeyboardAwareLinearLayout {
@Nullable
TextInputListener listener;
@Nullable
TextInputAttachmentController attachmentController;
AppCompatImageButton emojiToggle;
EmojiEditText editText;
@@ -69,12 +77,12 @@ public class TextInputView extends KeyboardAwareLinearLayout {
setOrientation(VERTICAL);
setLayoutTransition(new LayoutTransition());
inflateLayout(context);
setUpViews(context, attrs);
if (!isInEditMode()) setUpViews(context, attrs);
}
protected void inflateLayout(Context context) {
LayoutInflater inflater = (LayoutInflater) context
.getSystemService(LAYOUT_INFLATER_SERVICE);
LayoutInflater inflater = (LayoutInflater) requireNonNull(
context.getSystemService(LAYOUT_INFLATER_SERVICE));
inflater.inflate(R.layout.text_input_view, this, true);
}
@@ -96,18 +104,33 @@ public class TextInputView extends KeyboardAwareLinearLayout {
String hint = attributes.getString(R.styleable.TextInputView_hint);
attributes.recycle();
if (hint != null) editText.setHint(hint);
if (hint != null) setHint(hint);
emojiToggle.setOnClickListener(v -> emojiPopup.toggle());
editText.setOnClickListener(v -> showSoftKeyboard());
editText.setOnKeyListener((v, keyCode, event) -> {
if (keyCode == KEYCODE_ENTER && event.isCtrlPressed()) {
trySendMessage();
onSendButtonClicked();
return true;
}
return false;
});
sendButton.setOnClickListener(v -> trySendMessage());
sendButton.setOnClickListener(v -> onSendButtonClicked());
}
public void setListener(TextInputListener listener) {
this.listener = listener;
}
/**
* Call this during onCreate() to enable image attachment support.
* Do not call it twice!
*/
public void setAttachImageListener(AttachImageListener imageListener) {
if (attachmentController != null) throw new IllegalStateException();
attachmentController = new TextInputAttachmentController(getRootView(),
editText, sendButton, imageListener
);
}
private void showEmojiIcon() {
@@ -118,10 +141,23 @@ public class TextInputView extends KeyboardAwareLinearLayout {
emojiToggle.setImageResource(R.drawable.ic_keyboard);
}
private void trySendMessage() {
private void onSendButtonClicked() {
if (listener != null) {
listener.onSendClick(editText.getText().toString());
Editable editable = editText.getText();
String text = editable == null || editable.length() == 0 ?
null : editable.toString();
List<Uri> imageUris = attachmentController == null ? emptyList() :
attachmentController.getUris();
listener.onSendClick(text, imageUris);
}
if (attachmentController != null) {
attachmentController.afterSendButtonClicked();
}
}
public void onImageReceived(@Nullable Intent resultData) {
if (attachmentController == null) throw new IllegalStateException();
attachmentController.onImageReceived(resultData);
}
@Override
@@ -139,12 +175,17 @@ public class TextInputView extends KeyboardAwareLinearLayout {
editText.setText(text);
}
public Editable getText() {
return editText.getText();
public boolean isEmpty() {
return editText.getText() == null || editText.getText().length() == 0;
}
public void setHint(@StringRes int res) {
editText.setHint(res);
setHint(getContext().getString(res));
}
public void setHint(String hint) {
if (attachmentController != null) attachmentController.saveHint(hint);
editText.setHint(hint);
}
public void setSendButtonEnabled(boolean enabled) {
@@ -155,24 +196,26 @@ public class TextInputView extends KeyboardAwareLinearLayout {
editText.addTextChangedListener(watcher);
}
public void setListener(TextInputListener listener) {
this.listener = listener;
}
public void showSoftKeyboard() {
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).showSoftInput(editText, SHOW_IMPLICIT);
InputMethodManager imm = (InputMethodManager) requireNonNull(o);
imm.showSoftInput(editText, SHOW_IMPLICIT);
}
public void hideSoftKeyboard() {
if (emojiPopup.isShowing()) emojiPopup.dismiss();
IBinder token = editText.getWindowToken();
Object o = getContext().getSystemService(INPUT_METHOD_SERVICE);
((InputMethodManager) o).hideSoftInputFromWindow(token, 0);
InputMethodManager imm = (InputMethodManager) requireNonNull(o);
imm.hideSoftInputFromWindow(token, 0);
}
public interface AttachImageListener {
void onAttachImage(Intent intent);
}
public interface TextInputListener {
void onSendClick(String text);
void onSendClick(@Nullable String text, List<Uri> imageUris);
}
}