aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2018-03-11 19:24:32 -0400
committerGitHub <noreply@github.com>2018-03-11 19:24:32 -0400
commitfe51373f5a95323d64f6d966888a2c6c62a36deb (patch)
tree328bf7ab5bf00f01e070e9d84ff71cac59b0ecf1
parent67988a25d83fc10b187fcc821c3ceacfad0195d5 (diff)
downloadfrost-fe51373f5a95323d64f6d966888a2c6c62a36deb.tar.gz
frost-fe51373f5a95323d64f6d966888a2c6c62a36deb.tar.bz2
frost-fe51373f5a95323d64f6d966888a2c6c62a36deb.zip
Enhancement/debug mode (#779)
* Update changelog * Improve debugger * Remove need for mapping urls * Remove excess logs * Clean up
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt16
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt42
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt74
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt1
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/contracts/VideoViewHolder.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt27
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/FbRequest.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/requests/Images.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt1
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/injectors/CssHider.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/intro/IntroFragmentTheme.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/intro/IntroImageFragments.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt15
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/AnimatedVectorDelegate.kt6
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt8
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IabBinder.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt18
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt1
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt30
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt13
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/NestedWebView.kt12
-rw-r--r--app/src/main/res/values/strings_pref_debug.xml1
-rw-r--r--app/src/main/res/xml/frost_changelog.xml10
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt2
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/utils/StringEscapeUtilsTest.kt16
-rw-r--r--docs/Changelog.md4
34 files changed, 202 insertions, 141 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
index a4d98d8a..691f050e 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
@@ -59,7 +59,7 @@ class AboutActivity : AboutActivityBase(null, {
"subsamplingscaleimageview"
)
- val l = libs.prepareLibraries(this, include, null, false, true,true)
+ val l = libs.prepareLibraries(this, include, null, false, true, true)
// l.forEach { KL.d{"Lib ${it.definedName}"} }
return l
}
@@ -88,14 +88,18 @@ class AboutActivity : AboutActivityBase(null, {
if (item is LibraryIItem) {
val now = System.currentTimeMillis()
if (now - lastClick > 500)
- clickCount = 0
+ clickCount = 1
else
clickCount++
lastClick = now
- if (clickCount == 7 && !Prefs.debugSettings) {
- Prefs.debugSettings = true
- L.d { "Debugging section enabled" }
- toast(R.string.debug_toast_enabled)
+ if (clickCount == 8) {
+ if (!Prefs.debugSettings) {
+ Prefs.debugSettings = true
+ L.d { "Debugging section enabled" }
+ toast(R.string.debug_toast_enabled)
+ } else {
+ toast(R.string.debug_toast_already_enabled)
+ }
}
}
false
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt
index 685e6532..30f12c1d 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/DebugActivity.kt
@@ -15,10 +15,15 @@ import ca.allanwang.kau.utils.visible
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.pitchedapps.frost.R
import com.pitchedapps.frost.facebook.FbItem
+import com.pitchedapps.frost.injectors.JsActions
+import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.createFreshDir
import com.pitchedapps.frost.utils.setFrostColors
import com.pitchedapps.frost.web.DebugWebView
+import io.reactivex.Single
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.schedulers.Schedulers
import java.io.File
/**
@@ -33,6 +38,8 @@ class DebugActivity : KauBaseActivity() {
companion object {
const val RESULT_URL = "extra_result_url"
+ const val RESULT_SCREENSHOT = "extra_result_screenshot"
+ const val RESULT_BODY = "extra_result_body"
fun baseDir(context: Context) = File(context.externalCacheDir, "offline_debug")
}
@@ -61,13 +68,34 @@ class DebugActivity : KauBaseActivity() {
val parent = baseDir(this)
parent.createFreshDir()
- val file = File(parent, "screenshot.png")
- web.getScreenshot(file) {
- val intent = Intent()
- intent.putExtra(RESULT_URL, web.url)
- setResult(Activity.RESULT_OK, intent)
- finish()
- }
+ val rxScreenshot = Single.fromCallable {
+ web.getScreenshot(File(parent, "screenshot.png"))
+ }.subscribeOn(Schedulers.io())
+ val rxBody = Single.create<String> { emitter ->
+ web.evaluateJavascript(JsActions.RETURN_BODY.function) {
+ emitter.onSuccess(it)
+ }
+ }.subscribeOn(AndroidSchedulers.mainThread())
+ Single.zip(listOf(rxScreenshot, rxBody), {
+ val screenshot = it[0] == true
+ val body = it[1] as? String
+ screenshot to body
+ }).observeOn(AndroidSchedulers.mainThread())
+ .subscribe { (screenshot, body), err ->
+ if (err != null) {
+ L.e { "DebugActivity error ${err.message}" }
+ setResult(Activity.RESULT_CANCELED)
+ finish()
+ return@subscribe
+ }
+ val intent = Intent()
+ intent.putExtra(RESULT_URL, web.url)
+ intent.putExtra(RESULT_SCREENSHOT, screenshot)
+ if (body != null)
+ intent.putExtra(RESULT_BODY, body)
+ setResult(Activity.RESULT_OK, intent)
+ finish()
+ }
}
}
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 e563ff8a..9f191460 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt
@@ -92,7 +92,8 @@ class ImageActivity : KauBaseActivity() {
// a unique image identifier based on the id (if it exists), and its hash
private val IMAGE_HASH: String by lazy {
- "${Math.abs(FB_IMAGE_ID_MATCHER.find(IMAGE_URL)[1]?.hashCode() ?: 0)}_${Math.abs(IMAGE_URL.hashCode())}"
+ "${Math.abs(FB_IMAGE_ID_MATCHER.find(IMAGE_URL)[1]?.hashCode()
+ ?: 0)}_${Math.abs(IMAGE_URL.hashCode())}"
}
override fun onCreate(savedInstanceState: Bundle?) {
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 3b320cce..aa2e5871 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/LoginActivity.kt
@@ -83,32 +83,32 @@ class LoginActivity : BaseActivity() {
usernameSubject,
BiFunction(::Pair))
.observeOn(AndroidSchedulers.mainThread()).subscribe { (foundImage, name) ->
- refresh = false
- if (!foundImage) {
- L.e { "Could not get profile photo; Invalid userId?" }
- L._i { cookie }
- }
- textview.text = String.format(getString(R.string.welcome), name)
- textview.fadeIn()
- frostAnswers {
- logLogin(LoginEvent()
- .putMethod("frost_browser")
- .putSuccess(true))
- }
- /*
- * The user may have logged into an account that is already in the database
- * We will let the db handle duplicates and load it now after the new account has been saved
- */
- loadFbCookiesAsync {
- val cookies = ArrayList(it)
- Handler().postDelayed({
- if (Showcase.intro)
- launchNewTask<IntroActivity>(cookies, true)
- else
- launchNewTask<MainActivity>(cookies, true)
- }, 1000)
- }
- }
+ refresh = false
+ if (!foundImage) {
+ L.e { "Could not get profile photo; Invalid userId?" }
+ L._i { cookie }
+ }
+ textview.text = String.format(getString(R.string.welcome), name)
+ textview.fadeIn()
+ frostAnswers {
+ logLogin(LoginEvent()
+ .putMethod("frost_browser")
+ .putSuccess(true))
+ }
+ /*
+ * The user may have logged into an account that is already in the database
+ * We will let the db handle duplicates and load it now after the new account has been saved
+ */
+ loadFbCookiesAsync {
+ val cookies = ArrayList(it)
+ Handler().postDelayed({
+ if (Showcase.intro)
+ launchNewTask<IntroActivity>(cookies, true)
+ else
+ launchNewTask<MainActivity>(cookies, true)
+ }, 1000)
+ }
+ }
loadProfile(cookie.id)
loadUsername(cookie)
}
@@ -117,17 +117,17 @@ class LoginActivity : BaseActivity() {
private fun loadProfile(id: Long) {
profileLoader.load(PROFILE_PICTURE_URL(id))
.transform(FrostGlide.roundCorner).listener(object : RequestListener<Drawable> {
- override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
- profileSubject.onSuccess(true)
- return false
- }
-
- override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
- e.logFrostAnswers("Profile loading exception")
- profileSubject.onSuccess(false)
- return false
- }
- }).into(profile)
+ override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
+ profileSubject.onSuccess(true)
+ return false
+ }
+
+ override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
+ e.logFrostAnswers("Profile loading exception")
+ profileSubject.onSuccess(false)
+ return false
+ }
+ }).into(profile)
}
private fun loadUsername(cookie: CookieModel) {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt
index 2de7a843..8d4e521f 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt
@@ -52,7 +52,7 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IabSettings() {
ACTIVITY_REQUEST_DEBUG -> {
val url = data?.extras?.getString(DebugActivity.RESULT_URL)
if (resultCode == Activity.RESULT_OK && url?.isNotBlank() == true)
- sendDebug(url)
+ sendDebug(url, data.extras.getString(DebugActivity.RESULT_BODY))
return
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
index 43463752..ca6df366 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/ActivityContract.kt
@@ -17,6 +17,7 @@ interface MainActivityContract : ActivityContract, MainFabContract {
* Available on all threads
*/
fun collapseAppBar()
+
fun reloadFragment(fragment: BaseFragment)
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/contracts/VideoViewHolder.kt b/app/src/main/kotlin/com/pitchedapps/frost/contracts/VideoViewHolder.kt
index fc75ba14..13b6a7aa 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/contracts/VideoViewHolder.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/contracts/VideoViewHolder.kt
@@ -15,8 +15,7 @@ interface VideoViewHolder : FrameWrapper, FrostVideoContainerContract {
var videoViewer: FrostVideoViewer?
- fun showVideo(url: String)
- = showVideo(url, false)
+ fun showVideo(url: String) = showVideo(url, false)
/**
* Create new viewer and reuse existing one
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt
index 88a1383d..60f8c5a2 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/dbflow/NotificationDb.kt
@@ -29,7 +29,8 @@ class NotificationMigration2(modelClass: Class<NotificationModel>) : AlterTableM
@Table(database = NotificationDb::class, allFields = true, primaryKeyConflict = ConflictAction.REPLACE)
data class NotificationModel(@PrimaryKey var id: Long = -1L, var epoch: Long = -1L, var epochIm: Long = -1) : BaseModel()
-fun lastNotificationTime(id: Long): NotificationModel = (select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle() ?: NotificationModel(id = id)
+fun lastNotificationTime(id: Long): NotificationModel = (select from NotificationModel::class where (NotificationModel_Table.id eq id)).querySingle()
+ ?: NotificationModel(id = id)
fun saveNotificationTime(notificationModel: NotificationModel, callback: (() -> Unit)? = null) {
notificationModel.async save {
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt b/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt
index 791e6a69..95162932 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/debugger/OfflineWebsite.kt
@@ -9,8 +9,11 @@ import com.pitchedapps.frost.facebook.requests.zip
import com.pitchedapps.frost.utils.createFreshDir
import com.pitchedapps.frost.utils.createFreshFile
import com.pitchedapps.frost.utils.frostJsoup
+import com.pitchedapps.frost.utils.unescapeHtml
import okhttp3.Request
import okhttp3.ResponseBody
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import org.jsoup.nodes.Entities
import java.io.File
@@ -29,6 +32,8 @@ import java.util.zip.ZipOutputStream
*/
class OfflineWebsite(private val url: String,
private val cookie: String = "",
+ baseUrl: String? = null,
+ private val html: String? = null,
/**
* Directory that holds all the files
*/
@@ -38,7 +43,8 @@ class OfflineWebsite(private val url: String,
/**
* Supplied url without the queries
*/
- val baseUrl = url.substringBefore("?").trim('/')
+ private val baseUrl = (baseUrl ?: url.substringBefore("?")
+ .substringBefore(".com")).trim('/')
private val mainFile = File(baseDir, "index.html")
private val assetDir = File(baseDir, "assets")
@@ -50,7 +56,7 @@ class OfflineWebsite(private val url: String,
private val L = KauLoggerExtension("Offline", com.pitchedapps.frost.utils.L)
init {
- if (!baseUrl.startsWith("http"))
+ if (!this.baseUrl.startsWith("http"))
throw IllegalArgumentException("Base Url must start with http")
}
@@ -94,7 +100,13 @@ class OfflineWebsite(private val url: String,
if (cancelled) return
- val doc = frostJsoup(cookie, url)
+ val doc: Document
+ if (html == null || html.length < 100) {
+ doc = frostJsoup(cookie, url)
+ } else {
+ doc = Jsoup.parse("<html>${html.unescapeHtml()}</html>")
+ L.d { "Building data from supplied content of size ${html.length}" }
+ }
doc.setBaseUri(baseUrl)
doc.outputSettings().escapeMode(Entities.EscapeMode.extended)
if (doc.childNodeSize() == 0) {
@@ -125,8 +137,11 @@ class OfflineWebsite(private val url: String,
progress(50)
downloadCss().subscribe { cssLinks, cssThrowable ->
+
if (cssThrowable != null) {
- L.e { "CSS parsing failed" }
+ L.e { "CSS parsing failed: ${cssThrowable.message} $cssThrowable" }
+ callback(false)
+ return@subscribe
}
progress(70)
@@ -291,8 +306,8 @@ class OfflineWebsite(private val url: String,
private fun String.shorten() =
if (length <= 10) this else substring(length - 10)
- private fun Set<String>.clean()
- = filter(String::isNotBlank).filter { it.startsWith("http") }
+ private fun Set<String>.clean(): List<String> =
+ filter(String::isNotBlank).filter { it.startsWith("http") }
private fun reset() {
cancelled = false
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 45545336..c70ae1ae 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
@@ -130,6 +130,8 @@ fun String.getAuth(): RequestAuth {
inline fun <T, reified R : Any, O> Array<T>.zip(crossinline mapper: (List<R>) -> O,
crossinline caller: (T) -> R): Single<O> {
+ if (isEmpty())
+ return Single.just(mapper(emptyList()))
val singles = map { Single.fromCallable { caller(it) }.subscribeOn(Schedulers.io()) }
return Single.zip(singles) {
val results = it.mapNotNull { it as? R }
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 094e7fc9..453400d3 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
@@ -76,7 +76,8 @@ class HdImageLoading : ModelLoader<HdImageMaybe, InputStream> {
class HdImageFetcher(private val model: HdImageMaybe) : DataFetcher<InputStream> {
- @Volatile private var cancelled: Boolean = false
+ @Volatile
+ private var cancelled: Boolean = false
private var urlCall: Call? = null
private var inputStream: InputStream? = null
@@ -92,7 +93,8 @@ class HdImageFetcher(private val model: HdImageMaybe) : DataFetcher<InputStream>
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(model.id).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()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt
index e683b056..6555e076 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentContract.kt
@@ -1,6 +1,5 @@
package com.pitchedapps.frost.fragments
-import android.support.design.widget.FloatingActionButton
import com.pitchedapps.frost.contracts.*
import com.pitchedapps.frost.views.FrostRecyclerView
import io.reactivex.disposables.Disposable
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssHider.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssHider.kt
index 637a5092..6981fd1c 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssHider.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/CssHider.kt
@@ -9,7 +9,7 @@ import android.webkit.WebView
*/
enum class CssHider(vararg val items: String) : InjectorContract {
CORE("[data-sigil=m_login_upsell]", "role=progressbar"),
-// HEADER("#header", "[data-sigil=MTopBlueBarHeader]",
+ // HEADER("#header", "[data-sigil=MTopBlueBarHeader]",
// "#header-notices", "[data-sigil*=m-promo-jewel-header]"),
ADS("article[data-xt*=sponsor]",
"article[data-store*=sponsor]"),
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt
index 7be8cd3c..b4926355 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt
@@ -16,6 +16,7 @@ enum class JsActions(body: String) : InjectorContract {
LOGIN_CHECK("document.getElementById('signup-button')&&Frost.loadLogin();"),
BASE_HREF("""document.write("<base href='$FB_URL_BASE'/>");"""),
FETCH_BODY("""setTimeout(function(){var e=document.querySelector("main");e||(e=document.querySelector("body")),Frost.handleHtml(e.outerHTML)},1e2);"""),
+ RETURN_BODY("return(document.getElementsByTagName('html')[0].innerHTML);"),
CREATE_POST(clickBySelector("button[name=view_overview]")),
// CREATE_MSG(clickBySelector("a[rel=dialog]")),
/**
@@ -23,7 +24,7 @@ enum class JsActions(body: String) : InjectorContract {
*/
EMPTY("");
- val function = "!function(){$body}();"
+ val function = "(function(){$body})();"
override fun inject(webView: WebView, callback: (() -> Unit)?) =
JsInjector(function).inject(webView, callback)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt
index 2d067e44..1698ae13 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsInjector.kt
@@ -101,8 +101,7 @@ fun WebView.jsInject(vararg injectors: InjectorContract, callback: ((Int) -> Uni
}
fun FrostWebViewClient.jsInject(vararg injectors: InjectorContract,
- callback: ((Int) -> Unit)? = null)
- = web.jsInject(*injectors, callback = callback)
+ callback: ((Int) -> Unit)? = null) = web.jsInject(*injectors, callback = callback)
/**
* Wrapper class to convert a function into an injector
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroFragmentTheme.kt b/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroFragmentTheme.kt
index f91d77ea..42630d79 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroFragmentTheme.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroFragmentTheme.kt
@@ -22,8 +22,7 @@ class IntroFragmentTheme : BaseIntroFragment(R.layout.intro_theme) {
val themeList
get() = listOf(light, dark, amoled, glass)
- override fun viewArray(): Array<Array<out View>>
- = arrayOf(arrayOf(title), arrayOf(light, dark), arrayOf(amoled, glass))
+ override fun viewArray(): Array<Array<out View>> = arrayOf(arrayOf(title), arrayOf(light, dark), arrayOf(amoled, glass))
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroImageFragments.kt b/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroImageFragments.kt
index b9c83c50..c0ccd649 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroImageFragments.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/intro/IntroImageFragments.kt
@@ -25,8 +25,7 @@ abstract class BaseImageIntroFragment(
val screen: Drawable by lazyResettableRegistered { imageDrawable.findDrawableByLayerId(R.id.intro_phone_screen) }
val icon: ImageView by bindViewResettable(R.id.intro_button)
- override fun viewArray(): Array<Array<out View>>
- = arrayOf(arrayOf(title), arrayOf(desc))
+ override fun viewArray(): Array<Array<out View>> = arrayOf(arrayOf(title), arrayOf(desc))
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
title.setText(titleRes)
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 b6427e5b..30c94744 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
@@ -1,6 +1,5 @@
package com.pitchedapps.frost.services
-import android.annotation.SuppressLint
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
@@ -93,7 +92,7 @@ fun NotificationCompat.Builder.setFrostAlert(enable: Boolean, ringtone: String):
if (enable) Notification.GROUP_ALERT_CHILDREN
else Notification.GROUP_ALERT_SUMMARY)
} else if (!enable) {
- setDefaults(0)
+ setDefaults(0)
} else {
var defaults = 0
if (Prefs.notificationVibrate) defaults = defaults or Notification.DEFAULT_VIBRATE
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 df0fdef2..5dc0edab 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt
@@ -10,6 +10,7 @@ import com.pitchedapps.frost.activities.DebugActivity
import com.pitchedapps.frost.activities.SettingsActivity
import com.pitchedapps.frost.activities.SettingsActivity.Companion.ACTIVITY_REQUEST_DEBUG
import com.pitchedapps.frost.debugger.OfflineWebsite
+import com.pitchedapps.frost.facebook.FB_URL_BASE
import com.pitchedapps.frost.facebook.FbCookie
import com.pitchedapps.frost.facebook.FbItem
import com.pitchedapps.frost.parsers.FrostParser
@@ -90,18 +91,12 @@ private fun Context.createEmail(parser: FrostParser<*>, content: Any?) =
private const val ZIP_NAME = "debug"
-fun SettingsActivity.sendDebug(urlOrig: String) {
-
- val url = when {
- urlOrig.endsWith("soft=requests") -> FbItem.FRIENDS.url
- urlOrig.endsWith("soft=messages") -> FbItem.MESSAGES.url
- urlOrig.endsWith("soft=notifications") -> FbItem.NOTIFICATIONS.url
- urlOrig.endsWith("soft=search") -> "${FbItem._SEARCH.url}?q=a"
- else -> urlOrig
- }
+fun SettingsActivity.sendDebug(url: String, html: String?) {
val downloader = OfflineWebsite(url, FbCookie.webCookie ?: "",
- DebugActivity.baseDir(this))
+ baseUrl = FB_URL_BASE,
+ html = html,
+ baseDir = DebugActivity.baseDir(this))
val md = materialDialog {
title(R.string.parsing_data)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/AnimatedVectorDelegate.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/AnimatedVectorDelegate.kt
index 928f90d3..8d800e9b 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/AnimatedVectorDelegate.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/AnimatedVectorDelegate.kt
@@ -54,8 +54,10 @@ class AnimatedVectorDelegate(
override fun bind(view: ImageView) {
this.view = view
- view.context.drawable(avdStart) as? AnimatedVectorDrawable ?: throw IllegalArgumentException("AnimatedVectorDelegate has a starting drawable that isn't an avd")
- view.context.drawable(avdEnd) as? AnimatedVectorDrawable ?: throw IllegalArgumentException("AnimatedVectorDelegate has an ending drawable that isn't an avd")
+ view.context.drawable(avdStart) as? AnimatedVectorDrawable
+ ?: throw IllegalArgumentException("AnimatedVectorDelegate has a starting drawable that isn't an avd")
+ view.context.drawable(avdEnd) as? AnimatedVectorDrawable
+ ?: throw IllegalArgumentException("AnimatedVectorDelegate has an ending drawable that isn't an avd")
view.setImageResource(avdStart)
if (emitOnBind) animatedVectorListener?.invoke(avd!!, false)
}
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 3e102671..d73f29e9 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Utils.kt
@@ -32,6 +32,7 @@ import com.pitchedapps.frost.dbflow.CookieModel
import com.pitchedapps.frost.facebook.*
import com.pitchedapps.frost.facebook.FbUrlFormatter.Companion.VIDEO_REDIRECT
import com.pitchedapps.frost.utils.iab.IS_FROST_PRO
+import org.apache.commons.text.StringEscapeUtils
import org.jsoup.Jsoup
import org.jsoup.nodes.Element
import java.io.File
@@ -342,4 +343,9 @@ fun File.createFreshDir(): Boolean {
if (exists() && !deleteRecursively())
return false
return mkdirs()
-} \ No newline at end of file
+}
+
+fun String.unescapeHtml(): String =
+ StringEscapeUtils.unescapeXml(this)
+ .replace("\\u003C", "<")
+ .replace("\\\"", "\"")
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IabBinder.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IabBinder.kt
index cdfffb87..568127a2 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IabBinder.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/iab/IabBinder.kt
@@ -94,8 +94,8 @@ abstract class IabBinder : FrostBilling {
error.logFrostAnswers("IAB error $errorCode")
}
- override fun onActivityResultBilling(requestCode: Int, resultCode: Int, data: Intent?): Boolean
- = bp?.handleActivityResult(requestCode, resultCode, data) ?: false
+ override fun onActivityResultBilling(requestCode: Int, resultCode: Int, data: Intent?): Boolean = bp?.handleActivityResult(requestCode, resultCode, data)
+ ?: false
override fun purchasePro() {
val bp = this.bp
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 78847df4..7058ffe4 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/AccountItem.kt
@@ -34,16 +34,16 @@ class AccountItem(val cookie: CookieModel?) : KauIItem<AccountItem, AccountItem.
text.text = cookie.name
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()
- return false
- }
+ override fun onResourceReady(resource: Drawable?, model: Any?, target: Target<Drawable>?, dataSource: DataSource?, isFirstResource: Boolean): Boolean {
+ text.fadeIn()
+ return false
+ }
- override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
- text.fadeIn()
- return false
- }
- }).into(image)
+ override fun onLoadFailed(e: GlideException?, model: Any?, target: Target<Drawable>?, isFirstResource: Boolean): Boolean {
+ text.fadeIn()
+ return false
+ }
+ }).into(image)
} else {
text.visible()
image.setImageDrawable(GoogleMaterial.Icon.gmd_add_circle_outline.toDrawable(itemView.context, 100, Prefs.textColor))
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt
index aef9099a..ceb3d487 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt
@@ -16,7 +16,6 @@ import ca.allanwang.kau.utils.*
import com.devbrackets.android.exomedia.listener.VideoControlsVisibilityListener
import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.pitchedapps.frost.R
-import com.pitchedapps.frost.facebook.formattedFbUrl
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostDownload
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt
index 9edd671b..d005e58a 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/views/Keywords.kt
@@ -52,8 +52,7 @@ class Keywords @JvmOverloads constructor(
recycler.layoutManager = LinearLayoutManager(context)
recycler.adapter = adapter
adapter.withEventHook(object : ClickEventHook<KeywordItem>() {
- override fun onBind(viewHolder: RecyclerView.ViewHolder): View?
- = (viewHolder as? KeywordItem.ViewHolder)?.delete
+ override fun onBind(viewHolder: RecyclerView.ViewHolder): View? = (viewHolder as? KeywordItem.ViewHolder)?.delete
override fun onClick(v: View, position: Int, fastAdapter: FastAdapter<KeywordItem>, item: KeywordItem) {
adapter.remove(position)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt
index 7fd2286c..4ac9e600 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/DebugWebView.kt
@@ -4,6 +4,7 @@ import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Bitmap
import android.graphics.Color
+import android.support.annotation.WorkerThread
import android.util.AttributeSet
import android.view.View
import android.webkit.WebView
@@ -16,8 +17,6 @@ import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.createFreshFile
import com.pitchedapps.frost.utils.iab.IS_FROST_PRO
import com.pitchedapps.frost.utils.isFacebookUrl
-import org.jetbrains.anko.doAsync
-import org.jetbrains.anko.uiThread
import org.jetbrains.anko.withAlpha
import java.io.File
@@ -45,27 +44,22 @@ class DebugWebView @JvmOverloads constructor(
isDrawingCacheEnabled = true
}
- fun getScreenshot(output: File, callback: (Boolean) -> Unit) {
+ @WorkerThread
+ fun getScreenshot(output: File): Boolean {
if (!output.createFreshFile()) {
L.e { "Failed to create ${output.absolutePath} for debug screenshot" }
- return callback(false)
+ return false
}
- doAsync {
- var valid = true
- try {
- output.outputStream().use {
- drawingCache.compress(Bitmap.CompressFormat.PNG, 100, it)
- }
- L.d { "Created screenshot at ${output.absolutePath}" }
- } catch (e: Exception) {
- L.e { "An error occurred ${e.message}" }
- valid = false
- } finally {
- uiThread {
- callback(valid)
- }
+ return try {
+ output.outputStream().use {
+ drawingCache.compress(Bitmap.CompressFormat.PNG, 100, it)
}
+ L.d { "Created screenshot at ${output.absolutePath}" }
+ true
+ } catch (e: Exception) {
+ L.e { "An error occurred ${e.message}" }
+ false
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt
index 4afdd8d2..3c3c063a 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostChromeClients.kt
@@ -46,7 +46,8 @@ class FrostChromeClient(web: FrostWebView) : WebChromeClient() {
}
override fun onShowFileChooser(webView: WebView, filePathCallback: ValueCallback<Array<Uri>?>, fileChooserParams: FileChooserParams): Boolean {
- activity?.openFileChooser(filePathCallback, fileChooserParams) ?: webView.frostSnackbar(R.string.file_chooser_not_found)
+ activity?.openFileChooser(filePathCallback, fileChooserParams)
+ ?: webView.frostSnackbar(R.string.file_chooser_not_found)
return activity != null
}
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 0501e2e6..72e448b9 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/FrostRequestInterceptor.kt
@@ -46,15 +46,12 @@ val WebResourceRequest.isMedia: Boolean
* Generic filter passthrough
* If Resource is already nonnull, pass it, otherwise check if filter is met and override the response accordingly
*/
-fun WebResourceResponse?.filter(request: WebResourceRequest, filter: (url: String) -> Boolean)
- = filter(request.query { filter(it) })
+fun WebResourceResponse?.filter(request: WebResourceRequest, filter: (url: String) -> Boolean) = filter(request.query { filter(it) })
-fun WebResourceResponse?.filter(filter: Boolean): WebResourceResponse?
- = this ?: if (filter) blankResource else null
+fun WebResourceResponse?.filter(filter: Boolean): WebResourceResponse? = this
+ ?: if (filter) blankResource else null
-fun WebResourceResponse?.filterCss(request: WebResourceRequest): WebResourceResponse?
- = filter(request) { it.endsWith(".css") }
+fun WebResourceResponse?.filterCss(request: WebResourceRequest): WebResourceResponse? = filter(request) { it.endsWith(".css") }
-fun WebResourceResponse?.filterImage(request: WebResourceRequest): WebResourceResponse?
- = filter(request.isImage)
+fun WebResourceResponse?.filterImage(request: WebResourceRequest): WebResourceResponse? = filter(request.isImage)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/NestedWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/NestedWebView.kt
index 8fd4c0b4..297c4158 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/NestedWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/NestedWebView.kt
@@ -99,15 +99,11 @@ open class NestedWebView @JvmOverloads constructor(
final override fun hasNestedScrollingParent() = childHelper.hasNestedScrollingParent()
- final override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?)
- = childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
+ final override fun dispatchNestedScroll(dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, offsetInWindow: IntArray?) = childHelper.dispatchNestedScroll(dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, offsetInWindow)
- final override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?)
- = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
+ final override fun dispatchNestedPreScroll(dx: Int, dy: Int, consumed: IntArray?, offsetInWindow: IntArray?) = childHelper.dispatchNestedPreScroll(dx, dy, consumed, offsetInWindow)
- final override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean)
- = childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
+ final override fun dispatchNestedFling(velocityX: Float, velocityY: Float, consumed: Boolean) = childHelper.dispatchNestedFling(velocityX, velocityY, consumed)
- final override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float)
- = childHelper.dispatchNestedPreFling(velocityX, velocityY)
+ final override fun dispatchNestedPreFling(velocityX: Float, velocityY: Float) = childHelper.dispatchNestedPreFling(velocityX, velocityY)
} \ No newline at end of file
diff --git a/app/src/main/res/values/strings_pref_debug.xml b/app/src/main/res/values/strings_pref_debug.xml
index 771e5130..21e98311 100644
--- a/app/src/main/res/values/strings_pref_debug.xml
+++ b/app/src/main/res/values/strings_pref_debug.xml
@@ -2,6 +2,7 @@
<resources>
<string name="debug_toast_enabled">Debugging section is enabled! Go back to settings.</string>
+ <string name="debug_toast_already_enabled">Debugging section is already enabled. Go back to settings.</string>
<string name="debug_disclaimer_info">Though most private content is automatically removed in the report, some sensitive info may still be visible.
\nPlease have a look at the debug report before sending it.
diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml
index 7d479852..67461960 100644
--- a/app/src/main/res/xml/frost_changelog.xml
+++ b/app/src/main/res/xml/frost_changelog.xml
@@ -6,14 +6,16 @@
<item text="" />
-->
+ <version title="v1.8.3" />
+ <item text="Add full notification channel support" />
+ <item text="Fix sound spam for multiple notifications" />
+ <item text="" />
+ <item text="" />
+
<version title="v1.8.2" />
<item text="Fix duplicate notification sounds" />
<item text="Fix map redirecting to blackberry" />
<item text="Fix event reservation" />
- <item text="" />
- <item text="" />
- <item text="" />
- <item text="" />
<version title="v1.8.1" />
<item text="Theme new Facebook update" />
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt
index 9fbb6069..e30ec174 100644
--- a/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt
+++ b/app/src/test/kotlin/com/pitchedapps/frost/debugger/OfflineWebsiteTest.kt
@@ -15,7 +15,7 @@ class OfflineWebsiteTest {
fun basic() {
val countdown = CountDownLatch(1)
val buildPath = if (File(".").parentFile?.name == "app") "build/offline_test" else "app/build/offline_test"
- OfflineWebsite(FB_URL_BASE, COOKIE, File(buildPath))
+ OfflineWebsite(FB_URL_BASE, COOKIE, baseDir = File(buildPath))
.loadAndZip("test") {
println("Outcome $it")
countdown.countDown()
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/utils/StringEscapeUtilsTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/utils/StringEscapeUtilsTest.kt
new file mode 100644
index 00000000..86bd4ce5
--- /dev/null
+++ b/app/src/test/kotlin/com/pitchedapps/frost/utils/StringEscapeUtilsTest.kt
@@ -0,0 +1,16 @@
+package com.pitchedapps.frost.utils
+
+import org.junit.Test
+import kotlin.test.assertEquals
+
+/**
+ * Created by Allan Wang on 11/03/18.
+ */
+class StringEscapeUtilsTest {
+
+ @Test
+ fun utf() {
+ val escaped = "\\u003Chead&gt; color=\\\"#3b5998\\\""
+ assertEquals("<head> color=\"#3b5998\"", escaped.unescapeHtml())
+ }
+} \ No newline at end of file
diff --git a/docs/Changelog.md b/docs/Changelog.md
index d025fc5e..561fe768 100644
--- a/docs/Changelog.md
+++ b/docs/Changelog.md
@@ -1,5 +1,9 @@
# Changelog
+## v1.8.3
+* Add full notification channel support
+* Fix sound spam for multiple notifications
+
## v1.8.2
* Fix duplicate notification sounds
* Fix map redirecting to blackberry