Implement new attachment keyboard.

Such beauty. Such grace.
master
Greyson Parrelli 2020-01-29 22:13:44 -05:00 committed by Alex Hart
parent 9f7b2e2cfd
commit 109d67956f
35 changed files with 866 additions and 371 deletions

View File

@ -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<Integer, Integer> 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<Integer, Integer> 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<Integer, Integer> 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);
}
}

View File

@ -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);
}

View File

@ -344,6 +344,10 @@ public class ThumbnailView extends FrameLayout {
}
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri) {
return setImageResource(glideRequests, uri, 0, 0);
}
public ListenableFuture<Boolean> setImageResource(@NonNull GlideRequests glideRequests, @NonNull Uri uri, int width, int height) {
SettableFuture<Boolean> 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 {

View File

@ -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> 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();
}
}

View File

@ -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;
}
}

View File

@ -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<AttachmentKeyboardButtonAdapter.ButtonViewHolder> {
private final List<AttachmentKeyboardButton> 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<AttachmentKeyboardButton> 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);
}
}
}

View File

@ -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<AttachmentKeyboardMediaAdapter.MediaViewHolder> {
private final List<Media> media;
private final GlideRequests glideRequests;
private final Listener listener;
private final StableIdGenerator<Media> 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> 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);
}
}
}
}

View File

@ -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<MediaKeyboard> emojiDrawerStub;
protected HidingLinearLayout quickAttachmentToggle;
protected HidingLinearLayout inlineAttachmentToggle;
private InputPanel inputPanel;
private AttachmentManager attachmentManager;
private AudioRecorder audioRecorder;
private BroadcastReceiver securityUpdateReceiver;
private Stub<MediaKeyboard> emojiDrawerStub;
private Stub<AttachmentKeyboard> 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<Boolean>() {
@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<Boolean> 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) {

View File

@ -602,6 +602,7 @@ public class ConversationFragment extends Fragment
attachment.getWidth(),
attachment.getHeight(),
attachment.getSize(),
0,
Optional.absent(),
Optional.fromNullable(attachment.getCaption())));
}

View File

@ -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<List<Media>> 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<List<Media>> getRecentMedia() {
return recentMedia;
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationViewModel());
}
}
}

View File

@ -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()));
}

View File

@ -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<String> bucketId;
private Optional<String> caption;
public Media(@NonNull Uri uri, @NonNull String mimeType, long date, int width, int height, long size, Optional<String> bucketId, Optional<String> caption) {
public Media(@NonNull Uri uri,
@NonNull String mimeType,
long date,
int width,
int height,
long size,
long duration,
Optional<String> bucketId,
Optional<String> 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<String> 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());
}

View File

@ -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<List<Media>> callback) {
public void getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Callback<List<Media>> callback) {
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getMediaInBucket(context, bucketId)));
}
@ -141,9 +141,9 @@ class MediaRepository {
long thumbnailTimestamp = 0;
Map<String, FolderData> 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<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean hasOrientation) {
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean isImage) {
List<Media> 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<E> {
public interface Callback<E> {
void onComplete(@NonNull E result);
}
}

View File

@ -412,6 +412,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
width,
height,
length,
0,
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
Optional.absent()
);

View File

@ -303,7 +303,7 @@ class MediaSendViewModel extends ViewModel {
captionVisible = false;
List<Media> 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);

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/transparent_white_20">
<item android:id="@+id/mask">
<shape>
<corners android:radius="4dp" />
<solid android:color="@color/transparent_black" />
</shape>
</item>
<item>
<shape android:shape="rectangle" >
<corners android:radius="4dp" />
<solid android:color="@color/core_grey_85"/>
</shape>
</item>
</ripple>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple
xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/transparent_black_10">
<item android:id="@+id/mask">
<shape>
<corners android:radius="4dp" />
<solid android:color="@color/transparent_black" />
</shape>
</item>
<item>
<shape android:shape="rectangle" >
<corners android:radius="4dp" />
<solid android:color="@color/core_grey_05"/>
</shape>
</item>
</ripple>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<corners android:radius="4dp" />
<solid android:color="@color/core_grey_85"/>
</shape>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
<corners android:radius="4dp" />
<solid android:color="@color/core_grey_05"/>
</shape>

