aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-08-12 19:38:26 -0700
committerGitHub <noreply@github.com>2017-08-12 19:38:26 -0700
commit894c1c4d2c7568d26165baf0d9d192bdf3b288ef (patch)
tree1a9ae583970177ce7f4408a31d6cad29ad28dc52
parent209aa76264c796c9bd4485af3c43c615c2f3b12d (diff)
downloadfrost-894c1c4d2c7568d26165baf0d9d192bdf3b288ef.tar.gz
frost-894c1c4d2c7568d26165baf0d9d192bdf3b288ef.tar.bz2
frost-894c1c4d2c7568d26165baf0d9d192bdf3b288ef.zip
Feature/jsoup debugger (#152)v1.4.2.1
* Create debugger * Update debugger content * Create debugging logic * Finalize and test debugger * Add reload listener
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt20
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt16
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt156
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/JsoupCleaner.kt34
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt2
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/HeadlessHtmlExtractor.kt88
-rw-r--r--app/src/main/res/values/strings_pref_debug.xml18
-rw-r--r--app/src/main/res/values/strings_preferences.xml2
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/utils/JsoupCleanerTest.kt56
-rw-r--r--gradle.properties2
12 files changed, 390 insertions, 11 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
index 17ef972e..4fabf8b8 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/FrostApp.kt
@@ -77,11 +77,11 @@ class FrostApp : Application() {
L.d("Activity ${activity.localClassName} destroyed")
}
- override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
+ override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle?) {}
override fun onActivityStopped(activity: Activity) {}
- override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle) {
+ override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
L.d("Activity ${activity.localClassName} created")
}
})
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 4514eb10..304e4182 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/AboutActivity.kt
@@ -22,6 +22,7 @@ import com.mikepenz.google_material_typeface_library.GoogleMaterial
import com.mikepenz.iconics.typeface.IIcon
import com.pitchedapps.frost.BuildConfig
import com.pitchedapps.frost.R
+import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
@@ -62,6 +63,9 @@ class AboutActivity : AboutActivityBase(null, {
return l
}
+ var lastClick = -1L
+ var clickCount = 0
+
override fun postInflateMainPage(adapter: FastItemThemedAdapter<IItem<*, *>>) {
/**
* Frost may not be a library but we're conveying the same info
@@ -79,6 +83,22 @@ class AboutActivity : AboutActivityBase(null, {
}
}
adapter.add(LibraryIItem(frost)).add(AboutLinks())
+ adapter.withOnClickListener { _, _, item, _ ->
+ if (item is LibraryIItem) {
+ val now = System.currentTimeMillis()
+ if (now - lastClick > 500)
+ clickCount = 0
+ else
+ clickCount++
+ lastClick = now
+ if (clickCount == 7 && !Prefs.debugSettings) {
+ Prefs.debugSettings = true
+ L.d("Debugging section enabled")
+ toast(R.string.debug_toast_enabled)
+ }
+ }
+ false
+ }
}
class AboutLinks : AbstractItem<AboutLinks, AboutLinks.ViewHolder>(), ThemableIItem by ThemableIItemDelegate() {
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 3fd3e3b5..196aa461 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/SettingsActivity.kt
@@ -6,16 +6,12 @@ import android.content.Intent
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
-import ca.allanwang.kau.about.kauLaunchAbout
import ca.allanwang.kau.kpref.activity.CoreAttributeContract
import ca.allanwang.kau.kpref.activity.KPrefActivity
import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
import ca.allanwang.kau.kpref.activity.items.KPrefItemBase
import ca.allanwang.kau.ui.views.RippleCanvas
-import ca.allanwang.kau.utils.finishSlideOut
-import ca.allanwang.kau.utils.setMenuIcons
-import ca.allanwang.kau.utils.string
-import ca.allanwang.kau.utils.tint
+import ca.allanwang.kau.utils.*
import ca.allanwang.kau.xml.showChangelog
import com.mikepenz.community_material_typeface_library.CommunityMaterial
import com.mikepenz.google_material_typeface_library.GoogleMaterial
@@ -38,7 +34,7 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() {
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (!onActivityResultBilling(requestCode, resultCode, data))
super.onActivityResult(requestCode, resultCode, data)
- reload()
+ reloadList()
}
override fun kPrefCoreAttributes(): CoreAttributeContract.() -> Unit = {
@@ -86,7 +82,7 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() {
plainText(R.string.about_frost) {
descRes = R.string.about_frost_desc
iicon = GoogleMaterial.Icon.gmd_info
- onClick = { _, _, _ -> kauLaunchAbout(AboutActivity::class.java); true }
+ onClick = { _, _, _ -> startActivityForResult(AboutActivity::class.java, 9, true); true }
}
plainText(R.string.replay_intro) {
@@ -94,6 +90,12 @@ class SettingsActivity : KPrefActivity(), FrostBilling by IABSettings() {
onClick = { _, _, _ -> launchIntroActivity(cookies()); true }
}
+ subItems(R.string.debug_frost, getDebugPrefs()) {
+ descRes = R.string.debug_frost_desc
+ iicon = CommunityMaterial.Icon.cmd_android_debug_bridge
+ visible = { Prefs.debugSettings }
+ }
+
if (BuildConfig.DEBUG) {
checkbox(R.string.custom_pro, { Prefs.debugPro }, { Prefs.debugPro = it })
}
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 fae1846b..3fa03bcc 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/injectors/JsActions.kt
@@ -14,7 +14,8 @@ enum class JsActions(body: String) : InjectorContract {
* see [com.pitchedapps.frost.web.FrostJSI.loadLogin]
*/
LOGIN_CHECK("document.getElementById('signup-button')&&Frost.loadLogin();"),
- BASE_HREF("document.write(\"<base href='$FB_URL_BASE'/>\");"),
+ 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);"""),
/**
* Used as a pseudoinjector for maybe functions
*/
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt
new file mode 100644
index 00000000..d98a50c3
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/settings/Debug.kt
@@ -0,0 +1,156 @@
+package com.pitchedapps.frost.settings
+
+import android.content.Context
+import android.support.annotation.UiThread
+import ca.allanwang.kau.email.sendEmail
+import ca.allanwang.kau.kpref.activity.KPrefAdapterBuilder
+import ca.allanwang.kau.utils.string
+import com.afollestad.materialdialogs.MaterialDialog
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.activities.SettingsActivity
+import com.pitchedapps.frost.facebook.FACEBOOK_COM
+import com.pitchedapps.frost.facebook.FbCookie
+import com.pitchedapps.frost.facebook.FbTab
+import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
+import com.pitchedapps.frost.injectors.InjectorContract
+import com.pitchedapps.frost.injectors.JsActions
+import com.pitchedapps.frost.utils.L
+import com.pitchedapps.frost.utils.cleanHtml
+import com.pitchedapps.frost.utils.materialDialogThemed
+import com.pitchedapps.frost.web.launchHeadlessHtmlExtractor
+import com.pitchedapps.frost.web.query
+import io.reactivex.disposables.Disposable
+import org.jetbrains.anko.AnkoAsyncContext
+import org.jetbrains.anko.doAsync
+import org.jetbrains.anko.runOnUiThread
+import org.jetbrains.anko.uiThread
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Document
+
+/**
+ * Created by Allan Wang on 2017-06-30.
+ *
+ * A sub pref section that is enabled through a hidden preference
+ * Each category will load a page, extract the contents, remove private info, and create a report
+ */
+fun SettingsActivity.getDebugPrefs(): KPrefAdapterBuilder.() -> Unit = {
+
+ plainText(R.string.experimental_disclaimer) {
+ descRes = R.string.debug_disclaimer_info
+ }
+
+ Debugger.values().forEach {
+ plainText(it.data.titleId) {
+ iicon = it.data.icon
+ onClick = { itemView, _, _ -> it.debug(itemView.context); true }
+ }
+ }
+
+}
+
+private enum class Debugger(val data: FbTab, val injector: InjectorContract?, vararg query: String) {
+ NOTIFICATIONS(FbTab.NOTIFICATIONS, null, "#notifications_list"),
+ SEARCH(FbTab.SEARCH, JsActions.FETCH_BODY);
+
+ val query = if (query.isNotEmpty()) arrayOf(*query, "#root", "main", "body") else emptyArray()
+
+ fun debug(context: Context) {
+ val dialog = context.materialDialogThemed {
+ title("Debugging")
+ progress(true, 0)
+ canceledOnTouchOutside(false)
+ positiveText(R.string.kau_cancel)
+ onPositive { dialog, _ -> dialog.cancel() }
+ }
+ if (injector != null) dialog.extractHtml(injector)
+ else dialog.debugAsync {
+ loadJsoup()
+ }
+ }
+
+ fun MaterialDialog.debugAsync(task: AnkoAsyncContext<MaterialDialog>.() -> Unit) {
+ doAsync({ t: Throwable ->
+ val msg = t.message
+ L.e("Debugger failed: $msg")
+ context.runOnUiThread {
+ cancel()
+ context.materialDialogThemed {
+ title(R.string.debug_incomplete)
+ if (msg != null) content(msg)
+ }
+ }
+ }, task)
+ }
+
+ /**
+ * Wait for html to be returned from headless webview
+ *
+ * from [debug] to [simplifyJsoup] if [query] is not empty, or [createReport] otherwise
+ */
+ @UiThread
+ private fun MaterialDialog.extractHtml(injector: InjectorContract) {
+ setContent("Fetching webpage")
+ var disposable: Disposable? = null
+ setOnCancelListener { disposable?.dispose() }
+ context.launchHeadlessHtmlExtractor(data.url, injector) {
+ disposable = it.subscribe {
+ (html, errorRes) ->
+ debugAsync {
+ if (errorRes == -1) {
+ L.i("Debug report successful", html)
+ if (query.isNotEmpty()) simplifyJsoup(Jsoup.parseBodyFragment(html))
+ else createReport(html)
+ } else {
+ throw Throwable(context.string(errorRes))
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Get data directly from the link and search for our queries, returning the outerHTML
+ * of the first query found
+ *
+ * from [debug] to [simplifyJsoup]
+ */
+ private fun AnkoAsyncContext<MaterialDialog>.loadJsoup() {
+ uiThread {
+ it.setContent("Load Jsoup")
+ it.setOnCancelListener(null)
+ it.debugAsync {
+ val connection = Jsoup.connect(data.url).cookie(FACEBOOK_COM, FbCookie.webCookie).userAgent(USER_AGENT_BASIC)
+ val doc = connection.get()
+ simplifyJsoup(doc)
+ }
+ }
+ }
+
+ /**
+ * Takes snippet of given document that matches the first query in the [query] items
+ * before sending it to [createReport]
+ */
+ private fun AnkoAsyncContext<MaterialDialog>.simplifyJsoup(doc: Document) {
+ weakRef.get() ?: return
+ val q = query.first { doc.select(it).isNotEmpty() }
+ createReport(doc.select(q).outerHtml())
+ }
+
+ private fun AnkoAsyncContext<MaterialDialog>.createReport(html: String) {
+ uiThread {
+ it.context.materialDialogThemed {
+ title(R.string.debug_report_completed)
+ content(R.string.debug_report_completed_content)
+ positiveText(R.string.kau_yes)
+ negativeText(R.string.kau_no)
+ onPositive { dialog, _ ->
+ dialog.context.sendEmail(R.string.dev_email, R.string.kau_cancel) {
+ addItem("Query List", query.contentToString())
+ footer = html.cleanHtml()
+ }
+ }
+ }
+ it.dismiss()
+ }
+ }
+} \ No newline at end of file
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/JsoupCleaner.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/JsoupCleaner.kt
new file mode 100644
index 00000000..da8672f4
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/JsoupCleaner.kt
@@ -0,0 +1,34 @@
+package com.pitchedapps.frost.utils
+
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Attribute
+import org.jsoup.nodes.Element
+import org.jsoup.safety.Whitelist
+
+/**
+ * Created by Allan Wang on 2017-08-10.
+ *
+ * Parses html with Jsoup and cleans the data, emitting just the frame containing debugging info
+ *
+ * Removes text, removes unnecessary nodes
+ */
+fun String.cleanHtml() = cleanText().cleanJsoup()
+
+internal fun String.cleanText(): String = replace(Regex(">(?s).+?<"), "><")
+
+internal fun String.cleanJsoup(): String = Jsoup.clean(this, PrivacyWhitelist())
+
+class PrivacyWhitelist : Whitelist() {
+
+ val blacklistAttrs = arrayOf("style", "aria-label", "rel")
+ val blacklistTags = arrayOf("body", "html", "head", "i", "b", "u", "style", "script",
+ "br", "p", "span", "ul", "ol", "li")
+
+ override fun isSafeAttribute(tagName: String, el: Element, attr: Attribute): Boolean {
+ val key = attr.key
+ if (key == "href") attr.setValue("-")
+ return key !in blacklistAttrs
+ }
+
+ override fun isSafeTag(tag: String) = tag !in blacklistTags
+}
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 e52109c4..de759862 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Prefs.kt
@@ -130,4 +130,6 @@ object Prefs : KPref() {
var loadMediaOnMeteredNetwork: Boolean by kpref("media_on_metered_network", true)
+ var debugSettings: Boolean by kpref("debug_settings", false)
+
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/HeadlessHtmlExtractor.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/HeadlessHtmlExtractor.kt
new file mode 100644
index 00000000..a756aaa0
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/HeadlessHtmlExtractor.kt
@@ -0,0 +1,88 @@
+package com.pitchedapps.frost.web
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.webkit.JavascriptInterface
+import android.webkit.WebView
+import ca.allanwang.kau.utils.gone
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.facebook.USER_AGENT_BASIC
+import com.pitchedapps.frost.injectors.InjectorContract
+import com.pitchedapps.frost.utils.L
+import io.reactivex.Single
+import io.reactivex.SingleEmitter
+import io.reactivex.android.schedulers.AndroidSchedulers
+import io.reactivex.schedulers.Schedulers
+import org.jetbrains.anko.runOnUiThread
+import java.util.concurrent.TimeUnit
+
+/**
+ * Created by Allan Wang on 2017-08-12.
+ *
+ * Launches a headless html request and returns a result pair
+ * When successful, the pair will contain the html content and -1
+ * When unsuccessful, the pair will contain an empty string and a StringRes for the given error
+ *
+ * All errors are rerouted to success calls, so no exceptions should occur.
+ * The headless extractor will also destroy itself on cancellation or when the request is finished
+ */
+fun Context.launchHeadlessHtmlExtractor(url: String, injector: InjectorContract, action: (Single<Pair<String, Int>>) -> Unit) {
+ val single = Single.create<Pair<String, Int>> { e: SingleEmitter<Pair<String, Int>> ->
+ val extractor = HeadlessHtmlExtractor(this, url, injector, e)
+ e.setCancellable {
+ runOnUiThread { extractor.destroy() }
+ e.onSuccess("" to R.string.debug_request_cancelled)
+ }
+ }.subscribeOn(AndroidSchedulers.mainThread())
+ .timeout(20, TimeUnit.SECONDS, Schedulers.io(), { it.onSuccess("" to R.string.debug_request_timeout) })
+ .onErrorReturn { "" to R.string.debug_request_error }
+ action(single)
+}
+
+/**
+ * Given a link and some javascript, will load the link and load the JS on completion
+ * The JS is expected to call [HeadlessHtmlExtractor.HtmlJSI.handleHtml], which will be sent
+ * to the [emitter]
+ */
+@SuppressLint("ViewConstructor")
+private class HeadlessHtmlExtractor(
+ context: Context, url: String, val injector: InjectorContract, val emitter: SingleEmitter<Pair<String, Int>>
+) : WebView(context) {
+
+ val startTime = System.currentTimeMillis()
+
+ init {
+ L.v("Created HeadlessHtmlExtractor for $url")
+ gone()
+ setupWebview(url)
+ }
+
+ @SuppressLint("SetJavaScriptEnabled")
+ private fun setupWebview(url: String) {
+ settings.javaScriptEnabled = true
+ settings.userAgentString = USER_AGENT_BASIC
+ webViewClient = HeadlessWebViewClient(url, injector) // basic client that loads our JS once the page has loaded
+ webChromeClient = QuietChromeClient() // basic client that disables logging
+ addJavascriptInterface(HtmlJSI(), "Frost")
+ loadUrl(url)
+ }
+
+ inner class HtmlJSI {
+ @JavascriptInterface
+ fun handleHtml(html: String?) {
+ val time = System.currentTimeMillis() - startTime
+ emitter.onSuccess((html ?: "") to -1)
+ post {
+ L.d("HeadlessHtmlExtractor fetched $url in $time ms")
+ settings.javaScriptEnabled = false
+ settings.blockNetworkLoads = true
+ destroy()
+ }
+ }
+ }
+
+ override fun destroy() {
+ super.destroy()
+ L.d("HeadlessHtmlExtractor destroyed")
+ }
+} \ 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
new file mode 100644
index 00000000..fb52f2b4
--- /dev/null
+++ b/app/src/main/res/values/strings_pref_debug.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+ <string name="debug_toast_enabled">Debugging section is 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.
+ \n\nClicking one of the options below will prepare an email response with the web page data.
+ </string>
+
+ <string name="debug_incomplete">Incomplete report</string>
+ <string name="debug_request_error">An error occurred in the debugger.</string>
+ <string name="debug_request_cancelled">The request has been cancelled.</string>
+ <string name="debug_request_timeout">The request has timed out.</string>
+
+ <string name="debug_report_completed">Debug report completed</string>
+ <string name="debug_report_completed_content">Would you like to send the report?</string>
+</resources> \ No newline at end of file
diff --git a/app/src/main/res/values/strings_preferences.xml b/app/src/main/res/values/strings_preferences.xml
index 3cacf684..05335345 100644
--- a/app/src/main/res/values/strings_preferences.xml
+++ b/app/src/main/res/values/strings_preferences.xml
@@ -22,6 +22,8 @@
<string name="about_frost">About Frost for Facebook</string>
<string name="about_frost_desc">Version, Credits, and FAQs</string>
+ <string name="debug_frost">Frost Debugger</string>
+ <string name="debug_frost_desc">Send html data to help with debugging.</string>
<string name="replay_intro">Replay Introduction</string>
</resources> \ No newline at end of file
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/utils/JsoupCleanerTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/utils/JsoupCleanerTest.kt
new file mode 100644
index 00000000..4ec09ea6
--- /dev/null
+++ b/app/src/test/kotlin/com/pitchedapps/frost/utils/JsoupCleanerTest.kt
@@ -0,0 +1,56 @@
+package com.pitchedapps.frost.utils
+
+import org.junit.Test
+import kotlin.test.assertEquals
+
+/**
+ * Created by Allan Wang on 2017-08-10.
+ */
+class JsoupCleanerTest {
+
+ val whitespaceRegex = Regex("\\s+")
+
+ fun String.cleanWhitespace() = replace("\n", "").replace(whitespaceRegex, " ").replace("> <", "><")
+
+ private fun String.assertCleanHtml(expected: String) {
+ assertEquals(expected.cleanWhitespace(), cleanHtml().cleanWhitespace())
+ }
+
+ private fun String.assertCleanJsoup(expected: String) {
+ assertEquals(expected.cleanWhitespace(), cleanJsoup().cleanWhitespace())
+ }
+
+ private fun String.assertCleanText(expected: String) {
+ assertEquals(expected.cleanWhitespace(), cleanText().cleanWhitespace())
+ }
+
+ @Test
+ fun noChange() {
+ "<a><aa> HI </aa></a>".assertCleanJsoup("<a><aa> HI </aa></a>")
+ }
+
+ @Test
+ fun basicText() {
+ """<div class="test">Hello world</div>""".assertCleanHtml("""<div class="test"></div>""")
+ }
+
+ @Test
+ fun multiLineText() {
+ """<div class="test">Hello
+ world</div>""".assertCleanHtml("""<div class="test"></div>""")
+ }
+
+ @Test
+ fun textRemoval() {
+ """<div>Hello<a>World</a></div>""".assertCleanText("<div><a></a></div>")
+ }
+
+ @Test
+ fun kau() {
+ val html = """<div class="col s12 m6"> <div id="kau" class="card medium sticky-action"> <div class="card-image waves-effect waves-block waves-light"> <img class="activator" src="images/kau.jpg"> <span class="card-title activator background-gradient">KAU</span> </div><div class="card-content"><p>An extensive collection of Kotlin Android Utils</p></div><div class="card-action"> <a href="https://github.com/AllanWang/KAU" target="_blank" class="inline-block">Github</a> <a href="https://allanwang.github.io/KAU/" target="_blank" class="inline-block">Page</a> </div><div class="card-reveal"> <span class="card-title grey-text text-darken-4">KAU<i class="material-icons right">close</i></span> <ul class="browser-default"> <li>Huge package of one line extension functions</li><li>Custom UI views</li><li>Adapter items and animators</li><li>SearchView</li><li>Custom delegates</li></ul> </div></div></div>"""
+ val expected = """<div class="col s12 m6"><div id="kau" class="card medium sticky-action"><div class="card-image waves-effect waves-block waves-light"><img class="activator" src="images/kau.jpg"></div><div class="card-action"><a href="-" target="_blank" class="inline-block"></a><a href="-" target="_blank" class="inline-block"></a></div></div></div>"""
+ html.assertCleanHtml(expected)
+ }
+
+}
+
diff --git a/gradle.properties b/gradle.properties
index c71f8b82..59f1af52 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -17,7 +17,7 @@ MIN_SDK=21
TARGET_SDK=26
BUILD_TOOLS=26.0.1
-KAU=3.3.1
+KAU=6d9201e
KOTLIN=1.1.3-2
CRASHLYTICS=2.6.8