1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-09 12:32:30 +01:00

Fix view full image (#882)

* Test including full photo viewer

* Test cookie in glide

* Fix parser and add redirects to view full image

* Update changelog
This commit is contained in:
Allan Wang 2018-04-13 00:13:28 -04:00 committed by GitHub
parent d9e2562267
commit c5e769deab
14 changed files with 157 additions and 31 deletions

View File

@ -24,9 +24,11 @@ import com.pitchedapps.frost.R
import com.pitchedapps.frost.facebook.FB_IMAGE_ID_MATCHER
import com.pitchedapps.frost.facebook.get
import com.pitchedapps.frost.facebook.requests.call
import com.pitchedapps.frost.facebook.requests.getFullSizedImageUrl
import com.pitchedapps.frost.facebook.requests.requestBuilder
import com.pitchedapps.frost.utils.*
import com.sothree.slidinguppanel.SlidingUpPanelLayout
import okhttp3.Request
import okhttp3.Response
import org.jetbrains.anko.activityUiThreadWithContext
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
@ -86,8 +88,18 @@ class ImageActivity : KauBaseActivity() {
private val L = KauLoggerExtension("Image", com.pitchedapps.frost.utils.L)
}
private val cookie: String? by lazy { intent.getStringExtra(ARG_COOKIE) }
val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') }
private val trueImageUrl: String by lazy {
val result = if (!imageUrl.isIndirectImageUrl) imageUrl
else cookie?.getFullSizedImageUrl(imageUrl)?.blockingGet() ?: imageUrl
if (result != imageUrl)
L.v { "Launching with true url $result" }
result
}
private val imageText: String? by lazy { intent.getStringExtra(ARG_TEXT) }
// a unique image identifier based on the id (if it exists), and its hash
@ -205,12 +217,13 @@ class ImageActivity : KauBaseActivity() {
return File.createTempFile(imageFileName, IMG_EXTENSION, frostDir)
}
private fun getImageResponse() = Request.Builder()
.url(imageUrl)
private fun getImageResponse(): Response = cookie.requestBuilder()
.url(trueImageUrl)
.get()
.call()
.execute()
@Throws(IOException::class)
private fun downloadImageTo(file: File) {
val body = getImageResponse().body()
@ -263,7 +276,7 @@ class ImageActivity : KauBaseActivity() {
override fun onDestroy() {
tempFile = null
val purge = System.currentTimeMillis() - PURGE_TIME
tempDir.listFiles(FileFilter { it.isFile && it.lastModified() < purge }).forEach {
tempDir.listFiles(FileFilter { it.isFile && it.lastModified() < purge })?.forEach {
it.delete()
}
super.onDestroy()

View File

@ -20,6 +20,10 @@ import io.reactivex.subjects.SingleSubject
*/
object FbCookie {
/**
* Retrieves the facebook cookie if it exists
* Note that this is a synchronized call
*/
inline val webCookie: String?
get() = CookieManager.getInstance().getCookie(FB_URL_BASE)

View File

@ -26,6 +26,7 @@ val FB_MESSAGE_NOTIF_ID_MATCHER: Regex by lazy { Regex("[thread|user]_fbid_([0-9
val FB_CSS_URL_MATCHER: Regex by lazy { Regex("url\\([\"|']?(.*?)[\"|']?\\)") }
val FB_JSON_URL_MATCHER: Regex by lazy { Regex("\"(http.*?)\"") }
val FB_IMAGE_ID_MATCHER: Regex by lazy { Regex("fbcdn.*?/[0-9]+_([0-9]+)_") }
val FB_REDIRECT_URL_MATCHER: Regex by lazy { Regex("url=(.*?fbcdn.*?)\"") }
operator fun MatchResult?.get(groupIndex: Int) = this?.groupValues?.get(groupIndex)

View File

@ -43,6 +43,7 @@ class FbUrlFormatter(url: String) {
L.e(e) { "Failed url formatting" }
return url
}
cleanedUrl = cleanedUrl.replace("&amp;", "&")
if (changed && !cleanedUrl.contains("?")) //ensure we aren't missing '?'
cleanedUrl = cleanedUrl.replaceFirst("&", "?")
val qm = cleanedUrl.indexOf("?")
@ -54,8 +55,6 @@ class FbUrlFormatter(url: String) {
cleanedUrl = cleanedUrl.substring(0, qm)
}
discardableQueries.forEach { queries.remove(it) }
//final cleanup
misc.forEach { (k, v) -> cleanedUrl = cleanedUrl.replace(k, v, true) }
if (cleanedUrl.startsWith("/")) cleanedUrl = FB_URL_BASE + cleanedUrl.substring(1)
cleanedUrl = cleanedUrl.replaceFirst(".facebook.com//", ".facebook.com/") //sometimes we are given a bad url
L.v { "Formatted url from $url to $cleanedUrl" }
@ -101,8 +100,6 @@ class FbUrlFormatter(url: String) {
VIDEO_REDIRECT
)
val misc = arrayOf("&amp;" to "&")
val discardableQueries = arrayOf("ref", "refid", "acontext", "SharedWith")
val converter = listOf(

View File

@ -6,7 +6,10 @@ import com.pitchedapps.frost.rx.RxFlyweight
import com.pitchedapps.frost.utils.L
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import okhttp3.*
import okhttp3.Call
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.logging.HttpLoggingInterceptor
import org.apache.commons.text.StringEscapeUtils
@ -92,12 +95,16 @@ internal fun List<Pair<String, Any?>>.withEmptyData(vararg key: String): List<Pa
return newList
}
private fun String.requestBuilder() = Request.Builder()
.header("Cookie", this)
.header("User-Agent", USER_AGENT_BASIC)
.cacheControl(CacheControl.FORCE_NETWORK)
internal fun String?.requestBuilder(): Request.Builder {
val builder = Request.Builder()
.header("User-Agent", USER_AGENT_BASIC)
if (this != null)
builder.header("Cookie", this)
// .cacheControl(CacheControl.FORCE_NETWORK)
return builder
}
fun Request.Builder.call() = httpClient.newCall(build())!!
fun Request.Builder.call(): Call = httpClient.newCall(build())
fun String.getAuth(): RequestAuth {
L.v { "Getting auth for ${hashCode()}" }

View File

@ -11,9 +11,8 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.signature.ObjectKey
import com.pitchedapps.frost.facebook.FB_IMAGE_ID_MATCHER
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.get
import com.pitchedapps.frost.facebook.*
import io.reactivex.Maybe
import okhttp3.Call
import okhttp3.Request
import java.io.IOException
@ -27,6 +26,18 @@ fun RequestAuth.getFullSizedImage(fbid: Long) = frostRequest(::getJsonUrl) {
get()
}
val test: () -> InputStream? = { null }
/**
* Attempts to get the fbcdn url of the supplied image redirect url
*/
fun String.getFullSizedImageUrl(url: String): Maybe<String?> = Maybe.fromCallable {
val redirect = requestBuilder().url(url).get().call()
.execute().body()?.string() ?: return@fromCallable null
return@fromCallable FB_REDIRECT_URL_MATCHER.find(redirect)[1]?.formattedFbUrl
?: return@fromCallable null
}.onErrorComplete()
/**
* Request loader for a potentially hd version of a url
* In this case, each url may potentially return an id,

View File

@ -10,6 +10,11 @@ import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.utils.L
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
/**
* Created by Allan Wang on 28/12/17.
@ -35,6 +40,22 @@ fun <T> RequestBuilder<T>.transform(vararg transformation: BitmapTransformation)
class FrostGlideModule : AppGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
// registry.replace(GlideUrl::class.java,
// InputStream::class.java,
// OkHttpUrlLoader.Factory(getFrostHttpClient()))
// registry.prepend(HdImageMaybe::class.java, InputStream::class.java, HdImageLoadingFactory())
}
}
private fun getFrostHttpClient(): OkHttpClient =
OkHttpClient.Builder().addInterceptor(FrostCookieInterceptor()).build()
class FrostCookieInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val origRequest = chain.request()
val cookie = FbCookie.webCookie ?: return chain.proceed(origRequest)
L.v { "Add cookie to req $cookie" }
val request = origRequest.newBuilder().addHeader("Cookie", cookie).build()
return chain.proceed(request)
}
}

View File

@ -44,6 +44,7 @@ const val ARG_URL = "arg_url"
const val ARG_USER_ID = "arg_user_id"
const val ARG_IMAGE_URL = "arg_image_url"
const val ARG_TEXT = "arg_text"
const val ARG_COOKIE = "arg_cookie"
inline fun <reified T : Activity> Context.launchNewTask(cookieList: ArrayList<CookieModel> = arrayListOf(), clearStack: Boolean = false) {
startActivity<T>(clearStack, intentBuilder = {
@ -83,11 +84,12 @@ fun Context.launchWebOverlayBasic(url: String) = launchWebOverlayImpl<WebOverlay
private fun Context.fadeBundle() = ActivityOptions.makeCustomAnimation(this,
android.R.anim.fade_in, android.R.anim.fade_out).toBundle()
fun Context.launchImageActivity(imageUrl: String, text: String?) {
fun Context.launchImageActivity(imageUrl: String, text: String? = null, cookie: String? = null) {
startActivity<ImageActivity>(intentBuilder = {
putExtras(fadeBundle())
putExtra(ARG_IMAGE_URL, imageUrl)
putExtra(ARG_TEXT, text)
putExtra(ARG_COOKIE, cookie)
})
}
@ -241,10 +243,20 @@ inline val String.isVideoUrl
(startsWith("https://video-") && contains(FBCDN_NET))
/**
* [true] if url is or redirects to an explicit facebook image
* [true] if url directly leads to a usable image
*/
inline val String.isImageUrl
get() = contains(FBCDN_NET) && (contains(".png") || contains(".jpg"))
inline val String.isImageUrl: Boolean
get() {
return contains(FBCDN_NET) && (contains(".png") || contains(".jpg"))
}
/**
* [true] if url can be retrieved to get a direct image url
*/
inline val String.isIndirectImageUrl: Boolean
get() {
return contains("/photo/view_full_size/") && contains("fbid=")
}
/**
* [true] if url can be displayed in a different webview

View File

@ -4,6 +4,7 @@ import com.pitchedapps.frost.activities.WebOverlayActivity
import com.pitchedapps.frost.activities.WebOverlayActivityBase
import com.pitchedapps.frost.activities.WebOverlayBasicActivity
import com.pitchedapps.frost.contracts.VideoViewHolder
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
import com.pitchedapps.frost.facebook.formattedFbUrl
@ -37,7 +38,12 @@ fun FrostWebView.requestWebOverlay(url: String): Boolean {
}
if (url.isImageUrl) {
L.d { "Found fb image" }
context.launchImageActivity(url.formattedFbUrl, null)
context.launchImageActivity(url.formattedFbUrl)
return true
}
if (url.isIndirectImageUrl) {
L.d { "Found indirect fb image" }
context.launchImageActivity(url.formattedFbUrl, cookie = FbCookie.webCookie)
return true
}
if (!url.isIndependent) {

View File

@ -7,7 +7,9 @@ import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.injectors.*
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.views.FrostWebView
@ -57,7 +59,6 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() {
)
}
override fun onPageCommitVisible(view: WebView, url: String?) {
super.onPageCommitVisible(view, url)
injectBackgroundColor()
@ -91,7 +92,7 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() {
onPageFinishedActions(url)
}
open internal fun onPageFinishedActions(url: String) {
internal open fun onPageFinishedActions(url: String) {
if (url.startsWith("${FbItem.MESSAGES.url}/read/") && Prefs.messageScrollToBottom)
web.pageDown(true)
injectAndFinish()
@ -125,9 +126,9 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() {
return web.requestWebOverlay(request.url.toString())
}
private fun launchImage(url: String, text: String? = null): Boolean {
private fun launchImage(url: String, text: String? = null, cookie: String? = null): Boolean {
v { "Launching image: $url" }
web.context.launchImageActivity(url, text)
web.context.launchImageActivity(url, text, cookie)
if (web.canGoBack()) web.goBack()
return true
}
@ -143,7 +144,9 @@ open class FrostWebViewClient(val web: FrostWebView) : BaseWebViewClient() {
}
if (path.startsWith("/composer/")) return launchRequest(request)
if (url.isImageUrl)
return launchImage(url)
return launchImage(url.formattedFbUrl)
if (url.isIndirectImageUrl)
return launchImage(url.formattedFbUrl, cookie = FbCookie.webCookie)
if (Prefs.linksInDefaultApp && view.context.resolveActivityForUri(request.url)) return true
return super.shouldOverrideUrlLoading(view, request)
}

View File

@ -11,7 +11,7 @@
<item text="Catch crashes if device has no webview" />
<item text="Fix all the notifications coming in for the first load" />
<item text="Reorder settings" />
<item text="" />
<item text="Fix view full image" />
<item text="" />
<item text="" />
<item text="" />

View File

@ -0,0 +1,32 @@
package com.pitchedapps.frost.facebook
import com.pitchedapps.frost.facebook.requests.getFullSizedImage
import com.pitchedapps.frost.facebook.requests.getFullSizedImageUrl
import com.pitchedapps.frost.internal.COOKIE
import com.pitchedapps.frost.internal.authDependent
import org.junit.BeforeClass
import org.junit.Test
import kotlin.test.assertNotNull
import kotlin.test.assertTrue
/**
* Created by Allan Wang on 12/04/18.
*/
class FbFullImageTest {
companion object {
@BeforeClass
@JvmStatic
fun before() {
authDependent()
}
}
@Test
fun getFullImage() {
val url = "https://touch.facebook.com/photo/view_full_size/?fbid=107368839645039"
val result = COOKIE.getFullSizedImageUrl(url).blockingGet()
assertNotNull(result)
println(result)
}
}

View File

@ -1,6 +1,7 @@
package com.pitchedapps.frost.facebook
import com.pitchedapps.frost.utils.isImageUrl
import com.pitchedapps.frost.utils.isIndirectImageUrl
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
@ -47,6 +48,13 @@ class FbUrlTest {
assertFbFormat(expected, url)
}
@Test
fun ampersand() {
val url = "https://scontent-yyz1-1.xx.fbcdn.net/v/t31.0-8/fr/cp0/e15/q65/123.jpg?_nc_cat=0&amp;efg=asdf"
val formattedUrl = "https://scontent-yyz1-1.xx.fbcdn.net/v/t31.0-8/fr/cp0/e15/q65/123.jpg?_nc_cat=0&efg=asdf"
assertFbFormat(formattedUrl, url)
}
@Test
fun doubleDash() {
assertFbFormat("${FB_URL_BASE}relative", "$FB_URL_BASE/relative")
@ -72,6 +80,15 @@ class FbUrlTest {
}
}
@Test
fun indirectImage() {
arrayOf(
"#!/photo/view_full_size/?fbid=107368839645039"
).forEach {
assertTrue(it.isIndirectImageUrl, "Failed to match indirect image for $it")
}
}
@Test
fun antiImageRegex() {
arrayOf(

View File

@ -24,11 +24,13 @@ import kotlin.test.fail
private const val FILE = "priv.properties"
private val propPaths = arrayOf(FILE, "../$FILE")
val PROPS: Properties by lazy {
val props = Properties()
val file = File(FILE)
if (!file.exists()) {
println("$FILE not found")
val file = propPaths.map(::File).firstOrNull { it.isFile }
if (file == null) {
println("$FILE not found at ${File(".").absolutePath}")
return@lazy props
}
println("Found properties at ${file.absolutePath}")