From c2ca9066c6fd760bd6ef5d2f8f0530a89bfa7b66 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 24 Oct 2017 23:29:55 -0400 Subject: WIP: Feature/pip video 2 (#405) * Add dependency * Test new video view * Add initial video bindings * Implement drag to dismiss * Begin initial integration * Fix typo * Fix up url formatter * Update changelog * Create first fully integrated video build * Update translations * Update translations 2 --- app/build.gradle | 2 + app/src/main/AndroidManifest.xml | 2 + app/src/main/assets/js/click-debugger.js | 13 ++ app/src/main/assets/js/click-debugger.min.js | 6 + app/src/main/assets/js/click_a.js | 6 +- app/src/main/assets/js/click_a.min.js | 12 +- app/src/main/assets/js/media.js | 31 +++ app/src/main/assets/js/media.min.js | 21 ++ .../pitchedapps/frost/activities/MainActivity.kt | 23 +- .../pitchedapps/frost/activities/VideoActivity.kt | 33 +++ .../pitchedapps/frost/facebook/FbUrlFormatter.kt | 44 +--- .../com/pitchedapps/frost/injectors/JsAssets.kt | 2 +- .../com/pitchedapps/frost/utils/Downloader.kt | 27 ++- .../com/pitchedapps/frost/views/FrostVideoView.kt | 234 +++++++++++++++++++++ .../pitchedapps/frost/views/FrostVideoViewer.kt | 121 +++++++++++ .../kotlin/com/pitchedapps/frost/web/FrostJSI.kt | 7 + .../frost/web/FrostUrlOverlayValidator.kt | 11 +- .../pitchedapps/frost/web/FrostWebViewClients.kt | 1 + app/src/main/res/layout/view_video.xml | 53 +++++ app/src/main/res/menu/menu_video.xml | 15 ++ app/src/main/res/values-de/strings_download.xml | 1 + app/src/main/res/values-es/strings_download.xml | 1 + app/src/main/res/values-fr/strings_download.xml | 1 + app/src/main/res/values/strings.xml | 3 + app/src/main/res/values/strings_download.xml | 1 + app/src/main/res/values/styles.xml | 7 + app/src/main/res/xml/frost_changelog.xml | 22 +- .../com/pitchedapps/frost/facebook/FbUrlTest.kt | 15 +- 28 files changed, 641 insertions(+), 74 deletions(-) create mode 100644 app/src/main/assets/js/click-debugger.js create mode 100644 app/src/main/assets/js/click-debugger.min.js create mode 100644 app/src/main/assets/js/media.js create mode 100644 app/src/main/assets/js/media.min.js create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/VideoActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt create mode 100644 app/src/main/res/layout/view_video.xml create mode 100644 app/src/main/res/menu/menu_video.xml (limited to 'app') diff --git a/app/build.gradle b/app/build.gradle index e7b83ddd..2a474cd3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -157,6 +157,8 @@ dependencies { implementation "org.apache.commons:commons-text:${COMMONS_TEXT}" + implementation "com.devbrackets.android:exomedia:${EXOMEDIA}" + //noinspection GradleDependency releaseImplementation "com.squareup.leakcanary:leakcanary-android-no-op:${LEAK_CANARY}" //noinspection GradleDependency diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6ce65cea..2a23ea02 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -135,6 +135,8 @@ + ()!! @@ -157,9 +159,18 @@ class MainActivity : BaseActivity(), setFrostColors(toolbar, themeWindow = false, headers = arrayOf(appBar), backgrounds = arrayOf(viewPager)) tabs.setBackgroundColor(Prefs.mainActivityLayout.backgroundColor()) onCreateBilling() -// setNetworkObserver { connectivity -> -// shouldLoadImages = !connectivity.isRoaming -// } + } + + fun showVideo(url: String) { + if (videoViewer != null) { + videoViewer?.setVideo(url) + } else { + val viewer = FrostVideoViewer.showVideo(coordinator.parentViewGroup, url) { + L.d("Video view released") + videoViewer = null + } + videoViewer = viewer + } } fun tabsForEachView(action: (position: Int, view: BadgedIcon) -> Unit) { @@ -420,12 +431,18 @@ class MainActivity : BaseActivity(), super.onStart() } + override fun onStop() { + videoViewer?.pause() + super.onStop() + } + override fun onDestroy() { onDestroyBilling() super.onDestroy() } override fun onBackPressed() { + if (videoViewer?.onBackPressed() == true) return if (searchView?.onBackPressed() == true) return if (currentFragment.onBackPressed()) return super.onBackPressed() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/VideoActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/VideoActivity.kt new file mode 100644 index 00000000..5943c73c --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/VideoActivity.kt @@ -0,0 +1,33 @@ +package com.pitchedapps.frost.activities + +import android.os.Bundle +import android.view.ViewGroup +import ca.allanwang.kau.internal.KauBaseActivity +import ca.allanwang.kau.utils.bindView +import com.pitchedapps.frost.R +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.views.FrostVideoView + +/** + * Created by Allan Wang on 2017-06-01. + */ +class VideoActivity : KauBaseActivity() { + + val container: ViewGroup by bindView(R.id.video_container) + val video: FrostVideoView by bindView(R.id.video) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.view_video) + container.setOnTouchListener { _, event -> + val y = video.shouldParentAcceptTouch(event) + L.d("Video SPAT $y") + y + } + } + + override fun onStop() { + video.pause() + super.onStop() + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt index 4879e68b..3298ff15 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt @@ -1,6 +1,8 @@ package com.pitchedapps.frost.facebook import com.pitchedapps.frost.utils.L +import java.net.URLDecoder +import java.nio.charset.StandardCharsets /** * Created by Allan Wang on 2017-07-07. @@ -8,12 +10,12 @@ import com.pitchedapps.frost.utils.L * Custom url builder so we can easily test it without the Android framework */ inline val String.formattedFbUrl: String - get() = FbUrlFormatter(this, false).toString() + get() = FbUrlFormatter(this).toString() inline val String.formattedFbUrlCss: String - get() = FbUrlFormatter(this, true).toString() + get() = FbUrlFormatter(this).toString() -class FbUrlFormatter(url: String, css: Boolean = false) { +class FbUrlFormatter(url: String) { private val queries = mutableMapOf() private val cleaned: String @@ -23,19 +25,17 @@ class FbUrlFormatter(url: String, css: Boolean = false) { * The order is very important: * 1. Wrapper links (discardables) are stripped away, resulting in the actual link * 2. CSS encoding is converted to normal encoding - * 3. Query portions are separated from the cleaned url - * 4. The cleaned url is decoded. Queries are kept as is! + * 3. Url is completely decoded + * 4. Url is split into sections */ init { if (url.isBlank()) cleaned = "" else { var cleanedUrl = url discardable.forEach { cleanedUrl = cleanedUrl.replace(it, "", true) } - val changed = cleanedUrl != url //note that discardables strip away the first '?' converter.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) } - //must decode for css - if (css) decoder.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) } - val qm = cleanedUrl.indexOf(if (changed) "&" else "?") + cleanedUrl = URLDecoder.decode(cleanedUrl, StandardCharsets.UTF_8.name()) + val qm = cleanedUrl.indexOf("?") if (qm > -1) { cleanedUrl.substring(qm + 1).split("&").forEach { val p = it.split("=") @@ -43,8 +43,6 @@ class FbUrlFormatter(url: String, css: Boolean = false) { } cleanedUrl = cleanedUrl.substring(0, qm) } - //only decode non query portion of the url - if (!css) decoder.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) } discardableQueries.forEach { queries.remove(it) } //final cleanup misc.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) } @@ -89,32 +87,10 @@ class FbUrlFormatter(url: String, css: Boolean = false) { "/video_redirect/?src=" ) - val misc = listOf( - "&" to "&" - ) + val misc = arrayOf("&" to "&") val discardableQueries = arrayOf("ref", "refid") - val decoder = listOf( - "%3C" to "<", "%3E" to ">", "%23" to "#", "%25" to "%", - "%7B" to "{", "%7D" to "}", "%7C" to "|", "%5C" to "\\", - "%5E" to "^", "%7E" to "~", "%5B" to "[", "%5D" to "]", - "%60" to "`", "%3B" to ";", "%2F" to "/", "%3F" to "?", - "%3A" to ":", "%40" to "@", "%3D" to "=", "%26" to "&", - "%24" to "$", "%2B" to "+", "%22" to "\"", "%2C" to ",", - "%20" to " " - ) - - val cssDecoder = listOf( - "\\3C " to "<", "\\3E " to ">", "\\23 " to "#", "\\25 " to "%", - "\\7B " to "{", "\\7D " to "}", "\\7C " to "|", "\\5C " to "\\", - "\\5E " to "^", "\\7E " to "~", "\\5B " to "[", "\\5D " to "]", - "\\60 " to "`", "\\3B " to ";", "\\2F " to "/", "\\3F " to "?", - "\\3A " to ":", "\\40 " to "@", "\\3D " to "=", "\\26 " to "&", - "\\24 " to "$", "\\2B " to "+", "\\22 " to "\"", "\\2C " to ",", - "%20" to " " - ) - val converter = listOf( "\\3C " to "%3C", "\\3E " to "%3E", "\\23 " to "%23", "\\25 " to "%25", "\\7B " to "%7B", "\\7D " to "%7D", "\\7C " to "%7C", "\\5C " to "%5C", diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt index b4ce05a5..8e30346b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt @@ -11,7 +11,7 @@ import java.util.* * The enum name must match the css file name */ enum class JsAssets : InjectorContract { - MENU, MENU_DEBUG, CLICK_A, CONTEXT_A, HEADER_BADGES, SEARCH, TEXTAREA_LISTENER, NOTIF_MSG + MENU, MENU_DEBUG, CLICK_A, CONTEXT_A, MEDIA, HEADER_BADGES, SEARCH, TEXTAREA_LISTENER, NOTIF_MSG ; var file = "${name.toLowerCase(Locale.CANADA)}.min.js" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt index 53cede18..3e1e1dde 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt @@ -11,6 +11,7 @@ import ca.allanwang.kau.permissions.kauRequestPermissions import ca.allanwang.kau.utils.string import com.pitchedapps.frost.R import com.pitchedapps.frost.dbflow.loadFbCookie +import com.pitchedapps.frost.facebook.USER_AGENT_BASIC /** @@ -18,23 +19,37 @@ import com.pitchedapps.frost.dbflow.loadFbCookie * * With reference to Stack Overflow */ -fun Context.frostDownload(url: String, userAgent: String, contentDisposition: String, mimeType: String, contentLength: Long) { - L.d("Received download request", "Download $url") - val uri = Uri.parse(url) ?: return +fun Context.frostDownload(url: String?, + userAgent: String = USER_AGENT_BASIC, + contentDisposition: String? = null, + mimeType: String? = null, + contentLength: Long = 0L) { + url ?: return + frostDownload(Uri.parse(url), userAgent, contentDisposition, mimeType, contentLength) +} + +fun Context.frostDownload(uri: Uri?, + userAgent: String = USER_AGENT_BASIC, + contentDisposition: String? = null, + mimeType: String? = null, + contentLength: Long = 0L) { + uri ?: return + L.d("Received download request", "Download $uri") if (uri.scheme != "http" && uri.scheme != "https") - return L.e("Invalid download attempt", url) + return L.e("Invalid download attempt", uri.toString()) kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ -> if (!granted) return@kauRequestPermissions val request = DownloadManager.Request(uri) request.setMimeType(mimeType) val cookie = loadFbCookie(Prefs.userId) ?: return@kauRequestPermissions + val title = URLUtil.guessFileName(uri.toString(), contentDisposition, mimeType) request.addRequestHeader("cookie", cookie.cookie) request.addRequestHeader("User-Agent", userAgent) request.setDescription(string(R.string.downloading)) - request.setTitle(URLUtil.guessFileName(url, contentDisposition, mimeType)) + request.setTitle(title) request.allowScanningByMediaScanner() request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "Frost/" + URLUtil.guessFileName(url, contentDisposition, mimeType)) + request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "Frost/$title") val dm = getSystemService(DOWNLOAD_SERVICE) as DownloadManager dm.enqueue(request) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt new file mode 100644 index 00000000..eaa4e698 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt @@ -0,0 +1,234 @@ +package com.pitchedapps.frost.views + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.PointF +import android.util.AttributeSet +import android.view.GestureDetector +import android.view.MotionEvent +import android.view.View +import ca.allanwang.kau.utils.dpToPx +import ca.allanwang.kau.utils.scaleXY +import com.devbrackets.android.exomedia.ui.widget.VideoView +import com.pitchedapps.frost.utils.L + +/** + * Created by Allan Wang on 2017-10-13. + * + * VideoView with scalability + * Parent must have layout with both height & width as match_parent + */ +class FrostVideoView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : VideoView(context, attrs, defStyleAttr) { + + /** + * Shortcut for actual video view + */ + private inline val v + get() = videoViewImpl + + var backgroundView: View? = null + var onFinishedListener: () -> Unit = {} + var viewerContract: FrostVideoViewerContract? = null + + private val videoDimensions = PointF(0f, 0f) + + companion object { + + /** + * Padding between minimized video and the parent borders + * Note that this is double the actual padding + * as we are calculating then dividing by 2 + */ + private val MINIMIZED_PADDING = 10.dpToPx + private val SWIPE_TO_CLOSE_HORIZONTAL_THRESHOLD = 2f.dpToPx + private val SWIPE_TO_CLOSE_VERTICAL_THRESHOLD = 5f.dpToPx + private val SWIPE_TO_CLOSE_OFFSET_THRESHOLD = 75f.dpToPx + val ANIMATION_DURATION = 300L + private val FAST_ANIMATION_DURATION = 100L + } + + private var upperMinimizedX = 0f + private var upperMinimizedY = 0f + + var isExpanded: Boolean = true + set(value) { + if (field == value) return + if (videoDimensions.x <= 0f || videoDimensions.y <= 0f) + return L.d("Attempted to toggle video expansion when points have not been finalized") + field = value + if (field) { + animate().scaleXY(1f).translationX(0f).translationY(0f).setDuration(ANIMATION_DURATION).withStartAction { + backgroundView?.animate()?.alpha(1f)?.setDuration(ANIMATION_DURATION) + viewerContract?.onFade(1f, ANIMATION_DURATION) + } + } else { + hideControls() + val height = height + val width = width + val scale = Math.min(height / 4f / videoDimensions.y, width / 2.3f / videoDimensions.x) + val desiredHeight = scale * videoDimensions.y + val desiredWidth = scale * videoDimensions.x + val translationX = (width - MINIMIZED_PADDING - desiredWidth) / 2 + val translationY = (height - MINIMIZED_PADDING - desiredHeight) / 2 + upperMinimizedX = width - desiredWidth - MINIMIZED_PADDING + upperMinimizedY = height - desiredHeight - MINIMIZED_PADDING + animate().scaleXY(scale).translationX(translationX).translationY(translationY).setDuration(ANIMATION_DURATION).withStartAction { + backgroundView?.animate()?.alpha(0f)?.setDuration(ANIMATION_DURATION) + viewerContract?.onFade(0f, ANIMATION_DURATION) + } + } + } + + init { + setOnPreparedListener { + start() + showControls() + } + setOnCompletionListener { + viewerContract?.onVideoComplete() + } + setOnTouchListener(FrameTouchListener(context)) + v.setOnTouchListener(VideoTouchListener(context)) + setOnVideoSizedChangedListener { intrinsicWidth, intrinsicHeight -> + val ratio = Math.min(width.toFloat() / intrinsicWidth, height.toFloat() / intrinsicHeight.toFloat()) + videoDimensions.set(ratio * intrinsicWidth, ratio * intrinsicHeight) + } + } + + fun jumpToStart() { + pause() + videoControls?.hide() + v.seekTo(0) + videoControls?.finishLoading() + } + + private fun hideControls() { + if (videoControls?.isVisible == true) + videoControls?.hide() + } + + private fun toggleControls() { + if (videoControls?.isVisible == true) + hideControls() + else + showControls() + } + + fun shouldParentAcceptTouch(ev: MotionEvent): Boolean { + if (isExpanded) return true + return ev.x >= upperMinimizedX && ev.y >= upperMinimizedY + } + + fun destroy() { + stopPlayback() + if (alpha > 0f) + animate().alpha(0f).setDuration(FAST_ANIMATION_DURATION).withEndAction { onFinishedListener() }.withStartAction { + viewerContract?.onFade(0f, FAST_ANIMATION_DURATION) + }.start() + else + onFinishedListener() + } + + private fun onHorizontalSwipe(offset: Float) { + val alpha = Math.max((1f - Math.abs(offset / SWIPE_TO_CLOSE_OFFSET_THRESHOLD)) * 0.5f + 0.5f, 0f) + this.alpha = alpha + } + + /* + * ------------------------------------------------------------------- + * Touch Listeners + * ------------------------------------------------------------------- + */ + + private inner class FrameTouchListener(context: Context) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener { + + private val gestureDetector: GestureDetector = GestureDetector(context, this) + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(view: View, event: MotionEvent): Boolean { + if (!isExpanded) return false + gestureDetector.onTouchEvent(event) + return true + } + + override fun onSingleTapConfirmed(event: MotionEvent): Boolean { + if (viewerContract?.onSingleTapConfirmed(event) != true) + toggleControls() + return true + } + + override fun onDoubleTap(e: MotionEvent): Boolean { + isExpanded = !isExpanded + return true + } + } + + /** + * Monitors the view click events to show and hide the video controls if they have been specified. + */ + private inner class VideoTouchListener(context: Context) : GestureDetector.SimpleOnGestureListener(), View.OnTouchListener { + + private val gestureDetector: GestureDetector = GestureDetector(context, this) + private val downLoc = PointF() + private var baseSwipeX = -1f + private var baseTranslateX = -1f + private var checkForDismiss = true + private var onSwipe = false + + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(view: View, event: MotionEvent): Boolean { + gestureDetector.onTouchEvent(event) + when (event.actionMasked) { + MotionEvent.ACTION_DOWN -> { + checkForDismiss = !isExpanded + onSwipe = false + downLoc.x = event.rawX + downLoc.y = event.rawY + } + MotionEvent.ACTION_MOVE -> { + if (onSwipe) { + val dx = baseSwipeX - event.rawX + translationX = baseTranslateX - dx + onHorizontalSwipe(dx) + } else if (checkForDismiss) { + if (Math.abs(event.rawY - downLoc.y) > SWIPE_TO_CLOSE_VERTICAL_THRESHOLD) + checkForDismiss = false + else if (Math.abs(event.rawX - downLoc.x) > SWIPE_TO_CLOSE_HORIZONTAL_THRESHOLD) { + onSwipe = true + baseSwipeX = event.rawX + baseTranslateX = translationX + } + } + } + MotionEvent.ACTION_UP -> { + if (onSwipe) { + if (Math.abs(baseSwipeX - event.rawX) > SWIPE_TO_CLOSE_OFFSET_THRESHOLD) + destroy() + else + animate().translationX(baseTranslateX).setDuration(FAST_ANIMATION_DURATION).withStartAction { + animate().alpha(1f) + } + } + } + } + return true + } + + override fun onSingleTapConfirmed(event: MotionEvent): Boolean { + if (viewerContract?.onSingleTapConfirmed(event) == true) return true + if (!isExpanded) { + isExpanded = true + return true + } + toggleControls() + return true + } + + override fun onDoubleTap(e: MotionEvent): Boolean { + isExpanded = !isExpanded + return true + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt new file mode 100644 index 00000000..0f7d49e8 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt @@ -0,0 +1,121 @@ +package com.pitchedapps.frost.views + +import android.content.Context +import android.graphics.Color +import android.net.Uri +import android.support.constraint.ConstraintLayout +import android.support.v7.widget.Toolbar +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import ca.allanwang.kau.utils.* +import com.mikepenz.google_material_typeface_library.GoogleMaterial +import com.pitchedapps.frost.R +import com.pitchedapps.frost.facebook.formattedFbUrl +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.utils.Prefs +import com.pitchedapps.frost.utils.frostDownload + +/** + * Created by Allan Wang on 2017-10-13. + */ +class FrostVideoViewer @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr), FrostVideoViewerContract { + + override fun onFade(alpha: Float, duration: Long) { + toolbar.animate().alpha(alpha).setDuration(duration) + } + + override fun onSingleTapConfirmed(event: MotionEvent): Boolean { + if (restarter.isVisible) { + restarter.performClick() + return true + } + return false + } + + override fun onVideoComplete() { + video.jumpToStart() + restarter.fadeIn() + } + + val container: ViewGroup by bindView(R.id.video_container) + val toolbar: Toolbar by bindView(R.id.video_toolbar) + val background: View by bindView(R.id.video_background) + val video: FrostVideoView by bindView(R.id.video) + val restarter: ImageView by bindView(R.id.video_restart) + + + companion object { + /** + * Simplified binding to add video to layout, and remove it when finished + * This is under the assumption that the container allows for overlays, + * such as a FrameLayout + */ + inline fun showVideo(container: ViewGroup, url: String, crossinline onFinish: () -> Unit): FrostVideoViewer { + val videoViewer = FrostVideoViewer(container.context) + container.addView(videoViewer) + videoViewer.bringToFront() + L.d("Create video view", url) + videoViewer.setVideo(url) + videoViewer.video.onFinishedListener = { container.removeView(videoViewer); onFinish() } + return videoViewer + } + } + + init { + inflate(R.layout.view_video, true) + alpha = 0f + background.setBackgroundColor(if (Prefs.bgColor.isColorDark) Prefs.bgColor.withMinAlpha(200) else Color.BLACK) + video.backgroundView = background + video.viewerContract = this + video.pause() + toolbar.inflateMenu(R.menu.menu_video) + toolbar.setBackgroundColor(Prefs.headerColor) + context.setMenuIcons(toolbar.menu, Prefs.iconColor, + R.id.action_pip to GoogleMaterial.Icon.gmd_picture_in_picture_alt, + R.id.action_download to GoogleMaterial.Icon.gmd_file_download + ) + toolbar.setOnMenuItemClickListener { + when (it.itemId) { + R.id.action_pip -> video.isExpanded = false + R.id.action_download -> context.frostDownload(video.videoUri) + } + true + } + restarter.gone().setIcon(GoogleMaterial.Icon.gmd_replay, 64) + restarter.setOnClickListener { + video.restart() + restarter.fadeOut { restarter.gone() } + } + } + + fun setVideo(url: String) { + animate().alpha(1f).setDuration(FrostVideoView.ANIMATION_DURATION).start() + video.setVideoURI(Uri.parse(url.formattedFbUrl)) + } + + /** + * Handle back presses + * returns true if consumed, false otherwise + */ + fun onBackPressed(): Boolean { + if (video.isExpanded) { + video.isExpanded = false + return true + } + return false + } + + fun pause() = video.pause() + +} + +interface FrostVideoViewerContract { + fun onSingleTapConfirmed(event: MotionEvent): Boolean + fun onFade(alpha: Float, duration: Long) + fun onVideoComplete() +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt index 93d5c773..07703dde 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -6,6 +6,7 @@ import android.webkit.JavascriptInterface import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.FbCookie +import com.pitchedapps.frost.facebook.formattedFbUrl import com.pitchedapps.frost.utils.* import io.reactivex.subjects.Subject @@ -35,6 +36,12 @@ class FrostJSI(val webView: FrostWebViewCore) { fun loadUrl(url: String?): Boolean = if (url == null) false else webView.requestWebOverlay(url) + @JavascriptInterface + fun loadVideo(url: String?) { + if (url != null) + webView.post { activity?.showVideo(url) } + } + @JavascriptInterface fun reloadBaseUrl(animate: Boolean) { L.d("FrostJSI reload") diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt index 2d9915be..bf53c7eb 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt @@ -1,8 +1,10 @@ package com.pitchedapps.frost.web -import com.pitchedapps.frost.activities.WebOverlayBasicActivity +import com.pitchedapps.frost.activities.MainActivity import com.pitchedapps.frost.activities.WebOverlayActivity import com.pitchedapps.frost.activities.WebOverlayActivityBase +import com.pitchedapps.frost.activities.WebOverlayBasicActivity +import com.pitchedapps.frost.facebook.FB_URL_BASE import com.pitchedapps.frost.facebook.FbItem import com.pitchedapps.frost.facebook.USER_AGENT_BASIC import com.pitchedapps.frost.facebook.formattedFbUrl @@ -17,7 +19,7 @@ import com.pitchedapps.frost.utils.launchWebOverlay * cannot be resolved on a new window and must instead * by loaded in the current page * This helper method will collect all known cases and launch the overlay accordingly - * Returns {@code true} (default) if overlay is launched, {@code false} otherwise + * Returns {@code true} (default) if action is consumed, {@code false} otherwise * * If the request already comes from an instance of [WebOverlayActivity], we will then judge * whether the user agent string should be changed. All propagated results will return false, @@ -25,7 +27,6 @@ import com.pitchedapps.frost.utils.launchWebOverlay */ fun FrostWebViewCore.requestWebOverlay(url: String): Boolean { if (url == "#") return false - if (context is WebOverlayActivityBase) { L.v("Check web request from overlay", url) //already overlay; manage user agent @@ -70,10 +71,10 @@ fun FrostWebViewCore.requestWebOverlay(url: String): Boolean { /** * If the url contains any one of the whitelist segments, switch to the chat overlay */ -val messageWhitelist = setOf(FbItem.MESSAGES.url, FbItem.CHAT.url) +val messageWhitelist = setOf(FbItem.MESSAGES, FbItem.CHAT, FbItem.FEED_MOST_RECENT, FbItem.FEED_TOP_STORIES).map { it.url }.toSet() val String.shouldUseBasicAgent - get() = (messageWhitelist.any { contains(it) }) + get() = (messageWhitelist.any { contains(it) }) || this == FB_URL_BASE /** * The following components should never be launched in a new overlay diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt index 3275b2a6..e3803134 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt @@ -110,6 +110,7 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient JsAssets.TEXTAREA_LISTENER, CssHider.ADS.maybe(!Prefs.showFacebookAds && IS_FROST_PRO), JsAssets.CONTEXT_A, + JsAssets.MEDIA.maybe(webCore.baseEnum != null), JsAssets.HEADER_BADGES.maybe(webCore.baseEnum != null) ) } diff --git a/app/src/main/res/layout/view_video.xml b/app/src/main/res/layout/view_video.xml new file mode 100644 index 00000000..e8782459 --- /dev/null +++ b/app/src/main/res/layout/view_video.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_video.xml b/app/src/main/res/menu/menu_video.xml new file mode 100644 index 00000000..955a03ee --- /dev/null +++ b/app/src/main/res/menu/menu_video.xml @@ -0,0 +1,15 @@ + + + + + + diff --git a/app/src/main/res/values-de/strings_download.xml b/app/src/main/res/values-de/strings_download.xml index 77c1b68c..862049f5 100644 --- a/app/src/main/res/values-de/strings_download.xml +++ b/app/src/main/res/values-de/strings_download.xml @@ -2,6 +2,7 @@ Foto auswählen + Downloaden Downloade… Foto heruntergeladen Fehler beim Download des Fotos diff --git a/app/src/main/res/values-es/strings_download.xml b/app/src/main/res/values-es/strings_download.xml index c15378f4..4890b870 100644 --- a/app/src/main/res/values-es/strings_download.xml +++ b/app/src/main/res/values-es/strings_download.xml @@ -2,6 +2,7 @@ Seleccionar imagen + Descargar Descargando… Imagen descargada Descarga de imagen fallida diff --git a/app/src/main/res/values-fr/strings_download.xml b/app/src/main/res/values-fr/strings_download.xml index 3480ad1f..146d71e7 100644 --- a/app/src/main/res/values-fr/strings_download.xml +++ b/app/src/main/res/values-fr/strings_download.xml @@ -2,6 +2,7 @@ Sélectionner une image + Télécharger Téléchargement… Image téléchargée Échec du téléchargement de l\'image diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 82aa82f1..4c9b7285 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -59,4 +59,7 @@ Top Bar Bottom Bar + + PIP + diff --git a/app/src/main/res/values/strings_download.xml b/app/src/main/res/values/strings_download.xml index ef166508..c0cb8cd4 100644 --- a/app/src/main/res/values/strings_download.xml +++ b/app/src/main/res/values/strings_download.xml @@ -1,6 +1,7 @@ Pick Image + Download Downloading… Image downloaded Image failed to download diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b91710d2..ab893b3a 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -41,6 +41,13 @@ @style/KauFadeInFadeOut + + diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml index b46889c2..5cc8a2c8 100644 --- a/app/src/main/res/xml/frost_changelog.xml +++ b/app/src/main/res/xml/frost_changelog.xml @@ -5,14 +5,22 @@ --> - - - - - + + + + + + + + + + + + + - - + + diff --git a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt index 3a87a697..57589b5e 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt @@ -1,6 +1,9 @@ package com.pitchedapps.frost.facebook +import okhttp3.HttpUrl +import okio.Utf8 import org.junit.Test +import java.net.URLDecoder import kotlin.test.assertEquals @@ -26,11 +29,6 @@ class FbUrlTest { assertFbFormat("$FB_URL_BASE${url.substring(1)}", url) } - @Test - fun redirect() { - assertFbFormat("${FB_URL_BASE}relative/?asdf=1234&hjkl=7890", "https://touch.facebook.com/l.php?u=${FB_URL_BASE}relative/&asdf=1234&hjkl=7890") - } - @Test fun discard() { val prefix = "$FB_URL_BASE?test=1234" @@ -43,11 +41,4 @@ class FbUrlTest { assertFbFormat("${FB_URL_BASE}relative", "$FB_URL_BASE/relative") } - @Test - fun css() { - val expected = "https://test.com?efg=hi&oh=bye&oe=apple%3Fornot" - val orig = "https\\3a //test.com?efg=hi&oh=bye&oe=apple\\3F ornot" - assertFbFormat(expected, orig) - } - } \ No newline at end of file -- cgit v1.2.3