diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0f77db16..3535d9f5 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -89,6 +89,13 @@ + + + + + + + shareImage(directory, attachment.url) + Attachment.Type.AUDIO, Attachment.Type.VIDEO, - Attachment.Type.GIFV -> shareVideo(directory, attachment.url) + Attachment.Type.GIFV -> shareMediaFile(directory, attachment.url) else -> Log.e(TAG, "Unknown media format for sharing.") } } @@ -313,7 +320,7 @@ class ViewMediaActivity : BaseActivity(), ViewImageFragment.PhotoActionsListener } - private fun shareVideo(directory: File, url: String) { + private fun shareMediaFile(directory: File, url: String) { val uri = Uri.parse(url) val mimeTypeMap = MimeTypeMap.getSingleton() val extension = MimeTypeMap.getFileExtensionFromUrl(url) diff --git a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java index dd9503b1..9f7fe665 100644 --- a/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java +++ b/app/src/main/java/com/keylesspalace/tusky/adapter/NotificationsAdapter.java @@ -495,6 +495,11 @@ public class NotificationsAdapter extends RecyclerView.Adapter { boolean hasSpoiler = !TextUtils.isEmpty(statusViewData.getSpoilerText()); contentWarningDescriptionTextView.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE); contentWarningButton.setVisibility(hasSpoiler ? View.VISIBLE : View.GONE); + if (statusViewData.isExpanded()) { + contentWarningButton.setText(R.string.status_content_warning_show_less); + } else { + contentWarningButton.setText(R.string.status_content_warning_show_more); + } contentWarningButton.setOnClickListener(view -> { if (getAdapterPosition() != RecyclerView.NO_POSITION) { diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt index ca6111d3..8abecbd1 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeActivity.kt @@ -209,7 +209,7 @@ class ComposeActivity : BaseActivity(), * instance state will be re-queued. */ val type = intent.type if (type != null) { - if (type.startsWith("image/") || type.startsWith("video/")) { + if (type.startsWith("image/") || type.startsWith("video/") || type.startsWith("audio/")) { val uriList = ArrayList() if (intent.action != null) { when (intent.action) { @@ -368,8 +368,8 @@ class ComposeActivity : BaseActivity(), } combineOptionalLiveData(viewModel.media, viewModel.poll) { media, poll -> if(!viewModel.hasNoAttachmentLimits) { - val active = (poll == null && media!!.size != 4 - && media.firstOrNull()?.type != QueuedMedia.Type.VIDEO) + val active = poll == null && media!!.size != 4 + && (media.isEmpty() || media.first().type == QueuedMedia.Type.IMAGE) enableButton(composeAddMediaButton, active, active) enablePollButton(media.isNullOrEmpty()) } @@ -913,7 +913,7 @@ class ComposeActivity : BaseActivity(), intent.addCategory(Intent.CATEGORY_OPENABLE) if(!viewModel.hasNoAttachmentLimits) { - val mimeTypes = arrayOf("image/*", "video/*") + val mimeTypes = arrayOf("image/*", "video/*", "audio/*") intent.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes) } intent.type = "*/*" @@ -960,6 +960,9 @@ class ComposeActivity : BaseActivity(), is MediaSizeException -> { R.string.error_media_upload_size } + is AudioSizeException -> { + R.string.error_audio_upload_size + } is VideoOrImageException -> { R.string.error_media_upload_image_or_video } @@ -1088,7 +1091,8 @@ class ComposeActivity : BaseActivity(), companion object Type { public const val IMAGE: Int = 0 public const val VIDEO: Int = 1 - public const val UNKNOWN: Int = 2 + public const val AUDIO: Int = 2 + public const val UNKNOWN: Int = 3 } } @@ -1145,7 +1149,7 @@ class ComposeActivity : BaseActivity(), @JvmStatic fun canHandleMimeType(mimeType: String?): Boolean { - return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType == "text/plain") + return mimeType != null && (mimeType.startsWith("image/") || mimeType.startsWith("video/") || mimeType.startsWith("audio/") || mimeType == "text/plain") } } } diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt index 5acca508..b2e7bf5e 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/ComposeViewModel.kt @@ -181,7 +181,7 @@ class ComposeViewModel .map { (type, uri, size) -> val mediaItems = media.value!! if (!hasNoAttachmentLimits - && type == QueuedMedia.Type.VIDEO + && type != QueuedMedia.Type.IMAGE && mediaItems.isNotEmpty() && mediaItems[0].type == QueuedMedia.Type.IMAGE) { throw VideoOrImageException() @@ -449,6 +449,7 @@ class ComposeViewModel val mediaType = when (a.type) { Attachment.Type.VIDEO, Attachment.Type.GIFV -> QueuedMedia.Type.VIDEO Attachment.Type.UNKNOWN, Attachment.Type.IMAGE -> QueuedMedia.Type.IMAGE + Attachment.Type.AUDIO -> QueuedMedia.Type.AUDIO else -> QueuedMedia.Type.IMAGE } addUploadedMedia(a.id, mediaType, a.url.toUri(), a.description) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt index 54156c65..bde5f581 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaPreviewAdapter.kt @@ -89,6 +89,11 @@ class MediaPreviewAdapter( holder.view.setChecked(!item.description.isNullOrEmpty()) holder.view.setProgress(item.uploadPercent) } + ComposeActivity.QueuedMedia.Type.AUDIO -> { + (holder as PreviewViewHolder).view.setChecked(!item.description.isNullOrEmpty()) + holder.view.setProgress(item.uploadPercent) + holder.view.setImageResource(R.drawable.ic_music_box_preview_24dp) + } else -> { (holder as PreviewViewHolder).view.setChecked(!item.description.isNullOrEmpty()) holder.view.setProgress(item.uploadPercent) diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt index e07f2460..bc5dd858 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/MediaUploader.kt @@ -63,6 +63,7 @@ interface MediaUploader { fun uploadMedia(media: QueuedMedia, videoLimit: Int, imageLimit: Int): Observable } +class AudioSizeException : Exception() class VideoSizeException : Exception() class MediaSizeException : Exception() class MediaTypeException : Exception() @@ -129,6 +130,12 @@ class MediaUploaderImpl( "image" -> { PreparedMedia(QueuedMedia.Type.IMAGE, uri, mediaSize) } + "audio" -> { + if (mediaSize > videoLimit) { // TODO: CHANGE!!11 + throw AudioSizeException() + } + PreparedMedia(QueuedMedia.Type.AUDIO, uri, mediaSize) + } else -> { if (mediaSize > videoLimit) { throw MediaSizeException() diff --git a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt index e7cc36cb..d594601c 100644 --- a/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt +++ b/app/src/main/java/com/keylesspalace/tusky/components/compose/dialog/CaptionDialog.kt @@ -24,7 +24,6 @@ import android.text.InputType import android.util.DisplayMetrics import android.view.WindowManager import android.widget.EditText -import android.widget.ImageView import android.widget.LinearLayout import android.widget.Toast import androidx.appcompat.app.AlertDialog @@ -34,6 +33,7 @@ import at.connyduck.sparkbutton.helpers.Utils import com.bumptech.glide.Glide import com.bumptech.glide.request.target.CustomTarget import com.bumptech.glide.request.transition.Transition +import com.github.chrisbanes.photoview.PhotoView import com.keylesspalace.tusky.R import com.keylesspalace.tusky.util.withLifecycleContext @@ -50,7 +50,7 @@ fun T.makeCaptionDialog(existingDescription: String?, dialogLayout.setPadding(padding, padding, padding, padding) dialogLayout.orientation = LinearLayout.VERTICAL - val imageView = ImageView(this) + val imageView = PhotoView(this) val displayMetrics = DisplayMetrics() windowManager.defaultDisplay.getMetrics(displayMetrics) diff --git a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt index 34036107..0b3a23a7 100644 --- a/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt +++ b/app/src/main/java/com/keylesspalace/tusky/fragment/ViewVideoFragment.kt @@ -21,6 +21,7 @@ import android.annotation.SuppressLint import android.os.Bundle import android.os.Handler import android.os.Looper +import android.view.KeyEvent import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -31,6 +32,7 @@ import com.keylesspalace.tusky.ViewMediaActivity import com.keylesspalace.tusky.entity.Attachment import com.keylesspalace.tusky.util.hide import com.keylesspalace.tusky.util.visible +import com.keylesspalace.tusky.view.ExposedPlayPauseVideoView import kotlinx.android.synthetic.main.activity_view_media.* import kotlinx.android.synthetic.main.fragment_view_video.* @@ -41,11 +43,13 @@ class ViewVideoFragment : ViewMediaFragment() { // Hoist toolbar hiding to activity so it can track state across different fragments // This is explicitly stored as runnable so that we pass it to the handler later for cancellation mediaActivity.onPhotoTap() + mediaController.hide() } private lateinit var mediaActivity: ViewMediaActivity private val TOOLBAR_HIDE_DELAY_MS = 3000L override lateinit var descriptionView : TextView private lateinit var mediaController : MediaController + private var isAudio = false override fun setUserVisibleHint(isVisibleToUser: Boolean) { // Start/pause/resume video playback as fragment is shown/hidden @@ -72,14 +76,43 @@ class ViewVideoFragment : ViewMediaFragment() { videoView.transitionName = url videoView.setVideoPath(url) - mediaController = MediaController(mediaActivity) + mediaController = object : MediaController(mediaActivity) { + override fun show(timeout: Int) { + // We're doing manual auto-close management. + // Also, take focus back from the pause button so we can use the back button. + super.show(0) + mediaController.requestFocus() + } + + override fun dispatchKeyEvent(event: KeyEvent?): Boolean { + if (event?.keyCode == KeyEvent.KEYCODE_BACK) { + if (event.action == KeyEvent.ACTION_UP) { + hide() + activity?.supportFinishAfterTransition() + } + return true + } + return super.dispatchKeyEvent(event) + } + } + mediaController.setMediaPlayer(videoView) videoView.setMediaController(mediaController) videoView.requestFocus() - videoView.setOnTouchListener { _, _ -> - mediaActivity.onPhotoTap() - false - } + videoView.setPlayPauseListener(object: ExposedPlayPauseVideoView.PlayPauseListener { + override fun onPause() { + handler.removeCallbacks(hideToolbar) + } + override fun onPlay() { + // Audio doesn't cause the controller to show automatically, + // and we only want to hide the toolbar if it's a video. + if (isAudio) { + mediaController.show() + } else { + hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) + } + } + }) videoView.setOnPreparedListener { mp -> val containerWidth = videoContainer.measuredWidth.toFloat() val containerHeight = videoContainer.measuredHeight.toFloat() @@ -94,10 +127,16 @@ class ViewVideoFragment : ViewMediaFragment() { videoView.layoutParams.width = ViewGroup.LayoutParams.MATCH_PARENT } + // Wait until the media is loaded before accepting taps as we don't want toolbar to + // be hidden until then. + videoView.setOnTouchListener { _, _ -> + mediaActivity.onPhotoTap() + false + } + progressBar.hide() mp.isLooping = true if (arguments!!.getBoolean(ARG_START_POSTPONED_TRANSITION)) { - hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) videoView.start() } } @@ -126,6 +165,7 @@ class ViewVideoFragment : ViewMediaFragment() { throw IllegalArgumentException("attachment has to be set") } url = attachment.url + isAudio = attachment.type == Attachment.Type.AUDIO finalizeViewSetup(url, attachment.previewUrl, attachment.description) } @@ -136,6 +176,12 @@ class ViewVideoFragment : ViewMediaFragment() { isDescriptionVisible = showingDescription && visible val alpha = if (isDescriptionVisible) 1.0f else 0.0f + if (isDescriptionVisible) { + // If to be visible, need to make visible immediately and animate alpha + descriptionView.alpha = 0.0f + descriptionView.visible(isDescriptionVisible) + } + descriptionView.animate().alpha(alpha) .setListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { @@ -145,7 +191,7 @@ class ViewVideoFragment : ViewMediaFragment() { }) .start() - if (visible) { + if (visible && videoView.isPlaying && !isAudio) { hideToolbarAfterDelay(TOOLBAR_HIDE_DELAY_MS) } else { handler.removeCallbacks(hideToolbar) diff --git a/app/src/main/java/com/keylesspalace/tusky/view/ExposedPlayPauseVideoView.kt b/app/src/main/java/com/keylesspalace/tusky/view/ExposedPlayPauseVideoView.kt new file mode 100644 index 00000000..ec748e04 --- /dev/null +++ b/app/src/main/java/com/keylesspalace/tusky/view/ExposedPlayPauseVideoView.kt @@ -0,0 +1,33 @@ +package com.keylesspalace.tusky.view + +import android.content.Context +import android.util.AttributeSet +import android.widget.VideoView + +class ExposedPlayPauseVideoView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0) + : VideoView(context, attrs, defStyleAttr) { + + private var listener: PlayPauseListener? = null + + fun setPlayPauseListener(listener: PlayPauseListener) { + this.listener = listener + } + + override fun start() { + super.start() + listener?.onPlay() + } + + override fun pause() { + super.pause() + listener?.onPause() + } + + interface PlayPauseListener { + fun onPlay() + fun onPause() + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_music_box_preview_24dp.xml b/app/src/main/res/drawable/ic_music_box_preview_24dp.xml new file mode 100644 index 00000000..67901791 --- /dev/null +++ b/app/src/main/res/drawable/ic_music_box_preview_24dp.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_compose.xml b/app/src/main/res/layout/activity_compose.xml index 014ee181..27ea1d65 100644 --- a/app/src/main/res/layout/activity_compose.xml +++ b/app/src/main/res/layout/activity_compose.xml @@ -225,7 +225,7 @@ - The status is too long! The file must be less than 8MB. Video files must be less than 40MB. + Audio files must be less than 40MB. That type of file cannot be uploaded. That file could not be opened. Permission to read media is required.