aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/build.gradle14
-rw-r--r--app/src/androidTest/kotlin/com/pitchedapps/frost/activities/ImageActivityTest.kt124
-rw-r--r--app/src/androidTest/kotlin/com/pitchedapps/frost/helper/Helper.kt32
-rw-r--r--app/src/androidTest/resources/bayer-pattern.jpgbin0 -> 6195 bytes
-rw-r--r--app/src/androidTest/resources/magenta.pngbin0 -> 165 bytes
-rw-r--r--app/src/main/AndroidManifest.xml5
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt237
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt79
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt17
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/BaseJobService.kt59
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt4
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt3
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/LocalService.kt90
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt92
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/utils/AdBlocker.kt7
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt20
-rw-r--r--app/src/main/res/values/strings_web_context.xml1
-rw-r--r--app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt13
18 files changed, 575 insertions, 222 deletions
diff --git a/app/build.gradle b/app/build.gradle
index 1b8feda7..a869c7bf 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -156,6 +156,7 @@ dependencies {
androidTestImplementation kauDependency.espresso
androidTestImplementation kauDependency.testRules
androidTestImplementation kauDependency.testRunner
+ androidTestImplementation "com.squareup.okhttp3:mockwebserver:${OKHTTP}"
testImplementation kauDependency.kotlinTest
testImplementation "org.jetbrains.kotlin:kotlin-reflect:${KOTLIN}"
@@ -180,6 +181,17 @@ dependencies {
//noinspection GradleDependency
implementation "ca.allanwang.kau:core-ui:$KAU"
+ // TODO temp
+ implementation "org.jetbrains.anko:anko-commons:0.10.8"
+
+// implementation "org.koin:koin-android:${KOIN}"
+// testImplementation "org.koin:koin-test:${KOIN}"
+// androidTestImplementation "org.koin:koin-test:${KOIN}"
+
+// androidTestImplementation "io.mockk:mockk:${MOCKK}"
+
+ implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:${COROUTINES}"
+
implementation "org.apache.commons:commons-text:${COMMONS_TEXT}"
implementation "com.devbrackets.android:exomedia:${EXOMEDIA}"
@@ -214,6 +226,8 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:${OKHTTP}"
implementation "com.squareup.okhttp3:logging-interceptor:${OKHTTP}"
+ androidTestImplementation "com.squareup.okhttp3:mockwebserver:${OKHTTP}"
+
implementation "co.zsmb:materialdrawer-kt:${MATERIAL_DRAWER_KT}"
diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/activities/ImageActivityTest.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/activities/ImageActivityTest.kt
new file mode 100644
index 00000000..23f6dab9
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/activities/ImageActivityTest.kt
@@ -0,0 +1,124 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.activities
+
+import android.content.Intent
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.rule.ActivityTestRule
+import com.pitchedapps.frost.helper.getResource
+import com.pitchedapps.frost.utils.ARG_COOKIE
+import com.pitchedapps.frost.utils.ARG_IMAGE_URL
+import com.pitchedapps.frost.utils.ARG_TEXT
+import com.pitchedapps.frost.utils.isIndirectImageUrl
+import okhttp3.mockwebserver.Dispatcher
+import okhttp3.mockwebserver.MockResponse
+import okhttp3.mockwebserver.MockWebServer
+import okhttp3.mockwebserver.RecordedRequest
+import okio.Buffer
+import okio.Okio
+import org.junit.Rule
+import org.junit.Test
+import org.junit.rules.Timeout
+import org.junit.runner.RunWith
+import kotlin.test.assertEquals
+import kotlin.test.assertFalse
+import kotlin.test.assertNull
+import kotlin.test.assertTrue
+
+@RunWith(AndroidJUnit4::class)
+class ImageActivityTest {
+
+ @get:Rule
+ val activity: ActivityTestRule<ImageActivity> = ActivityTestRule(ImageActivity::class.java, true, false)
+
+ @get:Rule
+ val globalTimeout: Timeout = Timeout.seconds(15)
+
+ private fun launchActivity(imageUrl: String, text: String? = null, cookie: String? = null) {
+ assertFalse(
+ imageUrl.isIndirectImageUrl,
+ "For simplicity, urls that are direct will be used without modifications in the production code."
+ )
+ val intent = Intent().apply {
+ putExtra(ARG_IMAGE_URL, imageUrl)
+ putExtra(ARG_TEXT, text)
+ putExtra(ARG_COOKIE, cookie)
+ }
+ activity.launchActivity(intent)
+ }
+
+ private val mockServer: MockWebServer by lazy {
+ val magentaImg = Buffer()
+ magentaImg.writeAll(Okio.source(getResource("bayer-pattern.jpg")))
+ MockWebServer().apply {
+ setDispatcher(object : Dispatcher() {
+ override fun dispatch(request: RecordedRequest): MockResponse =
+ when {
+ request.path.contains("text") -> MockResponse().setResponseCode(200).setBody("Valid mock text response")
+ request.path.contains("image") -> MockResponse().setResponseCode(200).setBody(magentaImg)
+ else -> MockResponse().setResponseCode(404).setBody("Error mock response")
+ }
+ })
+ start()
+ }
+ }
+
+ @Test
+ fun validImageTest() {
+ launchActivity(mockServer.url("image").toString())
+ mockServer.takeRequest()
+ with(activity.activity) {
+ assertEquals(1, mockServer.requestCount, "One http request expected")
+ assertEquals(fabAction, FabStates.DOWNLOAD, "Image should be successful, image should be downloaded")
+ assertTrue(tempFile.exists(), "Image should be located at temp file")
+ assertTrue(
+ System.currentTimeMillis() - tempFile.lastModified() < 2000L,
+ "Image should have been modified within the last few seconds"
+ )
+ assertNull(errorRef, "No error should exist")
+ tempFile.delete()
+ }
+ }
+
+ @Test
+ fun invalidImageTest() {
+ launchActivity(mockServer.url("text").toString())
+ mockServer.takeRequest()
+ with(activity.activity) {
+ assertEquals(1, mockServer.requestCount, "One http request expected")
+ assertEquals(fabAction, FabStates.ERROR, "Text should not be a valid image format, error state expected")
+ assertEquals("Image format not supported", errorRef?.message, "Error message mismatch")
+ assertFalse(tempFile.exists(), "Temp file should have been removed")
+ }
+ }
+
+ @Test
+ fun errorTest() {
+ launchActivity(mockServer.url("error").toString())
+ mockServer.takeRequest()
+ with(activity.activity) {
+ assertEquals(1, mockServer.requestCount, "One http request expected")
+ assertEquals(fabAction, FabStates.ERROR, "Error response code, error state expected")
+ assertEquals(
+ "Unsuccessful response for image: Error mock response",
+ errorRef?.message,
+ "Error message mismatch"
+ )
+ assertFalse(tempFile.exists(), "Temp file should have been removed")
+ }
+ }
+}
diff --git a/app/src/androidTest/kotlin/com/pitchedapps/frost/helper/Helper.kt b/app/src/androidTest/kotlin/com/pitchedapps/frost/helper/Helper.kt
new file mode 100644
index 00000000..f7484cb3
--- /dev/null
+++ b/app/src/androidTest/kotlin/com/pitchedapps/frost/helper/Helper.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.helper
+
+import android.content.Context
+import androidx.test.platform.app.InstrumentationRegistry
+import java.io.InputStream
+
+val context: Context
+ get() = InstrumentationRegistry.getInstrumentation().targetContext
+
+fun getAsset(asset: String): InputStream =
+ context.assets.open(asset)
+
+private class Helper
+
+fun getResource(resource: String): InputStream =
+ Helper::class.java.classLoader!!.getResource(resource).openStream()
diff --git a/app/src/androidTest/resources/bayer-pattern.jpg b/app/src/androidTest/resources/bayer-pattern.jpg
new file mode 100644
index 00000000..672ac178
--- /dev/null
+++ b/app/src/androidTest/resources/bayer-pattern.jpg
Binary files differ
diff --git a/app/src/androidTest/resources/magenta.png b/app/src/androidTest/resources/magenta.png
new file mode 100644
index 00000000..14afbce8
--- /dev/null
+++ b/app/src/androidTest/resources/magenta.png
Binary files differ
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ed197510..213da26b 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -146,6 +146,11 @@
android:enabled="true"
android:label="@string/frost_requests"
android:permission="android.permission.BIND_JOB_SERVICE" />
+ <service
+ android:name=".services.LocalService"
+ android:enabled="true"
+ android:label="@string/local_service_name"
+ android:permission="android.permission.BIND_JOB_SERVICE" />
<receiver
android:name=".services.UpdateReceiver"
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 83f617ba..a5b90b09 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt
@@ -16,6 +16,7 @@
*/
package com.pitchedapps.frost.activities
+import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Color
@@ -28,13 +29,14 @@ 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.colorToForeground
+import ca.allanwang.kau.utils.copyFromInputStream
import ca.allanwang.kau.utils.fadeOut
import ca.allanwang.kau.utils.fadeScaleTransition
import ca.allanwang.kau.utils.isHidden
+import ca.allanwang.kau.utils.isVisible
import ca.allanwang.kau.utils.scaleXY
import ca.allanwang.kau.utils.setIcon
import ca.allanwang.kau.utils.tint
-import ca.allanwang.kau.utils.use
import ca.allanwang.kau.utils.withAlpha
import ca.allanwang.kau.utils.withMinAlpha
import com.davemorrissey.labs.subscaleview.ImageSource
@@ -48,12 +50,12 @@ import com.pitchedapps.frost.facebook.get
import com.pitchedapps.frost.facebook.requests.call
import com.pitchedapps.frost.facebook.requests.getFullSizedImageUrl
import com.pitchedapps.frost.facebook.requests.requestBuilder
+import com.pitchedapps.frost.services.LocalService
import com.pitchedapps.frost.utils.ARG_COOKIE
import com.pitchedapps.frost.utils.ARG_IMAGE_URL
import com.pitchedapps.frost.utils.ARG_TEXT
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
-import com.pitchedapps.frost.utils.createFreshFile
import com.pitchedapps.frost.utils.frostSnackbar
import com.pitchedapps.frost.utils.frostUriFromFile
import com.pitchedapps.frost.utils.isIndirectImageUrl
@@ -63,12 +65,13 @@ import com.pitchedapps.frost.utils.sendFrostEmail
import com.pitchedapps.frost.utils.setFrostColors
import com.sothree.slidinguppanel.SlidingUpPanelLayout
import kotlinx.android.synthetic.main.activity_image.*
-import okhttp3.Response
-import org.jetbrains.anko.activityUiThreadWithContext
-import org.jetbrains.anko.doAsync
-import org.jetbrains.anko.uiThread
+import kotlinx.coroutines.CoroutineExceptionHandler
+import kotlinx.coroutines.Deferred
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.async
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import java.io.File
-import java.io.FileFilter
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
@@ -79,14 +82,13 @@ import java.util.Locale
*/
class ImageActivity : KauBaseActivity() {
+ @Volatile
internal var errorRef: Throwable? = null
- private lateinit var tempDir: File
-
/**
* Reference to the temporary file path
*/
- private lateinit var tempFile: File
+ internal lateinit var tempFile: File
/**
* Reference to path for downloaded image
* Nonnull once the image is downloaded by the user
@@ -94,13 +96,12 @@ 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
- runOnUiThread { value.update(image_fab) }
+ value.update(image_fab)
}
companion object {
@@ -112,21 +113,18 @@ 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
+ const val PURGE_TIME: Long = 10 * 60 * 1000 // 10 min block
private val L = KauLoggerExtension("Image", com.pitchedapps.frost.utils.L)
+
+ fun cacheDir(context: Context): File =
+ File(context.cacheDir, IMAGE_FOLDER)
}
private val cookie: String? by lazy { intent.getStringExtra(ARG_COOKIE) }
val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') }
- private val trueImageUrl: String by lazy {
- val result = if (!imageUrl.isIndirectImageUrl) imageUrl
- else cookie?.getFullSizedImageUrl(imageUrl)?.blockingGet() ?: imageUrl
- if (result != imageUrl)
- L.v { "Launching with true url $result" }
- result
- }
+ private lateinit var trueImageUrl: Deferred<String>
private val imageText: String? by lazy { intent.getStringExtra(ARG_TEXT) }
@@ -138,11 +136,27 @@ class ImageActivity : KauBaseActivity() {
)}_${Math.abs(imageUrl.hashCode())}"
}
+ private fun loadError(e: Throwable) {
+ errorRef = e
+ e.logFrostEvent("Image load error")
+ if (image_progress.isVisible)
+ image_progress.fadeOut()
+ tempFile.delete()
+ fabAction = FabStates.ERROR
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
intent?.extras ?: return finish()
L.i { "Displaying image" }
- L.v { "Displaying image $imageUrl" }
+ trueImageUrl = async(Dispatchers.IO) {
+ val result = if (!imageUrl.isIndirectImageUrl) imageUrl
+ else cookie?.getFullSizedImageUrl(imageUrl)?.blockingGet() ?: imageUrl
+ if (result != imageUrl)
+ L.v { "Launching with true url $result" }
+ result
+ }
+
val layout = if (!imageText.isNullOrBlank()) R.layout.activity_image else R.layout.activity_image_textless
setContentView(layout)
image_container.setBackgroundColor(
@@ -165,82 +179,23 @@ class ImageActivity : KauBaseActivity() {
})
image_fab.setOnClickListener { fabAction.onClick(this) }
image_photo.setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() {
- override fun onImageLoadError(e: Exception?) {
- errorRef = e
- e.logFrostEvent("Image load error")
- L.e { "Failed to load image $imageUrl" }
- tempFile?.delete()
- fabAction = FabStates.ERROR
+ override fun onImageLoadError(e: Exception) {
+ loadError(e)
}
})
setFrostColors {
themeWindow = false
}
- tempDir = File(cacheDir, IMAGE_FOLDER)
- tempFile = File(tempDir, imageHash)
- doAsync({
- L.e(it) { "Failed to load image $imageHash" }
- errorRef = it
- runOnUiThread { image_progress.fadeOut() }
- tempFile.delete()
- fabAction = FabStates.ERROR
- }) {
- val loaded = loadImage(tempFile)
- uiThread {
- image_progress.fadeOut()
- if (!loaded) {
- fabAction = FabStates.ERROR
- } else {
- image_photo.setImage(ImageSource.uri(frostUriFromFile(tempFile)))
- fabAction = FabStates.DOWNLOAD
- image_photo.animate().alpha(1f).scaleXY(1f).start()
- }
- }
+ tempFile = File(cacheDir(this), imageHash)
+ launch(CoroutineExceptionHandler { _, throwable -> loadError(throwable) }) {
+ downloadImageTo(tempFile)
+ image_progress.fadeOut()
+ image_photo.setImage(ImageSource.uri(frostUriFromFile(tempFile)))
+ fabAction = FabStates.DOWNLOAD
+ image_photo.animate().alpha(1f).scaleXY(1f).start()
}
}
- /**
- * Attempts to load the image to [file]
- * Returns true if successful
- * Note that this is a long execution and should not be done on the UI thread
- */
- private fun loadImage(file: File): Boolean {
- if (file.exists() && file.length() > 1) {
- file.setLastModified(System.currentTimeMillis())
- L.d { "Loading from local cache ${file.absolutePath}" }
- return true
- }
- val response = getImageResponse()
-
- if (!response.isSuccessful) {
- L.e { "Unsuccessful response for image" }
- errorRef = Throwable("Unsuccessful response for image")
- return false
- }
-
- if (!file.createFreshFile()) {
- L.e { "Could not create temp file" }
- return false
- }
-
- var valid = false
-
- response.body()?.byteStream()?.use { input ->
- file.outputStream().use { output ->
- input.copyTo(output)
- valid = true
- }
- }
-
- if (!valid) {
- L.e { "Failed to copy file" }
- file.delete()
- return false
- }
-
- return true
- }
-
@Throws(IOException::class)
private fun createPublicMediaFile(): File {
val timeStamp = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()).format(Date())
@@ -251,20 +206,56 @@ class ImageActivity : KauBaseActivity() {
return File.createTempFile(imageFileName, IMG_EXTENSION, frostDir)
}
- private fun getImageResponse(): Response = cookie.requestBuilder()
- .url(trueImageUrl)
- .get()
- .call()
- .execute()
-
+ /**
+ * Saves the image to the specified file, creating it if it doesn't exist.
+ * Returns true if a change is made, false otherwise.
+ * Throws an error if something goes wrong.
+ */
@Throws(IOException::class)
- private fun downloadImageTo(file: File) {
- val body = getImageResponse().body()
- ?: throw IOException("Failed to retrieve image body")
- body.byteStream().use { input ->
- file.outputStream().use { output ->
- input.copyTo(output)
+ private suspend fun downloadImageTo(file: File): Boolean {
+ val exceptionHandler = CoroutineExceptionHandler { _, err ->
+ if (file.isFile && file.length() == 0L) {
+ file.delete()
}
+ throw err
+ }
+ return withContext(Dispatchers.IO + exceptionHandler) {
+ if (!file.isFile) {
+ file.parentFile.mkdirs()
+ file.createNewFile()
+ } else {
+ file.setLastModified(System.currentTimeMillis())
+ }
+
+ // Forbid overwrites
+ if (file.length() > 0) {
+ L.i { "Forbid image overwrite" }
+ return@withContext false
+ }
+ if (tempFile.isFile && tempFile.length() > 0) {
+ if (tempFile == file) {
+ return@withContext false
+ }
+ tempFile.copyTo(file)
+ return@withContext true
+ }
+
+ // No temp file, download ourselves
+ val response = cookie.requestBuilder()
+ .url(trueImageUrl.await())
+ .get()
+ .call()
+ .execute()
+
+ if (!response.isSuccessful) {
+ throw IOException("Unsuccessful response for image: ${response.peekBody(128).string()}")
+ }
+
+ val body = response.body() ?: throw IOException("Failed to retrieve image body")
+
+ file.copyFromInputStream(body.byteStream())
+
+ return@withContext true
}
}
@@ -272,45 +263,25 @@ class ImageActivity : KauBaseActivity() {
kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ ->
L.d { "Download image callback granted: $granted" }
if (granted) {
- doAsync {
+ val errorHandler = CoroutineExceptionHandler { _, throwable ->
+ loadError(throwable)
+ frostSnackbar(R.string.image_download_fail)
+ }
+ launch(errorHandler) {
val destination = createPublicMediaFile()
- var success = true
- try {
- val temp = tempFile
- if (temp != null)
- temp.copyTo(destination, true)
- else
- downloadImageTo(destination)
- } catch (e: Exception) {
- errorRef = e
- success = false
- } finally {
- L.d { "Download image async finished: $success" }
- if (success) {
- scanMedia(destination)
- savedFile = destination
- } else {
- try {
- destination.delete()
- } catch (ignore: Exception) {
- }
- }
- activityUiThreadWithContext {
- val text = if (success) R.string.image_download_success else R.string.image_download_fail
- frostSnackbar(text)
- if (success) fabAction = FabStates.SHARE
- }
- }
+ downloadImageTo(destination)
+ L.d { "Download image async finished" }
+ scanMedia(destination)
+ savedFile = destination
+ frostSnackbar(R.string.image_download_success)
+ fabAction = FabStates.SHARE
}
}
}
}
override fun onDestroy() {
- val purge = System.currentTimeMillis() - PURGE_TIME
- tempDir.listFiles(FileFilter { it.isFile && it.lastModified() < purge })?.forEach {
- it.delete()
- }
+ LocalService.schedule(this, LocalService.Flag.PURGE_IMAGE)
super.onDestroy()
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt
index c6583712..47c0ecc4 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/facebook/FbCookie.kt
@@ -30,50 +30,83 @@ import com.pitchedapps.frost.utils.launchLogin
import io.reactivex.Observable
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.subjects.SingleSubject
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
+import kotlin.coroutines.resume
+import kotlin.coroutines.suspendCoroutine
/**
* Created by Allan Wang on 2017-05-30.
*/
object FbCookie {
+ const val COOKIE_DOMAIN = FACEBOOK_COM
+
/**
* Retrieves the facebook cookie if it exists
* Note that this is a synchronized call
*/
inline val webCookie: String?
- get() = CookieManager.getInstance().getCookie(FB_URL_BASE)
+ get() = CookieManager.getInstance().getCookie(COOKIE_DOMAIN)
- private fun setWebCookie(cookie: String?, callback: (() -> Unit)?) {
- with(CookieManager.getInstance()) {
- removeAllCookies { _ ->
- if (cookie == null) {
+ private fun CookieManager.setWebCookie(cookie: String?, callback: (() -> Unit)?) {
+ removeAllCookies { _ ->
+ if (cookie == null) {
+ callback?.invoke()
+ return@removeAllCookies
+ }
+ L.d { "Setting cookie" }
+ val cookies = cookie.split(";").map { Pair(it, SingleSubject.create<Boolean>()) }
+ cookies.forEach { (cookie, callback) -> setCookie(COOKIE_DOMAIN, cookie) { callback.onSuccess(it) } }
+ Observable.zip<Boolean, Unit>(cookies.map { (_, callback) -> callback.toObservable() }) {}
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe {
callback?.invoke()
- return@removeAllCookies
+ L.d { "Cookies set" }
+ L._d { cookie }
+ flush()
}
- L.d { "Setting cookie" }
- val cookies = cookie.split(";").map { Pair(it, SingleSubject.create<Boolean>()) }
- cookies.forEach { (cookie, callback) -> setCookie(FB_URL_BASE, cookie) { callback.onSuccess(it) } }
- Observable.zip<Boolean, Unit>(cookies.map { (_, callback) -> callback.toObservable() }) {}
- .observeOn(AndroidSchedulers.mainThread())
- .subscribe {
- callback?.invoke()
- L.d { "Cookies set" }
- L._d { cookie }
- flush()
- }
- }
+ }
+ }
+
+ private suspend fun CookieManager.suspendSetWebCookie(cookie: String?): Boolean {
+ cookie ?: return true
+ L.test { "Orig $webCookie" }
+ removeAllCookies()
+ L.test { "Save $cookie" }
+ // Save all cookies regardless of result, then check if all succeeded
+ val result = cookie.split(";").map { setSingleWebCookie(it) }.all { it }
+ L.test { "AAAA $webCookie" }
+ flush()
+ L.test { "SSSS $webCookie" }
+ return result
+ }
+
+ private suspend fun CookieManager.removeAllCookies(): Boolean = suspendCoroutine { cont ->
+ removeAllCookies {
+ L.test { "Removed all cookies $webCookie" }
+ cont.resume(it)
+ }
+ }
+
+ private suspend fun CookieManager.setSingleWebCookie(cookie: String): Boolean = suspendCoroutine { cont ->
+ setCookie(COOKIE_DOMAIN, cookie.trim()) {
+ L.test { "Save single $cookie\n\n\t$webCookie" }
+ cont.resume(it)
}
}
operator fun invoke() {
L.d { "FbCookie Invoke User" }
- with(CookieManager.getInstance()) {
- setAcceptCookie(true)
- }
+ val manager = CookieManager.getInstance()
+ manager.setAcceptCookie(true)
val dbCookie = loadFbCookie(Prefs.userId)?.cookie
if (dbCookie != null && webCookie == null) {
L.d { "DbCookie found & WebCookie is null; setting webcookie" }
- setWebCookie(dbCookie, null)
+ GlobalScope.launch(Dispatchers.Main) {
+ manager.suspendSetWebCookie(dbCookie)
+ }
}
}
@@ -107,7 +140,7 @@ object FbCookie {
}
L.d { "Switching User" }
Prefs.userId = cookie.id
- setWebCookie(cookie.cookie, callback)
+ CookieManager.getInstance().setWebCookie(cookie.cookie, callback)
}
/**
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
index 98e28bd3..2c46edbc 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/fragments/FragmentBase.kt
@@ -41,6 +41,11 @@ import com.pitchedapps.frost.utils.REQUEST_TEXT_ZOOM
import com.pitchedapps.frost.utils.frostEvent
import io.reactivex.android.schedulers.AndroidSchedulers
import io.reactivex.disposables.Disposable
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.SupervisorJob
+import kotlin.coroutines.CoroutineContext
/**
* Created by Allan Wang on 2017-11-07.
@@ -48,7 +53,7 @@ import io.reactivex.disposables.Disposable
* All fragments pertaining to the main view
* Must be attached to activities implementing [MainActivityContract]
*/
-abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
+abstract class BaseFragment : Fragment(), CoroutineScope, FragmentContract, DynamicUiContract {
companion object {
private const val ARG_POSITION = "arg_position"
@@ -71,6 +76,10 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
}
}
+ open lateinit var job: Job
+ override val coroutineContext: CoroutineContext
+ get() = Dispatchers.Main + job
+
override val baseUrl: String by lazy { arguments!!.getString(ARG_URL) }
override val baseEnum: FbItem by lazy { FbItem[arguments]!! }
override val position: Int by lazy { arguments!!.getInt(ARG_POSITION) }
@@ -98,6 +107,7 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ job = SupervisorJob()
firstLoad = true
if (context !is MainActivityContract)
throw IllegalArgumentException("${this::class.java.simpleName} is not attached to a context implementing MainActivityContract")
@@ -207,6 +217,11 @@ abstract class BaseFragment : Fragment(), FragmentContract, DynamicUiContract {
super.onDestroyView()
}
+ override fun onDestroy() {
+ job.cancel()
+ super.onDestroy()
+ }
+
override fun reloadTheme() {
reloadThemeSelf()
content?.reloadTextSize()
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/BaseJobService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/BaseJobService.kt
new file mode 100644
index 00000000..3cc7deaf
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/BaseJobService.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.services
+
+import android.app.job.JobParameters
+import android.app.job.JobService
+import androidx.annotation.CallSuper
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.Job
+import kotlin.coroutines.CoroutineContext
+
+abstract class BaseJobService : JobService(), CoroutineScope {
+
+ private lateinit var job: Job
+ override val coroutineContext: CoroutineContext
+ get() = Dispatchers.Main + job
+
+ protected val startTime = System.currentTimeMillis()
+
+ /**
+ * Note that if a job plans on running asynchronously, it should return true
+ */
+ @CallSuper
+ override fun onStartJob(params: JobParameters?): Boolean {
+ job = Job()
+ return false
+ }
+
+ @CallSuper
+ override fun onStopJob(params: JobParameters?): Boolean {
+ job.cancel()
+ return false
+ }
+}
+
+/*
+ * Collection of ids for job services.
+ * These should all be unique
+ */
+
+const val NOTIFICATION_JOB_NOW = 6
+const val NOTIFICATION_PERIODIC_JOB = 7
+const val LOCAL_SERVICE_BASE = 110
+const val REQUEST_SERVICE_BASE = 220
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 ee515a55..d036d3a8 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostNotifications.kt
@@ -274,12 +274,8 @@ data class FrostNotification(
NotificationManagerCompat.from(context).notify(tag, id, notif.build())
}
-const val NOTIFICATION_PERIODIC_JOB = 7
-
fun Context.scheduleNotifications(minutes: Long): Boolean =
scheduleJob<NotificationService>(NOTIFICATION_PERIODIC_JOB, minutes)
-const val NOTIFICATION_JOB_NOW = 6
-
fun Context.fetchNotifications(): Boolean =
fetchJob<NotificationService>(NOTIFICATION_JOB_NOW)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt
index 22acc9fb..d41f0b3c 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/FrostRequestService.kt
@@ -82,7 +82,6 @@ private const val ARG_0 = "frost_request_arg_0"
private const val ARG_1 = "frost_request_arg_1"
private const val ARG_2 = "frost_request_arg_2"
private const val ARG_3 = "frost_request_arg_3"
-private const val JOB_REQUEST_BASE = 928
private fun BaseBundle.getCookie() = getString(ARG_COOKIE)
private fun BaseBundle.putCookie(cookie: String) = putString(ARG_COOKIE, cookie)
@@ -145,7 +144,7 @@ object FrostRunnable {
return false
}
- val builder = JobInfo.Builder(JOB_REQUEST_BASE + command.ordinal, serviceComponent)
+ val builder = JobInfo.Builder(REQUEST_SERVICE_BASE + command.ordinal, serviceComponent)
.setMinimumLatency(0L)
.setExtras(bundle)
.setOverrideDeadline(2000L)
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/services/LocalService.kt b/app/src/main/kotlin/com/pitchedapps/frost/services/LocalService.kt
new file mode 100644
index 00000000..3d66f1ee
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/LocalService.kt
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2018 Allan Wang
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+package com.pitchedapps.frost.services
+
+import android.app.job.JobInfo
+import android.app.job.JobParameters
+import android.app.job.JobScheduler
+import android.content.ComponentName
+import android.content.Context
+import android.os.PersistableBundle
+import com.pitchedapps.frost.activities.ImageActivity
+import com.pitchedapps.frost.utils.L
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import java.io.FileFilter
+
+class LocalService : BaseJobService() {
+
+ enum class Flag {
+ PURGE_IMAGE
+ }
+
+ companion object {
+ private const val FLAG = "extra_local_flag"
+
+ /**
+ * Launches a local service with the provided flag
+ */
+ fun schedule(context: Context, flag: Flag): Boolean {
+ val scheduler = context.getSystemService(Context.JOB_SCHEDULER_SERVICE) as JobScheduler
+ val serviceComponent = ComponentName(context, LocalService::class.java)
+ val bundle = PersistableBundle()
+ bundle.putString(FLAG, flag.name)
+
+ val builder = JobInfo.Builder(LOCAL_SERVICE_BASE + flag.ordinal, serviceComponent)
+ .setMinimumLatency(0L)
+ .setExtras(bundle)
+ .setOverrideDeadline(2000L)
+
+ val result = scheduler.schedule(builder.build())
+ if (result <= 0) {
+ L.eThrow("FrostRequestService scheduler failed for ${flag.name}")
+ return false
+ }
+ L.d { "Scheduled ${flag.name}" }
+ return true
+ }
+ }
+
+ override fun onStartJob(params: JobParameters?): Boolean {
+ super.onStartJob(params)
+ val flagString = params?.extras?.getString(FLAG)
+ val flag: Flag = try {
+ Flag.valueOf(flagString!!)
+ } catch (e: Exception) {
+ L.e { "Local service with invalid flag $flagString" }
+ return true
+ }
+ launch {
+ when (flag) {
+ Flag.PURGE_IMAGE -> purgeImages()
+ }
+ }
+ return false
+ }
+
+ private suspend fun purgeImages() {
+ withContext(Dispatchers.IO) {
+ val purge = System.currentTimeMillis() - ImageActivity.PURGE_TIME
+ ImageActivity.cacheDir(this@LocalService)
+ .listFiles(FileFilter { it.isFile && it.lastModified() < purge })
+ ?.forEach { it.delete() }
+ }
+ }
+}
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 4ede5163..7360c191 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/services/NotificationService.kt
@@ -17,7 +17,6 @@
package com.pitchedapps.frost.services
import android.app.job.JobParameters
-import android.app.job.JobService
import androidx.core.app.NotificationManagerCompat
import ca.allanwang.kau.utils.string
import com.pitchedapps.frost.BuildConfig
@@ -27,8 +26,10 @@ import com.pitchedapps.frost.dbflow.loadFbCookiesSync
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostEvent
-import org.jetbrains.anko.doAsync
-import java.util.concurrent.Future
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
/**
* Created by Allan Wang on 2017-06-14.
@@ -38,68 +39,69 @@ import java.util.concurrent.Future
*
* All fetching is done through parsers
*/
-class NotificationService : JobService() {
-
- private var future: Future<Unit>? = null
-
- private val startTime = System.currentTimeMillis()
+class NotificationService : BaseJobService() {
override fun onStopJob(params: JobParameters?): Boolean {
- val time = System.currentTimeMillis() - startTime
- L.d { "Notification service has finished abruptly in $time ms" }
- frostEvent(
- "NotificationTime",
- "Type" to "Service force stop",
- "IM Included" to Prefs.notificationsInstantMessages,
- "Duration" to time
- )
- future?.cancel(true)
- future = null
+ super.onStopJob(params)
+ prepareFinish(true)
return false
}
- fun finish(params: JobParameters?) {
+ private var preparedFinish = false
+
+ private fun prepareFinish(abrupt: Boolean) {
+ if (preparedFinish)
+ return
+ preparedFinish = true
val time = System.currentTimeMillis() - startTime
- L.i { "Notification service has finished in $time ms" }
+ L.i { "Notification service has ${if (abrupt) "finished abruptly" else "finished"} in $time ms" }
frostEvent(
"NotificationTime",
- "Type" to "Service",
+ "Type" to (if (abrupt) "Service force stop" else "Service"),
"IM Included" to Prefs.notificationsInstantMessages,
"Duration" to time
)
- jobFinished(params, false)
- future?.cancel(true)
- future = null
}
override fun onStartJob(params: JobParameters?): Boolean {
+ super.onStartJob(params)
L.i { "Fetching notifications" }
- future = doAsync {
- val currentId = Prefs.userId
- val cookies = loadFbCookiesSync()
- val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
- var notifCount = 0
- cookies.forEach {
- val current = it.id == currentId
- if (Prefs.notificationsGeneral &&
- (current || Prefs.notificationAllAccounts)
- )
- notifCount += fetch(jobId, NotificationType.GENERAL, it)
- if (Prefs.notificationsInstantMessages &&
- (current || Prefs.notificationsImAllAccounts)
- )
- notifCount += fetch(jobId, NotificationType.MESSAGE, it)
+ launch {
+ try {
+ sendNotifications(params)
+ } finally {
+ if (!isActive)
+ prepareFinish(false)
+ jobFinished(params, false)
}
-
- L.i { "Sent $notifCount notifications" }
- if (notifCount == 0 && jobId == NOTIFICATION_JOB_NOW)
- generalNotification(665, R.string.no_new_notifications, BuildConfig.DEBUG)
-
- finish(params)
}
return true
}
+ private suspend fun sendNotifications(params: JobParameters?): Unit = withContext(Dispatchers.Default) {
+ val currentId = Prefs.userId
+ val cookies = loadFbCookiesSync()
+ if (!isActive) return@withContext
+ val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
+ var notifCount = 0
+ for (cookie in cookies) {
+ if (!isActive) break
+ val current = cookie.id == currentId
+ if (Prefs.notificationsGeneral &&
+ (current || Prefs.notificationAllAccounts)
+ )
+ notifCount += fetch(jobId, NotificationType.GENERAL, cookie)
+ if (Prefs.notificationsInstantMessages &&
+ (current || Prefs.notificationsImAllAccounts)
+ )
+ notifCount += fetch(jobId, NotificationType.MESSAGE, cookie)
+ }
+
+ L.i { "Sent $notifCount notifications" }
+ if (notifCount == 0 && jobId == NOTIFICATION_JOB_NOW)
+ generalNotification(665, R.string.no_new_notifications, BuildConfig.DEBUG)
+ }
+
/**
* Implemented fetch to also notify when an error occurs
* Also normalized the output to return the number of notifications received
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/AdBlocker.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/AdBlocker.kt
index 61a90024..d14c6cd3 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/utils/AdBlocker.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/AdBlocker.kt
@@ -19,8 +19,9 @@ package com.pitchedapps.frost.utils
import android.content.Context
import android.text.TextUtils
import ca.allanwang.kau.utils.use
+import kotlinx.coroutines.GlobalScope
+import kotlinx.coroutines.launch
import okhttp3.HttpUrl
-import org.jetbrains.anko.doAsync
/**
* Created by Allan Wang on 2017-09-24.
@@ -38,7 +39,7 @@ open class AdBlocker(val assetPath: String) {
val data: MutableSet<String> = mutableSetOf()
fun init(context: Context) {
- doAsync {
+ GlobalScope.launch {
val content = context.assets.open(assetPath).bufferedReader().use { f ->
f.readLines().filter { !it.startsWith("#") }
}
@@ -58,7 +59,7 @@ open class AdBlocker(val assetPath: String) {
return false
val index = host.indexOf(".")
if (index < 0 || index + 1 < host.length) return false
- if (host.contains(host)) return true
+ if (data.contains(host)) return true
return isAdHost(host.substring(index + 1))
}
}
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
index 392cb353..2fe78f02 100644
--- a/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
+++ b/app/src/main/kotlin/com/pitchedapps/frost/web/LoginWebView.kt
@@ -39,8 +39,6 @@ import com.pitchedapps.frost.injectors.jsInject
import com.pitchedapps.frost.utils.L
import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.isFacebookUrl
-import org.jetbrains.anko.doAsync
-import org.jetbrains.anko.uiThread
/**
* Created by Allan Wang on 2017-05-29.
@@ -76,18 +74,18 @@ class LoginWebView @JvmOverloads constructor(
override fun onPageFinished(view: WebView, url: String?) {
super.onPageFinished(view, url)
- checkForLogin(url) { id, cookie -> loginCallback(CookieModel(id, "", cookie)) }
+ val cookieModel = checkForLogin(url)
+ if (cookieModel != null)
+ loginCallback(cookieModel)
if (!view.isVisible) view.fadeIn()
}
- fun checkForLogin(url: String?, onFound: (id: Long, cookie: String) -> Unit) {
- doAsync {
- if (!url.isFacebookUrl) return@doAsync
- val cookie = CookieManager.getInstance().getCookie(url) ?: return@doAsync
- L.d { "Checking cookie for login" }
- val id = FB_USER_MATCHER.find(cookie)[1]?.toLong() ?: return@doAsync
- uiThread { onFound(id, cookie) }
- }
+ fun checkForLogin(url: String?): CookieModel? {
+ if (!url.isFacebookUrl) return null
+ val cookie = CookieManager.getInstance().getCookie(url) ?: return null
+ L.d { "Checking cookie for login" }
+ val id = FB_USER_MATCHER.find(cookie)[1]?.toLong() ?: return null
+ return CookieModel(id, "", cookie)
}
override fun onPageCommitVisible(view: WebView, url: String?) {
diff --git a/app/src/main/res/values/strings_web_context.xml b/app/src/main/res/values/strings_web_context.xml
index 7c8a9196..756a681b 100644
--- a/app/src/main/res/values/strings_web_context.xml
+++ b/app/src/main/res/values/strings_web_context.xml
@@ -3,6 +3,7 @@
<string name="share_link">Share Link</string>
<string name="debug_link">Debug Link</string>
+ <string name="local_service_name" translatable="false">Local Frost Service</string>
<string name="debug_link_subject" translatable="false">Frost for Facebook: Link Debug</string>
<string name="debug_link_content" translatable="false">Write here. Note that your link may contain private information, but I won\'t be able to see it as the post isn\'t public. The url will still help with debugging though.</string>
<string name="debug_link_desc">If a link isn\'t loading properly, you can email me so I can help debug it. Clicking okay will open an email request</string>
diff --git a/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt b/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt
index 20610b2a..ce125298 100644
--- a/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt
+++ b/app/src/test/kotlin/com/pitchedapps/frost/MiscTest.kt
@@ -16,7 +16,9 @@
*/
package com.pitchedapps.frost
+import com.pitchedapps.frost.facebook.requests.call
import com.pitchedapps.frost.facebook.requests.zip
+import okhttp3.Request
import org.junit.Test
import kotlin.test.assertTrue
@@ -45,4 +47,15 @@ class MiscTest {
"zip did not seem to work on different threads"
)
}
+
+ @Test
+ fun a() {
+ val s = Request.Builder()
+ .url("https://www.allanwang.ca/ecse429/magenta.png")
+ .get()
+ .call().execute().body()!!.string()
+ "�PNG\n\u001A\nIDA�c����?\u0000\u0006�\u0002��p�\u0000\u0000\u0000\u0000IEND�B`�"
+ println("Hello")
+ println(s)
+ }
}