kopia lustrzana https://github.com/ryukoposting/Signal-Android
Create new Media Preview infrastructure, behind feature flag.
rodzic
1af576c157
commit
f63ce79f16
|
@ -686,6 +686,10 @@
|
|||
android:screenOrientation="portrait"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediapreview.MediaPreviewV2Activity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.Signal.DayNight.NoActionBar" />
|
||||
|
||||
<service android:enabled="true" android:name=".exporter.SignalSmsExportService" android:foregroundServiceType="dataSync" />
|
||||
<service android:enabled="true" android:name=".service.webrtc.WebRtcCallService" android:foregroundServiceType="camera|microphone"/>
|
||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||
|
|
|
@ -65,6 +65,7 @@ import org.thoughtcrime.securesms.database.MediaDatabase;
|
|||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
|
@ -98,18 +99,6 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
|
||||
private final static String TAG = Log.tag(MediaPreviewActivity.class);
|
||||
|
||||
private static final int NOT_IN_A_THREAD = -2;
|
||||
|
||||
public static final String THREAD_ID_EXTRA = "thread_id";
|
||||
public static final String DATE_EXTRA = "date";
|
||||
public static final String SIZE_EXTRA = "size";
|
||||
public static final String CAPTION_EXTRA = "caption";
|
||||
public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent";
|
||||
public static final String HIDE_ALL_MEDIA_EXTRA = "came_from_all_media";
|
||||
public static final String SHOW_THREAD_EXTRA = "show_thread";
|
||||
public static final String SORTING_EXTRA = "sorting";
|
||||
public static final String IS_VIDEO_GIF = "is_video_gif";
|
||||
|
||||
private ViewPager mediaPager;
|
||||
private View detailsContainer;
|
||||
private TextView caption;
|
||||
|
@ -127,7 +116,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
private ViewPagerListener viewPagerListener;
|
||||
|
||||
private int restartItem = -1;
|
||||
private long threadId = NOT_IN_A_THREAD;
|
||||
private long threadId = MediaIntentFactory.NOT_IN_A_THREAD;
|
||||
private boolean cameFromAllMedia;
|
||||
private boolean showThread;
|
||||
private MediaDatabase.Sorting sorting;
|
||||
|
@ -143,12 +132,12 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
{
|
||||
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
|
||||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
||||
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, mediaRecord.getThreadId());
|
||||
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, attachment.getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, attachment.getCaption());
|
||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, leftIsRecent);
|
||||
intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, attachment.isVideoGif());
|
||||
intent.putExtra(MediaIntentFactory.THREAD_ID_EXTRA, mediaRecord.getThreadId());
|
||||
intent.putExtra(MediaIntentFactory.DATE_EXTRA, mediaRecord.getDate());
|
||||
intent.putExtra(MediaIntentFactory.SIZE_EXTRA, attachment.getSize());
|
||||
intent.putExtra(MediaIntentFactory.CAPTION_EXTRA, attachment.getCaption());
|
||||
intent.putExtra(MediaIntentFactory.LEFT_IS_RECENT_EXTRA, leftIsRecent);
|
||||
intent.putExtra(MediaIntentFactory.IS_VIDEO_GIF, attachment.isVideoGif());
|
||||
intent.setDataAndType(attachment.getUri(), mediaRecord.getContentType());
|
||||
return intent;
|
||||
}
|
||||
|
@ -305,17 +294,17 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
private void initializeResources() {
|
||||
Intent intent = getIntent();
|
||||
|
||||
threadId = intent.getLongExtra(THREAD_ID_EXTRA, NOT_IN_A_THREAD);
|
||||
cameFromAllMedia = intent.getBooleanExtra(HIDE_ALL_MEDIA_EXTRA, false);
|
||||
showThread = intent.getBooleanExtra(SHOW_THREAD_EXTRA, false);
|
||||
sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(SORTING_EXTRA, 0)];
|
||||
threadId = intent.getLongExtra(MediaIntentFactory.THREAD_ID_EXTRA, MediaIntentFactory.NOT_IN_A_THREAD);
|
||||
cameFromAllMedia = intent.getBooleanExtra(MediaIntentFactory.HIDE_ALL_MEDIA_EXTRA, false);
|
||||
showThread = intent.getBooleanExtra(MediaIntentFactory.SHOW_THREAD_EXTRA, false);
|
||||
sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(MediaIntentFactory.SORTING_EXTRA, 0)];
|
||||
|
||||
initialMediaUri = intent.getData();
|
||||
initialMediaType = intent.getType();
|
||||
initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0);
|
||||
initialCaption = intent.getStringExtra(CAPTION_EXTRA);
|
||||
leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
initialMediaIsVideoGif = intent.getBooleanExtra(IS_VIDEO_GIF, false);
|
||||
initialMediaSize = intent.getLongExtra(MediaIntentFactory.SIZE_EXTRA, 0);
|
||||
initialCaption = intent.getStringExtra(MediaIntentFactory.CAPTION_EXTRA);
|
||||
leftIsRecent = intent.getBooleanExtra(MediaIntentFactory.LEFT_IS_RECENT_EXTRA, false);
|
||||
initialMediaIsVideoGif = intent.getBooleanExtra(MediaIntentFactory.IS_VIDEO_GIF, false);
|
||||
restartItem = -1;
|
||||
}
|
||||
|
||||
|
@ -533,7 +522,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
}
|
||||
|
||||
private boolean isMediaInDb() {
|
||||
return threadId != NOT_IN_A_THREAD;
|
||||
return threadId != MediaIntentFactory.NOT_IN_A_THREAD;
|
||||
}
|
||||
|
||||
private @Nullable MediaItem getCurrentMediaItem() {
|
||||
|
@ -789,7 +778,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
|
||||
cursor.moveToPosition(cursorPosition);
|
||||
|
||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(context, cursor);
|
||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(cursor);
|
||||
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
|
||||
MediaPreviewFragment fragment = MediaPreviewFragment.newInstance(attachment, autoPlay);
|
||||
|
||||
|
@ -819,7 +808,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
|
||||
cursor.moveToPosition(cursorPosition);
|
||||
|
||||
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
|
||||
MediaRecord mediaRecord = MediaRecord.from(cursor);
|
||||
DatabaseAttachment attachment = Objects.requireNonNull(mediaRecord.getAttachment());
|
||||
RecipientId recipientId = mediaRecord.getRecipientId();
|
||||
RecipientId threadRecipientId = mediaRecord.getThreadRecipientId();
|
||||
|
@ -890,4 +879,4 @@ public final class MediaPreviewActivity extends PassphraseRequiredActivity
|
|||
@Nullable View getPlaybackControls(int position);
|
||||
boolean hasFragmentFor(int position);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,11 +4,12 @@ import android.view.View;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
/**
|
||||
* Based on https://developer.android.com/training/animation/screen-slide#depth-page
|
||||
*/
|
||||
public final class DepthPageTransformer implements ViewPager.PageTransformer {
|
||||
public final class DepthPageTransformer implements ViewPager.PageTransformer, ViewPager2.PageTransformer {
|
||||
private static final float MIN_SCALE = 0.75f;
|
||||
|
||||
public void transformPage(@NonNull View view, float position) {
|
||||
|
|
|
@ -89,7 +89,7 @@ public class ThreadPhotoRailView extends FrameLayout {
|
|||
@Override
|
||||
public void onBindItemViewHolder(ThreadPhotoViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||
ThumbnailView imageView = viewHolder.imageView;
|
||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor);
|
||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(cursor);
|
||||
Slide slide = MediaUtil.getSlideForAttachment(getContext(), mediaRecord.getAttachment());
|
||||
|
||||
if (slide != null) {
|
||||
|
|
|
@ -18,6 +18,7 @@ package org.thoughtcrime.securesms.conversation;
|
|||
|
||||
import android.animation.ValueAnimator;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
@ -96,6 +97,7 @@ import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
|||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
|
@ -111,6 +113,8 @@ import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
|||
import org.thoughtcrime.securesms.jobs.SmsSendJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewV2Activity;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
|
@ -126,6 +130,7 @@ import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageView;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
|
||||
import org.thoughtcrime.securesms.util.LinkUtil;
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
|
@ -2312,17 +2317,19 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
} else if (!canPlayContent && mediaItem != null && eventListener != null) {
|
||||
eventListener.onPlayInlineContent(conversationMessage);
|
||||
} else if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.setDataAndType(slide.getUri(), slide.getContentType());
|
||||
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, messageRecord.getThreadId());
|
||||
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, messageRecord.getTimestamp());
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orElse(null));
|
||||
intent.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, slide.isVideoGif());
|
||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, false);
|
||||
|
||||
context.startActivity(intent);
|
||||
MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs(
|
||||
messageRecord.getThreadId(),
|
||||
messageRecord.getTimestamp(),
|
||||
slide.getUri(),
|
||||
slide.getContentType(),
|
||||
slide.asAttachment().getSize(),
|
||||
slide.getCaption().orElse(null),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
MediaDatabase.Sorting.Newest.ordinal(),
|
||||
slide.isVideoGif());
|
||||
context.startActivity(MediaIntentFactory.create(context, args));
|
||||
} else if (slide.getUri() != null) {
|
||||
Log.i(TAG, "Clicked: " + slide.getUri() + " , " + slide.getContentType());
|
||||
Uri publicUri = PartAuthority.getAttachmentPublicUri(slide.getUri());
|
||||
|
|
|
@ -192,7 +192,7 @@ public class MediaDatabase extends Database {
|
|||
this.outgoing = outgoing;
|
||||
}
|
||||
|
||||
public static MediaRecord from(@NonNull Context context, @NonNull Cursor cursor) {
|
||||
public static MediaRecord from(@NonNull Cursor cursor) {
|
||||
AttachmentDatabase attachmentDatabase = SignalDatabase.attachments();
|
||||
List<DatabaseAttachment> attachments = attachmentDatabase.getAttachments(cursor);
|
||||
RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.RECIPIENT_ID)));
|
||||
|
|
|
@ -75,7 +75,7 @@ public final class GroupedThreadMediaLoader extends AsyncTaskLoader<GroupedThrea
|
|||
|
||||
try (Cursor cursor = ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
mediaGrouping.add(MediaDatabase.MediaRecord.from(context, cursor));
|
||||
mediaGrouping.add(MediaDatabase.MediaRecord.from(cursor));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.components.menu.ActionItem;
|
||||
|
@ -38,6 +37,7 @@ import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
|||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.GroupedThreadMediaLoader;
|
||||
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.BottomOffsetDecoration;
|
||||
|
@ -46,6 +46,7 @@ import org.thoughtcrime.securesms.util.Util;
|
|||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Objects;
|
||||
|
||||
public final class MediaOverviewPageFragment extends Fragment
|
||||
implements MediaGalleryAllAdapter.ItemClickListener,
|
||||
|
@ -236,18 +237,19 @@ public final class MediaOverviewPageFragment extends Fragment
|
|||
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.putExtra(MediaPreviewActivity.IS_VIDEO_GIF, attachment.isVideoGif());
|
||||
|
||||
intent.setDataAndType(mediaRecord.getAttachment().getUri(), mediaRecord.getContentType());
|
||||
context.startActivity(intent);
|
||||
MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs(
|
||||
threadId,
|
||||
mediaRecord.getDate(),
|
||||
Objects.requireNonNull(mediaRecord.getAttachment().getUri()),
|
||||
mediaRecord.getContentType(),
|
||||
mediaRecord.getAttachment().getSize(),
|
||||
mediaRecord.getAttachment().getCaption(),
|
||||
true,
|
||||
true,
|
||||
threadId == MediaDatabase.ALL_THREADS,
|
||||
sorting.ordinal(),
|
||||
attachment.isVideoGif());
|
||||
context.startActivity(MediaIntentFactory.create(context, args));
|
||||
} else {
|
||||
if (!MediaUtil.isAudio(attachment)) {
|
||||
showFileExternally(context, mediaRecord);
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
package org.thoughtcrime.securesms.mediapreview
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
|
||||
object MediaIntentFactory {
|
||||
private const val ARGS_KEY = "args"
|
||||
|
||||
const val NOT_IN_A_THREAD = -2
|
||||
const val UNKNOWN_TIMESTAMP = -2
|
||||
const val THREAD_ID_EXTRA = "thread_id"
|
||||
const val DATE_EXTRA = "date"
|
||||
const val SIZE_EXTRA = "size"
|
||||
const val CAPTION_EXTRA = "caption"
|
||||
const val LEFT_IS_RECENT_EXTRA = "left_is_recent"
|
||||
const val HIDE_ALL_MEDIA_EXTRA = "came_from_all_media"
|
||||
const val SHOW_THREAD_EXTRA = "show_thread"
|
||||
const val SORTING_EXTRA = "sorting"
|
||||
const val IS_VIDEO_GIF = "is_video_gif"
|
||||
|
||||
@Parcelize
|
||||
data class MediaPreviewArgs(
|
||||
val threadId: Long,
|
||||
val date: Long,
|
||||
val initialMediaUri: Uri,
|
||||
val initialMediaType: String,
|
||||
val initialMediaSize: Long,
|
||||
val initialCaption: String? = null,
|
||||
val leftIsRecent: Boolean = false,
|
||||
val hideAllMedia: Boolean = false,
|
||||
val showThread: Boolean= false,
|
||||
val sorting: Int,
|
||||
val isVideoGif: Boolean
|
||||
) : Parcelable
|
||||
|
||||
@JvmStatic
|
||||
fun requireArguments(bundle: Bundle): MediaPreviewArgs = bundle.getParcelable(ARGS_KEY)!!
|
||||
|
||||
@JvmStatic
|
||||
fun create(context: Context, args: MediaPreviewArgs): Intent {
|
||||
|
||||
return if (FeatureFlags.mediaPreviewV2()) {
|
||||
val intent = Intent(context, MediaPreviewV2Activity::class.java)
|
||||
intent.putExtra(ARGS_KEY, args)
|
||||
return intent
|
||||
} else {
|
||||
val intent = Intent(context, MediaPreviewActivity::class.java).apply {
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
setDataAndType(args.initialMediaUri, args.initialMediaType)
|
||||
putExtra(THREAD_ID_EXTRA, args.threadId)
|
||||
putExtra(DATE_EXTRA, args.date)
|
||||
putExtra(SIZE_EXTRA, args.initialMediaSize)
|
||||
putExtra(CAPTION_EXTRA, args.initialCaption)
|
||||
putExtra(IS_VIDEO_GIF, args.isVideoGif)
|
||||
putExtra(LEFT_IS_RECENT_EXTRA, args.leftIsRecent)
|
||||
}
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
package org.thoughtcrime.securesms.mediapreview
|
||||
|
||||
import android.net.Uri
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.requireLong
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.Sorting
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase.Companion.media
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
|
||||
/**
|
||||
* Repository for accessing the attachments in the encrypted database.
|
||||
*/
|
||||
class MediaPreviewRepository {
|
||||
companion object {
|
||||
private val TAG: String = Log.tag(MediaPreviewRepository::class.java)
|
||||
}
|
||||
|
||||
/**
|
||||
* Accessor for database attachments.
|
||||
* @param startingUri the initial position to select from
|
||||
* @param threadId the thread to select from
|
||||
* @param sorting the ordering of the results
|
||||
* @param limit the maximum quantity of the results
|
||||
*/
|
||||
fun getAttachments(startingUri: Uri, threadId: Long, sorting: Sorting, limit: Int = 500): Flowable<List<DatabaseAttachment>> {
|
||||
return Single.fromCallable {
|
||||
val cursor = media.getGalleryMediaForThread(threadId, sorting)
|
||||
|
||||
val acc = mutableListOf<DatabaseAttachment>()
|
||||
var attachmentUri: Uri? = null
|
||||
while (cursor.moveToNext()) {
|
||||
val attachmentId = AttachmentId(cursor.requireLong(AttachmentDatabase.ROW_ID), cursor.requireLong(AttachmentDatabase.UNIQUE_ID))
|
||||
attachmentUri = PartAuthority.getAttachmentDataUri(attachmentId)
|
||||
if (attachmentUri == startingUri) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (attachmentUri == startingUri) {
|
||||
for (i in 0..limit) {
|
||||
val element = MediaDatabase.MediaRecord.from(cursor).attachment
|
||||
if (element != null) {
|
||||
acc.add(element)
|
||||
}
|
||||
if (!cursor.isLast) {
|
||||
cursor.moveToNext()
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
acc.toList()
|
||||
} else {
|
||||
Log.e(TAG, "Could not find $startingUri in thread $threadId")
|
||||
emptyList()
|
||||
}
|
||||
}.subscribeOn(Schedulers.io()).toFlowable()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package org.thoughtcrime.securesms.mediapreview
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.commit
|
||||
import org.thoughtcrime.securesms.R
|
||||
|
||||
class MediaPreviewV2Activity : AppCompatActivity(R.layout.activity_mediapreview_v2) {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
if (savedInstanceState == null) {
|
||||
val bundle = Bundle()
|
||||
val args = MediaIntentFactory.requireArguments(intent.extras!!)
|
||||
bundle.putParcelable(MediaPreviewV2Fragment.ARGS_KEY, args)
|
||||
supportFragmentManager.commit {
|
||||
setReorderingAllowed(true)
|
||||
add(R.id.fragment_container_view, MediaPreviewV2Fragment::class.java, bundle, FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FRAGMENT_TAG = "media_preview_fragment_v2"
|
||||
private const val NOT_IN_A_THREAD = -2
|
||||
|
||||
const val THREAD_ID_EXTRA = "thread_id"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
package org.thoughtcrime.securesms.mediapreview
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.viewModels
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.animation.DepthPageTransformer
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase
|
||||
import org.thoughtcrime.securesms.databinding.FragmentMediaPreviewV2Binding
|
||||
import org.thoughtcrime.securesms.util.LifecycleDisposable
|
||||
|
||||
class MediaPreviewV2Fragment : Fragment(R.layout.fragment_media_preview_v2), MediaPreviewFragment.Events {
|
||||
private val TAG = Log.tag(MediaPreviewV2Fragment::class.java)
|
||||
|
||||
private val lifecycleDisposable = LifecycleDisposable()
|
||||
private val binding by ViewBinderDelegate(FragmentMediaPreviewV2Binding::bind)
|
||||
private val viewModel: MediaPreviewV2ViewModel by viewModels()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
binding.mediaPager.offscreenPageLimit = 1
|
||||
binding.mediaPager.setPageTransformer(DepthPageTransformer())
|
||||
lifecycleDisposable += viewModel.state.distinctUntilChanged().observeOn(AndroidSchedulers.mainThread()).subscribe {
|
||||
if (it.loadState == MediaPreviewV2State.LoadState.READY) {
|
||||
binding.mediaPager.adapter = PreviewMediaAdapter(this, it.attachments)
|
||||
}
|
||||
}
|
||||
initializeViewModel()
|
||||
}
|
||||
|
||||
private fun initializeViewModel() {
|
||||
val args = MediaIntentFactory.requireArguments(requireArguments())
|
||||
val sorting = MediaDatabase.Sorting.values()[args.sorting]
|
||||
viewModel.fetchAttachments(args.initialMediaUri, args.threadId, sorting)
|
||||
}
|
||||
|
||||
override fun singleTapOnMedia(): Boolean {
|
||||
Log.d(TAG, "singleTapOnMedia()")
|
||||
return true
|
||||
}
|
||||
|
||||
override fun mediaNotAvailable() {
|
||||
Log.d(TAG, "mediaNotAvailable()")
|
||||
}
|
||||
|
||||
override fun onMediaReady() {
|
||||
Log.d(TAG, "onMediaReady()")
|
||||
}
|
||||
|
||||
companion object {
|
||||
val ARGS_KEY: String = "args"
|
||||
}
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package org.thoughtcrime.securesms.mediapreview
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
|
||||
data class MediaPreviewV2State(
|
||||
val attachments: List<Attachment> = emptyList(),
|
||||
val loadState: LoadState = LoadState.INIT
|
||||
) {
|
||||
enum class LoadState { INIT, READY, }
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package org.thoughtcrime.securesms.mediapreview
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Flowable
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase
|
||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||
|
||||
class MediaPreviewV2ViewModel : ViewModel() {
|
||||
private val TAG = Log.tag(MediaPreviewV2ViewModel::class.java)
|
||||
private val store = RxStore(MediaPreviewV2State())
|
||||
private val disposables = CompositeDisposable()
|
||||
private val repository: MediaPreviewRepository = MediaPreviewRepository()
|
||||
|
||||
val state: Flowable<MediaPreviewV2State> = store.stateFlowable.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
fun fetchAttachments(startingUri: Uri, threadId: Long, sorting: MediaDatabase.Sorting) {
|
||||
disposables += store.update(repository.getAttachments(startingUri, threadId, sorting)) { attachments, oldState ->
|
||||
oldState.copy(attachments = attachments, loadState = MediaPreviewV2State.LoadState.READY)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
disposables.dispose()
|
||||
}
|
||||
}
|
|
@ -47,14 +47,14 @@ public class MediaPreviewViewModel extends ViewModel {
|
|||
|
||||
cursor.moveToPosition(activePosition);
|
||||
|
||||
MediaRecord activeRecord = MediaRecord.from(context, cursor);
|
||||
MediaRecord activeRecord = MediaRecord.from(cursor);
|
||||
LinkedList<Media> rail = new LinkedList<>();
|
||||
|
||||
Media activeMedia = toMedia(activeRecord);
|
||||
if (activeMedia != null) rail.add(activeMedia);
|
||||
|
||||
while (cursor.moveToPrevious()) {
|
||||
MediaRecord record = MediaRecord.from(context, cursor);
|
||||
MediaRecord record = MediaRecord.from(cursor);
|
||||
if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) {
|
||||
Media media = toMedia(record);
|
||||
if (media != null) rail.addFirst(media);
|
||||
|
@ -66,7 +66,7 @@ public class MediaPreviewViewModel extends ViewModel {
|
|||
cursor.moveToPosition(activePosition);
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
MediaRecord record = MediaRecord.from(context, cursor);
|
||||
MediaRecord record = MediaRecord.from(cursor);
|
||||
if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) {
|
||||
Media media = toMedia(record);
|
||||
if (media != null) rail.addLast(media);
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package org.thoughtcrime.securesms.mediapreview
|
||||
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
|
||||
class PreviewMediaAdapter(val fragment: Fragment, val items: List<Attachment>) : FragmentStateAdapter(fragment) {
|
||||
var autoPlayPosition = -1
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.count()
|
||||
}
|
||||
|
||||
override fun createFragment(position: Int): Fragment {
|
||||
val attachment: Attachment = items[position]
|
||||
|
||||
val contentType = attachment.contentType
|
||||
val args = bundleOf(
|
||||
MediaPreviewFragment.DATA_URI to attachment.uri,
|
||||
MediaPreviewFragment.DATA_CONTENT_TYPE to contentType,
|
||||
MediaPreviewFragment.DATA_SIZE to attachment.size,
|
||||
MediaPreviewFragment.AUTO_PLAY to (position == autoPlayPosition),
|
||||
MediaPreviewFragment.VIDEO_GIF to attachment.isVideoGif,
|
||||
)
|
||||
val fragment = if (MediaUtil.isVideo(contentType)) {
|
||||
VideoMediaPreviewFragment()
|
||||
} else if (MediaUtil.isImageType(contentType)) {
|
||||
ImageMediaPreviewFragment()
|
||||
} else {
|
||||
throw AssertionError("Unexpected media type: $contentType")
|
||||
}
|
||||
|
||||
fragment.arguments = args
|
||||
|
||||
return fragment
|
||||
}
|
||||
}
|
|
@ -52,8 +52,10 @@ import org.thoughtcrime.securesms.components.location.SignalMapView;
|
|||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.thoughtcrime.securesms.conversation.MessageSendType;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||
import org.thoughtcrime.securesms.maps.PlacePickerActivity;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory;
|
||||
import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity;
|
||||
import org.thoughtcrime.securesms.payments.create.CreatePaymentFragmentArgs;
|
||||
import org.thoughtcrime.securesms.payments.preferences.PaymentsActivity;
|
||||
|
@ -469,13 +471,19 @@ public class AttachmentManager {
|
|||
|
||||
private void previewImageDraft(final @NonNull Slide slide) {
|
||||
if (MediaPreviewActivity.isContentTypeSupported(slide.getContentType()) && slide.getUri() != null) {
|
||||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orElse(null));
|
||||
intent.setDataAndType(slide.getUri(), slide.getContentType());
|
||||
|
||||
context.startActivity(intent);
|
||||
MediaIntentFactory.MediaPreviewArgs args = new MediaIntentFactory.MediaPreviewArgs(
|
||||
MediaIntentFactory.NOT_IN_A_THREAD,
|
||||
MediaIntentFactory.UNKNOWN_TIMESTAMP,
|
||||
slide.getUri(),
|
||||
slide.getContentType(),
|
||||
slide.asAttachment().getSize(),
|
||||
slide.getCaption().orElse(null),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
MediaDatabase.Sorting.Newest.ordinal(),
|
||||
slide.isVideoGif());
|
||||
context.startActivity(MediaIntentFactory.create(context, args));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -103,6 +103,7 @@ public final class FeatureFlags {
|
|||
private static final String CDS_V2_COMPAT = "android.cdsV2Compat.4";
|
||||
public static final String STORIES_LOCALE = "android.stories.locale";
|
||||
private static final String HIDE_CONTACTS = "android.hide.contacts";
|
||||
public static final String MEDIA_PREVIEW_V2 = "android.mediaPreviewV2";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -157,7 +158,8 @@ public final class FeatureFlags {
|
|||
SMS_EXPORTER,
|
||||
CDS_V2_COMPAT,
|
||||
STORIES_LOCALE,
|
||||
HIDE_CONTACTS
|
||||
HIDE_CONTACTS,
|
||||
MEDIA_PREVIEW_V2
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -220,7 +222,8 @@ public final class FeatureFlags {
|
|||
RECIPIENT_MERGE_V2,
|
||||
CDS_V2_LOAD_TEST,
|
||||
CDS_V2_COMPAT,
|
||||
STORIES
|
||||
STORIES,
|
||||
MEDIA_PREVIEW_V2
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -565,6 +568,13 @@ public final class FeatureFlags {
|
|||
return getBoolean(HIDE_CONTACTS, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not we should use the new media preview fragment implementation.
|
||||
*/
|
||||
public static boolean mediaPreviewV2() {
|
||||
return getBoolean(MEDIA_PREVIEW_V2, false);
|
||||
}
|
||||
|
||||
/** Only for rendering debug info. */
|
||||
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
|
||||
return new TreeMap<>(REMOTE_VALUES);
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.fragment.app.FragmentContainerView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:id="@+id/fragment_container_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
android:id="@+id/media_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clickable="true"
|
||||
android:focusable="true" />
|
||||
</FrameLayout>
|
Ładowanie…
Reference in New Issue