aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com/pitchedapps
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-10-24 23:29:55 -0400
committerGitHub <noreply@github.com>2017-10-24 23:29:55 -0400
commitc2ca9066c6fd760bd6ef5d2f8f0530a89bfa7b66 (patch)
treea54665bb873b650b8f6f03b76cd59456ef79e296 /app/src/main/kotlin/com/pitchedapps
parent64dbf74b7a44a25f41ed7ff2ebfa11db0bc91769 (diff)
downloadfrost-c2ca9066c6fd760bd6ef5d2f8f0530a89bfa7b66.tar.gz
frost-c2ca9066c6fd760bd6ef5d2f8f0530a89bfa7b66.tar.bz2
frost-c2ca9066c6fd760bd6ef5d2f8f0530a89bfa7b66.zip
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
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt23
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/VideoActivity.kt33
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt44
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/injectors/JsAssets.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt27
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt234
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt121
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt7
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt11
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt1
10 files changed, 454 insertions, 49 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
index 1a96601d..b6232272 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt
@@ -59,6 +59,7 @@ import com.pitchedapps.frost.utils.iab.FrostBilling
import com.pitchedapps.frost.utils.iab.IabMain
import com.pitchedapps.frost.utils.iab.IS_FROST_PRO
import com.pitchedapps.frost.views.BadgedIcon
+import com.pitchedapps.frost.views.FrostVideoViewer
import com.pitchedapps.frost.views.FrostViewPager
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
@@ -80,6 +81,7 @@ class MainActivity : BaseActivity(),
val tabs: TabLayout by bindView(R.id.tabs)
val appBar: AppBarLayout by bindView(R.id.appbar)
val coordinator: CoordinatorLayout by bindView(R.id.main_content)
+ var videoViewer: FrostVideoViewer? = null
lateinit var drawer: Drawer
lateinit var drawerHeader: AccountHeader
var webFragmentObservable = PublishSubject.create<Int>()!!
@@ -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<String, String>()
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(
- "&amp;" to "&"
- )
+ val misc = arrayOf("&amp;" 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 <a href="https://stackoverflow.com/questions/33434532/android-webview-download-files-like-browsers-do">Stack Overflow</a>
*/
-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
@@ -36,6 +37,12 @@ class FrostJSI(val webView: FrostWebViewCore) {
= 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")
webView.post {
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)
)
}