View File

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M16,10.5a6.25,6.25 0,1 1,-6.25 6.25A6.25,6.25 0,0 1,16 10.5M16,9a7.75,7.75 0,1 0,7.75 7.75A7.75,7.75 0,0 0,16 9Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M18.59,4.5A1.52,1.52 0,0 1,19.75 5L21.8,7.5H25A3.5,3.5 0,0 1,28.5 11V24A3.5,3.5 0,0 1,25 27.5H7A3.5,3.5 0,0 1,3.5 24V11A3.5,3.5 0,0 1,7 7.5h3.2L12.25,5a1.52,1.52 0,0 1,1.16 -0.54h5.18m0,-1.5H13.41A3,3 0,0 0,11.1 4.08L9.5,6H7a5,5 0,0 0,-5 5V24a5,5 0,0 0,5 5H25a5,5 0,0 0,5 -5V11a5,5 0,0 0,-5 -5H22.5L20.9,4.08A3,3 0,0 0,18.59 3Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M16,1A15,15 0,1 0,31 16,15 15,0 0,0 16,1ZM16,29.5a13.43,13.43 0,0 1,-9.73 -4.17A7.19,7.19 0,0 1,13 20.75h1.22a6.82,6.82 0,0 0,3.66 0h1.22a7.19,7.19 0,0 1,6.68 4.58A13.43,13.43 0,0 1,16 29.5ZM10.5,14A5.5,5.5 0,1 1,16 19.5,5.5 5.5,0 0,1 10.5,14ZM20.5,19.38a7,7 0,1 0,-9 0A8.67,8.67 0,0 0,5.2 24.06,13.38 13.38,0 0,1 2.5,16a13.5,13.5 0,0 1,27 0,13.38 13.38,0 0,1 -2.7,8.06A8.67,8.67 0,0 0,20.48 19.38Z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M26.05,12.82l-10,-10A3,3 0,0 0,14 2H8A3,3 0,0 0,5 5V27a3,3 0,0 0,3 3H24a3,3 0,0 0,3 -3V15A3,3 0,0 0,26.05 12.82ZM17,5.68 L23.32,12H18.5A1.5,1.5 0,0 1,17 10.5ZM25.5,27A1.5,1.5 0,0 1,24 28.5H8A1.5,1.5 0,0 1,6.5 27V5A1.5,1.5 0,0 1,8 3.5h6A1.5,1.5 0,0 1,15.5 5v5.5a3,3 0,0 0,3 3H24A1.5,1.5 0,0 1,25.5 15Z"/>
</vector>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:fillColor="#FF000000"
android:pathData="M16,2.5A13.5,13.5 0,1 1,2.5 16,13.52 13.52,0 0,1 16,2.5M16,1A15,15 0,1 0,31 16,15 15,0 0,0 16,1ZM21.71,10.29L16.26,23l-1.58,-4.74 -0.24,-0.71 -0.71,-0.24L9,15.74l12.72,-5.45M16.8,24.54h0M23.09,8.21a2.3,2.3 0,0 0,-0.82 0.2L7.19,14.88c-1.33,0.57 -1.3,1.41 0.08,1.87l6,2 2,6c0.24,0.7 0.57,1.06 0.92,1.06s0.67,-0.33 0.95,-1L23.59,9.73c0.4,-1 0.17,-1.52 -0.5,-1.52Z"/>
</vector>

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="1000dp" />
<solid android:color="@color/transparent_black_60" />
</shape>

View File

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.SquareFrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_marginEnd="8dp"
app:square_height="true">
<org.thoughtcrime.securesms.components.OutlinedThumbnailView
android:id="@+id/attachment_keyboard_item_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:otv_cornerRadius="4dp"/>
<TextView
android:id="@+id/attachment_keyboard_item_video_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="6dp"
android:layout_marginBottom="6dp"
android:layout_gravity="bottom|end"
android:paddingStart="8dp"
android:paddingEnd="8dp"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:background="@drawable/transparent_black_pill"
android:textColor="@color/core_white"
android:textSize="13sp"
android:fontFamily="sans-serif-medium"
android:visibility="gone" />
<ImageView
android:id="@+id/attachment_keyboard_item_video_icon"
android:layout_width="27dp"
android:layout_height="27dp"
android:layout_gravity="bottom|end"
android:layout_marginEnd="6dp"
android:layout_marginBottom="6dp"
android:paddingStart="9dp"
android:paddingEnd="5dp"
android:paddingTop="7dp"
android:paddingBottom="7dp"
android:background="@drawable/transparent_black_pill"
app:srcCompat="@drawable/triangle_right"
android:visibility="gone" />
</org.thoughtcrime.securesms.components.SquareFrameLayout>

