mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-21 09:20:19 +01:00
Add selected photos access.
This commit is contained in:
committed by
mtang-signal
parent
4f001a0c95
commit
57adab858c
@@ -1,10 +1,13 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
@@ -13,8 +16,10 @@ import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.util.StorageUtil;
|
||||
@@ -26,7 +31,8 @@ import java.util.stream.Collectors;
|
||||
|
||||
public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.InputView {
|
||||
|
||||
private static final List<AttachmentKeyboardButton> DEFAULT_BUTTONS = Arrays.asList(
|
||||
private static final int ANIMATION_DURATION = 150;
|
||||
private static final List<AttachmentKeyboardButton> DEFAULT_BUTTONS = Arrays.asList(
|
||||
AttachmentKeyboardButton.GALLERY,
|
||||
AttachmentKeyboardButton.FILE,
|
||||
AttachmentKeyboardButton.CONTACT,
|
||||
@@ -39,9 +45,10 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
|
||||
private AttachmentKeyboardButtonAdapter buttonAdapter;
|
||||
private Callback callback;
|
||||
|
||||
private RecyclerView mediaList;
|
||||
private View permissionText;
|
||||
private View permissionButton;
|
||||
private RecyclerView mediaList;
|
||||
private TextView permissionText;
|
||||
private MaterialButton permissionButton;
|
||||
private MaterialButton manageButton;
|
||||
|
||||
public AttachmentKeyboard(@NonNull Context context) {
|
||||
super(context);
|
||||
@@ -60,6 +67,7 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
|
||||
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);
|
||||
this.manageButton = findViewById(R.id.attachment_keyboard_manage_button);
|
||||
|
||||
RecyclerView buttonList = findViewById(R.id.attachment_keyboard_button_list);
|
||||
buttonList.setItemAnimator(null);
|
||||
@@ -76,7 +84,17 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
|
||||
}
|
||||
});
|
||||
|
||||
manageButton.measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
|
||||
|
||||
manageButton.setOnClickListener(v -> {
|
||||
if (callback != null) {
|
||||
callback.onDisplayMoreContextMenu(v, true, false);
|
||||
}
|
||||
});
|
||||
|
||||
mediaList.setAdapter(mediaAdapter);
|
||||
mediaList.addOnScrollListener(new ScrollListener(manageButton.getMeasuredWidth()));
|
||||
buttonList.setAdapter(buttonAdapter);
|
||||
|
||||
buttonAdapter.registerAdapterDataObserver(new AttachmentButtonCenterHelper(buttonList));
|
||||
@@ -100,14 +118,37 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
|
||||
}
|
||||
|
||||
public void onMediaChanged(@NonNull List<Media> media) {
|
||||
if (StorageUtil.canReadFromMediaStore()) {
|
||||
mediaAdapter.setMedia(media);
|
||||
if (StorageUtil.canReadAllFromMediaStore()) {
|
||||
mediaList.setVisibility(VISIBLE);
|
||||
mediaAdapter.setMedia(media, false);
|
||||
permissionButton.setVisibility(GONE);
|
||||
permissionText.setVisibility(GONE);
|
||||
} else {
|
||||
permissionButton.setVisibility(VISIBLE);
|
||||
manageButton.setVisibility(GONE);
|
||||
} else if (StorageUtil.canOnlyReadSelectedMediaStore() && media.isEmpty()) {
|
||||
mediaList.setVisibility(GONE);
|
||||
manageButton.setVisibility(GONE);
|
||||
permissionText.setVisibility(VISIBLE);
|
||||
|
||||
permissionText.setText(getContext().getString(R.string.AttachmentKeyboard_no_photos_found));
|
||||
permissionButton.setVisibility(VISIBLE);
|
||||
permissionButton.setText(getContext().getString(R.string.AttachmentKeyboard_manage));
|
||||
permissionButton.setOnClickListener(v -> {
|
||||
if (callback != null) {
|
||||
callback.onDisplayMoreContextMenu(v, true, true);
|
||||
}
|
||||
});
|
||||
} else if (StorageUtil.canOnlyReadSelectedMediaStore()) {
|
||||
mediaList.setVisibility(VISIBLE);
|
||||
mediaAdapter.setMedia(media, true);
|
||||
manageButton.setVisibility(VISIBLE);
|
||||
permissionText.setVisibility(GONE);
|
||||
permissionButton.setVisibility(GONE);
|
||||
} else {
|
||||
mediaList.setVisibility(GONE);
|
||||
manageButton.setVisibility(GONE);
|
||||
permissionButton.setVisibility(VISIBLE);
|
||||
permissionButton.setText(getContext().getString(R.string.AttachmentKeyboard_allow_access));
|
||||
permissionText.setVisibility(VISIBLE);
|
||||
permissionText.setText(getContext().getString(R.string.AttachmentKeyboard_Signal_needs_permission_to_show_your_photos_and_videos));
|
||||
permissionButton.setOnClickListener(v -> {
|
||||
if (callback != null) {
|
||||
callback.onAttachmentPermissionsRequested();
|
||||
@@ -144,9 +185,81 @@ public class AttachmentKeyboard extends FrameLayout implements InputAwareLayout.
|
||||
return getVisibility() == VISIBLE;
|
||||
}
|
||||
|
||||
private class ScrollListener extends RecyclerView.OnScrollListener {
|
||||
|
||||
private final int originalWidth;
|
||||
private final int iconWidth;
|
||||
|
||||
private ValueAnimator animator;
|
||||
private boolean isCollapsed;
|
||||
|
||||
public ScrollListener(int originalWidth) {
|
||||
this.originalWidth = originalWidth;
|
||||
this.iconWidth = manageButton.getIconSize() + manageButton.getPaddingLeft() + manageButton.getPaddingRight();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
if (manageButton == null || recyclerView.getLayoutManager() == null || recyclerView.getAdapter() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
GridLayoutManager layoutManager = (GridLayoutManager) recyclerView.getLayoutManager();
|
||||
View childView = layoutManager.getChildAt(0);
|
||||
int position = layoutManager.findLastVisibleItemPosition();
|
||||
|
||||
boolean visibleFirstChild = childView != null && childView.getTop() == 0 && layoutManager.getPosition(childView) == 0;
|
||||
boolean visibleLastChild = position == recyclerView.getAdapter().getItemCount() - 1;
|
||||
boolean shouldCollapse = !visibleFirstChild && !visibleLastChild;
|
||||
|
||||
if (shouldCollapse && !isCollapsed) {
|
||||
isCollapsed = true;
|
||||
if (animator != null) {
|
||||
animator.cancel();
|
||||
}
|
||||
animator = createWidthAnimator(manageButton, originalWidth, iconWidth, new AnimationCompleteListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
manageButton.setText("");
|
||||
}
|
||||
});
|
||||
animator.start();
|
||||
} else if (!shouldCollapse && isCollapsed) {
|
||||
isCollapsed = false;
|
||||
if (animator != null) {
|
||||
animator.cancel();
|
||||
}
|
||||
manageButton.setText(getContext().getString(R.string.AttachmentKeyboard_manage));
|
||||
animator = createWidthAnimator(manageButton, iconWidth, originalWidth, null);
|
||||
animator.start();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static ValueAnimator createWidthAnimator(@NonNull View view,
|
||||
int originalWidth,
|
||||
int finalWidth,
|
||||
@Nullable AnimationCompleteListener onAnimationComplete)
|
||||
{
|
||||
ValueAnimator animator = ValueAnimator.ofInt(originalWidth, finalWidth).setDuration(ANIMATION_DURATION);
|
||||
|
||||
animator.addUpdateListener(animation -> {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
params.width = (int) animation.getAnimatedValue();
|
||||
view.setLayoutParams(params);
|
||||
});
|
||||
|
||||
if (onAnimationComplete != null) {
|
||||
animator.addListener(onAnimationComplete);
|
||||
}
|
||||
|
||||
return animator;
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onAttachmentMediaClicked(@NonNull Media media);
|
||||
void onAttachmentSelectorClicked(@NonNull AttachmentKeyboardButton button);
|
||||
void onAttachmentPermissionsRequested();
|
||||
void onDisplayMoreContextMenu(View v, boolean showAbove, boolean showAtStart);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,12 +20,15 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class AttachmentKeyboardMediaAdapter extends RecyclerView.Adapter<AttachmentKeyboardMediaAdapter.MediaViewHolder> {
|
||||
class AttachmentKeyboardMediaAdapter extends RecyclerView.Adapter<AttachmentKeyboardMediaAdapter.ViewHolder> {
|
||||
|
||||
private final List<Media> media;
|
||||
private final RequestManager requestManager;
|
||||
private final Listener listener;
|
||||
private final StableIdGenerator<Media> idGenerator;
|
||||
private static final int VIEW_TYPE_MEDIA = 0;
|
||||
private static final int VIEW_TYPE_PLACEHOLDER = 1;
|
||||
|
||||
private final List<MediaContent> media;
|
||||
private final RequestManager requestManager;
|
||||
private final Listener listener;
|
||||
private final StableIdGenerator<MediaContent> idGenerator;
|
||||
|
||||
AttachmentKeyboardMediaAdapter(@NonNull RequestManager requestManager, @NonNull Listener listener) {
|
||||
this.requestManager = requestManager;
|
||||
@@ -42,17 +45,21 @@ class AttachmentKeyboardMediaAdapter extends RecyclerView.Adapter<AttachmentKeyb
|
||||
}
|
||||
|
||||
@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));
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return switch (viewType) {
|
||||
case VIEW_TYPE_MEDIA -> new MediaViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.attachment_keyboad_media_item, parent, false));
|
||||
case VIEW_TYPE_PLACEHOLDER -> new PlaceholderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.attachment_keyboad_media_placeholder_item, parent, false));
|
||||
default -> throw new IllegalArgumentException("Unsupported viewType: " + viewType);
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull MediaViewHolder holder, int position) {
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(media.get(position), requestManager, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull MediaViewHolder holder) {
|
||||
public void onViewRecycled(@NonNull ViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@@ -61,9 +68,17 @@ class AttachmentKeyboardMediaAdapter extends RecyclerView.Adapter<AttachmentKeyb
|
||||
return media.size();
|
||||
}
|
||||
|
||||
public void setMedia(@NonNull List<Media> media) {
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return media.get(position).isPlaceholder ? VIEW_TYPE_PLACEHOLDER : VIEW_TYPE_MEDIA;
|
||||
}
|
||||
|
||||
public void setMedia(@NonNull List<Media> media, boolean addFooter) {
|
||||
this.media.clear();
|
||||
this.media.addAll(media);
|
||||
this.media.addAll(media.stream().map(MediaContent::new).collect(java.util.stream.Collectors.toList()));
|
||||
if (addFooter) {
|
||||
this.media.add(new MediaContent(true));
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@@ -71,7 +86,36 @@ class AttachmentKeyboardMediaAdapter extends RecyclerView.Adapter<AttachmentKeyb
|
||||
void onMediaClicked(@NonNull Media media);
|
||||
}
|
||||
|
||||
static class MediaViewHolder extends RecyclerView.ViewHolder {
|
||||
private class MediaContent {
|
||||
private Media media;
|
||||
private boolean isPlaceholder;
|
||||
|
||||
public MediaContent(Media media) {
|
||||
this.media = media;
|
||||
}
|
||||
|
||||
public MediaContent(boolean isPlaceholder) {
|
||||
this.isPlaceholder = isPlaceholder;
|
||||
}
|
||||
}
|
||||
|
||||
static abstract class ViewHolder extends RecyclerView.ViewHolder {
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
void bind(@NonNull MediaContent media, @NonNull RequestManager requestManager, @NonNull Listener listener) {}
|
||||
|
||||
void recycle() {}
|
||||
}
|
||||
|
||||
static class PlaceholderViewHolder extends ViewHolder {
|
||||
public PlaceholderViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
|
||||
static class MediaViewHolder extends ViewHolder {
|
||||
|
||||
private final ThumbnailView image;
|
||||
private final TextView duration;
|
||||
@@ -84,7 +128,9 @@ class AttachmentKeyboardMediaAdapter extends RecyclerView.Adapter<AttachmentKeyb
|
||||
videoIcon = itemView.findViewById(R.id.attachment_keyboard_item_video_icon);
|
||||
}
|
||||
|
||||
void bind(@NonNull Media media, @NonNull RequestManager requestManager, @NonNull Listener listener) {
|
||||
@Override
|
||||
void bind(@NonNull MediaContent mediaContent, @NonNull RequestManager requestManager, @NonNull Listener listener) {
|
||||
Media media = mediaContent.media;
|
||||
image.setImageResource(requestManager, media.getUri(), 400, 400);
|
||||
image.setOnClickListener(v -> listener.onMediaClicked(media));
|
||||
|
||||
@@ -99,6 +145,7 @@ class AttachmentKeyboardMediaAdapter extends RecyclerView.Adapter<AttachmentKeyb
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void recycle() {
|
||||
image.setOnClickListener(null);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import org.signal.core.util.DimensionUnit
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem
|
||||
import org.thoughtcrime.securesms.components.menu.SignalContextMenu
|
||||
|
||||
/**
|
||||
* A context menu shown when handling selected media only permissions.
|
||||
* Will give users the ability to go to settings or to choose more media to give permission to
|
||||
*/
|
||||
object ManageContextMenu {
|
||||
|
||||
fun show(
|
||||
context: Context,
|
||||
anchorView: View,
|
||||
rootView: ViewGroup = anchorView.rootView as ViewGroup,
|
||||
showAbove: Boolean = false,
|
||||
showAtStart: Boolean = false,
|
||||
onSelectMore: () -> Unit,
|
||||
onSettings: () -> Unit
|
||||
) {
|
||||
show(
|
||||
context = context,
|
||||
anchorView = anchorView,
|
||||
rootView = rootView,
|
||||
showAbove = showAbove,
|
||||
showAtStart = showAtStart,
|
||||
callbacks = object : Callbacks {
|
||||
override fun onSelectMore() = onSelectMore()
|
||||
override fun onSettings() = onSettings()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private fun show(
|
||||
context: Context,
|
||||
anchorView: View,
|
||||
rootView: ViewGroup = anchorView.rootView as ViewGroup,
|
||||
showAbove: Boolean = false,
|
||||
showAtStart: Boolean = false,
|
||||
callbacks: Callbacks
|
||||
) {
|
||||
val actions = mutableListOf<ActionItem>().apply {
|
||||
add(
|
||||
ActionItem(R.drawable.symbol_settings_android_24, context.getString(R.string.AttachmentKeyboard_go_to_settings)) {
|
||||
callbacks.onSettings()
|
||||
}
|
||||
)
|
||||
add(
|
||||
ActionItem(R.drawable.symbol_album_tilt_24, context.getString(R.string.AttachmentKeyboard_select_more_photos)) {
|
||||
callbacks.onSelectMore()
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!showAbove) {
|
||||
actions.reverse()
|
||||
}
|
||||
|
||||
SignalContextMenu.Builder(anchorView, rootView)
|
||||
.preferredHorizontalPosition(if (showAtStart) SignalContextMenu.HorizontalPosition.START else SignalContextMenu.HorizontalPosition.END)
|
||||
.preferredVerticalPosition(if (showAbove) SignalContextMenu.VerticalPosition.ABOVE else SignalContextMenu.VerticalPosition.BELOW)
|
||||
.offsetY(DimensionUnit.DP.toPixels(8f).toInt())
|
||||
.show(actions)
|
||||
}
|
||||
|
||||
private interface Callbacks {
|
||||
fun onSelectMore()
|
||||
fun onSettings()
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.LoggingFragment
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.conversation.AttachmentKeyboard
|
||||
import org.thoughtcrime.securesms.conversation.AttachmentKeyboardButton
|
||||
import org.thoughtcrime.securesms.conversation.ManageContextMenu
|
||||
import org.thoughtcrime.securesms.conversation.v2.ConversationViewModel
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mediasend.Media
|
||||
@@ -96,9 +97,32 @@ class AttachmentKeyboardFragment : LoggingFragment(R.layout.attachment_keyboard_
|
||||
Permissions.with(requireParentFragment())
|
||||
.request(*PermissionCompat.forImagesAndVideos())
|
||||
.ifNecessary()
|
||||
.onAllGranted { viewModel.refreshRecentMedia() }
|
||||
.withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio), null, R.string.AttachmentManager_signal_allow_storage, R.string.AttachmentManager_signal_to_show_photos, parentFragmentManager)
|
||||
.onAnyDenied { Toast.makeText(requireContext(), R.string.AttachmentManager_signal_needs_storage_access, Toast.LENGTH_LONG).show() }
|
||||
.onAnyResult { viewModel.refreshRecentMedia() }
|
||||
.withPermanentDenialDialog(getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio), null, R.string.AttachmentManager_signal_allow_storage, R.string.AttachmentManager_signal_to_show_photos, true, parentFragmentManager)
|
||||
.onSomeDenied {
|
||||
val deniedPermissions = PermissionCompat.getRequiredPermissionsForDenial()
|
||||
if (it.containsAll(deniedPermissions.toList())) {
|
||||
Toast.makeText(requireContext(), R.string.AttachmentManager_signal_needs_storage_access, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
.execute()
|
||||
}
|
||||
|
||||
override fun onDisplayMoreContextMenu(v: View, showAbove: Boolean, showAtStart: Boolean) {
|
||||
ManageContextMenu.show(
|
||||
context = requireContext(),
|
||||
anchorView = v,
|
||||
showAbove = showAbove,
|
||||
showAtStart = showAtStart,
|
||||
onSelectMore = { selectMorePhotos() },
|
||||
onSettings = { requireContext().startActivity(Permissions.getApplicationSettingsIntent(requireContext())) }
|
||||
)
|
||||
}
|
||||
|
||||
private fun selectMorePhotos() {
|
||||
Permissions.with(requireParentFragment())
|
||||
.request(*PermissionCompat.forImagesAndVideos())
|
||||
.onAnyResult { viewModel.refreshRecentMedia() }
|
||||
.execute()
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user