mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 01:40:07 +01:00
Move all files to natural position.
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
package org.thoughtcrime.securesms.mediaoverview;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
final class MediaActions {
|
||||
|
||||
private MediaActions() {
|
||||
}
|
||||
|
||||
static void handleSaveMedia(@NonNull Fragment fragment,
|
||||
@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords,
|
||||
@Nullable Runnable postExecute)
|
||||
{
|
||||
Context context = fragment.requireContext();
|
||||
|
||||
SaveAttachmentTask.showWarningDialog(context, (dialogInterface, which) -> Permissions.with(fragment)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(fragment.getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(context, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() ->
|
||||
new ProgressDialogAsyncTask<Void, Void, List<SaveAttachmentTask.Attachment>>(context,
|
||||
R.string.MediaOverviewActivity_collecting_attachments,
|
||||
R.string.please_wait)
|
||||
{
|
||||
@Override
|
||||
protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) {
|
||||
List<SaveAttachmentTask.Attachment> attachments = new LinkedList<>();
|
||||
|
||||
for (MediaDatabase.MediaRecord mediaRecord : mediaRecords) {
|
||||
if (mediaRecord.getAttachment().getDataUri() != null) {
|
||||
attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getDataUri(),
|
||||
mediaRecord.getContentType(),
|
||||
mediaRecord.getDate(),
|
||||
mediaRecord.getAttachment().getFileName()));
|
||||
}
|
||||
}
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<SaveAttachmentTask.Attachment> attachments) {
|
||||
super.onPostExecute(attachments);
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(context, attachments.size());
|
||||
saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR,
|
||||
attachments.toArray(new SaveAttachmentTask.Attachment[0]));
|
||||
|
||||
if (postExecute != null) postExecute.run();
|
||||
}
|
||||
}.execute()
|
||||
).execute(), mediaRecords.size());
|
||||
}
|
||||
|
||||
static void handleDeleteMedia(@NonNull Context context,
|
||||
@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords)
|
||||
{
|
||||
int recordCount = mediaRecords.size();
|
||||
Resources res = context.getResources();
|
||||
String confirmTitle = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
|
||||
recordCount,
|
||||
recordCount);
|
||||
String confirmMessage = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
|
||||
recordCount,
|
||||
recordCount);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
.setIconAttribute(R.attr.dialog_alert_icon)
|
||||
.setTitle(confirmTitle)
|
||||
.setMessage(confirmMessage)
|
||||
.setCancelable(true);
|
||||
|
||||
builder.setPositiveButton(R.string.delete, (dialogInterface, i) ->
|
||||
new ProgressDialogAsyncTask<MediaDatabase.MediaRecord, Void, Void>(context,
|
||||
R.string.MediaOverviewActivity_Media_delete_progress_title,
|
||||
R.string.MediaOverviewActivity_Media_delete_progress_message)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(MediaDatabase.MediaRecord... records) {
|
||||
if (records == null || records.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (MediaDatabase.MediaRecord record : records) {
|
||||
AttachmentUtil.deleteAttachment(context, record.getAttachment());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[0]))
|
||||
);
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mediaoverview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.database.loaders.GroupedThreadMediaLoader.GroupedThreadMedia;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataPair;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
|
||||
|
||||
private final Context context;
|
||||
private final boolean showThread;
|
||||
private final GlideRequests glideRequests;
|
||||
private final ItemClickListener itemClickListener;
|
||||
private final Map<AttachmentId, MediaRecord> selected = new HashMap<>();
|
||||
|
||||
private GroupedThreadMedia media;
|
||||
private boolean showFileSizes;
|
||||
private boolean detailView;
|
||||
|
||||
private static final int AUDIO_DETAIL = 1;
|
||||
private static final int GALLERY = 2;
|
||||
private static final int GALLERY_DETAIL = 3;
|
||||
private static final int DOCUMENT_DETAIL = 4;
|
||||
|
||||
void pause(RecyclerView.ViewHolder holder) {
|
||||
if (holder instanceof AudioDetailViewHolder) {
|
||||
((AudioDetailViewHolder) holder).pause();
|
||||
}
|
||||
}
|
||||
|
||||
void detach(RecyclerView.ViewHolder holder) {
|
||||
if (holder instanceof SelectableViewHolder) {
|
||||
((SelectableViewHolder) holder).onDetached();
|
||||
}
|
||||
}
|
||||
|
||||
private static class HeaderHolder extends HeaderViewHolder {
|
||||
TextView textView;
|
||||
|
||||
HeaderHolder(View itemView) {
|
||||
super(itemView);
|
||||
textView = itemView.findViewById(R.id.text);
|
||||
}
|
||||
}
|
||||
|
||||
MediaGalleryAllAdapter(@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
GroupedThreadMedia media,
|
||||
ItemClickListener clickListener,
|
||||
boolean showFileSizes,
|
||||
boolean showThread)
|
||||
{
|
||||
this.context = context;
|
||||
this.glideRequests = glideRequests;
|
||||
this.media = media;
|
||||
this.itemClickListener = clickListener;
|
||||
this.showFileSizes = showFileSizes;
|
||||
this.showThread = showThread;
|
||||
}
|
||||
|
||||
public void setMedia(GroupedThreadMedia media) {
|
||||
this.media = media;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) {
|
||||
return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_item_header, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType) {
|
||||
switch (itemType) {
|
||||
case GALLERY:
|
||||
return new GalleryViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_gallery_item, parent, false));
|
||||
case GALLERY_DETAIL:
|
||||
return new GalleryDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_media, parent, false));
|
||||
case AUDIO_DETAIL:
|
||||
return new AudioDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_audio, parent, false));
|
||||
default:
|
||||
return new DocumentDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_document, parent, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionItemViewType(int section, int offset) {
|
||||
MediaDatabase.MediaRecord mediaRecord = media.get(section, offset);
|
||||
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
|
||||
|
||||
if (slide.hasAudio()) return AUDIO_DETAIL;
|
||||
if (slide.hasImage() || slide.hasVideo()) return detailView ? GALLERY_DETAIL : GALLERY;
|
||||
if (slide.hasDocument()) return DOCUMENT_DETAIL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int section) {
|
||||
((HeaderHolder)viewHolder).textView.setText(media.getName(section));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset) {
|
||||
MediaDatabase.MediaRecord mediaRecord = media.get(section, offset);
|
||||
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
|
||||
|
||||
((SelectableViewHolder)viewHolder).bind(context, mediaRecord, slide);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
|
||||
super.onViewDetachedFromWindow(holder);
|
||||
if (holder instanceof SelectableViewHolder) {
|
||||
((SelectableViewHolder) holder).onDetached();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionCount() {
|
||||
return media.getSectionCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionItemCount(int section) {
|
||||
return media.getSectionItemCount(section);
|
||||
}
|
||||
|
||||
public void toggleSelection(@NonNull MediaRecord mediaRecord) {
|
||||
AttachmentId attachmentId = mediaRecord.getAttachment().getAttachmentId();
|
||||
MediaDatabase.MediaRecord removed = selected.remove(attachmentId);
|
||||
if (removed == null) {
|
||||
selected.put(attachmentId, mediaRecord);
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public int getSelectedMediaCount() {
|
||||
return selected.size();
|
||||
}
|
||||
|
||||
public long getSelectedMediaTotalFileSize() {
|
||||
//noinspection ConstantConditions attacment cannot be null if selected
|
||||
return Stream.of(selected.values())
|
||||
.collect(Collectors.summingLong(a -> a.getAttachment().getSize()));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Collection<MediaRecord> getSelectedMedia() {
|
||||
return new HashSet<>(selected.values());
|
||||
}
|
||||
|
||||
public void clearSelection() {
|
||||
selected.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void selectAllMedia() {
|
||||
int sectionCount = media.getSectionCount();
|
||||
for (int section = 0; section < sectionCount; section++) {
|
||||
int sectionItemCount = media.getSectionItemCount(section);
|
||||
for (int item = 0; item < sectionItemCount; item++) {
|
||||
MediaRecord mediaRecord = media.get(section, item);
|
||||
selected.put(mediaRecord.getAttachment().getAttachmentId(), mediaRecord);
|
||||
}
|
||||
}
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setShowFileSizes(boolean showFileSizes) {
|
||||
this.showFileSizes = showFileSizes;
|
||||
}
|
||||
|
||||
void setDetailView(boolean detailView) {
|
||||
this.detailView = detailView;
|
||||
}
|
||||
|
||||
class SelectableViewHolder extends ItemViewHolder {
|
||||
|
||||
private final View selectedIndicator;
|
||||
private MediaDatabase.MediaRecord mediaRecord;
|
||||
private boolean bound;
|
||||
|
||||
SelectableViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.selectedIndicator = itemView.findViewById(R.id.selected_indicator);
|
||||
}
|
||||
|
||||
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
if (bound) {
|
||||
unbind();
|
||||
}
|
||||
this.mediaRecord = mediaRecord;
|
||||
updateSelectedView();
|
||||
bound = true;
|
||||
}
|
||||
|
||||
void unbind() {
|
||||
bound = false;
|
||||
}
|
||||
|
||||
private void updateSelectedView() {
|
||||
if (selectedIndicator != null) {
|
||||
selectedIndicator.setVisibility(selected.containsKey(mediaRecord.getAttachment().getAttachmentId()) ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
boolean onLongClick() {
|
||||
itemClickListener.onMediaLongClicked(mediaRecord);
|
||||
updateSelectedView();
|
||||
return true;
|
||||
}
|
||||
|
||||
void onDetached() {
|
||||
if (bound) {
|
||||
unbind();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class GalleryViewHolder extends SelectableViewHolder {
|
||||
|
||||
private final ThumbnailView thumbnailView;
|
||||
private final TextView imageFileSize;
|
||||
|
||||
GalleryViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.thumbnailView = itemView.findViewById(R.id.image);
|
||||
this.imageFileSize = itemView.findViewById(R.id.image_file_size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
super.bind(context, mediaRecord, slide);
|
||||
|
||||
if (showFileSizes | detailView) {
|
||||
imageFileSize.setText(Util.getPrettyFileSize(slide.getFileSize()));
|
||||
imageFileSize.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
imageFileSize.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
thumbnailView.setImageResource(glideRequests, slide, false, false);
|
||||
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
thumbnailView.setOnLongClickListener(view -> onLongClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
void unbind() {
|
||||
thumbnailView.clear(glideRequests);
|
||||
super.unbind();
|
||||
}
|
||||
}
|
||||
|
||||
private abstract class DetailViewHolder extends SelectableViewHolder implements Observer<Pair<Recipient, Recipient>> {
|
||||
|
||||
protected final View itemView;
|
||||
private final TextView line1;
|
||||
private final TextView line2;
|
||||
private LiveDataPair<Recipient, Recipient> liveDataPair;
|
||||
private Optional<String> fileName;
|
||||
private String fileTypeDescription;
|
||||
private Handler handler;
|
||||
private Runnable selectForMarque;
|
||||
|
||||
DetailViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.line1 = itemView.findViewById(R.id.line1);
|
||||
this.line2 = itemView.findViewById(R.id.line2);
|
||||
this.itemView = itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
super.bind(context, mediaRecord, slide);
|
||||
|
||||
fileName = slide.getFileName();
|
||||
fileTypeDescription = getFileTypeDescription(context, slide);
|
||||
|
||||
line1.setText(fileName.or(fileTypeDescription));
|
||||
line2.setText(getLine2(context, mediaRecord, slide));
|
||||
itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
itemView.setOnLongClickListener(view -> onLongClick());
|
||||
selectForMarque = () -> line1.setSelected(true);
|
||||
handler = new Handler();
|
||||
handler.postDelayed(selectForMarque, 2500);
|
||||
|
||||
LiveRecipient from = mediaRecord.isOutgoing() ? Recipient.self().live() : Recipient.live(mediaRecord.getRecipientId());
|
||||
LiveRecipient to = Recipient.live(mediaRecord.getThreadRecipientId());
|
||||
|
||||
liveDataPair = new LiveDataPair<>(from.getLiveData(), to.getLiveData(), Recipient.UNKNOWN, Recipient.UNKNOWN);
|
||||
liveDataPair.observeForever(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
void unbind() {
|
||||
liveDataPair.removeObserver(this);
|
||||
handler.removeCallbacks(selectForMarque);
|
||||
line1.setSelected(false);
|
||||
super.unbind();
|
||||
}
|
||||
|
||||
private String getLine2(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
return context.getString(R.string.MediaOverviewActivity_detail_line_3_part,
|
||||
Util.getPrettyFileSize(slide.getFileSize()),
|
||||
getFileTypeDescription(context, slide),
|
||||
DateUtils.formatDateWithoutDayOfWeek(Locale.getDefault(), mediaRecord.getDate()));
|
||||
}
|
||||
|
||||
protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide){
|
||||
return context.getString(R.string.MediaOverviewActivity_file);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged(Pair<Recipient, Recipient> fromToPair) {
|
||||
line1.setText(describe(fromToPair.first(), fromToPair.second()));
|
||||
}
|
||||
|
||||
private String describe(@NonNull Recipient from, @NonNull Recipient thread) {
|
||||
if (from == Recipient.UNKNOWN && thread == Recipient.UNKNOWN) {
|
||||
return fileName.or(fileTypeDescription);
|
||||
}
|
||||
|
||||
String sentFromToString = getSentFromToString(from, thread);
|
||||
|
||||
if (fileName.isPresent()) {
|
||||
return context.getString(R.string.MediaOverviewActivity_detail_line_2_part,
|
||||
fileName.get(),
|
||||
sentFromToString);
|
||||
} else {
|
||||
return sentFromToString;
|
||||
}
|
||||
}
|
||||
|
||||
private String getSentFromToString(@NonNull Recipient from, @NonNull Recipient thread) {
|
||||
if (from.isLocalNumber() && from == thread) {
|
||||
return context.getString(R.string.note_to_self);
|
||||
}
|
||||
|
||||
if (showThread && (from.isLocalNumber() || thread.isGroup())) {
|
||||
if (from.isLocalNumber()) {
|
||||
return context.getString(R.string.MediaOverviewActivity_sent_by_you_to_s, thread.toShortString(context));
|
||||
} else {
|
||||
return context.getString(R.string.MediaOverviewActivity_sent_by_s_to_s, from.toShortString(context), thread.toShortString(context));
|
||||
}
|
||||
} else {
|
||||
if (from.isLocalNumber()) {
|
||||
return context.getString(R.string.MediaOverviewActivity_sent_by_you);
|
||||
} else {
|
||||
return context.getString(R.string.MediaOverviewActivity_sent_by_s, from.toShortString(context));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class DocumentDetailViewHolder extends DetailViewHolder {
|
||||
|
||||
private final TextView documentType;
|
||||
|
||||
DocumentDetailViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.documentType = itemView.findViewById(R.id.document_extension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
super.bind(context, mediaRecord, slide);
|
||||
|
||||
documentType.setText(slide.getFileType(context).or("").toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
private class AudioDetailViewHolder extends DetailViewHolder {
|
||||
|
||||
private final AudioView audioView;
|
||||
|
||||
AudioDetailViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.audioView = itemView.findViewById(R.id.audio);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
super.bind(context, mediaRecord, slide);
|
||||
|
||||
if (!slide.hasAudio()) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
audioView.setAudio((AudioSlide) slide, true);
|
||||
audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
}
|
||||
|
||||
@Override
|
||||
void unbind() {
|
||||
audioView.stopPlaybackAndReset();
|
||||
super.unbind();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) {
|
||||
return context.getString(R.string.MediaOverviewActivity_audio);
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
audioView.stopPlaybackAndReset();
|
||||
}
|
||||
}
|
||||
|
||||
private class GalleryDetailViewHolder extends DetailViewHolder {
|
||||
|
||||
private final ThumbnailView thumbnailView;
|
||||
|
||||
GalleryDetailViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.thumbnailView = itemView.findViewById(R.id.image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
super.bind(context, mediaRecord, slide);
|
||||
|
||||
thumbnailView.setImageResource(glideRequests, slide, false, false);
|
||||
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
thumbnailView.setOnLongClickListener(view -> onLongClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getFileTypeDescription(@NonNull Context context, @NonNull Slide slide) {
|
||||
if (slide.hasVideo()) return context.getString(R.string.MediaOverviewActivity_video);
|
||||
if (slide.hasImage()) return context.getString(R.string.MediaOverviewActivity_image);
|
||||
return super.getFileTypeDescription(context, slide);
|
||||
}
|
||||
|
||||
@Override
|
||||
void unbind() {
|
||||
thumbnailView.clear(glideRequests);
|
||||
super.unbind();
|
||||
}
|
||||
}
|
||||
|
||||
interface ItemClickListener {
|
||||
void onMediaClicked(@NonNull MediaDatabase.MediaRecord mediaRecord);
|
||||
void onMediaLongClicked(MediaDatabase.MediaRecord mediaRecord);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
/*
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mediaoverview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.Sorting;
|
||||
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Activity for displaying media attachments in-app
|
||||
*/
|
||||
public final class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
private static final String THREAD_ID_EXTRA = "thread_id";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
private Toolbar toolbar;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private TextView sortOrder;
|
||||
private View sortOrderArrow;
|
||||
private Sorting currentSorting;
|
||||
private Boolean currentDetailLayout;
|
||||
private MediaOverviewViewModel model;
|
||||
private AnimatingToggle displayToggle;
|
||||
private View viewGrid;
|
||||
private View viewDetail;
|
||||
private long threadId;
|
||||
|
||||
public static Intent forThread(@NonNull Context context, long threadId) {
|
||||
Intent intent = new Intent(context, MediaOverviewActivity.class);
|
||||
intent.putExtra(MediaOverviewActivity.THREAD_ID_EXTRA, threadId);
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent forAll(@NonNull Context context) {
|
||||
return forThread(context, MediaDatabase.ALL_THREADS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
setContentView(R.layout.media_overview_activity);
|
||||
|
||||
initializeResources();
|
||||
initializeToolbar();
|
||||
|
||||
boolean allThreads = threadId == MediaDatabase.ALL_THREADS;
|
||||
|
||||
fillTabLayoutIfFits(tabLayout);
|
||||
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
viewPager.setAdapter(new MediaOverviewPagerAdapter(getSupportFragmentManager()));
|
||||
|
||||
model = MediaOverviewViewModel.getMediaOverviewViewModel(this);
|
||||
model.setSortOrder(allThreads ? Sorting.Largest : Sorting.Newest);
|
||||
model.setDetailLayout(allThreads);
|
||||
model.getSortOrder().observe(this, this::setSorting);
|
||||
model.getDetailLayout().observe(this, this::setDetailLayout);
|
||||
|
||||
sortOrder.setOnClickListener(this::showSortOrderDialog);
|
||||
sortOrderArrow.setOnClickListener(this::showSortOrderDialog);
|
||||
|
||||
displayToggle.setOnClickListener(v -> setDetailLayout(!currentDetailLayout));
|
||||
|
||||
viewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
boolean gridToggleEnabled = allowGridSelectionOnPage(position);
|
||||
displayToggle.animate()
|
||||
.alpha(gridToggleEnabled ? 1 : 0)
|
||||
.start();
|
||||
displayToggle.setEnabled(gridToggleEnabled);
|
||||
}
|
||||
});
|
||||
|
||||
viewPager.setCurrentItem(allThreads ? 3 : 0);
|
||||
}
|
||||
|
||||
private static boolean allowGridSelectionOnPage(int page) {
|
||||
return page == 0;
|
||||
}
|
||||
|
||||
private void setSorting(@NonNull Sorting sorting) {
|
||||
if (currentSorting == sorting) return;
|
||||
|
||||
sortOrder.setText(sortingToString(sorting));
|
||||
currentSorting = sorting;
|
||||
model.setSortOrder(sorting);
|
||||
}
|
||||
|
||||
private static @StringRes int sortingToString(@NonNull Sorting sorting) {
|
||||
switch (sorting) {
|
||||
case Oldest : return R.string.MediaOverviewActivity_Oldest;
|
||||
case Newest : return R.string.MediaOverviewActivity_Newest;
|
||||
case Largest : return R.string.MediaOverviewActivity_Storage_used;
|
||||
default : throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
private void setDetailLayout(@NonNull Boolean detailLayout) {
|
||||
if (currentDetailLayout == detailLayout) return;
|
||||
|
||||
currentDetailLayout = detailLayout;
|
||||
model.setDetailLayout(detailLayout);
|
||||
displayToggle.display(detailLayout ? viewGrid : viewDetail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
Intent intent = getIntent();
|
||||
long threadId = intent.getLongExtra(THREAD_ID_EXTRA, Long.MIN_VALUE);
|
||||
|
||||
if (threadId == Long.MIN_VALUE) throw new AssertionError();
|
||||
|
||||
this.viewPager = findViewById(R.id.pager);
|
||||
this.toolbar = findViewById(R.id.toolbar);
|
||||
this.tabLayout = findViewById(R.id.tab_layout);
|
||||
this.sortOrder = findViewById(R.id.sort_order);
|
||||
this.sortOrderArrow = findViewById(R.id.sort_order_arrow);
|
||||
this.displayToggle = findViewById(R.id.grid_list_toggle);
|
||||
this.viewDetail = findViewById(R.id.view_detail);
|
||||
this.viewGrid = findViewById(R.id.view_grid);
|
||||
this.threadId = threadId;
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
if (threadId == MediaDatabase.ALL_THREADS) {
|
||||
getSupportActionBar().setTitle(R.string.MediaOverviewActivity_All_storage_use);
|
||||
} else {
|
||||
SimpleTask.run(() -> DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId),
|
||||
(recipient) -> {
|
||||
if (recipient != null) {
|
||||
getSupportActionBar().setTitle(recipient.toShortString(this));
|
||||
recipient.live().observe(this, r -> getSupportActionBar().setTitle(r.toShortString(this)));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void onEnterMultiSelect() {
|
||||
tabLayout.setEnabled(false);
|
||||
viewPager.setEnabled(false);
|
||||
}
|
||||
|
||||
public void onExitMultiSelect() {
|
||||
tabLayout.setEnabled(true);
|
||||
viewPager.setEnabled(true);
|
||||
}
|
||||
|
||||
private void showSortOrderDialog(View v) {
|
||||
new AlertDialog.Builder(MediaOverviewActivity.this)
|
||||
.setTitle(R.string.MediaOverviewActivity_Sort_by)
|
||||
.setSingleChoiceItems(R.array.MediaOverviewActivity_Sort_by,
|
||||
currentSorting.ordinal(),
|
||||
(dialog, item) -> {
|
||||
setSorting(Sorting.values()[item]);
|
||||
dialog.dismiss();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private static void fillTabLayoutIfFits(@NonNull TabLayout tabLayout) {
|
||||
tabLayout.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||
int totalWidth = 0;
|
||||
int maxWidth = 0;
|
||||
ViewGroup tabs = (ViewGroup) tabLayout.getChildAt(0);
|
||||
|
||||
for (int i = 0; i < tabLayout.getTabCount(); i++) {
|
||||
int tabWidth = tabs.getChildAt(i).getWidth();
|
||||
totalWidth += tabWidth;
|
||||
maxWidth = Math.max(maxWidth, tabWidth);
|
||||
}
|
||||
|
||||
int viewWidth = right - left;
|
||||
if (totalWidth < viewWidth) {
|
||||
tabLayout.setTabMode(TabLayout.MODE_FIXED);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private class MediaOverviewPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
private final List<Pair<MediaLoader.MediaType, CharSequence>> pages;
|
||||
|
||||
MediaOverviewPagerAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
|
||||
pages = new ArrayList<>(4);
|
||||
pages.add(new Pair<>(MediaLoader.MediaType.GALLERY, getString(R.string.MediaOverviewActivity_Media)));
|
||||
pages.add(new Pair<>(MediaLoader.MediaType.DOCUMENT, getString(R.string.MediaOverviewActivity_Files)));
|
||||
pages.add(new Pair<>(MediaLoader.MediaType.AUDIO, getString(R.string.MediaOverviewActivity_Audio)));
|
||||
pages.add(new Pair<>(MediaLoader.MediaType.ALL, getString(R.string.MediaOverviewActivity_All)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Fragment getItem(int position) {
|
||||
MediaOverviewPageFragment.GridMode gridMode = allowGridSelectionOnPage(position)
|
||||
? MediaOverviewPageFragment.GridMode.FOLLOW_MODEL
|
||||
: MediaOverviewPageFragment.GridMode.FIXED_DETAIL;
|
||||
|
||||
return MediaOverviewPageFragment.newInstance(threadId, pages.get(position).first(), gridMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return pages.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return pages.get(position).second();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
package org.thoughtcrime.securesms.mediaoverview;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
|
||||
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.GroupedThreadMediaLoader;
|
||||
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public final class MediaOverviewPageFragment extends Fragment
|
||||
implements MediaGalleryAllAdapter.ItemClickListener,
|
||||
LoaderManager.LoaderCallbacks<GroupedThreadMediaLoader.GroupedThreadMedia>
|
||||
{
|
||||
|
||||
private static final String TAG = Log.tag(MediaOverviewPageFragment.class);
|
||||
|
||||
private static final String THREAD_ID_EXTRA = "thread_id";
|
||||
private static final String MEDIA_TYPE_EXTRA = "media_type";
|
||||
private static final String GRID_MODE = "grid_mode";
|
||||
|
||||
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
|
||||
private MediaDatabase.Sorting sorting = MediaDatabase.Sorting.Newest;
|
||||
private MediaLoader.MediaType mediaType = MediaLoader.MediaType.GALLERY;
|
||||
private long threadId;
|
||||
private TextView noMedia;
|
||||
private RecyclerView recyclerView;
|
||||
private StickyHeaderGridLayoutManager gridManager;
|
||||
private ActionMode actionMode;
|
||||
private boolean detail;
|
||||
private MediaGalleryAllAdapter adapter;
|
||||
private GridMode gridMode;
|
||||
|
||||
public static @NonNull Fragment newInstance(long threadId,
|
||||
@NonNull MediaLoader.MediaType mediaType,
|
||||
@NonNull GridMode gridMode)
|
||||
{
|
||||
MediaOverviewPageFragment mediaOverviewAllFragment = new MediaOverviewPageFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putLong(THREAD_ID_EXTRA, threadId);
|
||||
args.putInt(MEDIA_TYPE_EXTRA, mediaType.ordinal());
|
||||
args.putInt(GRID_MODE, gridMode.ordinal());
|
||||
mediaOverviewAllFragment.setArguments(args);
|
||||
|
||||
return mediaOverviewAllFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
Bundle arguments = requireArguments();
|
||||
|
||||
threadId = arguments.getLong(THREAD_ID_EXTRA, Long.MIN_VALUE);
|
||||
mediaType = MediaLoader.MediaType.values()[arguments.getInt(MEDIA_TYPE_EXTRA)];
|
||||
gridMode = GridMode.values()[arguments.getInt(GRID_MODE)];
|
||||
|
||||
if (threadId == Long.MIN_VALUE) throw new AssertionError();
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
Context context = requireContext();
|
||||
View view = inflater.inflate(R.layout.media_overview_page_fragment, container, false);
|
||||
|
||||
this.recyclerView = view.findViewById(R.id.media_grid);
|
||||
this.noMedia = view.findViewById(R.id.no_images);
|
||||
this.gridManager = new StickyHeaderGridLayoutManager(getResources().getInteger(R.integer.media_overview_cols));
|
||||
|
||||
this.adapter = new MediaGalleryAllAdapter(context,
|
||||
GlideApp.with(this),
|
||||
new GroupedThreadMediaLoader.EmptyGroupedThreadMedia(),
|
||||
this,
|
||||
sorting.isRelatedToFileSize(),
|
||||
threadId == MediaDatabase.ALL_THREADS);
|
||||
this.recyclerView.setAdapter(adapter);
|
||||
this.recyclerView.setLayoutManager(gridManager);
|
||||
this.recyclerView.setHasFixedSize(true);
|
||||
|
||||
MediaOverviewViewModel viewModel = MediaOverviewViewModel.getMediaOverviewViewModel(requireActivity());
|
||||
|
||||
viewModel.getSortOrder()
|
||||
.observe(this, sorting -> {
|
||||
if (sorting != null) {
|
||||
this.sorting = sorting;
|
||||
adapter.setShowFileSizes(sorting.isRelatedToFileSize());
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
refreshActionModeTitle();
|
||||
}
|
||||
});
|
||||
|
||||
if (gridMode == GridMode.FOLLOW_MODEL) {
|
||||
viewModel.getDetailLayout()
|
||||
.observe(this, this::setDetailView);
|
||||
} else {
|
||||
setDetailView(gridMode == GridMode.FIXED_DETAIL);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void setDetailView(boolean detail) {
|
||||
this.detail = detail;
|
||||
adapter.setDetailView(detail);
|
||||
refreshLayoutManager();
|
||||
refreshActionModeTitle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
if (gridManager != null) {
|
||||
refreshLayoutManager();
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshLayoutManager() {
|
||||
this.gridManager = new StickyHeaderGridLayoutManager(detail ? 1 : getResources().getInteger(R.integer.media_overview_cols));
|
||||
this.recyclerView.setLayoutManager(gridManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<GroupedThreadMediaLoader.GroupedThreadMedia> onCreateLoader(int i, Bundle bundle) {
|
||||
return new GroupedThreadMediaLoader(requireContext(), threadId, mediaType, sorting);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<GroupedThreadMediaLoader.GroupedThreadMedia> loader, GroupedThreadMediaLoader.GroupedThreadMedia groupedThreadMedia) {
|
||||
((MediaGalleryAllAdapter) recyclerView.getAdapter()).setMedia(groupedThreadMedia);
|
||||
((MediaGalleryAllAdapter) recyclerView.getAdapter()).notifyAllSectionsDataSetChanged();
|
||||
|
||||
noMedia.setVisibility(recyclerView.getAdapter().getItemCount() > 0 ? View.GONE : View.VISIBLE);
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<GroupedThreadMediaLoader.GroupedThreadMedia> cursorLoader) {
|
||||
((MediaGalleryAllAdapter) recyclerView.getAdapter()).setMedia(new GroupedThreadMediaLoader.EmptyGroupedThreadMedia());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaClicked(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
if (actionMode != null) {
|
||||
handleMediaMultiSelectClick(mediaRecord);
|
||||
} else {
|
||||
handleMediaPreviewClick(mediaRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
int childCount = recyclerView.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
adapter.pause(recyclerView.getChildViewHolder(recyclerView.getChildAt(i)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
int childCount = recyclerView.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
adapter.detach(recyclerView.getChildViewHolder(recyclerView.getChildAt(i)));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMediaMultiSelectClick(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
MediaGalleryAllAdapter adapter = getListAdapter();
|
||||
|
||||
adapter.toggleSelection(mediaRecord);
|
||||
if (adapter.getSelectedMediaCount() == 0) {
|
||||
actionMode.finish();
|
||||
} else {
|
||||
refreshActionModeTitle();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMediaPreviewClick(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
if (mediaRecord.getAttachment().getDataUri() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseAttachment attachment = mediaRecord.getAttachment();
|
||||
|
||||
if (MediaUtil.isVideo(attachment) || MediaUtil.isImage(attachment)) {
|
||||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
||||
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
|
||||
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, true);
|
||||
intent.putExtra(MediaPreviewActivity.HIDE_ALL_MEDIA_EXTRA, true);
|
||||
intent.putExtra(MediaPreviewActivity.SHOW_THREAD_EXTRA, threadId == MediaDatabase.ALL_THREADS);
|
||||
intent.putExtra(MediaPreviewActivity.SORTING_EXTRA, sorting.ordinal());
|
||||
|
||||
intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
|
||||
context.startActivity(intent);
|
||||
} else {
|
||||
if (!MediaUtil.isAudio(attachment)) {
|
||||
showFileExternally(context, mediaRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void showFileExternally(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
Uri uri = mediaRecord.getAttachment().getDataUri();
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.setDataAndType(PartAuthority.getAttachmentPublicUri(uri), mediaRecord.getContentType());
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "No activity existed to view the media.");
|
||||
Toast.makeText(context, R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaLongClicked(MediaDatabase.MediaRecord mediaRecord) {
|
||||
((MediaGalleryAllAdapter) recyclerView.getAdapter()).toggleSelection(mediaRecord);
|
||||
recyclerView.getAdapter().notifyDataSetChanged();
|
||||
|
||||
if (actionMode == null) {
|
||||
enterMultiSelect();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSelectAllMedia() {
|
||||
getListAdapter().selectAllMedia();
|
||||
refreshActionModeTitle();
|
||||
}
|
||||
|
||||
private void refreshActionModeTitle() {
|
||||
if (actionMode != null) {
|
||||
actionMode.setTitle(getActionModeTitle());
|
||||
}
|
||||
}
|
||||
|
||||
private String getActionModeTitle() {
|
||||
MediaGalleryAllAdapter adapter = getListAdapter();
|
||||
int mediaCount = adapter.getSelectedMediaCount();
|
||||
boolean showTotalFileSize = detail ||
|
||||
mediaType != MediaLoader.MediaType.GALLERY ||
|
||||
sorting == MediaDatabase.Sorting.Largest;
|
||||
|
||||
if (showTotalFileSize) {
|
||||
long totalFileSize = adapter.getSelectedMediaTotalFileSize();
|
||||
return getResources().getQuantityString(R.plurals.MediaOverviewActivity_d_items_s,
|
||||
mediaCount,
|
||||
mediaCount,
|
||||
Util.getPrettyFileSize(totalFileSize));
|
||||
} else {
|
||||
return getResources().getQuantityString(R.plurals.MediaOverviewActivity_d_items,
|
||||
mediaCount,
|
||||
mediaCount);
|
||||
}
|
||||
}
|
||||
|
||||
private MediaGalleryAllAdapter getListAdapter() {
|
||||
return (MediaGalleryAllAdapter) recyclerView.getAdapter();
|
||||
}
|
||||
|
||||
private void enterMultiSelect() {
|
||||
FragmentActivity activity = requireActivity();
|
||||
actionMode = ((AppCompatActivity) activity).startSupportActionMode(actionModeCallback);
|
||||
((MediaOverviewActivity) activity).onEnterMultiSelect();
|
||||
}
|
||||
|
||||
private class ActionModeCallback implements ActionMode.Callback {
|
||||
|
||||
private int originalStatusBarColor;
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.media_overview_context, menu);
|
||||
mode.setTitle(getActionModeTitle());
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Window window = requireActivity().getWindow();
|
||||
originalStatusBarColor = window.getStatusBarColor();
|
||||
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.save:
|
||||
MediaActions.handleSaveMedia(MediaOverviewPageFragment.this,
|
||||
getListAdapter().getSelectedMedia(),
|
||||
() -> actionMode.finish());
|
||||
return true;
|
||||
case R.id.delete:
|
||||
MediaActions.handleDeleteMedia(requireContext(), getListAdapter().getSelectedMedia());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.select_all:
|
||||
handleSelectAllMedia();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
actionMode = null;
|
||||
getListAdapter().clearSelection();
|
||||
|
||||
FragmentActivity activity = requireActivity();
|
||||
|
||||
((MediaOverviewActivity) activity).onExitMultiSelect();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
activity.getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum GridMode {
|
||||
FIXED_DETAIL,
|
||||
FOLLOW_MODEL
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package org.thoughtcrime.securesms.mediaoverview;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.SavedStateHandle;
|
||||
import androidx.lifecycle.SavedStateViewModelFactory;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.Sorting;
|
||||
|
||||
public class MediaOverviewViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Sorting> sortOrder;
|
||||
private final MutableLiveData<Boolean> detailLayout;
|
||||
|
||||
public MediaOverviewViewModel(@NonNull SavedStateHandle savedStateHandle) {
|
||||
sortOrder = savedStateHandle.getLiveData("SORT_ORDER", Sorting.Newest);
|
||||
detailLayout = savedStateHandle.getLiveData("DETAIL_LAYOUT", false);
|
||||
}
|
||||
|
||||
public LiveData<Sorting> getSortOrder() {
|
||||
return sortOrder;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getDetailLayout() {
|
||||
return detailLayout;
|
||||
}
|
||||
|
||||
public void setSortOrder(@NonNull Sorting sortOrder) {
|
||||
this.sortOrder.setValue(sortOrder);
|
||||
}
|
||||
|
||||
public void setDetailLayout(boolean detailLayout) {
|
||||
this.detailLayout.setValue(detailLayout);
|
||||
}
|
||||
|
||||
static MediaOverviewViewModel getMediaOverviewViewModel(@NonNull FragmentActivity activity) {
|
||||
SavedStateViewModelFactory savedStateViewModelFactory = new SavedStateViewModelFactory(activity.getApplication(), activity);
|
||||
|
||||
return ViewModelProviders.of(activity, savedStateViewModelFactory).get(MediaOverviewViewModel.class);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user