mirror of
https://github.com/oxen-io/session-android.git
synced 2025-12-31 05:46:15 +00:00
Implement new attachment keyboard.
Such beauty. Such grace.
This commit is contained in:
committed by
Alex Hart
parent
9f7b2e2cfd
commit
109d67956f
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -602,6 +602,7 @@ public class ConversationFragment extends Fragment
|
||||
attachment.getWidth(),
|
||||
attachment.getHeight(),
|
||||
attachment.getSize(),
|
||||
0,
|
||||
Optional.absent(),
|
||||
Optional.fromNullable(attachment.getCaption())));
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -412,6 +412,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||
width,
|
||||
height,
|
||||
length,
|
||||
0,
|
||||
Optional.of(Media.ALL_MEDIA_BUCKET_ID),
|
||||
Optional.absent()
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user