That fuck shit the fascists are using
1package org.tm.archive.mediaoverview;
2
3import android.app.ActivityOptions;
4import android.content.ActivityNotFoundException;
5import android.content.Context;
6import android.content.Intent;
7import android.content.res.Configuration;
8import android.net.Uri;
9import android.os.Bundle;
10import android.view.LayoutInflater;
11import android.view.Menu;
12import android.view.MenuItem;
13import android.view.View;
14import android.view.ViewGroup;
15import android.widget.TextView;
16import android.widget.Toast;
17
18import androidx.annotation.NonNull;
19import androidx.annotation.Nullable;
20import androidx.appcompat.app.AppCompatActivity;
21import androidx.appcompat.view.ActionMode;
22import androidx.fragment.app.Fragment;
23import androidx.fragment.app.FragmentActivity;
24import androidx.lifecycle.Observer;
25import androidx.loader.app.LoaderManager;
26import androidx.loader.content.Loader;
27import androidx.recyclerview.widget.RecyclerView;
28
29import com.bumptech.glide.Glide;
30import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
31
32import org.signal.core.util.DimensionUnit;
33import org.signal.core.util.logging.Log;
34import org.tm.archive.R;
35import org.tm.archive.attachments.DatabaseAttachment;
36import org.tm.archive.components.menu.ActionItem;
37import org.tm.archive.components.menu.SignalBottomActionBar;
38import org.tm.archive.components.voice.VoiceNoteMediaController;
39import org.tm.archive.components.voice.VoiceNotePlaybackState;
40import org.tm.archive.database.MediaTable;
41import org.tm.archive.database.loaders.GroupedThreadMediaLoader;
42import org.tm.archive.database.loaders.MediaLoader;
43import org.tm.archive.mediapreview.MediaIntentFactory;
44import org.tm.archive.mediapreview.MediaPreviewV2Activity;
45import org.tm.archive.mms.PartAuthority;
46import org.tm.archive.util.BottomOffsetDecoration;
47import org.tm.archive.util.MediaUtil;
48import org.tm.archive.util.Util;
49import org.tm.archive.util.ViewUtil;
50
51import java.util.Arrays;
52import java.util.Objects;
53
54public final class MediaOverviewPageFragment extends Fragment
55 implements MediaGalleryAllAdapter.ItemClickListener,
56 MediaGalleryAllAdapter.AudioItemListener,
57 LoaderManager.LoaderCallbacks<GroupedThreadMediaLoader.GroupedThreadMedia>
58{
59
60 private static final String TAG = Log.tag(MediaOverviewPageFragment.class);
61
62 private static final String THREAD_ID_EXTRA = "thread_id";
63 private static final String MEDIA_TYPE_EXTRA = "media_type";
64 private static final String GRID_MODE = "grid_mode";
65
66 private final ActionModeCallback actionModeCallback = new ActionModeCallback();
67 private MediaTable.Sorting sorting = MediaTable.Sorting.Newest;
68 private MediaLoader.MediaType mediaType = MediaLoader.MediaType.GALLERY;
69 private long threadId;
70 private TextView noMedia;
71 private RecyclerView recyclerView;
72 private StickyHeaderGridLayoutManager gridManager;
73 private ActionMode actionMode;
74 private boolean detail;
75 private MediaGalleryAllAdapter adapter;
76 private GridMode gridMode;
77 private VoiceNoteMediaController voiceNoteMediaController;
78 private SignalBottomActionBar bottomActionBar;
79
80 public static @NonNull Fragment newInstance(long threadId,
81 @NonNull MediaLoader.MediaType mediaType,
82 @NonNull GridMode gridMode)
83 {
84 MediaOverviewPageFragment mediaOverviewAllFragment = new MediaOverviewPageFragment();
85 Bundle args = new Bundle();
86 args.putLong(THREAD_ID_EXTRA, threadId);
87 args.putInt(MEDIA_TYPE_EXTRA, mediaType.ordinal());
88 args.putInt(GRID_MODE, gridMode.ordinal());
89 mediaOverviewAllFragment.setArguments(args);
90
91 return mediaOverviewAllFragment;
92 }
93
94 @Override
95 public void onCreate(Bundle bundle) {
96 super.onCreate(bundle);
97
98 Bundle arguments = requireArguments();
99
100 threadId = arguments.getLong(THREAD_ID_EXTRA, Long.MIN_VALUE);
101 mediaType = MediaLoader.MediaType.values()[arguments.getInt(MEDIA_TYPE_EXTRA)];
102 gridMode = GridMode.values()[arguments.getInt(GRID_MODE)];
103
104 if (threadId == Long.MIN_VALUE) throw new AssertionError();
105
106 LoaderManager.getInstance(this).initLoader(0, null, this);
107 }
108
109 @Override
110 public void onActivityCreated(@Nullable Bundle savedInstanceState) {
111 super.onActivityCreated(savedInstanceState);
112
113 voiceNoteMediaController = new VoiceNoteMediaController(requireActivity(), false);
114 }
115
116 @Override
117 public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
118 Context context = requireContext();
119 View view = inflater.inflate(R.layout.media_overview_page_fragment, container, false);
120 int spans = getResources().getInteger(R.integer.media_overview_cols);
121
122 this.recyclerView = view.findViewById(R.id.media_grid);
123 this.noMedia = view.findViewById(R.id.no_images);
124 this.bottomActionBar = view.findViewById(R.id.media_overview_bottom_action_bar);
125 this.gridManager = new StickyHeaderGridLayoutManager(spans);
126
127 this.adapter = new MediaGalleryAllAdapter(context,
128 Glide.with(this),
129 new GroupedThreadMediaLoader.EmptyGroupedThreadMedia(),
130 this,
131 this,
132 sorting.isRelatedToFileSize(),
133 threadId == MediaTable.ALL_THREADS);
134 this.recyclerView.setAdapter(adapter);
135 this.recyclerView.setLayoutManager(gridManager);
136 this.recyclerView.setHasFixedSize(true);
137 this.recyclerView.addItemDecoration(new MediaGridDividerDecoration(spans, ViewUtil.dpToPx(4), adapter));
138 this.recyclerView.addItemDecoration(new BottomOffsetDecoration(ViewUtil.dpToPx(160)));
139
140 MediaOverviewViewModel viewModel = MediaOverviewViewModel.getMediaOverviewViewModel(requireActivity());
141
142 viewModel.getSortOrder()
143 .observe(getViewLifecycleOwner(), sorting -> {
144 if (sorting != null) {
145 this.sorting = sorting;
146 adapter.setShowFileSizes(sorting.isRelatedToFileSize());
147 LoaderManager.getInstance(this).restartLoader(0, null, this);
148 updateMultiSelect();
149 }
150 });
151
152 if (gridMode == GridMode.FOLLOW_MODEL) {
153 viewModel.getDetailLayout()
154 .observe(getViewLifecycleOwner(), this::setDetailView);
155 } else {
156 setDetailView(gridMode == GridMode.FIXED_DETAIL);
157 }
158
159 return view;
160 }
161
162 private void setDetailView(boolean detail) {
163 this.detail = detail;
164 adapter.setDetailView(detail);
165 refreshLayoutManager();
166 updateMultiSelect();
167 }
168
169 @Override
170 public void onConfigurationChanged(@NonNull Configuration newConfig) {
171 super.onConfigurationChanged(newConfig);
172 if (gridManager != null) {
173 refreshLayoutManager();
174 }
175 }
176
177 private void refreshLayoutManager() {
178 this.gridManager = new StickyHeaderGridLayoutManager(detail ? 1 : getResources().getInteger(R.integer.media_overview_cols));
179 this.recyclerView.setLayoutManager(gridManager);
180 }
181
182 @Override
183 public @NonNull Loader<GroupedThreadMediaLoader.GroupedThreadMedia> onCreateLoader(int i, Bundle bundle) {
184 return new GroupedThreadMediaLoader(requireContext(), threadId, mediaType, sorting);
185 }
186
187 @Override
188 public void onLoadFinished(@NonNull Loader<GroupedThreadMediaLoader.GroupedThreadMedia> loader, GroupedThreadMediaLoader.GroupedThreadMedia groupedThreadMedia) {
189 ((MediaGalleryAllAdapter) recyclerView.getAdapter()).setMedia(groupedThreadMedia);
190 ((MediaGalleryAllAdapter) recyclerView.getAdapter()).notifyAllSectionsDataSetChanged();
191
192 noMedia.setVisibility(recyclerView.getAdapter().getItemCount() > 0 ? View.GONE : View.VISIBLE);
193 getActivity().invalidateOptionsMenu();
194 }
195
196 @Override
197 public void onLoaderReset(@NonNull Loader<GroupedThreadMediaLoader.GroupedThreadMedia> cursorLoader) {
198 ((MediaGalleryAllAdapter) recyclerView.getAdapter()).setMedia(new GroupedThreadMediaLoader.EmptyGroupedThreadMedia());
199 }
200
201 @Override
202 public void onMediaClicked(@NonNull View view, @NonNull MediaTable.MediaRecord mediaRecord) {
203 if (actionMode != null) {
204 handleMediaMultiSelectClick(mediaRecord);
205 } else {
206 handleMediaPreviewClick(view, mediaRecord);
207 }
208 }
209
210 @Override
211 public void onDestroy() {
212 super.onDestroy();
213 if (recyclerView != null) {
214 int childCount = recyclerView.getChildCount();
215 for (int i = 0; i < childCount; i++) {
216 adapter.detach(recyclerView.getChildViewHolder(recyclerView.getChildAt(i)));
217 }
218 }
219 }
220
221 private void handleMediaMultiSelectClick(@NonNull MediaTable.MediaRecord mediaRecord) {
222 MediaGalleryAllAdapter adapter = getListAdapter();
223
224 adapter.toggleSelection(mediaRecord);
225 if (adapter.getSelectedMediaCount() == 0) {
226 actionMode.finish();
227 } else {
228 updateMultiSelect();
229 }
230 }
231
232 private void handleMediaPreviewClick(@NonNull View view, @NonNull MediaTable.MediaRecord mediaRecord) {
233 if (mediaRecord.getAttachment().getUri() == null) {
234 return;
235 }
236
237 Context context = getContext();
238 if (context == null) {
239 return;
240 }
241
242 DatabaseAttachment attachment = mediaRecord.getAttachment();
243
244 if (MediaUtil.isVideo(attachment) || MediaUtil.isImage(attachment)) {
245 MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs(
246 threadId,
247 mediaRecord.getDate(),
248 Objects.requireNonNull(mediaRecord.getAttachment().getUri()),
249 mediaRecord.getContentType(),
250 mediaRecord.getAttachment().size,
251 mediaRecord.getAttachment().caption,
252 true,
253 true,
254 threadId == MediaTable.ALL_THREADS,
255 true,
256 sorting,
257 attachment.videoGif,
258 new MediaIntentFactory.SharedElementArgs(
259 attachment.width,
260 attachment.height,
261 DimensionUnit.DP.toDp(12),
262 DimensionUnit.DP.toDp(12),
263 DimensionUnit.DP.toDp(12),
264 DimensionUnit.DP.toDp(12)
265 ),
266 false);
267 view.setTransitionName(MediaPreviewV2Activity.SHARED_ELEMENT_TRANSITION_NAME);
268 ActivityOptions options = ActivityOptions.makeSceneTransitionAnimation(requireActivity(), view, MediaPreviewV2Activity.SHARED_ELEMENT_TRANSITION_NAME);
269 context.startActivity(MediaIntentFactory.create(context, args), options.toBundle());
270 } else {
271 if (!MediaUtil.isAudio(attachment)) {
272 showFileExternally(context, mediaRecord);
273 }
274 }
275 }
276
277 private static void showFileExternally(@NonNull Context context, @NonNull MediaTable.MediaRecord mediaRecord) {
278 Uri uri = mediaRecord.getAttachment().getUri();
279
280 Intent intent = new Intent(Intent.ACTION_VIEW);
281 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
282 intent.setDataAndType(PartAuthority.getAttachmentPublicUri(uri), mediaRecord.getContentType());
283 try {
284 context.startActivity(intent);
285 } catch (ActivityNotFoundException e) {
286 Log.w(TAG, "No activity existed to view the media.");
287 Toast.makeText(context, R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show();
288 }
289 }
290
291 @Override
292 public void onMediaLongClicked(MediaTable.MediaRecord mediaRecord) {
293 if (actionMode == null) {
294 enterMultiSelect();
295 }
296
297 handleMediaMultiSelectClick(mediaRecord);
298 }
299
300 private void handleSelectAllMedia() {
301 getListAdapter().selectAllMedia();
302 updateMultiSelect();
303 }
304
305 private String getActionModeTitle() {
306 MediaGalleryAllAdapter adapter = getListAdapter();
307 int mediaCount = adapter.getSelectedMediaCount();
308 long totalFileSize = adapter.getSelectedMediaTotalFileSize();
309
310 return getResources().getQuantityString(R.plurals.MediaOverviewActivity_d_selected_s,
311 mediaCount,
312 mediaCount,
313 Util.getPrettyFileSize(totalFileSize));
314 }
315
316 private MediaGalleryAllAdapter getListAdapter() {
317 return (MediaGalleryAllAdapter) recyclerView.getAdapter();
318 }
319
320 private void enterMultiSelect() {
321 FragmentActivity activity = requireActivity();
322 actionMode = ((AppCompatActivity) activity).startSupportActionMode(actionModeCallback);
323 ((MediaOverviewActivity) activity).onEnterMultiSelect();
324 ViewUtil.animateIn(bottomActionBar, bottomActionBar.getEnterAnimation());
325 updateMultiSelect();
326 }
327
328 private void exitMultiSelect() {
329 actionMode.finish();
330 actionMode = null;
331 ViewUtil.animateOut(bottomActionBar, bottomActionBar.getExitAnimation());
332 }
333
334 private void updateMultiSelect() {
335 if (actionMode != null) {
336 actionMode.setTitle(getActionModeTitle());
337
338 int selectionCount = getListAdapter().getSectionCount();
339
340 bottomActionBar.setItems(Arrays.asList(
341 new ActionItem(R.drawable.symbol_save_android_24, getResources().getQuantityString(R.plurals.MediaOverviewActivity_save_plural, selectionCount), () -> {
342 MediaActions.handleSaveMedia(MediaOverviewPageFragment.this,
343 getListAdapter().getSelectedMedia(),
344 this::exitMultiSelect);
345 }),
346 new ActionItem(R.drawable.symbol_check_circle_24, getString(R.string.MediaOverviewActivity_select_all), this::handleSelectAllMedia),
347 new ActionItem(R.drawable.symbol_trash_24, getResources().getQuantityString(R.plurals.MediaOverviewActivity_delete_plural, selectionCount), () -> {
348 MediaActions.handleDeleteMedia(requireContext(), getListAdapter().getSelectedMedia());
349 exitMultiSelect();
350 })
351 ));
352 }
353 }
354
355
356 @Override
357 public void onPlay(@NonNull Uri audioUri, double progress, long messageId) {
358 voiceNoteMediaController.startSinglePlayback(audioUri, messageId, progress);
359 }
360
361 @Override
362 public void onPause(@NonNull Uri audioUri) {
363 voiceNoteMediaController.pausePlayback(audioUri);
364 }
365
366 @Override
367 public void onSeekTo(@NonNull Uri audioUri, double progress) {
368 voiceNoteMediaController.seekToPosition(audioUri, progress);
369 }
370
371 @Override
372 public void onStopAndReset(@NonNull Uri audioUri) {
373 voiceNoteMediaController.stopPlaybackAndReset(audioUri);
374 }
375
376 @Override
377 public void registerPlaybackStateObserver(@NonNull Observer<VoiceNotePlaybackState> observer) {
378 voiceNoteMediaController.getVoiceNotePlaybackState().observe(getViewLifecycleOwner(), observer);
379 }
380
381 @Override
382 public void unregisterPlaybackStateObserver(@NonNull Observer<VoiceNotePlaybackState> observer) {
383 voiceNoteMediaController.getVoiceNotePlaybackState().removeObserver(observer);
384 }
385
386 private class ActionModeCallback implements ActionMode.Callback {
387
388 @Override
389 public boolean onCreateActionMode(ActionMode mode, Menu menu) {
390 mode.setTitle(getActionModeTitle());
391 return true;
392 }
393
394 @Override
395 public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
396 return false;
397 }
398
399 @Override
400 public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
401 return false;
402 }
403
404 @Override
405 public void onDestroyActionMode(ActionMode mode) {
406 getListAdapter().clearSelection();
407
408 FragmentActivity activity = requireActivity();
409 ((MediaOverviewActivity) activity).onExitMultiSelect();
410
411 exitMultiSelect();
412 }
413 }
414
415 public enum GridMode {
416 FIXED_DETAIL,
417 FOLLOW_MODEL
418 }
419}