From 2b51bc4bfa86863ed14b550fe3281840587ab038 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sun, 12 Nov 2017 02:48:36 -0500 Subject: enhancement/video-player (#480) * Add toolbar visibility toggle and draw it over viewer * Set contract bindings once available * Fix video url param error and prepare progressanimator * Add gif support and better transitions * Interface a lot of things * Reorder back press * Clean up files and fix selector * Add gif support * Redraw bounds when necessary --- app/src/main/AndroidManifest.xml | 6 +- app/src/main/assets/js/media.js | 6 +- app/src/main/assets/js/media.min.js | 20 +++---- .../pitchedapps/frost/activities/BaseActivity.kt | 33 +++++++--- .../pitchedapps/frost/activities/MainActivity.kt | 50 +++------------- .../pitchedapps/frost/activities/VideoActivity.kt | 33 ---------- .../frost/activities/WebOverlayActivity.kt | 20 ++++++- .../frost/contracts/VideoViewerContract.kt | 54 +++++++++++++++++ .../com/pitchedapps/frost/facebook/FbConst.kt | 2 +- .../pitchedapps/frost/facebook/FbUrlFormatter.kt | 6 +- .../kotlin/com/pitchedapps/frost/utils/Animator.kt | 70 ++++++++++++++++++++++ .../kotlin/com/pitchedapps/frost/utils/Utils.kt | 4 ++ .../com/pitchedapps/frost/views/FrostVideoView.kt | 63 +++++++++++++------ .../pitchedapps/frost/views/FrostVideoViewer.kt | 49 ++++++++++----- .../kotlin/com/pitchedapps/frost/web/FrostJSI.kt | 6 +- .../frost/web/FrostUrlOverlayValidator.kt | 14 +++-- .../pitchedapps/frost/web/FrostWebViewClients.kt | 4 +- app/src/main/res/layout/activity_frame_wrapper.xml | 3 +- app/src/main/res/layout/activity_selector.xml | 16 +---- app/src/main/res/layout/view_video.xml | 11 ++-- app/src/main/res/xml/frost_changelog.xml | 8 +++ .../com/pitchedapps/frost/facebook/FbUrlTest.kt | 9 +++ 22 files changed, 322 insertions(+), 165 deletions(-) delete mode 100644 app/src/main/kotlin/com/pitchedapps/frost/activities/VideoActivity.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/contracts/VideoViewerContract.kt create mode 100644 app/src/main/kotlin/com/pitchedapps/frost/utils/Animator.kt (limited to 'app/src') diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5ebb2118..5acf19d6 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -42,12 +42,14 @@ android:theme="@style/FrostTheme" /> + @@ -135,8 +139,6 @@ - super.onBackPressed() } checkBoxPromptRes(R.string.kau_do_not_show_again, false, { _, b -> Prefs.exitConfirmation = !b }) } - } else super.onBackPressed() + return + } + super.onBackPressed() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setFrostTheme() + if (this !is WebOverlayActivityBase) setFrostTheme() } private var networkDisposable: Disposable? = null @@ -47,8 +54,7 @@ abstract class BaseActivity : KauBaseActivity() { networkDisposable = ReactiveNetwork.observeNetworkConnectivity(applicationContext) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) - .subscribe { - connectivity: Connectivity -> + .subscribe { connectivity: Connectivity -> connectivity.apply { L.d("Network connectivity changed: isAvailable: $isAvailable isRoaming: $isRoaming") consumer(connectivity) @@ -64,12 +70,23 @@ abstract class BaseActivity : KauBaseActivity() { override fun onResume() { super.onResume() - disposeNetworkConnectivity() - observeNetworkConnectivity() +// disposeNetworkConnectivity() +// observeNetworkConnectivity() } override fun onPause() { super.onPause() - disposeNetworkConnectivity() +// disposeNetworkConnectivity() + } + + + override fun onStop() { + if (this is VideoViewerContract) videoOnStop() + super.onStop() + } + + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + if (this is VideoViewerContract) videoViewer?.updateLocation() } } \ No newline at end of file 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 2f4bd2e1..d4c30547 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/MainActivity.kt @@ -5,7 +5,6 @@ import android.app.AlarmManager import android.app.PendingIntent import android.content.Context import android.content.Intent -import android.content.res.Configuration import android.graphics.PointF import android.graphics.drawable.ColorDrawable import android.net.Uri @@ -48,6 +47,7 @@ import com.pitchedapps.frost.R import com.pitchedapps.frost.contracts.ActivityWebContract import com.pitchedapps.frost.contracts.FileChooserContract import com.pitchedapps.frost.contracts.FileChooserDelegate +import com.pitchedapps.frost.contracts.VideoViewerContract import com.pitchedapps.frost.dbflow.loadFbCookie import com.pitchedapps.frost.dbflow.loadFbTabs import com.pitchedapps.frost.enums.MainActivityLayout @@ -63,7 +63,6 @@ import com.pitchedapps.frost.utils.iab.FrostBilling import com.pitchedapps.frost.utils.iab.IS_FROST_PRO import com.pitchedapps.frost.utils.iab.IabMain import com.pitchedapps.frost.views.BadgedIcon -import com.pitchedapps.frost.views.FrostVideoContainerContract import com.pitchedapps.frost.views.FrostVideoViewer import com.pitchedapps.frost.views.FrostViewPager import io.reactivex.android.schedulers.AndroidSchedulers @@ -77,18 +76,18 @@ import java.util.concurrent.TimeUnit class MainActivity : BaseActivity(), ActivityWebContract, FileChooserContract by FileChooserDelegate(), - FrostVideoContainerContract, + VideoViewerContract, FrostBilling by IabMain() { lateinit var adapter: SectionsPagerAdapter - val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper) + override val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper) val toolbar: Toolbar by bindView(R.id.toolbar) val viewPager: FrostViewPager by bindView(R.id.container) val fab: FloatingActionButton by bindView(R.id.fab) 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 + override var videoViewer: FrostVideoViewer? = null lateinit var drawer: Drawer lateinit var drawerHeader: AccountHeader var webFragmentObservable = PublishSubject.create()!! @@ -130,8 +129,7 @@ class MainActivity : BaseActivity(), "Frost id" to Prefs.frostId) } } - setContentView(R.layout.activity_frame_wrapper) - frameWrapper.inflate(Prefs.mainActivityLayout.layoutRes, true) + setFrameContentView(Prefs.mainActivityLayout.layoutRes) setSupportActionBar(toolbar) adapter = SectionsPagerAdapter(supportFragmentManager, loadFbTabs()) viewPager.adapter = adapter @@ -147,7 +145,7 @@ class MainActivity : BaseActivity(), override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) { super.onPageScrolled(position, positionOffset, positionOffsetPixels) - val delta: Float by lazy { positionOffset * (255 - 128).toFloat() } + val delta = positionOffset * (255 - 128).toFloat() tabsForEachView { tabPosition, view -> view.setAllAlpha(when (tabPosition) { position -> 255.0f - delta @@ -169,14 +167,6 @@ class MainActivity : BaseActivity(), onCreateBilling() } - fun showVideo(url: String) { - if (videoViewer != null) { - videoViewer?.setVideo(url) - } else { - videoViewer = FrostVideoViewer.showVideo(url, this) - } - } - fun tabsForEachView(action: (position: Int, view: BadgedIcon) -> Unit) { (0 until tabs.tabCount).asSequence().forEach { i -> action(i, tabs.getTabAt(i)!!.customView as BadgedIcon) @@ -312,7 +302,7 @@ class MainActivity : BaseActivity(), } } - fun Builder.primaryFrostItem(item: FbItem) = this.primaryItem(item.titleId) { + private fun Builder.primaryFrostItem(item: FbItem) = this.primaryItem(item.titleId) { iicon = item.icon iconColor = Prefs.textColor.toLong() textColor = Prefs.textColor.toLong() @@ -331,7 +321,7 @@ class MainActivity : BaseActivity(), } } - fun Builder.secondaryFrostItem(@StringRes title: Int, onClick: () -> Unit) = this.secondaryItem(title) { + private fun Builder.secondaryFrostItem(@StringRes title: Int, onClick: () -> Unit) = this.secondaryItem(title) { textColor = Prefs.textColor.toLong() selectedIconColor = Prefs.textColor.toLong() selectedTextColor = Prefs.textColor.toLong() @@ -436,23 +426,11 @@ 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() - } - inline val currentFragment get() = supportFragmentManager.findFragmentByTag("android:switcher:${R.id.container}:${viewPager.currentItem}") as WebFragment @@ -489,16 +467,4 @@ class MainActivity : BaseActivity(), else PointF(0f, 0f) - override val videoContainer: FrameLayout - get() = frameWrapper - - override fun onVideoFinished() { - L.d("Video view released") - videoViewer = null - } - - override fun onConfigurationChanged(newConfig: Configuration) { - super.onConfigurationChanged(newConfig) - videoViewer?.updateLocation() - } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/VideoActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/VideoActivity.kt deleted file mode 100644 index 5943c73c..00000000 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/VideoActivity.kt +++ /dev/null @@ -1,33 +0,0 @@ -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/activities/WebOverlayActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt index c2556563..e20cfbf2 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/WebOverlayActivity.kt @@ -1,6 +1,7 @@ package com.pitchedapps.frost.activities import android.content.Intent +import android.graphics.PointF import android.net.Uri import android.os.Bundle import android.support.design.widget.CoordinatorLayout @@ -10,6 +11,7 @@ import android.view.Menu import android.view.MenuItem import android.webkit.ValueCallback import android.webkit.WebChromeClient +import android.widget.FrameLayout import ca.allanwang.kau.internal.KauBaseActivity import ca.allanwang.kau.swipe.kauSwipeOnCreate import ca.allanwang.kau.swipe.kauSwipeOnDestroy @@ -20,9 +22,11 @@ import com.pitchedapps.frost.R import com.pitchedapps.frost.contracts.ActivityWebContract import com.pitchedapps.frost.contracts.FileChooserContract import com.pitchedapps.frost.contracts.FileChooserDelegate +import com.pitchedapps.frost.contracts.VideoViewerContract import com.pitchedapps.frost.enums.OverlayContext import com.pitchedapps.frost.facebook.* import com.pitchedapps.frost.utils.* +import com.pitchedapps.frost.views.FrostVideoViewer import com.pitchedapps.frost.web.FrostWebView import io.reactivex.disposables.Disposable import okhttp3.HttpUrl @@ -95,9 +99,10 @@ class WebOverlayBasicActivity : WebOverlayActivityBase(true) */ class WebOverlayActivity : WebOverlayActivityBase(false) -open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : KauBaseActivity(), - ActivityWebContract, FileChooserContract by FileChooserDelegate() { +open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : BaseActivity(), + ActivityWebContract, VideoViewerContract, FileChooserContract by FileChooserDelegate() { + override val frameWrapper: FrameLayout by bindView(R.id.frame_wrapper) val toolbar: Toolbar by bindView(R.id.overlay_toolbar) val frostWeb: FrostWebView by bindView(R.id.overlay_frost_webview) val coordinator: CoordinatorLayout by bindView(R.id.overlay_main_content) @@ -122,7 +127,7 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : KauBas finish() return } - setContentView(R.layout.activity_web_overlay) + setFrameContentView(R.layout.activity_web_overlay) setSupportActionBar(toolbar) supportActionBar?.setDisplayShowHomeEnabled(true) supportActionBar?.setDisplayHomeAsUpEnabled(true) @@ -219,4 +224,13 @@ open class WebOverlayActivityBase(private val forceBasicAgent: Boolean) : KauBas } return true } + + /* + * ---------------------------------------------------- + * Video Contract + * ---------------------------------------------------- + */ + override var videoViewer: FrostVideoViewer? = null + override val lowerVideoPadding: PointF = PointF(0f, 0f) + } \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/VideoViewerContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/VideoViewerContract.kt new file mode 100644 index 00000000..2e6ad04f --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/VideoViewerContract.kt @@ -0,0 +1,54 @@ +package com.pitchedapps.frost.contracts + +import android.app.Activity +import android.widget.FrameLayout +import ca.allanwang.kau.utils.inflate +import com.pitchedapps.frost.R +import com.pitchedapps.frost.utils.L +import com.pitchedapps.frost.views.FrostVideoContainerContract +import com.pitchedapps.frost.views.FrostVideoViewer + +/** + * Created by Allan Wang on 2017-11-10. + */ +interface VideoViewerContract : FrameWrapper, FrostVideoContainerContract { + + var videoViewer: FrostVideoViewer? + + fun showVideo(url: String) + = showVideo(url, false) + + /** + * Create new viewer and reuse existing one + * The url will be formatted upon loading + */ + fun showVideo(url: String, repeat: Boolean) { + if (videoViewer != null) + videoViewer?.setVideo(url, repeat) + else + videoViewer = FrostVideoViewer.showVideo(url, repeat, this) + } + + fun videoOnStop() = videoViewer?.pause() + + fun videoOnBackPress() = videoViewer?.onBackPressed() ?: false + + override val videoContainer: FrameLayout + get() = frameWrapper + + override fun onVideoFinished() { + L.d("Video view released") + videoViewer = null + } +} + +interface FrameWrapper { + + val frameWrapper: FrameLayout + + fun Activity.setFrameContentView(layoutRes: Int) { + setContentView(R.layout.activity_frame_wrapper) + frameWrapper.inflate(layoutRes, true) + } + +} diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt index 65fae493..64f0b652 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbConst.kt @@ -5,7 +5,7 @@ package com.pitchedapps.frost.facebook */ const val HTTPS_FACEBOOK_COM = "https://facebook.com" const val FACEBOOK_COM = "facebook.com" -const val FB_URL_BASE = "https://m.facebook.com/" +const val FB_URL_BASE = "https://m.$FACEBOOK_COM/" fun PROFILE_PICTURE_URL(id: Long) = "https://graph.facebook.com/$id/picture?type=large" const val USER_AGENT_FULL = "Mozilla/5.0 (Linux; Android 4.4.2; en-us; SAMSUNG SM-G900T Build/KOT49H) AppleWebKit/537.36 (KHTML, like Gecko) Version/1.6 Chrome/28.0.1500.94 Mobile Safari/537.36" 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 22fc275f..0e74bb59 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbUrlFormatter.kt @@ -31,7 +31,7 @@ class FbUrlFormatter(url: String) { var cleanedUrl = url discardable.forEach { cleanedUrl = cleanedUrl.replace(it, "", true) } converter.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) } - if (cleanedUrl != url) cleanedUrl = cleanedUrl.replaceFirst("&", "?") + if (cleanedUrl != url && !cleanedUrl.contains("?")) cleanedUrl = cleanedUrl.replaceFirst("&", "?") cleanedUrl = URLDecoder.decode(cleanedUrl, StandardCharsets.UTF_8.name()) val qm = cleanedUrl.indexOf("?") if (qm > -1) { @@ -70,6 +70,8 @@ class FbUrlFormatter(url: String) { } companion object { + + const val VIDEO_REDIRECT = "/video_redirect/?src=" /** * Items here are explicitly removed from the url * Taken from FaceSlim @@ -82,7 +84,7 @@ class FbUrlFormatter(url: String) { "https://m.facebook.com/l.php?u=", "http://touch.facebook.com/l.php?u=", "https://touch.facebook.com/l.php?u=", - "/video_redirect/?src=" + VIDEO_REDIRECT ) val misc = arrayOf("&" to "&") diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Animator.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Animator.kt new file mode 100644 index 00000000..da852e6e --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Animator.kt @@ -0,0 +1,70 @@ +package com.pitchedapps.frost.utils + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.view.animation.Interpolator + +/** + * Created by Allan Wang on 2017-11-10. + */ +class ProgressAnimator private constructor(private vararg val values: Float) { + + companion object { + inline fun ofFloat(crossinline builder: ProgressAnimator.() -> Unit) = ofFloat(0f, 1f) { builder() } + + fun ofFloat(vararg values: Float, builder: ProgressAnimator.() -> Unit) = ProgressAnimator(*values).apply { + builder() + build() + } + } + + private val animators: MutableList<(Float) -> Unit> = mutableListOf() + private val startActions: MutableList<() -> Unit> = mutableListOf() + private val endActions: MutableList<() -> Unit> = mutableListOf() + + var duration: Long = -1L + var interpolator: Interpolator? = null + + /** + * Add more changes to the [ValueAnimator] before running + */ + var extraConfigs: ValueAnimator.() -> Unit = {} + + fun withAnimator(from: Float, to: Float, animator: (Float) -> Unit) = animators.add { + val range = to - from + animator(range * it + from) + } + + fun withAnimator(animator: (Float) -> Unit) = animators.add(animator) + + fun withAnimatorInv(animator: (Float) -> Unit) = animators.add { animator(1f - it) } + + fun withStartAction(action: () -> Unit) = startActions.add(action) + + fun withEndAction(action: () -> Unit) = endActions.add(action) + + fun build() { + ValueAnimator.ofFloat(*values).apply { + if (this@ProgressAnimator.duration > 0L) + duration = this@ProgressAnimator.duration + if (this@ProgressAnimator.interpolator != null) + interpolator = this@ProgressAnimator.interpolator + addUpdateListener { + val progress = it.animatedValue as Float + animators.forEach { it(progress) } + } + addListener(object : AnimatorListenerAdapter() { + override fun onAnimationStart(animation: Animator?) { + startActions.forEach { it() } + } + + override fun onAnimationEnd(animation: Animator?) { + endActions.forEach { it() } + } + }) + extraConfigs() + start() + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt index 6d6c5381..22c77f5f 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -30,6 +30,7 @@ import com.pitchedapps.frost.R import com.pitchedapps.frost.activities.* import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.* +import com.pitchedapps.frost.facebook.FbUrlFormatter.Companion.VIDEO_REDIRECT import com.pitchedapps.frost.utils.iab.IS_FROST_PRO import org.jsoup.Jsoup import org.jsoup.nodes.Element @@ -200,6 +201,9 @@ fun Context.resolveActivityForUri(uri: Uri): Boolean { inline val String?.isFacebookUrl get() = this != null && this.contains(FACEBOOK_COM) +inline val String?.isVideoUrl + get() = this != null && this.startsWith(VIDEO_REDIRECT) + fun Context.frostChangelog() = showChangelog(R.xml.frost_changelog, Prefs.textColor) { theme() if (System.currentTimeMillis() - Prefs.installDate > 2592000000) { //show after 1 month diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt index c5508a4d..639dc9ba 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoView.kt @@ -8,10 +8,12 @@ import android.util.AttributeSet import android.view.GestureDetector import android.view.MotionEvent import android.view.View +import ca.allanwang.kau.utils.AnimHolder 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 +import com.pitchedapps.frost.utils.ProgressAnimator /** * Created by Allan Wang on 2017-10-13. @@ -29,10 +31,10 @@ class FrostVideoView @JvmOverloads constructor( private inline val v get() = videoViewImpl - var backgroundView: View? = null var onFinishedListener: () -> Unit = {} - lateinit var viewerContract: FrostVideoViewerContract + private lateinit var viewerContract: FrostVideoViewerContract lateinit var containerContract: FrostVideoContainerContract + var repeat: Boolean = false private val videoDimensions = PointF(0f, 0f) @@ -47,8 +49,8 @@ class FrostVideoView @JvmOverloads constructor( 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 + const val ANIMATION_DURATION = 200L + private const val FAST_ANIMATION_DURATION = 100L } private var videoBounds = RectF() @@ -59,19 +61,32 @@ class FrostVideoView @JvmOverloads constructor( if (videoDimensions.x <= 0f || videoDimensions.y <= 0f) return L.d("Attempted to toggle video expansion when points have not been finalized") field = value + val origX = translationX + val origY = translationY + val origScale = scaleX 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) - }.withEndAction { - if (!isPlaying) showControls() + ProgressAnimator.ofFloat { + duration = ANIMATION_DURATION + interpolator = AnimHolder.fastOutSlowInInterpolator(context) + withAnimator { viewerContract.onExpand(it) } + withAnimator(origScale, 1f) { scaleXY = it } + withAnimator(origX, 0f) { translationX = it } + withAnimator(origY, 0f) { translationY = it } + withEndAction { + if (!isPlaying) showControls() + else viewerContract.onControlsHidden() + } } } else { hideControls() val (scale, tX, tY) = mapBounds() - animate().scaleXY(scale).translationX(tX).translationY(tY).setDuration(ANIMATION_DURATION).withStartAction { - backgroundView?.animate()?.alpha(0f)?.setDuration(ANIMATION_DURATION) - viewerContract.onFade(0f, ANIMATION_DURATION) + ProgressAnimator.ofFloat { + duration = ANIMATION_DURATION + interpolator = AnimHolder.fastOutSlowInInterpolator(context) + withAnimatorInv { viewerContract.onExpand(it) } + withAnimator(origScale, scale) { scaleXY = it } + withAnimator(origX, tX) { translationX = it } + withAnimator(origY, tY) { translationY = it } } } } @@ -110,16 +125,28 @@ class FrostVideoView @JvmOverloads constructor( if (isExpanded) showControls() } setOnCompletionListener { - viewerContract.onVideoComplete() + if (repeat) restart() + else viewerContract.onVideoComplete() } setOnTouchListener(FrameTouchListener(context)) v.setOnTouchListener(VideoTouchListener(context)) setOnVideoSizedChangedListener { intrinsicWidth, intrinsicHeight -> val ratio = Math.min(width.toFloat() / intrinsicWidth, height.toFloat() / intrinsicHeight.toFloat()) + /** + * Only remap if not expanded and if dimensions have changed + */ + val shouldRemap = !isExpanded + && (videoDimensions.x != ratio * intrinsicWidth || videoDimensions.y != ratio * intrinsicHeight) videoDimensions.set(ratio * intrinsicWidth, ratio * intrinsicHeight) + if (shouldRemap) updateLocation() } } + fun setViewerContract(contract: FrostVideoViewerContract) { + this.viewerContract = contract + videoControls?.setVisibilityListener(viewerContract) + } + fun jumpToStart() { pause() v.seekTo(0) @@ -136,7 +163,7 @@ class FrostVideoView @JvmOverloads constructor( override fun restart(): Boolean { videoUri ?: return false - if (videoViewImpl.restart() && isExpanded) { + if (videoViewImpl.restart() && isExpanded && !repeat) { videoControls?.showLoading(true) return true } @@ -163,9 +190,11 @@ class FrostVideoView @JvmOverloads constructor( fun destroy() { stopPlayback() if (alpha > 0f) - animate().alpha(0f).setDuration(FAST_ANIMATION_DURATION).withEndAction { onFinishedListener() }.withStartAction { - viewerContract.onFade(0f, FAST_ANIMATION_DURATION) - }.start() + ProgressAnimator.ofFloat(alpha, 0f) { + duration = FAST_ANIMATION_DURATION + withAnimator { alpha = it } + withEndAction { onFinishedListener() } + } else onFinishedListener() } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt index bf4df8fe..3a773288 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt @@ -13,6 +13,7 @@ import android.view.ViewTreeObserver import android.widget.FrameLayout import android.widget.ImageView import ca.allanwang.kau.utils.* +import com.devbrackets.android.exomedia.listener.VideoControlsVisibilityListener import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.pitchedapps.frost.R import com.pitchedapps.frost.facebook.formattedFbUrl @@ -34,18 +35,22 @@ class FrostVideoViewer @JvmOverloads constructor( val restarter: ImageView by bindView(R.id.video_restart) companion object { + /** + * Matches VideoControls.CONTROL_VISIBILITY_ANIMATION_LENGTH + */ + private const val CONTROL_ANIMATION_DURATION = 300L + /** * 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 */ - fun showVideo(url: String, contract: FrostVideoContainerContract): FrostVideoViewer { + fun showVideo(url: String, repeat: Boolean, contract: FrostVideoContainerContract): FrostVideoViewer { val container = contract.videoContainer val videoViewer = FrostVideoViewer(container.context) container.addView(videoViewer) videoViewer.bringToFront() - L.d("Create video view", url) - videoViewer.setVideo(url) + videoViewer.setVideo(url, repeat) videoViewer.video.containerContract = contract videoViewer.video.onFinishedListener = { container.removeView(videoViewer); contract.onVideoFinished() } return videoViewer @@ -56,11 +61,9 @@ class FrostVideoViewer @JvmOverloads constructor( 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.setViewerContract(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 @@ -77,12 +80,14 @@ class FrostVideoViewer @JvmOverloads constructor( video.restart() restarter.fadeOut { restarter.gone() } } -// toolbar.setOnTouchListener { _, event -> video.shouldParentAcceptTouch(event) } } - fun setVideo(url: String) { + fun setVideo(url: String, repeat: Boolean = false) { + val formattedUrl = url.formattedFbUrl + L.d("Load video view; repeat: $repeat", url) animate().alpha(1f).setDuration(FrostVideoView.ANIMATION_DURATION).start() - video.setVideoURI(Uri.parse(url.formattedFbUrl)) + video.setVideoURI(Uri.parse(formattedUrl)) + video.repeat = repeat } /** @@ -106,10 +111,9 @@ class FrostVideoViewer @JvmOverloads constructor( * ------------------------------------------------------------- */ - override fun onFade(alpha: Float, duration: Long) { - toolbar.visible().animate().alpha(alpha).setDuration(duration).withEndAction { - if (alpha == 0f) toolbar.gone() - } + override fun onExpand(progress: Float) { + toolbar.goneIf(progress == 0f).alpha = progress + background.alpha = progress } override fun onSingleTapConfirmed(event: MotionEvent): Boolean { @@ -134,11 +138,26 @@ class FrostVideoViewer @JvmOverloads constructor( }) } + override fun onControlsShown() { + if (video.isExpanded) + toolbar.fadeIn(duration = CONTROL_ANIMATION_DURATION, onStart = { toolbar.visible() }) + } + + override fun onControlsHidden() { + if (!toolbar.isGone) + toolbar.fadeOut(duration = CONTROL_ANIMATION_DURATION) { toolbar.gone() } + } + } -interface FrostVideoViewerContract { +interface FrostVideoViewerContract : VideoControlsVisibilityListener { fun onSingleTapConfirmed(event: MotionEvent): Boolean - fun onFade(alpha: Float, duration: Long) + /** + * Process of expansion + * 1f represents an expanded view, 0f represents a minimized view + */ + fun onExpand(progress: Float) + fun onVideoComplete() } 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 4700c894..71ceb4ca 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostJSI.kt @@ -4,6 +4,7 @@ import android.content.Context import android.support.v4.widget.SwipeRefreshLayout import android.webkit.JavascriptInterface import com.pitchedapps.frost.activities.MainActivity +import com.pitchedapps.frost.contracts.VideoViewerContract import com.pitchedapps.frost.dbflow.CookieModel import com.pitchedapps.frost.facebook.FbCookie import com.pitchedapps.frost.utils.* @@ -36,9 +37,10 @@ class FrostJSI(val webView: FrostWebViewCore) { = if (url == null) false else webView.requestWebOverlay(url) @JavascriptInterface - fun loadVideo(url: String?) { + fun loadVideo(url: String?, isGif: Boolean) { if (url != null) - webView.post { activity?.showVideo(url) } + webView.post { (context as? VideoViewerContract)?.showVideo(url, isGif) + ?: L.d("Could not load video; contract not implemented") } } @JavascriptInterface 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 ac5cde29..334ef51b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt @@ -3,13 +3,12 @@ package com.pitchedapps.frost.web import com.pitchedapps.frost.activities.WebOverlayActivity import com.pitchedapps.frost.activities.WebOverlayActivityBase import com.pitchedapps.frost.activities.WebOverlayBasicActivity +import com.pitchedapps.frost.contracts.VideoViewerContract 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 -import com.pitchedapps.frost.utils.L -import com.pitchedapps.frost.utils.isFacebookUrl -import com.pitchedapps.frost.utils.launchWebOverlay +import com.pitchedapps.frost.utils.* /** * Created by Allan Wang on 2017-08-15. @@ -26,6 +25,12 @@ import com.pitchedapps.frost.utils.launchWebOverlay */ fun FrostWebViewCore.requestWebOverlay(url: String): Boolean { if (url == "#") return false + if (url.isVideoUrl && context is VideoViewerContract) { + L.i("Found video", url) + (context as VideoViewerContract).showVideo(url) + return true + } + if (!Prefs.overlayEnabled) return false if (context is WebOverlayActivityBase) { L.v("Check web request from overlay", url) //already overlay; manage user agent @@ -73,7 +78,8 @@ fun FrostWebViewCore.requestWebOverlay(url: String): Boolean { 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) }) || this == FB_URL_BASE + get() = !contains("story.php") //we will use basic agent for anything that isn't a comment section +// 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 e3803134..c8c7e2e7 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt @@ -106,11 +106,11 @@ open class FrostWebViewClient(val webCore: FrostWebViewCore) : BaseWebViewClient injectBackgroundColor() webCore.jsInject( JsActions.LOGIN_CHECK, - JsAssets.CLICK_A.maybe(Prefs.overlayEnabled), + JsAssets.CLICK_A, JsAssets.TEXTAREA_LISTENER, CssHider.ADS.maybe(!Prefs.showFacebookAds && IS_FROST_PRO), JsAssets.CONTEXT_A, - JsAssets.MEDIA.maybe(webCore.baseEnum != null), + JsAssets.MEDIA, JsAssets.HEADER_BADGES.maybe(webCore.baseEnum != null) ) } diff --git a/app/src/main/res/layout/activity_frame_wrapper.xml b/app/src/main/res/layout/activity_frame_wrapper.xml index 585789ef..e51d3391 100644 --- a/app/src/main/res/layout/activity_frame_wrapper.xml +++ b/app/src/main/res/layout/activity_frame_wrapper.xml @@ -2,4 +2,5 @@ \ No newline at end of file + android:layout_height="match_parent" + android:fitsSystemWindows="true" /> \ No newline at end of file diff --git a/app/src/main/res/layout/activity_selector.xml b/app/src/main/res/layout/activity_selector.xml index a5f90aab..d9625731 100644 --- a/app/src/main/res/layout/activity_selector.xml +++ b/app/src/main/res/layout/activity_selector.xml @@ -2,9 +2,9 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/view_video.xml b/app/src/main/res/layout/view_video.xml index b8226ff2..ca256477 100644 --- a/app/src/main/res/layout/view_video.xml +++ b/app/src/main/res/layout/view_video.xml @@ -14,16 +14,10 @@ android:layout_height="match_parent" android:clickable="false" /> - - + \ No newline at end of file diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml index 65d577e0..6f1dc7a3 100644 --- a/app/src/main/res/xml/frost_changelog.xml +++ b/app/src/main/res/xml/frost_changelog.xml @@ -5,6 +5,14 @@ --> + + + + + + + + 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 5ac484b5..79cde137 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt @@ -48,4 +48,13 @@ class FbUrlTest { assertFbFormat("${FB_URL_BASE}relative", "$FB_URL_BASE/relative") } + @Test + fun video() { + //note that the video numbers have been changed to maintain privacy + val url = "/video_redirect/?src=https%3A%2F%2Fvideo-yyz1-1.xx.fbcdn.net%2Fv%2Ft42.1790-2%2F2349078999904_n.mp4%3Fefg%3DeyJ87J9%26oh%3Df5777784%26oe%3D56FD4&source=media_collage&id=1735049&refid=8&_ft_=qid.6484464%3Amf_story_key.-43172431214%3Atop_level_post_id.102773&__tn__=FEH-R" + val expected = "https://video-yyz1-1.xx.fbcdn.net/v/t42.1790-2/2349078999904_n.mp4?efg=eyJ87J9&oh=f5777784&oe=56FD4?source&id=1735049&_ft_=qid.6484464:mf_story_key.-43172431214:top_level_post_id.102773&__tn__=FEH-R" + assertFbFormat(expected, url) + } + + } \ No newline at end of file -- cgit v1.2.3