Add selected photos access.

This commit is contained in:
Michelle Tang
2024-07-30 11:35:48 -04:00
committed by mtang-signal
parent 4f001a0c95
commit 57adab858c
19 changed files with 505 additions and 65 deletions

View File

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

View File

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

View File

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

View File

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