View File

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<merge
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout_height="200dp">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/attachment_keyboard_media_list"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginBottom="8dp"
android:paddingStart="8dp"
android:clipToPadding="false"
android:clipChildren="false"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/attachment_keyboard_button_list"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/attachment_keyboard_button_list"
android:layout_width="match_parent"
android:layout_height="80dp"
android:paddingStart="8dp"
android:clipToPadding="false"
android:clipChildren="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<TextView
android:id="@+id/attachment_keyboard_permission_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:text="@string/AttachmentKeyboard_Signal_requires_the_storage_permission"
android:textColor="?attachment_keyboard_button_foreground"
android:gravity="center"
android:visibility="gone"
style="@style/Signal.Text.Body"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toTopOf="@id/attachment_keyboard_permission_button"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
<Button
android:id="@+id/attachment_keyboard_permission_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/AttachmentKeyboard_grant_access"
android:visibility="gone"
style="@style/Button.Primary"
app:layout_constraintTop_toBottomOf="@id/attachment_keyboard_permission_text"
app:layout_constraintBottom_toTopOf="@id/attachment_keyboard_button_list"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</merge>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="80dp"
android:layout_marginEnd="6dp"
android:minWidth="80dp"
android:padding="8dp"
android:background="?attachment_keyboard_button_background">
<ImageView
android:id="@+id/attachment_button_image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:tint="?attachment_keyboard_button_foreground"
app:layout_constraintBottom_toTopOf="@id/attachment_button_title"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:src="@drawable/ic_sticker_32"/>
<TextView
android:id="@+id/attachment_button_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Signal.Text.Preview"
android:fontFamily="sans-serif-medium"
android:gravity="center"
android:textColor="?attachment_keyboard_button_foreground"
app:layout_constraintTop_toBottomOf="@id/attachment_button_image"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:text="Web Shooter"/>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -129,6 +129,13 @@
android:inflatedId="@+id/emoji_drawer"
android:layout="@layout/conversation_activity_emojidrawer_stub" />
<ViewStub
android:id="@+id/attachment_keyboard_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inflatedId="@+id/attachment_keyboard"
android:layout="@layout/conversation_activity_attachment_keyboard_stub" />
</LinearLayout>
</org.thoughtcrime.securesms.components.InputAwareLayout>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.conversation.AttachmentKeyboard
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/attachment_keyboard"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone" />

View File

@ -14,6 +14,9 @@
<attr name="attachment_document_icon_small" format="reference" />
<attr name="attachment_document_icon_large" format="reference" />
<attr name="attachment_keyboard_button_background" format="color|reference" />
<attr name="attachment_keyboard_button_foreground" format="color" />
<attr name="conversation_list_item_background" format="reference"/>
<attr name="conversation_list_item_contact_color" format="reference|color"/>
<attr name="conversation_list_item_subject_color" format="reference|color"/>
@ -430,4 +433,16 @@
<declare-styleable name="ContactFilterToolbar">
<attr name="searchTextStyle" format="reference" />
</declare-styleable>
<declare-styleable name="SquareImageView">
<attr name="siv_dominant" format="enum">
<enum name="width" value="0" />
<enum name="height" value="1" />
</attr>
</declare-styleable>
<declare-styleable name="OutlinedThumbnailView">
<attr name="otv_cornerRadius" format="dimension|reference" />
</declare-styleable>
</resources>

View File

@ -13,6 +13,7 @@
<color name="black">@color/core_black</color>
<color name="transparent_black_05">#0D000000</color>
<color name="transparent_black_10">#18000000</color>
<color name="transparent_black_20">#33000000</color>
<color name="transparent_black_40">#66000000</color>
<color name="transparent_black_60">#99000000</color>

View File

@ -66,6 +66,15 @@
<string name="DraftDatabase_Draft_location_snippet">(location)</string>
<string name="DraftDatabase_Draft_quote_snippet">(reply)</string>
<!-- AttachmentKeyboard -->
<string name="AttachmentKeyboard_gallery">Gallery</string>
<string name="AttachmentKeyboard_gif">GIF</string>
<string name="AttachmentKeyboard_file">File</string>
<string name="AttachmentKeyboard_contact">Contact</string>
<string name="AttachmentKeyboard_location">Location</string>
<string name="AttachmentKeyboard_Signal_requires_the_storage_permission">Signal requires the Storage permission to show your recent photos and videos.</string>
<string name="AttachmentKeyboard_grant_access">Grant Access</string>
<!-- AttachmentManager -->
<string name="AttachmentManager_cant_open_media_selection">Can\'t find an app to select media.</string>
<string name="AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio">Signal requires the Storage permission in order to attach photos, videos, or audio, but it has been permanently denied. Please continue to the app settings menu, select \"Permissions\", and enable \"Storage\".</string>

View File

@ -192,6 +192,9 @@
<item name="attachment_document_icon_small">@drawable/ic_document_small_light</item>
<item name="attachment_document_icon_large">@drawable/ic_document_large_light</item>
<item name="attachment_keyboard_button_background">@drawable/attachment_keyboard_button_background_light</item>
<item name="attachment_keyboard_button_foreground">@color/core_grey_65</item>
<item name="compose_icon_tint">?icon_tint</item>
<item name="contact_filter_toolbar_icon_tint">?icon_tint</item>
@ -441,6 +444,9 @@
<item name="attachment_document_icon_small">@drawable/ic_document_small_dark</item>
<item name="attachment_document_icon_large">@drawable/ic_document_large_dark</item>
<item name="attachment_keyboard_button_background">@drawable/attachment_keyboard_button_background_dark</item>
<item name="attachment_keyboard_button_foreground">@color/core_grey_05</item>
<item name="compose_icon_tint">?icon_tint</item>
<item name="contact_filter_toolbar_icon_tint">?icon_tint</item>