diff --git a/briar-android/build.gradle b/briar-android/build.gradle index c52aced57..e0463f369 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -105,6 +105,7 @@ dependencies { implementation "com.android.support:cardview-v7:$supportVersion" implementation "com.android.support:support-annotations:$supportVersion" implementation "com.android.support:exifinterface:$supportVersion" + implementation "com.android.support:palette-v7:$supportVersion" implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation "android.arch.lifecycle:extensions:1.1.1" diff --git a/briar-android/src/main/java/org/briarproject/briar/android/TestingConstants.java b/briar-android/src/main/java/org/briarproject/briar/android/TestingConstants.java index e3b970dc9..9363db287 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/TestingConstants.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/TestingConstants.java @@ -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; + } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java b/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java index 5d087d88b..0fc18c60b 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/activity/RequestCodes.java @@ -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; } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java index a21557d16..bb3dda396 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/ReblogFragment.java @@ -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 imageUris) { ui.input.hideSoftKeyboard(); - String comment = getComment(); - feedController.repeatPost(item, comment, + feedController.repeatPost(item, text, new UiExceptionHandler(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); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java index d077f97f8..e54764d78 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/blog/WriteBlogPostActivity.java @@ -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 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); } diff --git a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java index 20a359573..aa183ef74 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/conversation/ConversationActivity.java @@ -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 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); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java index ea7858e48..96be4eae5 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/introduction/IntroductionMessageFragment.java @@ -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 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); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java b/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java index c4fee6429..bcdfdedd5 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/sharing/BaseMessageFragment.java @@ -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 imageUris) { + if (text == null) return; if (utf8IsTooLong(text, listener.getMaximumTextLength())) { Snackbar.make(message, R.string.text_too_long, LENGTH_SHORT).show(); return; diff --git a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java index f99598fc5..d9534edb6 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/threaded/ThreadListActivity.java @@ -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 imageUris) { + if (text == null || text.trim().length() == 0) return; if (utf8IsTooLong(text, getMaxTextLength())) { displaySnackbar(R.string.text_too_long); diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/TextInputAttachmentController.java b/briar-android/src/main/java/org/briarproject/briar/android/view/TextInputAttachmentController.java new file mode 100644 index 000000000..b711c456d --- /dev/null +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/TextInputAttachmentController.java @@ -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 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() { + @Override + public boolean onLoadFailed(@Nullable GlideException e, + Object model, Target target, + boolean isFirstResource) { + return false; + } + + @Override + public boolean onResourceReady(Bitmap resource, + Object model, Target 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 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); + } + +} diff --git a/briar-android/src/main/java/org/briarproject/briar/android/view/TextInputView.java b/briar-android/src/main/java/org/briarproject/briar/android/view/TextInputView.java index 638db9af9..a935b8e9c 100644 --- a/briar-android/src/main/java/org/briarproject/briar/android/view/TextInputView.java +++ b/briar-android/src/main/java/org/briarproject/briar/android/view/TextInputView.java @@ -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 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 imageUris); } } diff --git a/briar-android/src/main/res/drawable/ic_image.xml b/briar-android/src/main/res/drawable/ic_image.xml new file mode 100644 index 000000000..46efff4c9 --- /dev/null +++ b/briar-android/src/main/res/drawable/ic_image.xml @@ -0,0 +1,9 @@ + + + diff --git a/briar-android/src/main/res/layout/text_input_view.xml b/briar-android/src/main/res/layout/text_input_view.xml index d1730c469..1ceb3a43b 100644 --- a/briar-android/src/main/res/layout/text_input_view.xml +++ b/briar-android/src/main/res/layout/text_input_view.xml @@ -5,12 +5,43 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:animateLayoutChanges="true" tools:showIn="@layout/activity_conversation"> + + + + + + + + - + android:layout_gravity="bottom"> + + + + + + diff --git a/briar-android/src/main/res/layout/text_input_view_large.xml b/briar-android/src/main/res/layout/text_input_view_large.xml index fcc944a7c..e3abf0f82 100644 --- a/briar-android/src/main/res/layout/text_input_view_large.xml +++ b/briar-android/src/main/res/layout/text_input_view_large.xml @@ -64,6 +64,7 @@ android:layout_marginEnd="@dimen/margin_small" android:layout_marginLeft="@dimen/margin_small" android:layout_marginRight="@dimen/margin_small" - android:layout_marginStart="@dimen/margin_small"/> + android:layout_marginStart="@dimen/margin_small" + tools:text="@string/send"/> diff --git a/briar-android/src/main/res/values/dimens.xml b/briar-android/src/main/res/values/dimens.xml index 81f7d4377..608470e78 100644 --- a/briar-android/src/main/res/values/dimens.xml +++ b/briar-android/src/main/res/values/dimens.xml @@ -70,6 +70,7 @@ 42dp + 150dp 16sp 32sp 2dp diff --git a/briar-android/src/main/res/values/strings.xml b/briar-android/src/main/res/values/strings.xml index 2f87d2a26..e093fc0fb 100644 --- a/briar-android/src/main/res/values/strings.xml +++ b/briar-android/src/main/res/values/strings.xml @@ -127,6 +127,8 @@ No messages. No messages to show Type message + Add a caption (optional) + Attach image Change contact name Contact name Change diff --git a/briar-android/witness.gradle b/briar-android/witness.gradle index ee7273494..eaf6c6271 100644 --- a/briar-android/witness.gradle +++ b/briar-android/witness.gradle @@ -40,6 +40,7 @@ dependencyVerification { 'com.android.support:interpolator:28.0.0:interpolator-28.0.0.aar:7bc7ee86a0db39a4b51956f3e89842d2bd962118d57d779eb6ed6b34ba0677ea', 'com.android.support:loader:28.0.0:loader-28.0.0.aar:920b85efd72dc33e915b0f88a883fe73b88483c6df8751a741e17611f2460341', 'com.android.support:localbroadcastmanager:28.0.0:localbroadcastmanager-28.0.0.aar:d287c823af5fdde72c099fcfc5f630efe9687af7a914343ae6fd92de32c8a806', + 'com.android.support:palette-v7:28.0.0:palette-v7-28.0.0.aar:317202dddb953d152d0677dbd8bb3b9d8ef8dcd0bdee0da4f40c98826e4960e6', 'com.android.support:preference-v14:28.0.0:preference-v14-28.0.0.aar:8133c6e19233fa51e036a341e6d3f4adeead3375cebf777efced0fe154c3267e', 'com.android.support:preference-v7:28.0.0:preference-v7-28.0.0.aar:75eabe936d1fc3b178450a554c4d433466036f2be6d6dccdf971eac9590fdbf5', 'com.android.support:print:28.0.0:print-28.0.0.aar:4be8a812d73e4a80e35b91ceae127def3f0bb9726bf3bc439aa0cc81503f5728',