Move all files to natural position.

This commit is contained in:
Alan Evans
2020-01-06 10:52:48 -05:00
parent 0df36047e7
commit 9ebe920195
3016 changed files with 6 additions and 36 deletions

View File

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

View File

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

View File

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

View File

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

View File

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