From 109d67956fe1d77362d8c416b6d8788f1712a4d0 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Wed, 29 Jan 2020 22:13:44 -0500 Subject: [PATCH] Implement new attachment keyboard. Such beauty. Such grace. --- .../components/AttachmentTypeSelector.java | 292 ------------------ .../components/OutlinedThumbnailView.java | 22 +- .../securesms/components/ThumbnailView.java | 8 + .../conversation/AttachmentKeyboard.java | 127 ++++++++ .../AttachmentKeyboardButton.java | 31 ++ .../AttachmentKeyboardButtonAdapter.java | 89 ++++++ .../AttachmentKeyboardMediaAdapter.java | 129 ++++++++ .../conversation/ConversationActivity.java | 126 ++++---- .../conversation/ConversationFragment.java | 1 + .../conversation/ConversationViewModel.java | 44 +++ .../mediapreview/MediaPreviewViewModel.java | 1 + .../securesms/mediasend/Media.java | 19 +- .../securesms/mediasend/MediaRepository.java | 33 +- .../mediasend/MediaSendActivity.java | 1 + .../mediasend/MediaSendViewModel.java | 4 +- ...chment_keyboard_button_background_dark.xml | 19 ++ ...hment_keyboard_button_background_light.xml | 19 ++ ...chment_keyboard_button_background_dark.xml | 5 + ...hment_keyboard_button_background_light.xml | 5 + .../res/drawable/ic_camera_outline_32.xml | 12 + .../drawable/ic_contact_circle_outline_32.xml | 9 + .../main/res/drawable/ic_file_outline_32.xml | 9 + .../main/res/drawable/ic_gif_outline_32.xml | 9 + .../res/drawable/ic_location_outline_32.xml | 9 + .../drawable/ic_photo_album_outline_32.xml | 9 + .../res/drawable/transparent_black_pill.xml | 5 + .../layout/attachment_keyboad_media_item.xml | 49 +++ .../main/res/layout/attachment_keyboard.xml | 68 ++++ .../attachment_keyboard_button_item.xml | 38 +++ .../main/res/layout/conversation_activity.xml | 7 + ...tion_activity_attachment_keyboard_stub.xml | 7 + app/src/main/res/values/attrs.xml | 15 + app/src/main/res/values/colors.xml | 1 + app/src/main/res/values/strings.xml | 9 + app/src/main/res/values/themes.xml | 6 + 35 files changed, 866 insertions(+), 371 deletions(-) delete mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonAdapter.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardMediaAdapter.java create mode 100644 app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java create mode 100644 app/src/main/res/drawable-v21/attachment_keyboard_button_background_dark.xml create mode 100644 app/src/main/res/drawable-v21/attachment_keyboard_button_background_light.xml create mode 100644 app/src/main/res/drawable/attachment_keyboard_button_background_dark.xml create mode 100644 app/src/main/res/drawable/attachment_keyboard_button_background_light.xml create mode 100644 app/src/main/res/drawable/ic_camera_outline_32.xml create mode 100644 app/src/main/res/drawable/ic_contact_circle_outline_32.xml create mode 100644 app/src/main/res/drawable/ic_file_outline_32.xml create mode 100644 app/src/main/res/drawable/ic_gif_outline_32.xml create mode 100644 app/src/main/res/drawable/ic_location_outline_32.xml create mode 100644 app/src/main/res/drawable/ic_photo_album_outline_32.xml create mode 100644 app/src/main/res/drawable/transparent_black_pill.xml create mode 100644 app/src/main/res/layout/attachment_keyboad_media_item.xml create mode 100644 app/src/main/res/layout/attachment_keyboard.xml create mode 100644 app/src/main/res/layout/attachment_keyboard_button_item.xml create mode 100644 app/src/main/res/layout/conversation_activity_attachment_keyboard_stub.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java b/app/src/main/java/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java deleted file mode 100644 index cc75059fb2..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/AttachmentTypeSelector.java +++ /dev/null @@ -1,292 +0,0 @@ -package org.thoughtcrime.securesms.components; - -import android.Manifest; -import android.animation.Animator; -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.Context; -import android.graphics.drawable.BitmapDrawable; -import android.net.Uri; -import android.os.Build; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.loader.app.LoaderManager; -import android.util.Pair; -import android.view.Gravity; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewAnimationUtils; -import android.view.ViewTreeObserver; -import android.view.animation.Animation; -import android.view.animation.AnimationSet; -import android.view.animation.OvershootInterpolator; -import android.view.animation.ScaleAnimation; -import android.view.animation.TranslateAnimation; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.PopupWindow; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.permissions.Permissions; -import org.thoughtcrime.securesms.util.ViewUtil; - -public class AttachmentTypeSelector extends PopupWindow { - - public static final int ADD_GALLERY = 1; - public static final int ADD_DOCUMENT = 2; - public static final int ADD_SOUND = 3; - public static final int ADD_CONTACT_INFO = 4; - public static final int TAKE_PHOTO = 5; - public static final int ADD_LOCATION = 6; - public static final int ADD_GIF = 7; - - private static final int ANIMATION_DURATION = 300; - - @SuppressWarnings("unused") - private static final String TAG = AttachmentTypeSelector.class.getSimpleName(); - - private final @NonNull LoaderManager loaderManager; - private final @NonNull RecentPhotoViewRail recentRail; - private final @NonNull ImageView imageButton; - private final @NonNull ImageView audioButton; - private final @NonNull ImageView documentButton; - private final @NonNull ImageView contactButton; - private final @NonNull ImageView cameraButton; - private final @NonNull ImageView locationButton; - private final @NonNull ImageView gifButton; - private final @NonNull ImageView closeButton; - - private @Nullable View currentAnchor; - private @Nullable AttachmentClickedListener listener; - - public AttachmentTypeSelector(@NonNull Context context, @NonNull LoaderManager loaderManager, @Nullable AttachmentClickedListener listener) { - super(context); - - LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); - LinearLayout layout = (LinearLayout) inflater.inflate(R.layout.attachment_type_selector, null, true); - - this.listener = listener; - this.loaderManager = loaderManager; - this.recentRail = ViewUtil.findById(layout, R.id.recent_photos); - this.imageButton = ViewUtil.findById(layout, R.id.gallery_button); - this.audioButton = ViewUtil.findById(layout, R.id.audio_button); - this.documentButton = ViewUtil.findById(layout, R.id.document_button); - this.contactButton = ViewUtil.findById(layout, R.id.contact_button); - this.cameraButton = ViewUtil.findById(layout, R.id.camera_button); - this.locationButton = ViewUtil.findById(layout, R.id.location_button); - this.gifButton = ViewUtil.findById(layout, R.id.giphy_button); - this.closeButton = ViewUtil.findById(layout, R.id.close_button); - - this.imageButton.setOnClickListener(new PropagatingClickListener(ADD_GALLERY)); - this.audioButton.setOnClickListener(new PropagatingClickListener(ADD_SOUND)); - this.documentButton.setOnClickListener(new PropagatingClickListener(ADD_DOCUMENT)); - this.contactButton.setOnClickListener(new PropagatingClickListener(ADD_CONTACT_INFO)); - this.cameraButton.setOnClickListener(new PropagatingClickListener(TAKE_PHOTO)); - this.locationButton.setOnClickListener(new PropagatingClickListener(ADD_LOCATION)); - this.gifButton.setOnClickListener(new PropagatingClickListener(ADD_GIF)); - this.closeButton.setOnClickListener(new CloseClickListener()); - this.recentRail.setListener(new RecentPhotoSelectedListener()); - - setContentView(layout); - setWidth(LinearLayout.LayoutParams.MATCH_PARENT); - setHeight(LinearLayout.LayoutParams.WRAP_CONTENT); - setBackgroundDrawable(new BitmapDrawable()); - setAnimationStyle(0); - setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); - setFocusable(true); - setTouchable(true); - - loaderManager.initLoader(1, null, recentRail); - } - - public void show(@NonNull Activity activity, final @NonNull View anchor) { - if (Permissions.hasAll(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { - recentRail.setVisibility(View.VISIBLE); - loaderManager.restartLoader(1, null, recentRail); - } else { - recentRail.setVisibility(View.GONE); - } - - this.currentAnchor = anchor; - - showAtLocation(anchor, Gravity.BOTTOM, 0, 0); - - getContentView().getViewTreeObserver().addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { - @Override - public void onGlobalLayout() { - getContentView().getViewTreeObserver().removeGlobalOnLayoutListener(this); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - animateWindowInCircular(anchor, getContentView()); - } else { - animateWindowInTranslate(getContentView()); - } - } - }); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - animateButtonIn(imageButton, ANIMATION_DURATION / 2); - animateButtonIn(cameraButton, ANIMATION_DURATION / 2); - - animateButtonIn(audioButton, ANIMATION_DURATION / 3); - animateButtonIn(locationButton, ANIMATION_DURATION / 3); - animateButtonIn(documentButton, ANIMATION_DURATION / 4); - animateButtonIn(gifButton, ANIMATION_DURATION / 4); - animateButtonIn(contactButton, 0); - animateButtonIn(closeButton, 0); - } - } - - @Override - public void dismiss() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - animateWindowOutCircular(currentAnchor, getContentView()); - } else { - animateWindowOutTranslate(getContentView()); - } - } - - public void setListener(@Nullable AttachmentClickedListener listener) { - this.listener = listener; - } - - private void animateButtonIn(View button, int delay) { - AnimationSet animation = new AnimationSet(true); - Animation scale = new ScaleAnimation(0.0f, 1.0f, 0.0f, 1.0f, - Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.0f); - - animation.addAnimation(scale); - animation.setInterpolator(new OvershootInterpolator(1)); - animation.setDuration(ANIMATION_DURATION); - animation.setStartOffset(delay); - button.startAnimation(animation); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private void animateWindowInCircular(@Nullable View anchor, @NonNull View contentView) { - Pair coordinates = getClickOrigin(anchor, contentView); - Animator animator = ViewAnimationUtils.createCircularReveal(contentView, - coordinates.first, - coordinates.second, - 0, - Math.max(contentView.getWidth(), contentView.getHeight())); - animator.setDuration(ANIMATION_DURATION); - animator.start(); - } - - private void animateWindowInTranslate(@NonNull View contentView) { - Animation animation = new TranslateAnimation(0, 0, contentView.getHeight(), 0); - animation.setDuration(ANIMATION_DURATION); - - getContentView().startAnimation(animation); - } - - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - private void animateWindowOutCircular(@Nullable View anchor, @NonNull View contentView) { - Pair coordinates = getClickOrigin(anchor, contentView); - Animator animator = ViewAnimationUtils.createCircularReveal(getContentView(), - coordinates.first, - coordinates.second, - Math.max(getContentView().getWidth(), getContentView().getHeight()), - 0); - - animator.setDuration(ANIMATION_DURATION); - animator.addListener(new Animator.AnimatorListener() { - @Override - public void onAnimationStart(Animator animation) { - } - - @Override - public void onAnimationEnd(Animator animation) { - AttachmentTypeSelector.super.dismiss(); - } - - @Override - public void onAnimationCancel(Animator animation) { - } - - @Override - public void onAnimationRepeat(Animator animation) { - } - }); - - animator.start(); - } - - private void animateWindowOutTranslate(@NonNull View contentView) { - Animation animation = new TranslateAnimation(0, 0, 0, contentView.getTop() + contentView.getHeight()); - animation.setDuration(ANIMATION_DURATION); - animation.setAnimationListener(new Animation.AnimationListener() { - @Override - public void onAnimationStart(Animation animation) { - } - - @Override - public void onAnimationEnd(Animation animation) { - AttachmentTypeSelector.super.dismiss(); - } - - @Override - public void onAnimationRepeat(Animation animation) { - } - }); - - getContentView().startAnimation(animation); - } - - private Pair getClickOrigin(@Nullable View anchor, @NonNull View contentView) { - if (anchor == null) return new Pair<>(0, 0); - - final int[] anchorCoordinates = new int[2]; - anchor.getLocationOnScreen(anchorCoordinates); - anchorCoordinates[0] += anchor.getWidth() / 2; - anchorCoordinates[1] += anchor.getHeight() / 2; - - final int[] contentCoordinates = new int[2]; - contentView.getLocationOnScreen(contentCoordinates); - - int x = anchorCoordinates[0] - contentCoordinates[0]; - int y = anchorCoordinates[1] - contentCoordinates[1]; - - return new Pair<>(x, y); - } - - private class RecentPhotoSelectedListener implements RecentPhotoViewRail.OnItemClickedListener { - @Override - public void onItemClicked(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size) { - animateWindowOutTranslate(getContentView()); - - if (listener != null) listener.onQuickAttachment(uri, mimeType, bucketId, dateTaken, width, height, size); - } - } - - private class PropagatingClickListener implements View.OnClickListener { - - private final int type; - - private PropagatingClickListener(int type) { - this.type = type; - } - - @Override - public void onClick(View v) { - animateWindowOutTranslate(getContentView()); - - if (listener != null) listener.onClick(type); - } - - } - - private class CloseClickListener implements View.OnClickListener { - @Override - public void onClick(View v) { - dismiss(); - } - } - - public interface AttachmentClickedListener { - void onClick(int type); - void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size); - } - -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java index f94d8a315f..ef5221bd9f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/OutlinedThumbnailView.java @@ -1,10 +1,14 @@ package org.thoughtcrime.securesms.components; import android.content.Context; +import android.content.res.TypedArray; import android.graphics.Canvas; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.UiThread; + +import android.graphics.Color; import android.util.AttributeSet; import org.thoughtcrime.securesms.R; @@ -21,20 +25,30 @@ public class OutlinedThumbnailView extends ThumbnailView { public OutlinedThumbnailView(Context context) { super(context); - init(); + init(null); } public OutlinedThumbnailView(Context context, AttributeSet attrs) { super(context, attrs); - init(); + init(attrs); } - private void init() { + private void init(@Nullable AttributeSet attrs) { cornerMask = new CornerMask(this); outliner = new Outliner(); outliner.setColor(ThemeUtil.getThemedColor(getContext(), R.attr.conversation_item_image_outline_color)); - setRadius(0); + + int radius = 0; + + if (attrs != null) { + TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.OutlinedThumbnailView, 0, 0); + radius = typedArray.getDimensionPixelOffset(R.styleable.OutlinedThumbnailView_otv_cornerRadius, 0); + } + + setRadius(radius); + setCorners(radius, radius, radius, radius); + setWillNotDraw(false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java index 874ea6b876..4ab1b7db5a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -344,6 +344,10 @@ public class ThumbnailView extends FrameLayout { } public ListenableFuture setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) { + return setImageResource(glideRequests, uri, 0, 0); + } + + public ListenableFuture setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri, int width, int height) { SettableFuture future = new SettableFuture<>(); if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE); @@ -352,6 +356,10 @@ public class ThumbnailView extends FrameLayout { .diskCacheStrategy(DiskCacheStrategy.NONE) .transition(withCrossFade()); + if (width > 0 && height > 0) { + request = request.override(width, height); + } + if (radius > 0) { request = request.transforms(new CenterCrop(), new RoundedCorners(radius)); } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java new file mode 100644 index 0000000000..71c506d417 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboard.java @@ -0,0 +1,127 @@ +package org.thoughtcrime.securesms.conversation; + +import android.Manifest; +import android.content.Context; +import android.content.pm.PackageManager; +import android.util.AttributeSet; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.InputAwareLayout; +import org.thoughtcrime.securesms.mediasend.Media; +import org.thoughtcrime.securesms.mms.GlideApp; + +import java.util.Arrays; +import java.util.List; + +public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.InputView { + + private AttachmentKeyboardMediaAdapter mediaAdapter; + private AttachmentKeyboardButtonAdapter buttonAdapter; + private Callback callback; + + private RecyclerView mediaList; + private View permissionText; + private View permissionButton; + + public AttachmentKeyboard(@NonNull Context context) { + super(context); + init(context); + } + + public AttachmentKeyboard(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(context); + } + + private void init(@NonNull Context context) { + inflate(context, R.layout.attachment_keyboard, this); + + this.mediaList = findViewById(R.id.attachment_keyboard_media_list ); + this.permissionText = findViewById(R.id.attachment_keyboard_permission_text ); + this.permissionButton = findViewById(R.id.attachment_keyboard_permission_button); + + RecyclerView buttonList = findViewById(R.id.attachment_keyboard_button_list); + + mediaAdapter = new AttachmentKeyboardMediaAdapter(GlideApp.with(this), media -> { + if (callback != null) { + callback.onAttachmentMediaClicked(media); + } + }); + + buttonAdapter = new AttachmentKeyboardButtonAdapter(button -> { + if (callback != null) { + callback.onAttachmentSelectorClicked(button); + } + }); + + mediaList.setAdapter(mediaAdapter); + buttonList.setAdapter(buttonAdapter); + + mediaList.setLayoutManager(new GridLayoutManager(context, 1, GridLayoutManager.HORIZONTAL, false)); + buttonList.setLayoutManager(new LinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)); + + buttonAdapter.setButtons(Arrays.asList( + AttachmentKeyboardButton.GALLERY, + AttachmentKeyboardButton.GIF, + AttachmentKeyboardButton.FILE, + AttachmentKeyboardButton.CONTACT, + AttachmentKeyboardButton.LOCATION + )); + } + + public void setCallback(@NonNull Callback callback) { + this.callback = callback; + } + + public void onMediaChanged(@NonNull List media) { + if (ContextCompat.checkSelfPermission(getContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { + mediaAdapter.setMedia(media); + permissionButton.setVisibility(GONE); + permissionText.setVisibility(GONE); + } else { + permissionButton.setVisibility(VISIBLE); + permissionText.setVisibility(VISIBLE); + + permissionButton.setOnClickListener(v -> { + if (callback != null) { + callback.onAttachmentPermissionsRequested(); + } + }); + } + } + + @Override + public void show(int height, boolean immediate) { + ViewGroup.LayoutParams params = getLayoutParams(); + params.height = height; + setLayoutParams(params); + + setVisibility(VISIBLE); + } + + @Override + public void hide(boolean immediate) { + setVisibility(GONE); + } + + @Override + public boolean isShowing() { + return getVisibility() == VISIBLE; + } + + public interface Callback { + void onAttachmentMediaClicked(@NonNull Media media); + void onAttachmentSelectorClicked(@NonNull AttachmentKeyboardButton button); + void onAttachmentPermissionsRequested(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java new file mode 100644 index 0000000000..cc3a366065 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButton.java @@ -0,0 +1,31 @@ +package org.thoughtcrime.securesms.conversation; + +import androidx.annotation.DrawableRes; +import androidx.annotation.StringRes; + +import org.thoughtcrime.securesms.R; + +public enum AttachmentKeyboardButton { + + GALLERY(R.string.AttachmentKeyboard_gallery, R.drawable.ic_photo_album_outline_32), + GIF(R.string.AttachmentKeyboard_gif, R.drawable.ic_gif_outline_32), + FILE(R.string.AttachmentKeyboard_file, R.drawable.ic_file_outline_32), + CONTACT(R.string.AttachmentKeyboard_contact, R.drawable.ic_contact_circle_outline_32), + LOCATION(R.string.AttachmentKeyboard_location, R.drawable.ic_location_outline_32); + + private final int titleRes; + private final int iconRes; + + AttachmentKeyboardButton(@StringRes int titleRes, @DrawableRes int iconRes) { + this.titleRes = titleRes; + this.iconRes = iconRes; + } + + public @StringRes int getTitleRes() { + return titleRes; + } + + public @DrawableRes int getIconRes() { + return iconRes; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonAdapter.java new file mode 100644 index 0000000000..bbc027cf4e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardButtonAdapter.java @@ -0,0 +1,89 @@ +package org.thoughtcrime.securesms.conversation; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.R; + +import java.util.ArrayList; +import java.util.List; + +class AttachmentKeyboardButtonAdapter extends RecyclerView.Adapter { + + private final List buttons; + private final Listener listener; + + AttachmentKeyboardButtonAdapter(@NonNull Listener listener) { + this.buttons = new ArrayList<>(); + this.listener = listener; + + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + return buttons.get(position).getTitleRes(); + } + + @Override + public @NonNull + ButtonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ButtonViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.attachment_keyboard_button_item, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull ButtonViewHolder holder, int position) { + holder.bind(buttons.get(position), listener); + } + + @Override + public void onViewRecycled(@NonNull ButtonViewHolder holder) { + holder.recycle(); + } + + @Override + public int getItemCount() { + return buttons.size(); + } + + + public void setButtons(@NonNull List buttons) { + this.buttons.clear(); + this.buttons.addAll(buttons); + notifyDataSetChanged(); + } + + interface Listener { + void onClick(@NonNull AttachmentKeyboardButton button); + } + + static class ButtonViewHolder extends RecyclerView.ViewHolder { + + private final ImageView image; + private final TextView title; + + public ButtonViewHolder(@NonNull View itemView) { + super(itemView); + + this.image = itemView.findViewById(R.id.attachment_button_image); + this.title = itemView.findViewById(R.id.attachment_button_title); + } + + void bind(@NonNull AttachmentKeyboardButton button, @NonNull Listener listener) { + image.setImageResource(button.getIconRes()); + title.setText(button.getTitleRes()); + + itemView.setOnClickListener(v -> listener.onClick(button)); + } + + void recycle() { + itemView.setOnClickListener(null); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardMediaAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardMediaAdapter.java new file mode 100644 index 0000000000..4ca527bd3f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/AttachmentKeyboardMediaAdapter.java @@ -0,0 +1,129 @@ +package org.thoughtcrime.securesms.conversation; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.components.OutlinedThumbnailView; +import org.thoughtcrime.securesms.mediasend.Media; +import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.adapter.StableIdGenerator; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; + +class AttachmentKeyboardMediaAdapter extends RecyclerView.Adapter { + + private final List media; + private final GlideRequests glideRequests; + private final Listener listener; + private final StableIdGenerator idGenerator; + + AttachmentKeyboardMediaAdapter(@NonNull GlideRequests glideRequests, @NonNull Listener listener) { + this.glideRequests = glideRequests; + this.listener = listener; + this.media = new ArrayList<>(); + this.idGenerator = new StableIdGenerator<>(); + + setHasStableIds(true); + } + + @Override + public long getItemId(int position) { + return idGenerator.getId(media.get(position)); + } + + @Override + public @NonNull MediaViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new MediaViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.attachment_keyboad_media_item, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) { + holder.bind(media.get(position), glideRequests, listener); + } + + @Override + public void onViewRecycled(@NonNull MediaViewHolder holder) { + holder.recycle(); + } + + @Override + public int getItemCount() { + return media.size(); + } + + public void setMedia(@NonNull List media) { + this.media.clear(); + this.media.addAll(media); + notifyDataSetChanged(); + } + + interface Listener { + void onMediaClicked(@NonNull Media media); + } + + static class MediaViewHolder extends RecyclerView.ViewHolder { + + private final OutlinedThumbnailView image; + private final TextView duration; + private final View videoIcon; + + public MediaViewHolder(@NonNull View itemView) { + super(itemView); + image = itemView.findViewById(R.id.attachment_keyboard_item_image); + duration = itemView.findViewById(R.id.attachment_keyboard_item_video_time); + videoIcon = itemView.findViewById(R.id.attachment_keyboard_item_video_icon); + } + + void bind(@NonNull Media media, @NonNull GlideRequests glideRequests, @NonNull Listener listener) { + image.setImageResource(glideRequests, media.getUri(), 400, 400); + image.setOnClickListener(v -> listener.onMediaClicked(media)); + + duration.setVisibility(View.GONE); + videoIcon.setVisibility(View.GONE); + + if (media.getDuration() > 0) { + duration.setVisibility(View.VISIBLE); + duration.setText(formatTime(media.getDuration())); + } else if (MediaUtil.isVideoType(media.getMimeType())) { + videoIcon.setVisibility(View.VISIBLE); + } + } + + void recycle() { + image.setOnClickListener(null); + } + + @NonNull static String formatTime(long time) { + long hours = TimeUnit.MILLISECONDS.toHours(time); + time -= TimeUnit.HOURS.toMillis(hours); + + long minutes = TimeUnit.MILLISECONDS.toMinutes(time); + time -= TimeUnit.MINUTES.toHours(time); + + long seconds = TimeUnit.MILLISECONDS.toSeconds(time); + + if (hours > 0) { + return zeroPad(hours) + ":" + zeroPad(minutes) + ":" + zeroPad(seconds); + } else { + return zeroPad(minutes) + ":" + zeroPad(seconds); + } + } + + @NonNull static String zeroPad(long value) { + if (value < 10) { + return "0" + value; + } else { + return String.valueOf(value); + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java index d8e4f3a4d1..06cae0658f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java @@ -97,7 +97,6 @@ import org.thoughtcrime.securesms.audio.AudioRecorder; import org.thoughtcrime.securesms.audio.AudioSlidePlayer; import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.components.AnimatingToggle; -import org.thoughtcrime.securesms.components.AttachmentTypeSelector; import org.thoughtcrime.securesms.components.ComposeText; import org.thoughtcrime.securesms.components.ConversationSearchBottomBar; import org.thoughtcrime.securesms.components.HidingLinearLayout; @@ -262,7 +261,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity InputPanel.MediaListener, ComposeText.CursorPositionChangedListener, ConversationSearchBottomBar.EventListener, - StickerKeyboardProvider.StickerEventListener + StickerKeyboardProvider.StickerEventListener, + AttachmentKeyboard.Callback { private static final String TAG = ConversationActivity.class.getSimpleName(); @@ -311,18 +311,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private FrameLayout messageRequestOverlay; private ConversationReactionOverlay reactionOverlay; - private AttachmentTypeSelector attachmentTypeSelector; - private AttachmentManager attachmentManager; - private AudioRecorder audioRecorder; - private BroadcastReceiver securityUpdateReceiver; - private Stub emojiDrawerStub; - protected HidingLinearLayout quickAttachmentToggle; - protected HidingLinearLayout inlineAttachmentToggle; - private InputPanel inputPanel; + private AttachmentManager attachmentManager; + private AudioRecorder audioRecorder; + private BroadcastReceiver securityUpdateReceiver; + private Stub emojiDrawerStub; + private Stub attachmentKeyboardStub; + protected HidingLinearLayout quickAttachmentToggle; + protected HidingLinearLayout inlineAttachmentToggle; + private InputPanel inputPanel; private LinkPreviewViewModel linkPreviewViewModel; private ConversationSearchViewModel searchViewModel; private ConversationStickerViewModel stickerViewModel; + private ConversationViewModel viewModel; private InviteReminderModel inviteReminderModel; private LiveRecipient recipient; @@ -391,6 +392,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity initializeLinkPreviewObserver(); initializeSearchObserver(); initializeStickerObserver(); + initializeViewModel(); initializeSecurity(recipient.get().isRegistered(), isDefaultSms).addListener(new AssertedSuccessListener() { @Override public void onSuccess(Boolean result) { @@ -844,7 +846,44 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } - //////// Event Handlers + @Override + public void onAttachmentMediaClicked(@NonNull Media media) { + linkPreviewViewModel.onUserCancel(); + startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER); + } + + @Override + public void onAttachmentSelectorClicked(@NonNull AttachmentKeyboardButton button) { + switch (button) { + case GALLERY: + AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()); + break; + case GIF: + AttachmentManager.selectGif(this, PICK_GIF, !isSecureText, recipient.get().getColor().toConversationColor(this)); + break; + case FILE: + AttachmentManager.selectDocument(this, PICK_DOCUMENT); + break; + case CONTACT: + AttachmentManager.selectContactInfo(this, PICK_CONTACT); + break; + case LOCATION: + AttachmentManager.selectLocation(this, PICK_LOCATION); + break; + } + // TODO [greyson] [attachment] Add these +// attachmentManager.capturePhoto(this, TAKE_PHOTO); break; + } + + @Override + public void onAttachmentPermissionsRequested() { + Permissions.with(this) + .request(Manifest.permission.READ_EXTERNAL_STORAGE) + .onAllGranted(() -> viewModel.onAttachmentKeyboardOpen()) + .execute(); + } + +//////// Event Handlers private void handleSelectMessageExpiration() { if (isPushGroupConversation() && !isActiveGroup()) { @@ -1168,10 +1207,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private void handleAddAttachment() { if (this.isMmsEnabled || isSecureText) { - if (attachmentTypeSelector == null) { - attachmentTypeSelector = new AttachmentTypeSelector(this, getSupportLoaderManager(), new AttachmentTypeListener()); + viewModel.getRecentMedia().removeObservers(this); + + if (attachmentKeyboardStub.resolved() && container.isInputOpen() && container.getCurrentInput() == attachmentKeyboardStub.get()) { + container.showSoftkey(composeText); + } else { + viewModel.getRecentMedia().observe(this, media -> attachmentKeyboardStub.get().onMediaChanged(media)); + attachmentKeyboardStub.get().setCallback(this); + container.show(composeText, attachmentKeyboardStub.get()); + + viewModel.onAttachmentKeyboardOpen(); } - attachmentTypeSelector.show(this, attachButton); } else { handleManualMmsRequired(); } @@ -1564,6 +1610,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity composeText = ViewUtil.findById(this, R.id.embedded_text_editor); charactersLeft = ViewUtil.findById(this, R.id.space_left); emojiDrawerStub = ViewUtil.findStubById(this, R.id.emoji_drawer_stub); + attachmentKeyboardStub = ViewUtil.findStubById(this, R.id.attachment_keyboard_stub); unblockButton = ViewUtil.findById(this, R.id.unblock_button); makeDefaultSmsButton = ViewUtil.findById(this, R.id.make_default_sms_button); registerButton = ViewUtil.findById(this, R.id.register_button); @@ -1586,10 +1633,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity inputPanel.setListener(this); inputPanel.setMediaListener(this); - attachmentTypeSelector = null; - attachmentManager = new AttachmentManager(this, this); - audioRecorder = new AudioRecorder(this); - typingTextWatcher = new TypingStatusTextWatcher(); + attachmentManager = new AttachmentManager(this, this); + audioRecorder = new AudioRecorder(this); + typingTextWatcher = new TypingStatusTextWatcher(); SendButtonListener sendButtonListener = new SendButtonListener(); ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener(); @@ -1732,6 +1778,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity }); } + private void initializeViewModel() { + this.viewModel = ViewModelProviders.of(this, new ConversationViewModel.Factory()).get(ConversationViewModel.class); + } + private void showStickerIntroductionTooltip() { TextSecurePreferences.setMediaKeyboardMode(this, MediaKeyboardMode.STICKER); inputPanel.setMediaKeyboardToggleMode(true); @@ -1835,28 +1885,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity //////// Helper Methods - private void addAttachment(int type) { - linkPreviewViewModel.onUserCancel(); - - Log.i(TAG, "Selected: " + type); - switch (type) { - case AttachmentTypeSelector.ADD_GALLERY: - AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()); break; - case AttachmentTypeSelector.ADD_DOCUMENT: - AttachmentManager.selectDocument(this, PICK_DOCUMENT); break; - case AttachmentTypeSelector.ADD_SOUND: - AttachmentManager.selectAudio(this, PICK_AUDIO); break; - case AttachmentTypeSelector.ADD_CONTACT_INFO: - AttachmentManager.selectContactInfo(this, PICK_CONTACT); break; - case AttachmentTypeSelector.ADD_LOCATION: - AttachmentManager.selectLocation(this, PICK_LOCATION); break; - case AttachmentTypeSelector.TAKE_PHOTO: - attachmentManager.capturePhoto(this, TAKE_PHOTO); break; - case AttachmentTypeSelector.ADD_GIF: - AttachmentManager.selectGif(this, PICK_GIF, !isSecureText, recipient.get().getColor().toConversationColor(this)); break; - } - } - private ListenableFuture setMedia(@Nullable Uri uri, @NonNull MediaType mediaType) { return setMedia(uri, mediaType, 0, 0); } @@ -1870,7 +1898,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity openContactShareEditor(uri); return new SettableFuture<>(false); } else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) { - Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, 0, Optional.absent(), Optional.absent()); + Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, 0, 0, Optional.absent(), Optional.absent()); startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER); return new SettableFuture<>(false); } else { @@ -2583,7 +2611,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private void sendSticker(@NonNull StickerLocator stickerLocator, @NonNull Uri uri, long size, boolean clearCompose) { if (sendButton.getSelectedTransport().isSms()) { - Media media = new Media(uri, MediaUtil.IMAGE_WEBP, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, Optional.absent(), Optional.absent()); + Media media = new Media(uri, MediaUtil.IMAGE_WEBP, System.currentTimeMillis(), StickerSlide.WIDTH, StickerSlide.HEIGHT, size, 0, Optional.absent(), Optional.absent()); Intent intent = MediaSendActivity.buildEditorIntent(this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()); startActivityForResult(intent, MEDIA_SENDER); return; @@ -2610,20 +2638,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity // Listeners - private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener { - @Override - public void onClick(int type) { - addAttachment(type); - } - - @Override - public void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height, long size) { - linkPreviewViewModel.onUserCancel(); - Media media = new Media(uri, mimeType, dateTaken, width, height, size, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent()); - startActivityForResult(MediaSendActivity.buildEditorIntent(ConversationActivity.this, Collections.singletonList(media), recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER); - } - } - private class QuickCameraToggleListener implements OnClickListener { @Override public void onClick(View v) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 29e9087c86..11c94e5b6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -602,6 +602,7 @@ public class ConversationFragment extends Fragment attachment.getWidth(), attachment.getHeight(), attachment.getSize(), + 0, Optional.absent(), Optional.fromNullable(attachment.getCaption()))); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java new file mode 100644 index 0000000000..2fe31aa14b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java @@ -0,0 +1,44 @@ +package org.thoughtcrime.securesms.conversation; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.mediasend.Media; +import org.thoughtcrime.securesms.mediasend.MediaRepository; + +import java.util.List; + +class ConversationViewModel extends ViewModel { + + private final Context context; + private final MediaRepository mediaRepository; + private final MutableLiveData> recentMedia; + + private ConversationViewModel() { + this.context = ApplicationDependencies.getApplication(); + this.mediaRepository = new MediaRepository(); + this.recentMedia = new MutableLiveData<>(); + } + + void onAttachmentKeyboardOpen() { + mediaRepository.getMediaInBucket(context, Media.ALL_MEDIA_BUCKET_ID, recentMedia::postValue); + } + + @NonNull LiveData> getRecentMedia() { + return recentMedia; + } + + static class Factory extends ViewModelProvider.NewInstanceFactory { + @Override + public @NonNull T create(@NonNull Class modelClass) { + //noinspection ConstantConditions + return modelClass.cast(new ConversationViewModel()); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java index 64ef2a9ce7..c6514d458b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewViewModel.java @@ -110,6 +110,7 @@ public class MediaPreviewViewModel extends ViewModel { mediaRecord.getAttachment().getWidth(), mediaRecord.getAttachment().getHeight(), mediaRecord.getAttachment().getSize(), + 0, Optional.absent(), Optional.fromNullable(mediaRecord.getAttachment().getCaption())); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.java index 842d8371a3..7b18bed177 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/Media.java @@ -20,17 +20,28 @@ public class Media implements Parcelable { private final int width; private final int height; private final long size; + private final long duration; private Optional bucketId; private Optional caption; - public Media(@NonNull Uri uri, @NonNull String mimeType, long date, int width, int height, long size, Optional bucketId, Optional caption) { + public Media(@NonNull Uri uri, + @NonNull String mimeType, + long date, + int width, + int height, + long size, + long duration, + Optional bucketId, + Optional caption) + { this.uri = uri; this.mimeType = mimeType; this.date = date; this.width = width; this.height = height; this.size = size; + this.duration = duration; this.bucketId = bucketId; this.caption = caption; } @@ -42,6 +53,7 @@ public class Media implements Parcelable { width = in.readInt(); height = in.readInt(); size = in.readLong(); + duration = in.readLong(); bucketId = Optional.fromNullable(in.readString()); caption = Optional.fromNullable(in.readString()); } @@ -70,6 +82,10 @@ public class Media implements Parcelable { return size; } + public long getDuration() { + return duration; + } + public Optional getBucketId() { return bucketId; } @@ -95,6 +111,7 @@ public class Media implements Parcelable { dest.writeInt(width); dest.writeInt(height); dest.writeLong(size); + dest.writeLong(duration); dest.writeString(bucketId.orNull()); dest.writeString(caption.orNull()); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java index 4380b9fa59..6a0f6476a8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java @@ -42,7 +42,7 @@ import java.util.Map; /** * Handles the retrieval of media present on the user's device. */ -class MediaRepository { +public class MediaRepository { private static final String TAG = Log.tag(MediaRepository.class); @@ -56,7 +56,7 @@ class MediaRepository { /** * Retrieves a list of media items (images and videos) that are present int he specified bucket. */ - void getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Callback> callback) { + public void getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Callback> callback) { SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getMediaInBucket(context, bucketId))); } @@ -141,9 +141,9 @@ class MediaRepository { long thumbnailTimestamp = 0; Map folders = new HashMap<>(); - String[] projection = new String[] { Images.Media.DATA, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_TAKEN }; + String[] projection = new String[] { Images.Media.DATA, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME, Images.Media.DATE_MODIFIED }; String selection = Images.Media.DATA + " NOT NULL"; - String sortBy = Images.Media.BUCKET_DISPLAY_NAME + " COLLATE NOCASE ASC, " + Images.Media.DATE_TAKEN + " DESC"; + String sortBy = Images.Media.BUCKET_DISPLAY_NAME + " COLLATE NOCASE ASC, " + Images.Media.DATE_MODIFIED + " DESC"; try (Cursor cursor = context.getContentResolver().query(contentUri, projection, selection, null, sortBy)) { while (cursor != null && cursor.moveToNext()) { @@ -189,18 +189,18 @@ class MediaRepository { } @WorkerThread - private @NonNull List getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean hasOrientation) { + private @NonNull List getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean isImage) { List media = new LinkedList<>(); String selection = Images.Media.BUCKET_ID + " = ? AND " + Images.Media.DATA + " NOT NULL"; String[] selectionArgs = new String[] { bucketId }; - String sortBy = Images.Media.DATE_TAKEN + " DESC"; + String sortBy = Images.Media.DATE_MODIFIED + " DESC"; String[] projection; - if (hasOrientation) { - projection = new String[]{Images.Media.DATA, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE}; + if (isImage) { + projection = new String[]{Images.Media.DATA, Images.Media.MIME_TYPE, Images.Media.DATE_MODIFIED, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE}; } else { - projection = new String[]{Images.Media.DATA, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE}; + projection = new String[]{Images.Media.DATA, Images.Media.MIME_TYPE, Images.Media.DATE_MODIFIED, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE, Video.Media.DURATION}; } if (Media.ALL_MEDIA_BUCKET_ID.equals(bucketId)) { @@ -213,13 +213,14 @@ class MediaRepository { String path = cursor.getString(cursor.getColumnIndexOrThrow(projection[0])); Uri uri = Uri.fromFile(new File(path)); String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE)); - long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_TAKEN)); - int orientation = hasOrientation ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0; + long date = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_MODIFIED)); + int orientation = isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0; int width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation))); int height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation))); long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE)); + long duration = !isImage ? cursor.getInt(cursor.getColumnIndexOrThrow(Video.Media.DURATION)) : 0; - media.add(new Media(uri, mimetype, dateTaken, width, height, size, Optional.of(bucketId), Optional.absent())); + media.add(new Media(uri, mimetype, date, width, height, size, duration, Optional.of(bucketId), Optional.absent())); } } @@ -268,7 +269,7 @@ class MediaRepository { .withMimeType(MediaUtil.IMAGE_JPEG) .createForSingleSessionOnDisk(context); - Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), media.getBucketId(), media.getCaption()); + Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), 0, media.getBucketId(), media.getCaption()); updatedMedia.put(media, updated); } catch (IOException e) { @@ -332,7 +333,7 @@ class MediaRepository { height = dimens.second; } - return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, media.getBucketId(), media.getCaption()); + return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption()); } private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException { @@ -358,7 +359,7 @@ class MediaRepository { height = dimens.second; } - return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, media.getBucketId(), media.getCaption()); + return new Media(media.getUri(), media.getMimeType(), media.getDate(), width, height, size, 0, media.getBucketId(), media.getCaption()); } private static class FolderResult { @@ -433,7 +434,7 @@ class MediaRepository { } } - interface Callback { + public interface Callback { void onComplete(@NonNull E result); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java index 4dfe322e80..5c0c0b34b5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java @@ -412,6 +412,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple width, height, length, + 0, Optional.of(Media.ALL_MEDIA_BUCKET_ID), Optional.absent() ); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java index 48982b3225..a0cb8813aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java @@ -303,7 +303,7 @@ class MediaSendViewModel extends ViewModel { captionVisible = false; List uncaptioned = Stream.of(getSelectedMediaOrDefault()) - .map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getBucketId(), Optional.absent())) + .map(m -> new Media(m.getUri(), m.getMimeType(), m.getDate(), m.getWidth(), m.getHeight(), m.getSize(), m.getDuration(), m.getBucketId(), Optional.absent())) .toList(); selectedMedia.setValue(uncaptioned); @@ -476,7 +476,7 @@ class MediaSendViewModel extends ViewModel { if (splitMessage.getTextSlide().isPresent()) { Slide slide = splitMessage.getTextSlide().get(); - uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), Optional.absent(), Optional.absent()), recipient); + uploadRepository.startUpload(new Media(Objects.requireNonNull(slide.getUri()), slide.getContentType(), System.currentTimeMillis(), 0, 0, slide.getFileSize(), 0, Optional.absent(), Optional.absent()), recipient); } uploadRepository.applyMediaUpdates(oldToNew, recipient); diff --git a/app/src/main/res/drawable-v21/attachment_keyboard_button_background_dark.xml b/app/src/main/res/drawable-v21/attachment_keyboard_button_background_dark.xml new file mode 100644 index 0000000000..59dfdd9f65 --- /dev/null +++ b/app/src/main/res/drawable-v21/attachment_keyboard_button_background_dark.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v21/attachment_keyboard_button_background_light.xml b/app/src/main/res/drawable-v21/attachment_keyboard_button_background_light.xml new file mode 100644 index 0000000000..e4ca23c0cd --- /dev/null +++ b/app/src/main/res/drawable-v21/attachment_keyboard_button_background_light.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/attachment_keyboard_button_background_dark.xml b/app/src/main/res/drawable/attachment_keyboard_button_background_dark.xml new file mode 100644 index 0000000000..accbfe6a22 --- /dev/null +++ b/app/src/main/res/drawable/attachment_keyboard_button_background_dark.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/attachment_keyboard_button_background_light.xml b/app/src/main/res/drawable/attachment_keyboard_button_background_light.xml new file mode 100644 index 0000000000..9617c71cbf --- /dev/null +++ b/app/src/main/res/drawable/attachment_keyboard_button_background_light.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_camera_outline_32.xml b/app/src/main/res/drawable/ic_camera_outline_32.xml new file mode 100644 index 0000000000..4008534f8a --- /dev/null +++ b/app/src/main/res/drawable/ic_camera_outline_32.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_contact_circle_outline_32.xml b/app/src/main/res/drawable/ic_contact_circle_outline_32.xml new file mode 100644 index 0000000000..20a1c5a6f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_contact_circle_outline_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_file_outline_32.xml b/app/src/main/res/drawable/ic_file_outline_32.xml new file mode 100644 index 0000000000..aae0a89531 --- /dev/null +++ b/app/src/main/res/drawable/ic_file_outline_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_gif_outline_32.xml b/app/src/main/res/drawable/ic_gif_outline_32.xml new file mode 100644 index 0000000000..03260f34fe --- /dev/null +++ b/app/src/main/res/drawable/ic_gif_outline_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_location_outline_32.xml b/app/src/main/res/drawable/ic_location_outline_32.xml new file mode 100644 index 0000000000..00c94ebc2c --- /dev/null +++ b/app/src/main/res/drawable/ic_location_outline_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_photo_album_outline_32.xml b/app/src/main/res/drawable/ic_photo_album_outline_32.xml new file mode 100644 index 0000000000..3acdfd9724 --- /dev/null +++ b/app/src/main/res/drawable/ic_photo_album_outline_32.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/transparent_black_pill.xml b/app/src/main/res/drawable/transparent_black_pill.xml new file mode 100644 index 0000000000..22b877ce96 --- /dev/null +++ b/app/src/main/res/drawable/transparent_black_pill.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/attachment_keyboad_media_item.xml b/app/src/main/res/layout/attachment_keyboad_media_item.xml new file mode 100644 index 0000000000..eef858ce66 --- /dev/null +++ b/app/src/main/res/layout/attachment_keyboad_media_item.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + diff --git a/app/src/main/res/layout/attachment_keyboard.xml b/app/src/main/res/layout/attachment_keyboard.xml new file mode 100644 index 0000000000..bd4ecc702e --- /dev/null +++ b/app/src/main/res/layout/attachment_keyboard.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + +