diff options
author | Allan Wang <me@allanwang.ca> | 2017-10-24 23:29:55 -0400 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-24 23:29:55 -0400 |
commit | c2ca9066c6fd760bd6ef5d2f8f0530a89bfa7b66 (patch) | |
tree | a54665bb873b650b8f6f03b76cd59456ef79e296 /app | |
parent | 64dbf74b7a44a25f41ed7ff2ebfa11db0bc91769 (diff) | |
download | frost-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')
28 files changed, 641 insertions, 74 deletions
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 @@ <activity android:name=".activities.ImageActivity" android:theme="@style/FrostTheme.Transparent" /> + <activity android:name=".activities.VideoActivity" + android:theme="@style/FrostTheme.Video" /> <service android:name=".services.NotificationService" diff --git a/app/src/main/assets/js/click-debugger.js b/app/src/main/assets/js/click-debugger.js new file mode 100644 index 00000000..702a62c9 --- /dev/null +++ b/app/src/main/assets/js/click-debugger.js @@ -0,0 +1,13 @@ +//for desktop only +var _frostAContext = function(e) { + /* + * Commonality; check for valid target + */ + var element = e.target || e.currentTarget || e.srcElement; + if (!element) return; + console.log("Clicked element:"); + console.log(element.tagName); + console.log(element.className); +} + +document.addEventListener('contextmenu', _frostAContext, true); diff --git a/app/src/main/assets/js/click-debugger.min.js b/app/src/main/assets/js/click-debugger.min.js new file mode 100644 index 00000000..0f986b07 --- /dev/null +++ b/app/src/main/assets/js/click-debugger.min.js @@ -0,0 +1,6 @@ +var _frostAContext=function(e){ +var t=e.target||e.currentTarget||e.srcElement +;t&&(console.log("Clicked element:"), +console.log(t.tagName),console.log(t.className)) +} +;document.addEventListener("contextmenu",_frostAContext,!0);
\ No newline at end of file diff --git a/app/src/main/assets/js/click_a.js b/app/src/main/assets/js/click_a.js index 24a08f56..2f5d759f 100644 --- a/app/src/main/assets/js/click_a.js +++ b/app/src/main/assets/js/click_a.js @@ -12,6 +12,7 @@ if (!window.hasOwnProperty('frost_click_a')) { * Commonality; check for valid target */ var element = e.target || e.srcElement; + if (element.tagName !== 'A') element = element.parentNode; //Notifications is two layers under if (element.tagName !== 'A') element = element.parentNode; @@ -41,11 +42,14 @@ if (!window.hasOwnProperty('frost_click_a')) { document.addEventListener('click', _frostAClick, true); + var clickTimeout; + document.addEventListener('touchstart', function _frostStart(e) { - setTimeout(_frostPreventClick, 400); + clickTimeout = setTimeout(_frostPreventClick, 400); }, true); document.addEventListener('touchend', function _frostEnd(e) { prevented = false; + clearTimeout(clickTimeout); }, true); } diff --git a/app/src/main/assets/js/click_a.min.js b/app/src/main/assets/js/click_a.min.js index 2033fd31..a0df6912 100644 --- a/app/src/main/assets/js/click_a.min.js +++ b/app/src/main/assets/js/click_a.min.js @@ -5,16 +5,18 @@ window.frost_click_a=!0 var t=e.target||e.srcElement ;if("A"!==t.tagName&&(t=t.parentNode),"A"!==t.tagName&&(t=t.parentNode), "A"===t.tagName&&!prevented){ -var n=t.getAttribute("href") -;console.log("Click Intercept",n),"undefined"!=typeof Frost&&Frost.loadUrl(n)&&(e.stopPropagation(), +var o=t.getAttribute("href") +;console.log("Click Intercept",o),"undefined"!=typeof Frost&&Frost.loadUrl(o)&&(e.stopPropagation(), e.preventDefault()) } },_frostPreventClick=function(){ console.log("Click prevented"),prevented=!0 } -;document.addEventListener("click",_frostAClick,!0),document.addEventListener("touchstart",function(e){ -setTimeout(_frostPreventClick,400) +;document.addEventListener("click",_frostAClick,!0) +;var clickTimeout +;document.addEventListener("touchstart",function(e){ +clickTimeout=setTimeout(_frostPreventClick,400) },!0),document.addEventListener("touchend",function(e){ -prevented=!1 +prevented=!1,clearTimeout(clickTimeout) },!0) }
\ No newline at end of file diff --git a/app/src/main/assets/js/media.js b/app/src/main/assets/js/media.js new file mode 100644 index 00000000..852a1e8c --- /dev/null +++ b/app/src/main/assets/js/media.js @@ -0,0 +1,31 @@ +// we will media events +if (!window.hasOwnProperty('frost_media')) { + console.log('Registering frost_media'); + window.frost_media = true; + + var _frostMediaClick = function(e) { + + /* + * Commonality; check for valid target + */ + var element = e.target || e.srcElement; + if (!element.hasAttribute("data-sigil") || !element.getAttribute("data-sigil").includes("playInlineVideo")) return; + console.log("Found inline video"); + element = element.parentNode; + if (!element.hasAttribute("data-store")) return; + var dataStore; + try { + dataStore = JSON.parse(element.getAttribute("data-store")); + } catch (e) { + return; + } + if (!dataStore.src) return; + console.log("Inline video", dataStore.src); + if (typeof Frost !== 'undefined') Frost.loadVideo(dataStore.src); + e.stopPropagation(); + e.preventDefault(); + return; + } + + document.addEventListener('click', _frostMediaClick, true); +} diff --git a/app/src/main/assets/js/media.min.js b/app/src/main/assets/js/media.min.js new file mode 100644 index 00000000..c965f515 --- /dev/null +++ b/app/src/main/assets/js/media.min.js @@ -0,0 +1,21 @@ +if(!window.hasOwnProperty("frost_media")){ +console.log("Registering frost_media"), +window.frost_media=!0 +;var _frostMediaClick=function(t){ +var e=t.target||t.srcElement +;if(e.hasAttribute("data-sigil")&&e.getAttribute("data-sigil").includes("playInlineVideo")&&(console.log("Found inline video"), +e=e.parentNode, +e.hasAttribute("data-store"))){ +var i +;try{ +i=JSON.parse(e.getAttribute("data-store")) +}catch(t){ +return +} +i.src&&(console.log("Inline video",i.src),"undefined"!=typeof Frost&&Frost.loadVideo(i.src), +t.stopPropagation(), +t.preventDefault()) +} +} +;document.addEventListener("click",_frostMediaClick,!0) +}
\ 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 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( - "&" 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 <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) ) } 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/video_container" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="@android:color/transparent" + android:clickable="false" + android:theme="@style/FrostTheme.Video"> + + <View + android:id="@+id/video_background" + android:layout_width="0dp" + android:layout_height="0dp" + android:clickable="false" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <android.support.v7.widget.Toolbar + android:id="@+id/video_toolbar" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:minHeight="?attr/actionBarSize" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" /> + + <com.pitchedapps.frost.views.FrostVideoView + android:id="@+id/video" + android:layout_width="0dp" + android:layout_height="0dp" + android:background="@android:color/transparent" + android:theme="@style/FrostTheme.Video" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@id/video_toolbar" + app:useDefaultControls="true" + app:useTextureViewBacking="true"> + + <ImageView + android:id="@+id/video_restart" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:foregroundGravity="center" /> + + </com.pitchedapps.frost.views.FrostVideoView> + + +</android.support.constraint.ConstraintLayout>
\ 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 @@ +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/action_pip" + android:orderInCategory="100" + android:title="@string/pip" + app:showAsAction="always" /> + <item + android:id="@+id/action_download" + android:orderInCategory="200" + android:title="@string/download" + app:showAsAction="ifRoom" /> +</menu> + 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 @@ <!--Generated by crowdin.com--> <resources> <string name="pick_image">Foto auswählen</string> + <string name="download">Downloaden</string> <string name="downloading">Downloade…</string> <string name="image_download_success">Foto heruntergeladen</string> <string name="image_download_fail">Fehler beim Download des Fotos</string> 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 @@ <!--Generated by crowdin.com--> <resources> <string name="pick_image">Seleccionar imagen</string> + <string name="download">Descargar</string> <string name="downloading">Descargando…</string> <string name="image_download_success">Imagen descargada</string> <string name="image_download_fail">Descarga de imagen fallida</string> 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 @@ <!--Generated by crowdin.com--> <resources> <string name="pick_image">Sélectionner une image</string> + <string name="download">Télécharger</string> <string name="downloading">Téléchargement…</string> <string name="image_download_success">Image téléchargée</string> <string name="image_download_fail">Échec du téléchargement de l\'image</string> 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 @@ <string name="top_bar">Top Bar</string> <string name="bottom_bar">Bottom Bar</string> + + <string name="pip" translatable="false">PIP</string> + </resources> 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 @@ <?xml version="1.0" encoding="utf-8"?> <resources> <string name="pick_image">Pick Image</string> + <string name="download">Download</string> <string name="downloading">Downloading…</string> <string name="image_download_success">Image downloaded</string> <string name="image_download_fail">Image failed to download</string> 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 @@ <item name="android:windowAnimationStyle">@style/KauFadeInFadeOut</item> </style> + <style name="FrostTheme.Video" parent="FrostTheme.Overlay.Fade"> + <item name="android:windowBackground">@android:color/transparent</item> + <item name="android:backgroundDimEnabled">false</item> + <item name="android:colorBackgroundCacheHint">@null</item> + <item name="android:windowIsTranslucent">true</item> + </style> + <style name="FrostTheme.Settings" parent="FrostTheme"> <item name="android:windowAnimationStyle">@style/KauSlideInFadeOut</item> </style> 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 @@ <version title="v" /> <item text="" /> --> - - <version title="Translations are opened!" /> - <item text="If you want to have Frost in your language, please consider helping translate it. Link is in settings." /> - - <version title="v1.5.9" /> + + <version title="v1.6.0" /> + <item text="Add Spanish translations" /> + <item text="Add French translations" /> + <item text="Add German translations" /> + <item text="Check permissions before attempting upload or download" /> + <item text="Add pip video support" /> + <item text="Add video downloader" /> + <item text="Fix bugs with parsing url queries" /> + <item text="" /> + <item text="" /> + + <version title="v1.5.9" /> <item text="Add notification support for Android O" /> - - <version title="v1.5.8" /> + + <version title="v1.5.8" /> <item text="Fix theme for newer comments layout" /> <item text="Revert media picker to use system default" /> 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 @@ -27,11 +30,6 @@ class FbUrlTest { } @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" val suffix = "&apple=notorange" @@ -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 |