diff options
Diffstat (limited to 'app')
43 files changed, 530 insertions, 196 deletions
diff --git a/app/build.gradle b/app/build.gradle index 54284b09..9bd6c298 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt index 8897e804..67785a60 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt @@ -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) } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt index ffcbadab..3a01e05b 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/BaseMainActivity.kt @@ -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 } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt index 99fa6eee..cdde8311 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt @@ -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)) + doAsync({ + L.e(it) { "Failed to load image $IMAGE_HASH" } + errorRef = it + fabAction = FabStates.ERROR + }) { + 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).withEndAction(fab::show).start() + photo.animate().alpha(1f).scaleXY(1f).start() } - }) - } else { - fabAction = FabStates.ERROR - fab.show() + } } } /** - * 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) + 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() - override fun onLoadFailed(errorDrawable: Drawable?) = - callback(null, false) + if (!response.isSuccessful) { + L.e { "Unsuccessful response for image" } + errorRef = Throwable("Unsuccessful response for image") + return callback(null) + } - override fun getSize(cb: SizeReadyCallback) = - cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) + if (!local.createFreshFile()) { + L.e { "Could not create temp file" } + return callback(null) + } - } + var valid = false - 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) + 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") } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt index 2434c8c2..3b320cce 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt @@ -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) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt index acc23cad..4ba3c80d 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbRegex.kt @@ -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) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt index ae8652e6..3ca37bb4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt @@ -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) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt index 61a94ac5..fa78bbfa 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt @@ -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) } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Messages.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Messages.kt new file mode 100644 index 00000000..0e37a61e --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Messages.kt @@ -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 +}
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt b/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt index 6d2b3cda..651e57d8 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/glide/GlideUtils.kt @@ -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()) + } +}
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt index 690d1be8..f5b1df17 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/iitems/MenuIItem.kt @@ -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 diff --git a/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt index 2e16f386..8bc2c1fe 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/iitems/NotificationIItem.kt @@ -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 diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt index f0938eca..d5730e16 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/FrostParser.kt @@ -25,6 +25,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 */ val url: String diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt index 24ddd601..697cbbe8 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/MessageParser.kt @@ -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? { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt index 23852852..812f12e3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/NotifParser.kt @@ -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? { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt b/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt index 7fbc0f08..5300bf11 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/parsers/SearchParser.kt @@ -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? { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/rx/RxFlyweight.kt b/app/src/main/kotlin/com/pitchedapps/frost/rx/RxFlyweight.kt new file mode 100644 index 00000000..159a9bf2 --- /dev/null +++ b/app/src/main/kotlin/com/pitchedapps/frost/rx/RxFlyweight.kt @@ -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() + } + } + +}
\ No newline at end of file diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt index 50392dea..e8c5e7c1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt @@ -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) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt index 3d19606b..fc946772 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt @@ -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)) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt index 3e795e80..3b37d1c3 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt @@ -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" diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt index 1108f5d4..2ee086c0 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Notifications.kt @@ -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_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) }) { + 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) } } diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt index 7bec8ce0..7422fb36 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt @@ -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) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt index 1fb41dca..486fbae1 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt @@ -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) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt index 64cf34a1..78847df4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt @@ -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() diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt index 454e2a4b..0501e2e6 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt @@ -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 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 9a3dc331..6c09de7c 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostUrlOverlayValidator.kt @@ -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 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 e23ec0f8..d0f7d490 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostWebViewClients.kt @@ -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.* diff --git a/app/src/main/res/values-de-rDE/strings_pref_notifications.xml b/app/src/main/res/values-de-rDE/strings_pref_notifications.xml index d2280858..c2ab299a 100644 --- a/app/src/main/res/values-de-rDE/strings_pref_notifications.xml +++ b/app/src/main/res/values-de-rDE/strings_pref_notifications.xml @@ -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> diff --git a/app/src/main/res/values-es-rES/strings_pref_notifications.xml b/app/src/main/res/values-es-rES/strings_pref_notifications.xml index 63192d10..69aa9b60 100644 --- a/app/src/main/res/values-es-rES/strings_pref_notifications.xml +++ b/app/src/main/res/values-es-rES/strings_pref_notifications.xml @@ -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> diff --git a/app/src/main/res/values-fr-rFR/strings_pref_notifications.xml b/app/src/main/res/values-fr-rFR/strings_pref_notifications.xml index 89687082..fb519054 100644 --- a/app/src/main/res/values-fr-rFR/strings_pref_notifications.xml +++ b/app/src/main/res/values-fr-rFR/strings_pref_notifications.xml @@ -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> diff --git a/app/src/main/res/values-gl-rES/strings_pref_notifications.xml b/app/src/main/res/values-gl-rES/strings_pref_notifications.xml index aea99fb3..f3ce204d 100644 --- a/app/src/main/res/values-gl-rES/strings_pref_notifications.xml +++ b/app/src/main/res/values-gl-rES/strings_pref_notifications.xml @@ -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> diff --git a/app/src/main/res/values-it-rIT/strings_pref_notifications.xml b/app/src/main/res/values-it-rIT/strings_pref_notifications.xml index 9e3fb335..5f0b530c 100644 --- a/app/src/main/res/values-it-rIT/strings_pref_notifications.xml +++ b/app/src/main/res/values-it-rIT/strings_pref_notifications.xml @@ -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> diff --git a/app/src/main/res/values-ko-rKR/strings_pref_notifications.xml b/app/src/main/res/values-ko-rKR/strings_pref_notifications.xml index beba9ee0..c6777c41 100644 --- a/app/src/main/res/values-ko-rKR/strings_pref_notifications.xml +++ b/app/src/main/res/values-ko-rKR/strings_pref_notifications.xml @@ -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> diff --git a/app/src/main/res/values-pl-rPL/strings_pref_notifications.xml b/app/src/main/res/values-pl-rPL/strings_pref_notifications.xml index 0f2a4cf8..12de118d 100644 --- a/app/src/main/res/values-pl-rPL/strings_pref_notifications.xml +++ b/app/src/main/res/values-pl-rPL/strings_pref_notifications.xml @@ -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> diff --git a/app/src/main/res/values-pt-rBR/strings_pref_notifications.xml b/app/src/main/res/values-pt-rBR/strings_pref_notifications.xml index cd8a8a25..1e128f7d 100644 --- a/app/src/main/res/values-pt-rBR/strings_pref_notifications.xml +++ b/app/src/main/res/values-pt-rBR/strings_pref_notifications.xml @@ -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> diff --git a/app/src/main/res/values-vi-rVN/strings_pref_notifications.xml b/app/src/main/res/values-vi-rVN/strings_pref_notifications.xml index 92889571..d077173c 100644 --- a/app/src/main/res/values-vi-rVN/strings_pref_notifications.xml +++ b/app/src/main/res/values-vi-rVN/strings_pref_notifications.xml @@ -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> diff --git a/app/src/main/res/values-zh-rCN/strings_pref_notifications.xml b/app/src/main/res/values-zh-rCN/strings_pref_notifications.xml index 66ae53a0..38a2722d 100644 --- a/app/src/main/res/values-zh-rCN/strings_pref_notifications.xml +++ b/app/src/main/res/values-zh-rCN/strings_pref_notifications.xml @@ -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> diff --git a/app/src/main/res/values/strings_pref_debug.xml b/app/src/main/res/values/strings_pref_debug.xml index 8e8c051c..771e5130 100644 --- a/app/src/main/res/values/strings_pref_debug.xml +++ b/app/src/main/res/values/strings_pref_debug.xml @@ -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>
\ No newline at end of file diff --git a/app/src/main/res/values/strings_pref_notifications.xml b/app/src/main/res/values/strings_pref_notifications.xml index 49665f8e..8db493b8 100644 --- a/app/src/main/res/values/strings_pref_notifications.xml +++ b/app/src/main/res/values/strings_pref_notifications.xml @@ -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> diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml index 28978193..0ddc28f3 100644 --- a/app/src/main/res/xml/frost_changelog.xml +++ b/app/src/main/res/xml/frost_changelog.xml @@ -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="" /> diff --git a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt index a79ccf3f..08853466 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbRegexTest.kt @@ -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()) + } + }
\ No newline at end of file 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 62b7cac2..79d5ea64 100644 --- a/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt +++ b/app/src/test/kotlin/com/pitchedapps/frost/facebook/FbUrlTest.kt @@ -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") + } + + } + }
\ No newline at end of file diff --git a/app/src/test/kotlin/com/pitchedapps/frost/rx/ResettableFlyweightTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/rx/ResettableFlyweightTest.kt new file mode 100644 index 00000000..ec92b059 --- /dev/null +++ b/app/src/test/kotlin/com/pitchedapps/frost/rx/ResettableFlyweightTest.kt @@ -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() + } + + +}
\ No newline at end of file |