1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-08 20:12:39 +01:00
* Add locale log

* Add flyweight design for authenticator

* Add option to have instant messages only

* Update interceptor

* Add hd image model loader (#613)

* Launch image view for view full image

* Update changelog

* Greatly improve ImageActivity loading

* Update hashes

* Add back keyword filter

* Clean up
This commit is contained in:
Allan Wang 2018-01-10 22:13:28 -05:00 committed by GitHub
parent ad97b4ff94
commit fd5f2a82eb
44 changed files with 541 additions and 205 deletions

View File

@ -177,6 +177,7 @@ dependencies {
implementation"com.mikepenz:fastadapter-extensions:${FAST_ADAPTER_EXTENSIONS}@aar"
implementation "com.github.bumptech.glide:okhttp3-integration:${GLIDE}"
kapt "com.github.bumptech.glide:compiler:${GLIDE}"
implementation "com.fasterxml.jackson.core:jackson-databind:2.9.3"

View File

@ -7,7 +7,6 @@ import android.net.Uri
import android.os.Bundle
import android.widget.ImageView
import ca.allanwang.kau.logging.KL
import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.signature.ApplicationVersionSignature
import com.crashlytics.android.Crashlytics
@ -18,6 +17,7 @@ import com.pitchedapps.frost.dbflow.CookiesDb
import com.pitchedapps.frost.dbflow.FbTabsDb
import com.pitchedapps.frost.dbflow.NotificationDb
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.glide.GlideApp
import com.pitchedapps.frost.services.scheduleNotifications
import com.pitchedapps.frost.services.setupNotificationChannels
import com.pitchedapps.frost.utils.FrostPglAdBlock
@ -86,8 +86,9 @@ class FrostApp : Application() {
DrawerImageLoader.init(object : AbstractDrawerImageLoader() {
override fun set(imageView: ImageView, uri: Uri, placeholder: Drawable, tag: String) {
val c = imageView.context
val old = Glide.with(c).load(uri).apply(RequestOptions().placeholder(placeholder))
Glide.with(c).load(uri).apply(RequestOptions()
val request = GlideApp.with(c)
val old = request.load(uri).apply(RequestOptions().placeholder(placeholder))
request.load(uri).apply(RequestOptions()
.signature(ApplicationVersionSignature.obtain(c)))
.thumbnail(old).into(imageView)
}

View File

@ -119,10 +119,6 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
viewPager.offscreenPageLimit = TAB_COUNT
setupDrawer(savedInstanceState)
// fab.setOnClickListener { view ->
// Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
// .setAction("Action", null).show()
// }
setFrostColors {
toolbar(toolbar)
themeWindow = false
@ -198,7 +194,7 @@ abstract class BaseMainActivity : BaseActivity(), MainActivityContract,
}
}
-3L -> launchNewTask<LoginActivity>(clearStack = false)
-4L -> launchNewTask<SelectorActivity>( cookies(), false)
-4L -> launchNewTask<SelectorActivity>(cookies(), false)
else -> {
FbCookie.switchUser(profile.identifier, this@BaseMainActivity::refreshAll)
tabsForEachView { _, view -> view.badgeText = null }

View File

@ -1,12 +1,8 @@
package com.pitchedapps.frost.activities
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.Color
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Bundle
import android.os.Environment
import android.support.design.widget.FloatingActionButton
@ -20,24 +16,22 @@ import ca.allanwang.kau.mediapicker.scanMedia
import ca.allanwang.kau.permissions.PERMISSION_WRITE_EXTERNAL_STORAGE
import ca.allanwang.kau.permissions.kauRequestPermissions
import ca.allanwang.kau.utils.*
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import com.bumptech.glide.request.target.BaseTarget
import com.bumptech.glide.request.target.SizeReadyCallback
import com.bumptech.glide.request.target.Target
import com.bumptech.glide.request.transition.Transition
import com.davemorrissey.labs.subscaleview.ImageSource
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.typeface.IIcon
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.utils.*
import com.sothree.slidinguppanel.SlidingUpPanelLayout
import okhttp3.Request
import org.jetbrains.anko.activityUiThreadWithContext
import org.jetbrains.anko.doAsync
import org.jetbrains.anko.uiThread
import java.io.File
import java.io.FileFilter
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.*
@ -70,12 +64,13 @@ class ImageActivity : KauBaseActivity() {
internal var savedFile: File? = null
/**
* Indicator for fab's click result
* Can be called from any thread
*/
internal var fabAction: FabStates = FabStates.NOTHING
set(value) {
if (field == value) return
field = value
value.update(fab)
runOnUiThread { value.update(fab) }
}
companion object {
@ -87,25 +82,27 @@ class ImageActivity : KauBaseActivity() {
private const val TIME_FORMAT = "yyyyMMdd_HHmmss"
private const val IMG_TAG = "Frost"
private const val IMG_EXTENSION = ".png"
private const val PURGE_TIME: Long = 10 * 60 * 1000 // 10 min block
private val L = KauLoggerExtension("Image", com.pitchedapps.frost.utils.L)
}
val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') }
val IMAGE_URL: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') }
val text: String? by lazy { intent.getStringExtra(ARG_TEXT) }
val TEXT: String? by lazy { intent.getStringExtra(ARG_TEXT) }
private val glide: RequestManager by lazy { Glide.with(this) }
// a unique image identifier based on the id (if it exists), and its hash
val IMAGE_HASH: String by lazy { "${Math.abs(FB_IMAGE_ID_MATCHER.find(IMAGE_URL)[1]?.hashCode() ?: 0)}_${Math.abs(IMAGE_URL.hashCode())}" }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.extras ?: return finish()
L.i { "Displaying image" }
val layout = if (!text.isNullOrBlank()) R.layout.activity_image else R.layout.activity_image_textless
val layout = if (!TEXT.isNullOrBlank()) R.layout.activity_image else R.layout.activity_image_textless
setContentView(layout)
container.setBackgroundColor(Prefs.bgColor.withMinAlpha(222))
caption?.setTextColor(Prefs.textColor)
caption?.setBackgroundColor(Prefs.bgColor.colorToForeground(0.2f).withAlpha(255))
caption?.text = text
caption?.text = TEXT
progress.tint(Prefs.accentColor)
panel?.addPanelSlideListener(object : SlidingUpPanelLayout.SimplePanelSlideListener() {
override fun onPanelSlide(panel: View, slideOffset: Float) {
@ -119,90 +116,76 @@ class ImageActivity : KauBaseActivity() {
override fun onImageLoadError(e: Exception?) {
errorRef = e
e.logFrostAnswers("Image load error")
imageCallback(null, false)
fabAction = FabStates.ERROR
}
})
glide.asBitmap().load(imageUrl).into(PhotoTarget(this::imageCallback))
setFrostColors {
themeWindow = false
}
}
/**
* Callback to add image to view
* [resource] is guaranteed to be nonnull when [success] is true
* and null when it is false
*/
private fun imageCallback(resource: Bitmap?, success: Boolean) {
if (progress.isVisible) progress.fadeOut()
if (success) {
saveTempImage(resource!!, {
if (it == null) {
imageCallback(null, false)
} else {
photo.setImage(ImageSource.uri(it))
fabAction = FabStates.DOWNLOAD
photo.animate().alpha(1f).scaleXY(1f).withEndAction(fab::show).start()
}
})
} else {
doAsync({
L.e(it) { "Failed to load image $IMAGE_HASH" }
errorRef = it
fabAction = FabStates.ERROR
fab.show()
}) {
loadImage { file ->
if (file == null) {
fabAction = FabStates.ERROR
return@loadImage
}
tempFile = file
L.d { "Temp image path ${file.absolutePath}" }
uiThread {
photo.setImage(ImageSource.uri(frostUriFromFile(file)))
fabAction = FabStates.DOWNLOAD
photo.animate().alpha(1f).scaleXY(1f).start()
}
}
}
}
/**
* Bitmap load handler
* Returns a file pointing to the image, or null if something goes wrong
*/
class PhotoTarget(val callback: (resource: Bitmap?, success: Boolean) -> Unit) : BaseTarget<Bitmap>() {
override fun removeCallback(cb: SizeReadyCallback?) {}
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) =
callback(resource, true)
override fun onLoadFailed(errorDrawable: Drawable?) =
callback(null, false)
override fun getSize(cb: SizeReadyCallback) =
cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
private inline fun loadImage(callback: (file: File?) -> Unit) {
val local = File(tempDir, IMAGE_HASH)
if (local.exists() && local.length() > 1) {
local.setLastModified(System.currentTimeMillis())
L.d { "Loading from local cache ${local.absolutePath}" }
return callback(local)
}
val response = Request.Builder()
.url(IMAGE_URL)
.get()
.call()
.execute()
if (!response.isSuccessful) {
L.e { "Unsuccessful response for image" }
errorRef = Throwable("Unsuccessful response for image")
return callback(null)
}
private fun saveTempImage(resource: Bitmap, callback: (uri: Uri?) -> Unit) {
var photoFile: File? = null
try {
photoFile = createPrivateMediaFile()
} catch (e: IOException) {
errorRef = e
logImage(e)
} finally {
if (photoFile == null) {
callback(null)
} else {
tempFile = photoFile
L.d { "Temp image path ${tempFile?.absolutePath}" }
// File created; proceed with request
val photoURI = frostUriFromFile(photoFile)
photoFile.outputStream().use { resource.compress(Bitmap.CompressFormat.PNG, 100, it) }
callback(photoURI)
if (!local.createFreshFile()) {
L.e { "Could not create temp file" }
return callback(null)
}
var valid = false
response.body()?.byteStream()?.use { input ->
local.outputStream().use { output ->
input.copyTo(output)
valid = true
}
}
private fun logImage(e: Exception?) {
if (!Prefs.analytics) return
val error = e ?: IOException("$imageUrl failed to load")
L.e(error) { "$imageUrl failed to load" }
if (!valid) {
L.e { "Failed to copy file" }
local.delete()
return callback(null)
}
@Throws(IOException::class)
private fun createPrivateMediaFile(): File {
val timeStamp = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()).format(Date())
val imageFileName = "${IMG_TAG}_${timeStamp}_"
if (!tempDir.exists())
tempDir.mkdirs()
return File.createTempFile(imageFileName, IMG_EXTENSION, tempDir)
callback(local)
}
@Throws(IOException::class)
@ -218,7 +201,7 @@ class ImageActivity : KauBaseActivity() {
@Throws(IOException::class)
private fun downloadImageTo(file: File) {
val body = Request.Builder()
.url(imageUrl)
.url(IMAGE_URL)
.get()
.call()
.execute()
@ -270,8 +253,10 @@ class ImageActivity : KauBaseActivity() {
override fun onDestroy() {
tempFile = null
tempDir.deleteRecursively()
L.d { "Closing $localClassName" }
val purge = System.currentTimeMillis() - PURGE_TIME
tempDir.listFiles(FileFilter { it.isFile && it.lastModified() < purge }).forEach {
it.delete()
}
super.onDestroy()
}
}
@ -287,7 +272,7 @@ internal enum class FabStates(val iicon: IIcon, val iconColor: Int = Prefs.iconC
if (activity.errorRef != null)
L.e(activity.errorRef) { "ImageActivity error report" }
activity.sendFrostEmail(R.string.debug_image_link_subject) {
addItem("Url", activity.imageUrl)
addItem("Url", activity.IMAGE_URL)
addItem("Message", activity.errorRef?.message ?: "Null")
}
}

View File

@ -10,7 +10,6 @@ import android.widget.ImageView
import ca.allanwang.kau.utils.bindView
import ca.allanwang.kau.utils.fadeIn
import ca.allanwang.kau.utils.fadeOut
import com.bumptech.glide.Glide
import com.bumptech.glide.RequestManager
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
@ -24,6 +23,7 @@ import com.pitchedapps.frost.dbflow.loadFbCookiesAsync
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL
import com.pitchedapps.frost.glide.FrostGlide
import com.pitchedapps.frost.glide.GlideApp
import com.pitchedapps.frost.glide.transform
import com.pitchedapps.frost.utils.*
import com.pitchedapps.frost.web.LoginWebView
@ -62,7 +62,7 @@ class LoginActivity : BaseActivity() {
setContentView(R.layout.activity_login)
setSupportActionBar(toolbar)
setTitle(R.string.kau_login)
setFrostColors{
setFrostColors {
toolbar(toolbar)
}
web.loadLogin({ refresh = it != 100 }) { cookie ->
@ -73,7 +73,7 @@ class LoginActivity : BaseActivity() {
loadInfo(cookie)
})
}
profileLoader = Glide.with(profile)
profileLoader = GlideApp.with(profile)
}
private fun loadInfo(cookie: CookieModel) {

View File

@ -25,6 +25,7 @@ val FB_NOTIF_ID_MATCHER: Regex by lazy { Regex("notif_([0-9]+)") }
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]+)_") }
operator fun MatchResult?.get(groupIndex: Int) = this?.groupValues?.get(groupIndex)

View File

@ -2,6 +2,7 @@ package com.pitchedapps.frost.facebook.requests
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.facebook.*
import com.pitchedapps.frost.rx.RxFlyweight
import com.pitchedapps.frost.utils.L
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
@ -12,7 +13,17 @@ import org.apache.commons.text.StringEscapeUtils
/**
* Created by Allan Wang on 21/12/17.
*/
private val authMap: MutableMap<String, RequestAuth> = mutableMapOf()
private class RxAuth : RxFlyweight<String, Long, RequestAuth>() {
override fun call(input: String) = input.getAuth()
override fun validate(input: String, cond: Long) =
System.currentTimeMillis() - cond < 3600000 // valid for an hour
override fun cache(input: String) = System.currentTimeMillis()
}
private val auth = RxAuth()
/**
* Synchronously fetch [RequestAuth] from cookie
@ -21,18 +32,13 @@ private val authMap: MutableMap<String, RequestAuth> = mutableMapOf()
*/
fun String?.fbRequest(fail: () -> Unit = {}, action: RequestAuth.() -> Unit) {
if (this == null) return fail()
val savedAuth = authMap[this]
if (savedAuth != null) {
savedAuth.action()
} else {
val auth = getAuth()
if (!auth.isValid) {
L.e { "Attempted fbrequest with invalid auth" }
return fail()
auth(this).subscribe { a: RequestAuth?, _ ->
if (a?.isValid == true)
a.action()
else {
L.e { "Failed auth for ${hashCode()}" }
fail()
}
authMap.put(this, auth)
L._i { "Found auth $auth" }
auth.action()
}
}
@ -94,6 +100,7 @@ private fun String.requestBuilder() = Request.Builder()
fun Request.Builder.call() = client.newCall(build())!!
fun String.getAuth(): RequestAuth {
L.v { "Getting auth for ${hashCode()}" }
var auth = RequestAuth(cookie = this)
val id = FB_USER_MATCHER.find(this)[1]?.toLong() ?: return auth
auth = auth.copy(userId = id)

View File

@ -1,9 +1,19 @@
package com.pitchedapps.frost.facebook.requests
import com.bumptech.glide.Priority
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.model.ModelLoader
import com.bumptech.glide.load.model.ModelLoaderFactory
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 okhttp3.Call
import okhttp3.Request
import java.io.IOException
@ -17,8 +27,54 @@ fun RequestAuth.getFullSizedImage(fbid: Long) = frostRequest(::getJsonUrl) {
get()
}
class ImageFbidFetcher(private val fbid: Long,
private val cookie: String) : DataFetcher<InputStream> {
/**
* Request loader for a potentially hd version of a url
* In this case, each url may potentially return an id,
* which may potentially be used to fetch a higher res image url
* The following aims to allow such loading while adhering to Glide's lifecycle
*/
data class HdImageMaybe(val url: String, val cookie: String) {
val id: Long by lazy { FB_IMAGE_ID_MATCHER.find(url)[1]?.toLongOrNull() ?: -1 }
val isValid: Boolean by lazy {
id != -1L && cookie.isNotBlank()
}
}
/*
* The following was a test to see if hd image loading would work
*
* It's working and tested, though the improvements aren't really worth the extra data use
* and reload
*/
class HdImageLoadingFactory : ModelLoaderFactory<HdImageMaybe, InputStream> {
override fun build(multiFactory: MultiModelLoaderFactory) = HdImageLoading()
override fun teardown() = Unit
}
fun <T> RequestBuilder<T>.loadWithPotentialHd(model: HdImageMaybe) =
thumbnail(clone().load(model.url))
.load(model)
.apply(RequestOptions().override(Target.SIZE_ORIGINAL))
class HdImageLoading : ModelLoader<HdImageMaybe, InputStream> {
override fun buildLoadData(model: HdImageMaybe,
width: Int,
height: Int,
options: Options?): ModelLoader.LoadData<InputStream>? =
if (!model.isValid) null
else ModelLoader.LoadData(ObjectKey(model), HdImageFetcher(model))
override fun handles(model: HdImageMaybe) = model.isValid
}
class HdImageFetcher(private val model: HdImageMaybe) : DataFetcher<InputStream> {
@Volatile private var cancelled: Boolean = false
private var urlCall: Call? = null
@ -33,10 +89,12 @@ class ImageFbidFetcher(private val fbid: Long,
override fun getDataSource(): DataSource = DataSource.REMOTE
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
cookie.fbRequest(fail = { callback.fail("Invalid auth") }) {
if (!model.isValid) return callback.fail("Model is invalid")
model.cookie.fbRequest(fail = { callback.fail("Invalid auth") }) {
if (cancelled) return@fbRequest callback.fail("Cancelled")
val url = getFullSizedImage(fbid).invoke() ?: return@fbRequest callback.fail("Null url")
val url = getFullSizedImage(model.id).invoke() ?: return@fbRequest callback.fail("Null url")
if (cancelled) return@fbRequest callback.fail("Cancelled")
if (!url.contains("png") && !url.contains("jpg")) return@fbRequest callback.fail("Invalid format")
urlCall = Request.Builder().url(url).get().call()
inputStream = try {
@ -44,7 +102,6 @@ class ImageFbidFetcher(private val fbid: Long,
} catch (e: IOException) {
null
}
callback.onDataReady(inputStream)
}
}

View File

@ -0,0 +1,32 @@
package com.pitchedapps.frost.facebook.requests
import com.pitchedapps.frost.facebook.FB_URL_BASE
import okhttp3.Call
/**
* Created by Allan Wang on 07/01/18.
*/
fun RequestAuth.sendMessage(group: String, content: String): FrostRequest<Boolean> {
// todo test more; only tested against tids=cid...
val body = listOf(
"tids" to group,
"body" to content,
"fb_dtsg" to fb_dtsg,
"__user" to userId
).withEmptyData("m_sess", "__dyn", "__req", "__ajax__")
return frostRequest(::validateMessage) {
url("${FB_URL_BASE}messages/send")
post(body.toForm())
}
}
/**
* Messages are a bit weird with their responses
*/
private fun validateMessage(call: Call): Boolean {
val body = call.execute().body() ?: return false
// todo
return true
}

View File

@ -1,9 +1,14 @@
package com.pitchedapps.frost.glide
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.Registry
import com.bumptech.glide.RequestBuilder
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.MultiTransformation
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
/**
@ -25,3 +30,11 @@ fun <T> RequestBuilder<T>.transform(vararg transformation: BitmapTransformation)
1 -> apply(RequestOptions.bitmapTransform(transformation[0]))
else -> apply(RequestOptions.bitmapTransform(MultiTransformation(*transformation)))
}
@GlideModule
class FrostGlideModule : AppGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
// registry.prepend(HdImageMaybe::class.java, InputStream::class.java, HdImageLoadingFactory())
}
}

View File

@ -9,14 +9,13 @@ import ca.allanwang.kau.ui.createSimpleRippleDrawable
import ca.allanwang.kau.utils.bindView
import ca.allanwang.kau.utils.gone
import ca.allanwang.kau.utils.visible
import com.bumptech.glide.Glide
import com.mikepenz.fastadapter.FastAdapter
import com.pitchedapps.frost.R
import com.pitchedapps.frost.facebook.requests.MenuFooterItem
import com.pitchedapps.frost.facebook.requests.MenuHeader
import com.pitchedapps.frost.facebook.requests.MenuItem
import com.pitchedapps.frost.glide.FrostGlide
import com.pitchedapps.frost.glide.transform
import com.pitchedapps.frost.glide.GlideApp
import com.pitchedapps.frost.utils.Prefs
/**
@ -42,7 +41,8 @@ class MenuContentIItem(val data: MenuItem)
badge.setTextColor(Prefs.textColor)
val iconUrl = item.data.pic
if (iconUrl != null)
Glide.with(itemView).load(iconUrl)
GlideApp.with(itemView)
.load(iconUrl)
.transform(FrostGlide.roundCorner)
.into(icon.visible())
else

View File

@ -10,12 +10,11 @@ import ca.allanwang.kau.utils.bindView
import ca.allanwang.kau.utils.gone
import ca.allanwang.kau.utils.visible
import ca.allanwang.kau.utils.withAlpha
import com.bumptech.glide.Glide
import com.mikepenz.fastadapter.FastAdapter
import com.mikepenz.fastadapter.adapters.ItemAdapter
import com.pitchedapps.frost.R
import com.pitchedapps.frost.glide.FrostGlide
import com.pitchedapps.frost.glide.transform
import com.pitchedapps.frost.glide.GlideApp
import com.pitchedapps.frost.parsers.FrostNotif
import com.pitchedapps.frost.services.FrostRunnable
import com.pitchedapps.frost.utils.Prefs
@ -52,7 +51,7 @@ class NotificationIItem(val notification: FrostNotif, val cookie: String) : KauI
val thumbnail: ImageView by bindView(R.id.item_thumbnail)
private val glide
get() = Glide.with(itemView)
get() = GlideApp.with(itemView)
override fun bindView(item: NotificationIItem, payloads: MutableList<Any>) {
val notif = item.notification

View File

@ -24,6 +24,12 @@ import org.jsoup.select.Elements
*/
interface FrostParser<out T : Any> {
/**
* Name associated to parser
* Purely for display
*/
var nameRes: Int
/**
* Url to request from
*/

View File

@ -69,6 +69,8 @@ data class FrostThread(val id: Long,
private class MessageParserImpl : FrostParserBase<FrostMessages>(true) {
override var nameRes = FbItem.MESSAGES.titleId
override val url = FbItem.MESSAGES.url
override fun textToDoc(text: String): Document? {

View File

@ -60,6 +60,8 @@ data class FrostNotif(val id: Long,
private class NotifParserImpl : FrostParserBase<FrostNotifs>(false) {
override var nameRes = FbItem.NOTIFICATIONS.titleId
override val url = FbItem.NOTIFICATIONS.url
override fun parseImpl(doc: Document): FrostNotifs? {

View File

@ -55,6 +55,8 @@ data class FrostSearch(val href: String, val title: String, val description: Str
private class SearchParserImpl : FrostParserBase<FrostSearches>(false) {
override var nameRes = FbItem._SEARCH.titleId
override val url = "${FbItem._SEARCH.url}?q=a"
override fun parseImpl(doc: Document): FrostSearches? {

View File

@ -0,0 +1,86 @@
package com.pitchedapps.frost.rx
import io.reactivex.Single
import io.reactivex.schedulers.Schedulers
import java.util.concurrent.TimeUnit
/**
* Created by Allan Wang on 07/01/18.
*
* Reactive flyweight to help deal with prolonged executions
* Each call will output a [Single], which may be new if none exist or the old one is invalidated,
* or reused if an old one is still valid
*
* Types:
* T input argument for caller
* C condition condition to check against for validity
* R response response within reactive output
*/
abstract class RxFlyweight<in T : Any, C : Any, R : Any> {
/**
* Given an input emit the desired response
* This will be executed in a separate thread
*/
protected abstract fun call(input: T): R
/**
* Given an input and condition, check if
* we may used cache data or if we need to make a new request
* Return [true] to use cache, [false] otherwise
*/
protected abstract fun validate(input: T, cond: C): Boolean
/**
* Given an input, create a new condition to be used
* for future requests
*/
protected abstract fun cache(input: T): C
private val conditionals = mutableMapOf<T, C>()
private val sources = mutableMapOf<T, Single<R>>()
private val lock = Any()
/**
* Entry point to give an input a receive a [Single]
* Note that the observer is not bound to any particular thread,
* as it is dependent on [createNewSource]
*/
operator fun invoke(input: T): Single<R> {
synchronized(lock) {
val source = sources[input]
// update condition and retrieve old one
val condition = conditionals.put(input, cache(input))
// check to reuse observable
if (source != null && condition != null && validate(input, condition))
return source
val newSource = createNewSource(input).cache().doOnError { sources.remove(input) }
sources.put(input, newSource)
return newSource
}
}
/**
* Open source creator
* Result will then be created with [Single.cache]
* If you don't have a need for cache,
* you likely won't have a need for flyweights
*/
open protected fun createNewSource(input: T): Single<R> =
Single.fromCallable { call(input) }
.timeout(20, TimeUnit.SECONDS)
.subscribeOn(Schedulers.io())
fun reset() {
synchronized(lock) {
sources.clear()
conditionals.clear()
}
}
}

View File

@ -19,7 +19,6 @@ import android.support.v4.app.NotificationManagerCompat
import ca.allanwang.kau.utils.color
import ca.allanwang.kau.utils.dpToPx
import ca.allanwang.kau.utils.string
import com.bumptech.glide.Glide
import com.bumptech.glide.request.target.SimpleTarget
import com.bumptech.glide.request.transition.Transition
import com.pitchedapps.frost.BuildConfig
@ -31,7 +30,7 @@ import com.pitchedapps.frost.dbflow.lastNotificationTime
import com.pitchedapps.frost.enums.OverlayContext
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.glide.FrostGlide
import com.pitchedapps.frost.glide.transform
import com.pitchedapps.frost.glide.GlideApp
import com.pitchedapps.frost.parsers.FrostParser
import com.pitchedapps.frost.parsers.MessageParser
import com.pitchedapps.frost.parsers.NotifParser
@ -146,7 +145,10 @@ enum class NotificationType(
fun fetch(context: Context, data: CookieModel) {
val response = parser.parse(data.cookie)
?: return L.v { "$name notification data not found" }
val notifs = response.data.getUnreadNotifications(data)
val notifs = response.data.getUnreadNotifications(data).filter {
val text = it.text
Prefs.notificationKeywords.any { text.contains(it, true) }
}
if (notifs.isEmpty()) return
var notifCount = 0
val userId = data.id
@ -201,7 +203,7 @@ enum class NotificationType(
if (profileUrl != null) {
context.runOnUiThread {
//todo verify if context is valid?
Glide.with(context)
GlideApp.with(context)
.asBitmap()
.load(profileUrl)
.transform(FrostGlide.circleCrop)

View File

@ -61,7 +61,8 @@ class NotificationService : JobService() {
val cookies = loadFbCookiesSync()
cookies.forEach {
val current = it.id == currentId
if (current || Prefs.notificationAllAccounts)
if (Prefs.notificationsGeneral
&& (current || Prefs.notificationAllAccounts))
NotificationType.GENERAL.fetch(context, it)
if (Prefs.notificationsInstantMessages
&& (current || Prefs.notificationsImAllAccounts))

View File

@ -3,6 +3,7 @@ package com.pitchedapps.frost.settings
import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
import ca.allanwang.kau.utils.materialDialog
import ca.allanwang.kau.utils.startActivityForResult
import ca.allanwang.kau.utils.string
import com.pitchedapps.frost.R
import com.pitchedapps.frost.activities.DebugActivity
import com.pitchedapps.frost.activities.SettingsActivity
@ -10,6 +11,9 @@ import com.pitchedapps.frost.activities.SettingsActivity.Companion.ACTIVITY_REQU
import com.pitchedapps.frost.debugger.OfflineWebsite
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.parsers.MessageParser
import com.pitchedapps.frost.parsers.NotifParser
import com.pitchedapps.frost.parsers.SearchParser
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.frostUriFromFile
import com.pitchedapps.frost.utils.sendFrostEmail
@ -34,6 +38,23 @@ fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = {
descRes = R.string.debug_web_desc
onClick = { this@getDebugPrefs.startActivityForResult<DebugActivity>(ACTIVITY_REQUEST_DEBUG) }
}
plainText(R.string.debug_parsers) {
descRes = R.string.debug_parsers_desc
onClick = {
val parsers = arrayOf(NotifParser, MessageParser, SearchParser)
materialDialog {
items(parsers.map { string(it.nameRes) })
itemsCallback { dialog, _, position, _ ->
dialog.dismiss()
// todo add debugging
}
}
}
}
}
private const val ZIP_NAME = "debug"

View File

@ -23,7 +23,7 @@ import com.pitchedapps.frost.views.Keywords
fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = {
text(R.string.notification_frequency, Prefs::notificationFreq, { Prefs.notificationFreq = it }) {
val options = longArrayOf(-1, 15, 30, 60, 120, 180, 300, 1440, 2880)
val options = longArrayOf(15, 30, 60, 120, 180, 300, 1440, 2880)
val texts = options.map { if (it <= 0) string(R.string.no_notifications) else minuteToText(it) }
onClick = {
materialDialogThemed {
@ -36,6 +36,12 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = {
})
}
}
enabler = {
val enabled = Prefs.notificationsGeneral || Prefs.notificationsInstantMessages
if (!enabled)
scheduleNotifications(-1)
enabled
}
textGetter = { minuteToText(it) }
}
@ -52,15 +58,34 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = {
}
}
checkbox(R.string.notification_all_accounts, Prefs::notificationAllAccounts, { Prefs.notificationAllAccounts = it }) {
descRes = R.string.notification_all_accounts_desc
checkbox(R.string.notification_general, Prefs::notificationsGeneral,
{
Prefs.notificationsGeneral = it
reloadByTitle(R.string.notification_general_all_accounts)
if (!Prefs.notificationsInstantMessages)
reloadByTitle(R.string.notification_frequency)
}) {
descRes = R.string.notification_general_desc
}
checkbox(R.string.notification_messages, Prefs::notificationsInstantMessages, { Prefs.notificationsInstantMessages = it; reloadByTitle(R.string.notification_messages_all_accounts) }) {
checkbox(R.string.notification_general_all_accounts, Prefs::notificationAllAccounts,
{ Prefs.notificationAllAccounts = it }) {
descRes = R.string.notification_general_all_accounts_desc
enabler = Prefs::notificationsGeneral
}
checkbox(R.string.notification_messages, Prefs::notificationsInstantMessages,
{
Prefs.notificationsInstantMessages = it
reloadByTitle(R.string.notification_messages_all_accounts)
if (!Prefs.notificationsGeneral)
reloadByTitle(R.string.notification_frequency)
}) {
descRes = R.string.notification_messages_desc
}
checkbox(R.string.notification_messages_all_accounts, Prefs::notificationsImAllAccounts, { Prefs.notificationsImAllAccounts = it }) {
checkbox(R.string.notification_messages_all_accounts, Prefs::notificationsImAllAccounts,
{ Prefs.notificationsImAllAccounts = it }) {
descRes = R.string.notification_messages_all_accounts_desc
enabler = Prefs::notificationsInstantMessages
}
@ -91,22 +116,28 @@ fun SettingsActivity.getNotificationPrefs(): KPrefAdapterBuilder.() -> Unit = {
}
}
text(R.string.notification_ringtone, Prefs::notificationRingtone, { Prefs.notificationRingtone = it }) {
text(R.string.notification_ringtone, Prefs::notificationRingtone,
{ Prefs.notificationRingtone = it }) {
ringtone(SettingsActivity.REQUEST_NOTIFICATION_RINGTONE)
}
text(R.string.message_ringtone, Prefs::messageRingtone, { Prefs.messageRingtone = it }) {
text(R.string.message_ringtone, Prefs::messageRingtone,
{ Prefs.messageRingtone = it }) {
ringtone(SettingsActivity.REQUEST_MESSAGE_RINGTONE)
}
checkbox(R.string.notification_vibrate, Prefs::notificationVibrate, { Prefs.notificationVibrate = it })
checkbox(R.string.notification_vibrate, Prefs::notificationVibrate,
{ Prefs.notificationVibrate = it })
checkbox(R.string.notification_lights, Prefs::notificationLights, { Prefs.notificationLights = it })
checkbox(R.string.notification_lights, Prefs::notificationLights,
{ Prefs.notificationLights = it })
plainText(R.string.notification_fetch_now) {
descRes = R.string.notification_fetch_now_desc
onClick = {
val text = if (fetchNotifications()) R.string.notification_fetch_success else R.string.notification_fetch_fail
val text =
if (fetchNotifications()) R.string.notification_fetch_success
else R.string.notification_fetch_fail
frostSnackbar(text)
}
}

View File

@ -113,6 +113,8 @@ object Prefs : KPref() {
var notificationKeywords: StringSet by kpref("notification_keywords", mutableSetOf())
var notificationsGeneral: Boolean by kpref("notification_general", true)
var notificationAllAccounts: Boolean by kpref("notification_all_accounts", true)
var notificationsInstantMessages: Boolean by kpref("notification_im", true)

View File

@ -250,8 +250,15 @@ inline val String?.isFacebookUrl
/**
* [true] if url is a video and can be accepted by VideoViewer
*/
inline val String?.isVideoUrl
get() = this != null && (startsWith(VIDEO_REDIRECT) || startsWith("https://video-"))
inline val String.isVideoUrl
get() = startsWith(VIDEO_REDIRECT) || startsWith("https://video-")
/**
* [true] if url is or redirects to an explicit facebook image
*/
inline val String.isImageUrl
get() = (contains("fbcdn.net") && (contains(".png") || contains(".jpg")))
|| contains("/photo/view_full_size")
/**
* [true] if url can be displayed in a different webview
@ -308,6 +315,7 @@ fun EmailBuilder.addFrostDetails() {
addItem("Prev version", Prefs.prevVersionCode.toString())
val proTag = if (IS_FROST_PRO) "TY" else "FP"
addItem("Random Frost ID", "${Prefs.frostId}-$proTag")
addItem("Locale", Locale.getDefault().displayName)
}
fun frostJsoup(url: String)

View File

@ -7,7 +7,6 @@ import android.view.View
import android.widget.ImageView
import ca.allanwang.kau.iitems.KauIItem
import ca.allanwang.kau.utils.*
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.request.RequestListener
@ -17,7 +16,7 @@ import com.pitchedapps.frost.R
import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.PROFILE_PICTURE_URL
import com.pitchedapps.frost.glide.FrostGlide
import com.pitchedapps.frost.glide.transform
import com.pitchedapps.frost.glide.GlideApp
import com.pitchedapps.frost.utils.Prefs
/**
@ -33,7 +32,7 @@ class AccountItem(val cookie: CookieModel?) : KauIItem<AccountItem, AccountItem.
text.setTextColor(Prefs.textColor)
if (cookie != null) {
text.text = cookie.name
Glide.with(itemView).load(PROFILE_PICTURE_URL(cookie.id))
GlideApp.with(itemView).load(PROFILE_PICTURE_URL(cookie.id))
.transform(FrostGlide.roundCorner).listener(object : RequestListener<Drawable> {
override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
text.fadeIn()

View File

@ -17,35 +17,13 @@ import java.io.ByteArrayInputStream
*/
private val blankResource: WebResourceResponse by lazy { WebResourceResponse("text/plain", "utf-8", ByteArrayInputStream("".toByteArray())) }
//these hosts will redirect to a blank resource
private val blacklistHost: Set<String> =
setOf(
// "edge-chat.facebook.com" //todo make more specific? This is required for message responses
)
//these hosts will return null and skip logging
private val whitelistHost: Set<String> =
setOf(
"static.xx.fbcdn.net",
"m.facebook.com",
"touch.facebook.com"
)
//these hosts will skip ad inspection
//this list does not have to include anything from the two above
private val adWhitelistHost: Set<String> =
setOf(
"scontent-sea1-1.xx.fbcdn.net"
)
fun WebView.shouldFrostInterceptRequest(request: WebResourceRequest): WebResourceResponse? {
request.url ?: return null
val httpUrl = HttpUrl.parse(request.url.toString()) ?: return null
val requestUrl = request.url?.toString() ?: return null
val httpUrl = HttpUrl.parse(requestUrl) ?: return null
val host = httpUrl.host()
val url = httpUrl.toString()
// if (blacklistHost.contains(host)) return blankResource
if (whitelistHost.contains(host)) return null
if (!adWhitelistHost.contains(host) && FrostPglAdBlock.isAdHost(host)) return blankResource
if (host.contains("facebook") || host.contains("fbcdn")) return null
if (FrostPglAdBlock.isAdHost(host)) return blankResource
// if (!shouldLoadImages && !Prefs.loadMediaOnMeteredNetwork && request.isMedia) return blankResource
L.v { "Intercept Request: $host $url" }
return null

View File

@ -3,7 +3,6 @@ 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.VideoViewHolder
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
@ -36,6 +35,11 @@ fun FrostWebView.requestWebOverlay(url: String): Boolean {
context.runOnUiThread { context.showVideo(url) }
return true
}
if (url.isImageUrl) {
L.d { "Found fb image" }
context.launchImageActivity(url.formattedFbUrl, null)
return true
}
if (!url.isIndependent) {
L.d { "Forbid overlay switch" }
return false
@ -43,13 +47,14 @@ fun FrostWebView.requestWebOverlay(url: String): Boolean {
if (!Prefs.overlayEnabled) return false
if (context is WebOverlayActivityBase) {
L.v { "Check web request from overlay" }
val shouldUseBasic = url.formattedFbUrl.shouldUseBasicAgent
//already overlay; manage user agent
if (userAgentString != USER_AGENT_BASIC && url.formattedFbUrl.shouldUseBasicAgent) {
if (userAgentString != USER_AGENT_BASIC && shouldUseBasic) {
L.i { "Switch to basic agent overlay" }
context.launchWebOverlayBasic(url)
return true
}
if (context is WebOverlayBasicActivity && !url.formattedFbUrl.shouldUseBasicAgent) {
if (context is WebOverlayBasicActivity && !shouldUseBasic) {
L.i { "Switch from basic agent" }
context.launchWebOverlay(url)
return true

View File

@ -1,15 +1,11 @@
package com.pitchedapps.frost.web
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebView
import android.webkit.WebViewClient
import com.pitchedapps.frost.activities.LoginActivity
import com.pitchedapps.frost.activities.MainActivity
import com.pitchedapps.frost.activities.SelectorActivity
import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.injectors.*

View File

@ -8,8 +8,8 @@
<string name="add_keyword">Schlagwort hinzufügen</string>
<string name="hint_keyword">Gebe das Schlagwort ein und drücke +</string>
<string name="empty_keyword">Leeres Schlagwort</string>
<string name="notification_all_accounts">Benachrichtigungen von allen Accounts</string>
<string name="notification_all_accounts_desc">Bekomme Benachrichtigungen von allen Accounts die eingeloggt sind. Deaktiviere dies, um nur noch Benachrichtigungen von dem aktuellen Account zu bekommen.</string>
<string name="notification_general_all_accounts">Benachrichtigungen von allen Accounts</string>
<string name="notification_general_all_accounts_desc">Bekomme Benachrichtigungen von allen Accounts die eingeloggt sind. Deaktiviere dies, um nur noch Benachrichtigungen von dem aktuellen Account zu bekommen.</string>
<string name="notification_messages">Aktiviere Nachrichten Benachrichtigungen</string>
<string name="notification_messages_desc">Bekomme sofortige Benachrichtigungen für deine Nachrichten für den aktuellen Account.</string>
<string name="notification_messages_all_accounts">Benachrichtigungen für Nachrichten von allen Accounts</string>

View File

@ -8,8 +8,8 @@
<string name="add_keyword">Añadir palabra clave</string>
<string name="hint_keyword">Escriba la palabra clave y pulse +</string>
<string name="empty_keyword">Palabra clave vacía</string>
<string name="notification_all_accounts">Notificar de todas las cuentas</string>
<string name="notification_all_accounts_desc">Obten notificaciones de todas las cuentas en las que estes logueado. Desactivar esto hará que se muestren solo las notificaciones de la cuenta en la que estés.</string>
<string name="notification_general_all_accounts">Notificar de todas las cuentas</string>
<string name="notification_general_all_accounts_desc">Obten notificaciones de todas las cuentas en las que estes logueado. Desactivar esto hará que se muestren solo las notificaciones de la cuenta en la que estés.</string>
<string name="notification_messages">Activar notificaciones de mensajes</string>
<string name="notification_messages_desc">Obten notificaciones instantáneas de mensajes para tu cuenta actual.</string>
<string name="notification_messages_all_accounts">Notificar mensajes de todas las cuentas</string>

View File

@ -8,8 +8,8 @@
<string name="add_keyword">Ajouter un mot-clé</string>
<string name="hint_keyword">Tapez le mot-clé et appuyez sur +</string>
<string name="empty_keyword">Mot-clé vide</string>
<string name="notification_all_accounts">Notifier de tous les comptes</string>
<string name="notification_all_accounts_desc">Recevoir des notifications pour chaque compte connecté. Désactiver cette option fera une récupération uniquement pour le compte sélectionné.</string>
<string name="notification_general_all_accounts">Notifier de tous les comptes</string>
<string name="notification_general_all_accounts_desc">Recevoir des notifications pour chaque compte connecté. Désactiver cette option fera une récupération uniquement pour le compte sélectionné.</string>
<string name="notification_messages">Activer les notifications de message</string>
<string name="notification_messages_desc">Recevoir des notifications de messagerie instantanée pour le compte actuel.</string>
<string name="notification_messages_all_accounts">Notifier les messages de tous les comptes</string>

View File

@ -8,8 +8,8 @@
<string name="add_keyword">Engadir palabra chave</string>
<string name="hint_keyword">Escribe a palabra e toca en +</string>
<string name="empty_keyword">Palabra chave baleira</string>
<string name="notification_all_accounts">Notificar de todas as contas</string>
<string name="notification_all_accounts_desc">Obtén notificacións de todas as contas con que iniciaras sesión. Ao desactivar isto, só obterás notificacións da conta actualmente seleccionada.</string>
<string name="notification_general_all_accounts">Notificar de todas as contas</string>
<string name="notification_general_all_accounts_desc">Obtén notificacións de todas as contas con que iniciaras sesión. Ao desactivar isto, só obterás notificacións da conta actualmente seleccionada.</string>
<string name="notification_messages">Activar notificacións de mensaxes</string>
<string name="notification_messages_desc">Obtén notificacións instantáneas de mensaxes para a túa conta actual.</string>
<string name="notification_messages_all_accounts">Notificar mensaxes de todas as contas</string>

View File

@ -8,8 +8,8 @@
<string name="add_keyword">Aggiungi parole chiave</string>
<string name="hint_keyword">Scrivi la parola e premi +</string>
<string name="empty_keyword">Parola Chiave Vuota</string>
<string name="notification_all_accounts">Notifica da tutti gli accounts</string>
<string name="notification_all_accounts_desc">Ricevi notifiche per ogni account registrato. Disattivando questa opzione riceverai notifiche solo dall\'account selezionato.</string>
<string name="notification_general_all_accounts">Notifica da tutti gli accounts</string>
<string name="notification_general_all_accounts_desc">Ricevi notifiche per ogni account registrato. Disattivando questa opzione riceverai notifiche solo dall\'account selezionato.</string>
<string name="notification_messages">Attiva notifiche messaggi</string>
<string name="notification_messages_desc">Ricevi notifiche istantanee di messaggi per il tuo account.</string>
<string name="notification_messages_all_accounts">Notifica messaggi da tutti gli account</string>

View File

@ -8,8 +8,8 @@
<string name="add_keyword">키워드 추가</string>
<string name="hint_keyword">키워드를 작성하고 + 를 누르세요.</string>
<string name="empty_keyword">키워드 비어있음</string>
<string name="notification_all_accounts">모든 계정에서 알림</string>
<string name="notification_all_accounts_desc">로그인 되어 있는 모든 계정에서 알림을 받습니다. 이 옺션을 해제하면 현재 로그인된 계정의 알림만 받게 됩니다.</string>
<string name="notification_general_all_accounts">모든 계정에서 알림</string>
<string name="notification_general_all_accounts_desc">로그인 되어 있는 모든 계정에서 알림을 받습니다. 이 옺션을 해제하면 현재 로그인된 계정의 알림만 받게 됩니다.</string>
<string name="notification_messages">메시지 알림 활성화</string>
<string name="notification_messages_desc">현재 계정의 메시지 알림을 받습니다.</string>
<string name="notification_messages_all_accounts">모든 계정의 메시지 받기</string>

View File

@ -8,8 +8,8 @@
<string name="add_keyword">Dodaj słowo kluczowe</string>
<string name="hint_keyword">Wpisz słowo kluczowe i naciśnij +</string>
<string name="empty_keyword">Puste słowo kluczowe</string>
<string name="notification_all_accounts">Powiadamiaj ze wszystkich kont</string>
<string name="notification_all_accounts_desc">Otrzymuj powiadomienia dla każdego zalogowanego konta. Wyłączenie spowoduje sprawdzanie powiadomień, tylko z obecnie wybranego konta.</string>
<string name="notification_general_all_accounts">Powiadamiaj ze wszystkich kont</string>
<string name="notification_general_all_accounts_desc">Otrzymuj powiadomienia dla każdego zalogowanego konta. Wyłączenie spowoduje sprawdzanie powiadomień, tylko z obecnie wybranego konta.</string>
<string name="notification_messages">Włącz powiadomienia o wiadomościach</string>
<string name="notification_messages_desc">Otrzymuj natychmiastowe powiadomienia dla obecnego konta.</string>
<string name="notification_messages_all_accounts">Powiadamiaj ze wszystkich kont</string>

View File

@ -8,8 +8,8 @@
<string name="add_keyword">Adicionar Palavra-Chave</string>
<string name="hint_keyword">Escreva a palavra-chave e pressione +</string>
<string name="empty_keyword">Palavra-chave Vazia</string>
<string name="notification_all_accounts">Notificações de todas as contas</string>
<string name="notification_all_accounts_desc">Receber notificações para qualquer conta que esteja logada. Desativar esta opção vai apenas buscar as notificações da conta atualmente selecionada.</string>
<string name="notification_general_all_accounts">Notificações de todas as contas</string>
<string name="notification_general_all_accounts_desc">Receber notificações para qualquer conta que esteja logada. Desativar esta opção vai apenas buscar as notificações da conta atualmente selecionada.</string>
<string name="notification_messages">Ativar as notificações de mensagem</string>
<string name="notification_messages_desc">Receber notificações de conversas de sua conta atual.</string>
<string name="notification_messages_all_accounts">Notificar mensagens de todas as contas</string>

View File

@ -8,8 +8,8 @@
<string name="add_keyword">Thêm từ khoá</string>
<string name="hint_keyword">Nhập từ khoá rồi bấm +</string>
<string name="empty_keyword">Từ khoá trống</string>
<string name="notification_all_accounts">Thông báo từ tất cả các tài khoản</string>
<string name="notification_all_accounts_desc">Hiện thông báo từ tất cả các tài khoản đăng đăng nhập. Khi tắt, sẽ chỉ hiện thông báo từ tài khoản hiện tại.</string>
<string name="notification_general_all_accounts">Thông báo từ tất cả các tài khoản</string>
<string name="notification_general_all_accounts_desc">Hiện thông báo từ tất cả các tài khoản đăng đăng nhập. Khi tắt, sẽ chỉ hiện thông báo từ tài khoản hiện tại.</string>
<string name="notification_messages">Bật thông báo tin nhắn</string>
<string name="notification_messages_desc">Nhận thông báo tin nhắn cho tài khoản hiện tại.</string>
<string name="notification_messages_all_accounts">Thông báo tin nhắn từ tất cả các tài khoản</string>

View File

@ -8,8 +8,8 @@
<string name="add_keyword">添加关键字</string>
<string name="hint_keyword">输入关键字并按 +</string>
<string name="empty_keyword">空关键字</string>
<string name="notification_all_accounts">所有帐户通知</string>
<string name="notification_all_accounts_desc">从每个登录帐户中获取通知。如果禁用则只从当前选择帐户中获取通知。</string>
<string name="notification_general_all_accounts">所有帐户通知</string>
<string name="notification_general_all_accounts_desc">从每个登录帐户中获取通知。如果禁用则只从当前选择帐户中获取通知。</string>
<string name="notification_messages">启用通知</string>
<string name="notification_messages_desc">获得当前帐户即时消息通知。</string>
<string name="notification_messages_all_accounts">所有帐户消息通知</string>

View File

@ -16,4 +16,7 @@
<string name="debug_web_desc">Navigate to the page with an issue and send the resources for debugging.</string>
<string name="parsing_data">Parsing Data</string>
<string name="debug_parsers">Debug Parsers</string>
<string name="debug_parsers_desc">Launch one of the available parsers to debug its response data</string>
</resources>

View File

@ -8,14 +8,16 @@
<string name="add_keyword">Add Keyword</string>
<string name="hint_keyword">Type keyword and press +</string>
<string name="empty_keyword">Empty Keyword</string>
<string name="notification_all_accounts">Notify from all accounts</string>
<string name="notification_all_accounts_desc">Get notifications for every account that is logged in. Disabling this will only fetch notifications form the currently selected account.</string>
<string name="notification_general">Enable general notifications</string>
<string name="notification_general_desc">Get general notifications for your current account.</string>
<string name="notification_general_all_accounts">Notify from all accounts</string>
<string name="notification_general_all_accounts_desc">Get general notifications for every account that is logged in.</string>
<string name="notification_messages">Enable message notifications</string>
<string name="notification_messages_desc">Get instant message notifications for your current account.</string>
<string name="notification_messages_all_accounts">Notify messages from all accounts</string>
<string name="notification_messages_all_accounts_desc">Get instant message notifications from all accounts</string>
<string name="notification_fetch_now">Fetch Notifications Now</string>
<string name="notification_fetch_now_desc">Trigger the notification fetcher once. Note that fetching instant messages takes time.</string>
<string name="notification_fetch_now_desc">Trigger the notification fetcher once.</string>
<string name="notification_fetch_success">Fetching Notifications…</string>
<string name="notification_fetch_fail">Couldn\'t fetch notifications</string>
<string name="notification_sound">Notification sound</string>

View File

@ -9,8 +9,8 @@
<version title="v1.7.7" />
<item text="Fix overlay loading" />
<item text="Improve image loading" />
<item text="" />
<item text="" />
<item text="Launch image viewer when opening full sized image" />
<item text="Improve filtering system" />
<item text="" />
<item text="" />
<item text="" />

View File

@ -49,4 +49,12 @@ class FbRegexTest {
val data = "\"uri\":\"$url\"}"
assertEquals(url, FB_JSON_URL_MATCHER.find(data)[1])
}
@Test
fun imageIdRegex() {
val id = 123456L
val img = "https://scontent-yyz1-1.xx.fbcdn.net/v/t31.0-8/fr/cp0/e15/q65/89056_${id}_98239_o.jpg"
assertEquals(id, FB_IMAGE_ID_MATCHER.find(img)[1]?.toLongOrNull())
}
}

View File

@ -1,7 +1,10 @@
package com.pitchedapps.frost.facebook
import com.pitchedapps.frost.utils.isImageUrl
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertTrue
/**
@ -57,4 +60,28 @@ class FbUrlTest {
assertFbFormat(expected, url)
}
@Test
fun imageRegex() {
arrayOf(
"https://scontent-yyz1-1.xx.fbcdn.net/v/t1.0-9/fr/cp0/e15/q65/229_546131_836546862_n.jpg?efg=e343J9&oh=d4245b1&oe=5453",
"/photo/view_full_size/?fbid=1523&ref_component=mbasic_photo_permalink&ref_page=%2Fwap%2Fphoto.php&refid=153&_ft_=...",
"#!/photo/view_full_size/?fbid=1523&ref_component=mbasic_photo_permalink&ref_page=%2Fwap%2Fphoto.php&refid=153&_ft_=..."
).forEach {
assertTrue(it.isImageUrl, "Failed to match image for $it")
}
}
@Test
fun antiImageRegex() {
arrayOf(
"http...fbcdn.net...mp4",
"/photo/...png",
"https://www.google.ca"
).forEach {
assertFalse(it.isImageUrl, "Should not have matched image for $it")
}
}
}

View File

@ -0,0 +1,61 @@
package com.pitchedapps.frost.rx
import org.junit.Before
import org.junit.Test
import java.util.concurrent.CountDownLatch
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
/**
* Created by Allan Wang on 07/01/18.
*/
private inline val threadId
get() = Thread.currentThread().id
class ResettableFlyweightTest {
class IntFlyweight : RxFlyweight<Int, Long, Long>() {
override fun call(input: Int): Long {
println("Call for $input on thread $threadId")
Thread.sleep(20)
return System.currentTimeMillis()
}
override fun validate(input: Int, cond: Long) = System.currentTimeMillis() - cond < 500
override fun cache(input: Int): Long = System.currentTimeMillis()
}
private lateinit var flyweight: IntFlyweight
private lateinit var latch: CountDownLatch
@Before
fun init() {
flyweight = IntFlyweight()
latch = CountDownLatch(1)
}
@Test
fun testCache() {
flyweight(1).subscribe { i ->
flyweight(1).subscribe { j ->
assertEquals(i, j, "Did not use cache during calls")
latch.countDown()
}
}
latch.await()
}
@Test
fun testNoCache() {
flyweight(1).subscribe { i ->
flyweight(2).subscribe { j ->
assertNotEquals(i, j, "Should not use cache for calls with different keys")
latch.countDown()
}
}
latch.await()
}
}

View File

@ -3,6 +3,8 @@
## v1.7.7
* Fix overlay loading
* Improve image loading
* Launch image viewer when opening full sized image
* Improve filtering system
## v1.7.5
* Mark notifications as read when clicked!