From e10054c4ee793562feab1b74befa1534b168abd4 Mon Sep 17 00:00:00 2001 From: AL-Session <160798022+AL-Session@users.noreply.github.com> Date: Wed, 19 Feb 2025 09:14:12 +1100 Subject: [PATCH] Fix/qa 882 mic button (#943) * WIP spam prevention * Record voice button spam UI state confusion addressed * Remove leftover commented code * Unused variable removed * Simplifying voice recording logic * Clean up * Clean up * Hopefully fix toast window layout exception on microphone button spam * Refactored voice message too short detection mechanism to avoid using deprecated call to 'someToast.view?.isShown' --------- Co-authored-by: alansley Co-authored-by: ThomasSession --- .../conversation/v2/ConversationActivityV2.kt | 180 +++++++++++------- .../conversation/v2/input_bar/InputBar.kt | 9 +- .../v2/input_bar/InputBarButton.kt | 5 +- .../v2/input_bar/InputBarRecordingView.kt | 32 ++-- .../securesms/mediasend/MediaRepository.java | 7 - 5 files changed, 134 insertions(+), 99 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index fcd161748e..72b54221d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -93,7 +93,6 @@ import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.MediaTypes import org.session.libsession.utilities.StringSubstitutionConstants.APP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.CONVERSATION_NAME_KEY -import org.session.libsession.utilities.StringSubstitutionConstants.DATE_KEY import org.session.libsession.utilities.StringSubstitutionConstants.GROUP_NAME_KEY import org.session.libsession.utilities.StringSubstitutionConstants.NAME_KEY import org.session.libsession.utilities.Stub @@ -131,8 +130,7 @@ import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarButton import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarDelegate import org.thoughtcrime.securesms.conversation.v2.input_bar.InputBarRecordingViewDelegate -import org.thoughtcrime.securesms.conversation.v2.input_bar.VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS -import org.thoughtcrime.securesms.conversation.v2.input_bar.VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS +import org.thoughtcrime.securesms.conversation.v2.input_bar.VoiceRecorderConstants import org.thoughtcrime.securesms.conversation.v2.input_bar.VoiceRecorderState import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCandidateAdapter import org.thoughtcrime.securesms.conversation.v2.mention.MentionViewModel @@ -392,6 +390,45 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, private lateinit var reactionDelegate: ConversationReactionDelegate private val reactWithAnyEmojiStartPage = -1 + private val voiceNoteTooShortToast: Toast by lazy { + Toast.makeText( + applicationContext, + applicationContext.getString(R.string.messageVoiceErrorShort), + Toast.LENGTH_SHORT + ).apply { + // On Android API 30 and above we can use callbacks to control our toast visible flag. + // Note: We have to do this hoop-jumping to prevent the possibility of a window layout + // crash when attempting to show a toast that is already visible should the user spam + // the microphone button, and because `someToast.view?.isShown` is deprecated. + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + addCallback(object : Toast.Callback() { + override fun onToastShown() { isVoiceToastShowing = true } + override fun onToastHidden() { isVoiceToastShowing = false } + }) + } + } + } + + private var isVoiceToastShowing = false + + // Only show a toast related to voice messages if the toast is not already showing (used if to + // rate limit & prevent toast queueing when the user spams the microphone button). + private fun showVoiceMessageToastIfNotAlreadyVisible() { + if (!isVoiceToastShowing) { + voiceNoteTooShortToast.show() + + // Use a delayed callback to reset the toast visible flag after Toast.LENGTH_SHORT duration (~2000ms) ONLY on + // Android APIs < 30 which lack the onToastShown & onToastHidden callbacks. + // Note: While Toast.LENGTH_SHORT is roughly 2000ms, it is subject to change with varying Android versions or + // even between devices - we have no control over this. + // TODO: Remove the lines below and just use the callbacks when our minimum API is >= 30. + isVoiceToastShowing = true + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + Handler(Looper.getMainLooper()).postDelayed( { isVoiceToastShowing = false }, 2000) + } + } + } + // Properties for what message indices are visible previously & now, as well as the scroll state private var previousLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION private var currentLastVisibleRecyclerViewIndex: Int = RecyclerView.NO_POSITION @@ -1086,11 +1123,6 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } } - private fun acceptMessageRequest() { - binding.messageRequestBar.isVisible = false - viewModel.acceptMessageRequest() - } - override fun inputBarEditTextContentChanged(newContent: CharSequence) { val inputBarText = binding.inputBar.text // TODO check if we should be referencing newContent here instead if (textSecurePreferences.isLinkPreviewsEnabled()) { @@ -1134,60 +1166,62 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, override fun showVoiceMessageUI() { binding.inputBarRecordingView.show(lifecycleScope) - binding.inputBar.alpha = 0.0f - val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) - animation.duration = SHOW_HIDE_VOICE_UI_DURATION_MS - animation.addUpdateListener { animator -> - binding.inputBar.alpha = animator.animatedValue as Float - } - animation.start() + + // Cancel any previous input bar animations and fade out the bar + val inputBar = binding.inputBar + inputBar.animate().cancel() + inputBar.animate() + .alpha(0f) + .setDuration(VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS) + .start() } private fun expandVoiceMessageLockView() { val lockView = binding.inputBarRecordingView.lockView - val animation = ValueAnimator.ofObject(FloatEvaluator(), lockView.scaleX, 1.10f) - animation.duration = ANIMATE_LOCK_DURATION_MS - animation.addUpdateListener { animator -> - lockView.scaleX = animator.animatedValue as Float - lockView.scaleY = animator.animatedValue as Float - } - animation.start() + + lockView.animate().cancel() + lockView.animate() + .scaleX(1.10f) + .scaleY(1.10f) + .setDuration(VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS) + .start() } private fun collapseVoiceMessageLockView() { val lockView = binding.inputBarRecordingView.lockView - val animation = ValueAnimator.ofObject(FloatEvaluator(), lockView.scaleX, 1.0f) - animation.duration = ANIMATE_LOCK_DURATION_MS - animation.addUpdateListener { animator -> - lockView.scaleX = animator.animatedValue as Float - lockView.scaleY = animator.animatedValue as Float - } - animation.start() + + lockView.animate().cancel() + lockView.animate() + .scaleX(1.0f) + .scaleY(1.0f) + .setDuration(VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS) + .start() } private fun hideVoiceMessageUI() { - val chevronImageView = binding.inputBarRecordingView.chevronImageView - val slideToCancelTextView = binding.inputBarRecordingView.slideToCancelTextView - listOf( chevronImageView, slideToCancelTextView ).forEach { view -> - val animation = ValueAnimator.ofObject(FloatEvaluator(), view.translationX, 0.0f) - animation.duration = ANIMATE_LOCK_DURATION_MS - animation.addUpdateListener { animator -> - view.translationX = animator.animatedValue as Float - } - animation.start() + listOf( + binding.inputBarRecordingView.chevronImageView, + binding.inputBarRecordingView.slideToCancelTextView + ).forEach { view -> + view.animate().cancel() + view.animate() + .translationX(0.0f) + .setDuration(VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS) + .start() } + binding.inputBarRecordingView.hide() } override fun handleVoiceMessageUIHidden() { val inputBar = binding.inputBar - inputBar.alpha = 1.0f - val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) - animation.duration = SHOW_HIDE_VOICE_UI_DURATION_MS - animation.addUpdateListener { animator -> - inputBar.alpha = animator.animatedValue as Float - } - animation.start() + + // Cancel any previous input bar animations and fade in the bar + inputBar.animate().cancel() + inputBar.animate() + .alpha(1.0f) + .setDuration(VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS) + .start() } private fun handleRecyclerViewScrolled() { @@ -1716,6 +1750,11 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } override fun onMicrophoneButtonUp(event: MotionEvent) { + if(binding.inputBar.voiceRecorderState != VoiceRecorderState.Recording){ + cancelVoiceMessage() + return + } + val x = event.rawX.roundToInt() val y = event.rawY.roundToInt() @@ -1725,7 +1764,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, // to recording audio on a quick tap as the lock area animates out from the record // audio message button and the pointer-up event catches it mid-animation. val currentVoiceMessageDurationMS = System.currentTimeMillis() - voiceMessageStartTimestamp - if (isValidLockViewLocation(x, y) && currentVoiceMessageDurationMS >= ANIMATE_LOCK_DURATION_MS) { + if (isValidLockViewLocation(x, y) && currentVoiceMessageDurationMS >= VoiceRecorderConstants.ANIMATE_LOCK_DURATION_MS) { binding.inputBarRecordingView.lock() // If the user put the record audio button into the lock state then we are still recording audio @@ -1736,23 +1775,17 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, // If the user didn't lock voice recording on then we're stopping voice recording binding.inputBar.voiceRecorderState = VoiceRecorderState.ShuttingDownAfterRecord - val rba = binding.inputBarRecordingView?.recordButtonOverlay - if (rba != null) { - val location = IntArray(2) { 0 } - rba.getLocationOnScreen(location) - val hitRect = Rect(location[0], location[1], location[0] + rba.width, location[1] + rba.height) + val recordButtonOverlay = binding.inputBarRecordingView.recordButtonOverlay - // If the up event occurred over the record button overlay we send the voice message.. - if (hitRect.contains(x, y)) { - sendVoiceMessage() - } else { - // ..otherwise if they've released off the button we'll cancel sending. - cancelVoiceMessage() - } - } - else - { - // Just to cover all our bases, if for whatever reason the record button overlay was null we'll also cancel recording + val location = IntArray(2) { 0 } + recordButtonOverlay.getLocationOnScreen(location) + val hitRect = Rect(location[0], location[1], location[0] + recordButtonOverlay.width, location[1] + recordButtonOverlay.height) + + // If the up event occurred over the record button overlay we send the voice message.. + if (hitRect.contains(x, y)) { + sendVoiceMessage() + } else { + // ..otherwise if they've released off the button we'll cancel sending. cancelVoiceMessage() } } @@ -2067,7 +2100,8 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } override fun startRecordingVoiceMessage() { - Log.i(TAG, "Starting voice message recording at: ${System.currentTimeMillis()}") + Log.i(TAG, "Starting voice message recording at: ${System.currentTimeMillis()} --- ${binding.inputBar.voiceRecorderState}") + binding.inputBar.voiceRecorderState = VoiceRecorderState.SettingUpToRecord if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) { showVoiceMessageUI() @@ -2085,13 +2119,14 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, } } - binding.inputBar.voiceRecorderState = VoiceRecorderState.SettingUpToRecord voiceMessageStartTimestamp = System.currentTimeMillis() audioRecorder.startRecording(callback) // Limit voice messages to 5 minute each stopAudioHandler.postDelayed(stopVoiceMessageRecordingTask, 5.minutes.inWholeMilliseconds) } else { + binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle + Permissions.with(this) .request(Manifest.permission.RECORD_AUDIO) .withPermanentDenialDialog(Phrase.from(applicationContext, R.string.permissionsMicrophoneAccessRequired) @@ -2115,9 +2150,10 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, // update the voice message duration based on the current time here. val voiceMessageDurationMS = System.currentTimeMillis() - voiceMessageStartTimestamp - val voiceMessageDurationValid = MediaUtil.voiceMessageMeetsMinimumDuration(voiceMessageDurationMS) - val future = audioRecorder.stopRecording(voiceMessageDurationValid) + val voiceMessageMeetsMinimumDuration = MediaUtil.voiceMessageMeetsMinimumDuration(voiceMessageDurationMS) + val future = audioRecorder.stopRecording(voiceMessageMeetsMinimumDuration) stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) + binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle // Generate a filename from the current time such as: "Session-VoiceMessage_2025-01-08-152733.aac" @@ -2125,9 +2161,9 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, // Voice message too short? Warn with toast instead of sending. // Note: The 0L check prevents the warning toast being shown when leaving the conversation activity. - val voiceMessageBelowMinimumDuration = !MediaUtil.voiceMessageMeetsMinimumDuration(voiceMessageDurationMS) - if (voiceMessageDurationMS != 0L && voiceMessageBelowMinimumDuration) { - Toast.makeText(this@ConversationActivityV2, R.string.messageVoiceErrorShort, Toast.LENGTH_SHORT).show() + if (voiceMessageDurationMS != 0L && !voiceMessageMeetsMinimumDuration) { + voiceNoteTooShortToast.setText(applicationContext.getString(R.string.messageVoiceErrorShort)) + showVoiceMessageToastIfNotAlreadyVisible() return } @@ -2144,7 +2180,7 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val dataSizeBytes = result.second // Only proceed with sending the voice message if it's long enough - if (!voiceMessageBelowMinimumDuration) { + if (voiceMessageMeetsMinimumDuration) { val formattedAudioDuration = MediaUtil.getFormattedVoiceMessageDuration(voiceMessageDurationMS) val audioSlide = AudioSlide(this@ConversationActivityV2, uri, voiceMessageFilename, dataSizeBytes, MediaTypes.AUDIO_AAC, true, formattedAudioDuration) val slideDeck = SlideDeck() @@ -2169,11 +2205,13 @@ class ConversationActivityV2 : ScreenLockActionBarActivity(), InputBarDelegate, val voiceMessageMeetsMinimumDuration = MediaUtil.voiceMessageMeetsMinimumDuration(voiceMessageDurationMS) audioRecorder.stopRecording(voiceMessageMeetsMinimumDuration) stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) + binding.inputBar.voiceRecorderState = VoiceRecorderState.Idle // Note: The 0L check prevents the warning toast being shown when leaving the conversation activity if (voiceMessageDurationMS != 0L && !voiceMessageMeetsMinimumDuration) { - Toast.makeText(applicationContext, applicationContext.getString(R.string.messageVoiceErrorShort), Toast.LENGTH_SHORT).show() + voiceNoteTooShortToast.setText(applicationContext.getString(R.string.messageVoiceErrorShort)) + showVoiceMessageToastIfNotAlreadyVisible() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt index 7680c4ec2f..d9904b0226 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBar.kt @@ -15,6 +15,7 @@ import android.widget.RelativeLayout import android.widget.TextView import androidx.core.view.isGone import androidx.core.view.isVisible +import com.bumptech.glide.RequestManager import network.loki.messenger.R import network.loki.messenger.databinding.ViewInputBarBinding import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview @@ -26,7 +27,6 @@ import org.thoughtcrime.securesms.conversation.v2.messages.QuoteView import org.thoughtcrime.securesms.conversation.v2.messages.QuoteViewDelegate import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord -import com.bumptech.glide.RequestManager import org.thoughtcrime.securesms.util.addTextChangedListener import org.thoughtcrime.securesms.util.contains @@ -112,12 +112,14 @@ class InputBar @JvmOverloads constructor( when (event.action) { MotionEvent.ACTION_DOWN -> { + // Only start spinning up the voice recorder if we're not already recording, setting up, or tearing down if (voiceRecorderState == VoiceRecorderState.Idle) { startRecordingVoiceMessage() } } MotionEvent.ACTION_UP -> { + // Handle the pointer up event appropriately, whether that's to keep recording if recording was locked // on, or finishing recording if just hold-to-record. delegate?.onMicrophoneButtonUp(event) @@ -172,7 +174,10 @@ class InputBar @JvmOverloads constructor( private fun toggleAttachmentOptions() { delegate?.toggleAttachmentOptions() } - private fun startRecordingVoiceMessage() { delegate?.startRecordingVoiceMessage() } + + private fun startRecordingVoiceMessage() { + delegate?.startRecordingVoiceMessage() + } fun draftQuote(thread: Recipient, message: MessageRecord, glide: RequestManager) { quoteView?.let(binding.inputBarAdditionalContentContainer::removeView) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarButton.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarButton.kt index 655879edc9..38dc21fa69 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarButton.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarButton.kt @@ -5,7 +5,6 @@ import android.animation.ValueAnimator import android.content.Context import android.content.res.ColorStateList import android.graphics.PointF -import android.os.Build import android.os.Handler import android.os.Looper import android.util.AttributeSet @@ -153,7 +152,7 @@ class InputBarButton : RelativeLayout { longPressCallback?.let { gestureHandler.removeCallbacks(it) } val newLongPressCallback = Runnable { onLongPress?.invoke() } this.longPressCallback = newLongPressCallback - gestureHandler.postDelayed(newLongPressCallback, InputBarButton.longPressDurationThreshold) + gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold) onDownTimestamp = Date().time } @@ -170,7 +169,7 @@ class InputBarButton : RelativeLayout { private fun onUp(event: MotionEvent) { onUp?.invoke(event) collapse() - if ((Date().time - onDownTimestamp) < InputBarButton.longPressDurationThreshold) { + if ((Date().time - onDownTimestamp) < longPressDurationThreshold) { longPressCallback?.let { gestureHandler.removeCallbacks(it) } onPress?.invoke() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt index a873ba9e06..f44390e8d9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/InputBarRecordingView.kt @@ -70,13 +70,14 @@ class InputBarRecordingView : RelativeLayout { binding.inputBarMiddleContentContainer.alpha = 1.0f binding.lockView.alpha = 1.0f isVisible = true - alpha = 0.0f - val animation = ValueAnimator.ofObject(FloatEvaluator(), 0.0f, 1.0f) - animation.duration = 250L - animation.addUpdateListener { animator -> - alpha = animator.animatedValue as Float - } - animation.start() + + animate().cancel() + animate() + .alpha(1f) + .setDuration(VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS) + .withEndAction(null) + .start() + animateDotView() pulse() animateLockViewUp() @@ -84,18 +85,17 @@ class InputBarRecordingView : RelativeLayout { } fun hide() { - alpha = 1.0f - val animation = ValueAnimator.ofObject(FloatEvaluator(), 1.0f, 0.0f) - animation.duration = VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS - animation.addUpdateListener { animator -> - alpha = animator.animatedValue as Float - if (animator.animatedFraction == 1.0f) { + animate().cancel() + animate() + .alpha(0f) + .setDuration(VoiceRecorderConstants.SHOW_HIDE_VOICE_UI_DURATION_MS) + .withEndAction { isVisible = false dotViewAnimation?.repeatCount = 0 pulseAnimation?.removeAllUpdateListeners() } - } - animation.start() + .start() + delegate?.handleVoiceMessageUIHidden() stopTimer() } @@ -110,7 +110,7 @@ class InputBarRecordingView : RelativeLayout { val durationMS = (Date().time - startTimestamp) binding.recordingViewDurationTextView.text = MediaUtil.getFormattedVoiceMessageDuration(durationMS) - delay(500) + delay(500) // Update the voice message duration timer value every half a second } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java index 46f52dc783..bfe23f7d24 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaRepository.java @@ -101,8 +101,6 @@ class MediaRepository { return mediaFolders; } - - @WorkerThread private @NonNull FolderResult getFolders(@NonNull Context context, @NonNull Uri contentUri) { Uri globalThumbnail = null; @@ -152,7 +150,6 @@ class MediaRepository { return new FolderResult(globalThumbnail, thumbnailTimestamp, folders); } - @WorkerThread private @NonNull List getMediaInBucket(@NonNull Context context, @NonNull String bucketId) { List images = getMediaInBucket(context, bucketId, Images.Media.EXTERNAL_CONTENT_URI, true); @@ -165,7 +162,6 @@ class MediaRepository { return media; } - @WorkerThread private @NonNull List getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean isImage) { List media = new LinkedList<>(); @@ -204,7 +200,6 @@ class MediaRepository { return media; } - @WorkerThread private List getPopulatedMedia(@NonNull Context context, @NonNull List media) { return Stream.of(media).map(m -> { @@ -260,7 +255,6 @@ class MediaRepository { return new Media(media.getUri(), media.getFilename(), media.getMimeType(), media.getDate(), width, height, size, media.getBucketId(), media.getCaption()); } - private Media getContentResolverPopulatedMedia(@NonNull Context context, @NonNull Media media) throws IOException { int width = media.getWidth(); int height = media.getHeight(); @@ -364,7 +358,6 @@ class MediaRepository { } } - interface Callback { void onComplete(@NonNull E result); }