1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-09-20 07:31:40 +02:00

Update imageactivity and add tests, resolves #1107

This commit is contained in:
Allan Wang 2018-12-25 22:14:56 -05:00
parent 697e457da4
commit 49a67bc7c6
No known key found for this signature in database
GPG Key ID: C93E3F9C679D7A56
15 changed files with 374 additions and 93 deletions

View File

@ -184,6 +184,12 @@ dependencies {
// 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}"
@ -220,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}"

View File

@ -1,14 +1,43 @@
/*
* 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 org.junit.Rule
import org.junit.runner.RunWith
import android.content.Intent
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 {
@ -16,7 +45,14 @@ 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)
@ -25,8 +61,64 @@ class ImageActivityTest {
activity.launchActivity(intent)
}
@Test
fun 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")
}
}
}

View File

@ -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()

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

View File

@ -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"

View File

@ -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
@ -49,6 +50,7 @@ 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
@ -64,12 +66,12 @@ import com.pitchedapps.frost.utils.setFrostColors
import com.sothree.slidinguppanel.SlidingUpPanelLayout
import kotlinx.android.synthetic.main.activity_image.*
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 okhttp3.Response
import java.io.File
import java.io.FileFilter
import java.io.IOException
import java.text.SimpleDateFormat
import java.util.Date
@ -83,12 +85,10 @@ 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
@ -96,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 {
@ -114,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) }
@ -143,7 +139,6 @@ class ImageActivity : KauBaseActivity() {
private fun loadError(e: Throwable) {
errorRef = e
e.logFrostEvent("Image load error")
L.e { "Failed to load image $imageHash" }
if (image_progress.isVisible)
image_progress.fadeOut()
tempFile.delete()
@ -154,7 +149,14 @@ class ImageActivity : KauBaseActivity() {
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(
@ -184,9 +186,8 @@ class ImageActivity : KauBaseActivity() {
setFrostColors {
themeWindow = false
}
tempDir = File(cacheDir, IMAGE_FOLDER)
tempFile = File(tempDir, imageHash)
launch(CoroutineExceptionHandler { _, err -> loadError(err) }) {
tempFile = File(cacheDir(this), imageHash)
launch(CoroutineExceptionHandler { _, throwable -> loadError(throwable) }) {
downloadImageTo(tempFile)
image_progress.fadeOut()
image_photo.setImage(ImageSource.uri(frostUriFromFile(tempFile)))
@ -205,12 +206,6 @@ 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.
@ -218,30 +213,39 @@ class ImageActivity : KauBaseActivity() {
*/
@Throws(IOException::class)
private suspend fun downloadImageTo(file: File): Boolean {
val exceptionHandler = CoroutineExceptionHandler { _, _ ->
val exceptionHandler = CoroutineExceptionHandler { _, err ->
if (file.isFile && file.length() == 0L) {
file.delete()
}
throw err
}
return withContext(Dispatchers.IO + exceptionHandler) {
if (!file.isFile) {
file.mkdirs()
file.parentFile.mkdirs()
file.createNewFile()
} else {
file.setLastModified(System.currentTimeMillis())
}
file.setLastModified(System.currentTimeMillis())
// Forbid overwrites
if (file.length() > 1)
if (file.length() > 0) {
L.i { "Forbid image overwrite" }
return@withContext false
if (tempFile.isFile && tempFile.length() > 1) {
if (tempFile == file)
}
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 = getImageResponse()
val response = cookie.requestBuilder()
.url(trueImageUrl.await())
.get()
.call()
.execute()
if (!response.isSuccessful) {
throw IOException("Unsuccessful response for image: ${response.peekBody(128).string()}")
@ -259,39 +263,25 @@ class ImageActivity : KauBaseActivity() {
kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ ->
L.d { "Download image callback granted: $granted" }
if (granted) {
launch {
val errorHandler = CoroutineExceptionHandler { _, throwable ->
loadError(throwable)
frostSnackbar(R.string.image_download_fail)
}
launch(errorHandler) {
val destination = createPublicMediaFile()
var success = true
try {
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) {
}
}
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()
}
}

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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() }
}
}
}

View File

@ -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,14 +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 kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.withContext
/**
* Created by Allan Wang on 2017-06-14.
@ -44,22 +39,20 @@ import kotlin.coroutines.CoroutineContext
*
* All fetching is done through parsers
*/
class NotificationService : JobService(), CoroutineScope {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
private val startTime = System.currentTimeMillis()
class NotificationService : BaseJobService() {
override fun onStopJob(params: JobParameters?): Boolean {
super.onStopJob(params)
prepareFinish(true)
return false
}
private var preparedFinish = false
private fun prepareFinish(abrupt: Boolean) {
if (job.isCancelled)
if (preparedFinish)
return
preparedFinish = true
val time = System.currentTimeMillis() - startTime
L.i { "Notification service has ${if (abrupt) "finished abruptly" else "finished"} in $time ms" }
frostEvent(
@ -68,15 +61,14 @@ class NotificationService : JobService(), CoroutineScope {
"IM Included" to Prefs.notificationsInstantMessages,
"Duration" to time
)
job.cancel()
}
override fun onStartJob(params: JobParameters?): Boolean {
super.onStartJob(params)
L.i { "Fetching notifications" }
job = Job()
launch {
try {
async { sendNotifications(params) }.await()
sendNotifications(params)
} finally {
if (!isActive)
prepareFinish(false)
@ -86,14 +78,14 @@ class NotificationService : JobService(), CoroutineScope {
return true
}
private suspend fun sendNotifications(params: JobParameters?): Unit = suspendCancellableCoroutine {
private suspend fun sendNotifications(params: JobParameters?): Unit = withContext(Dispatchers.Default) {
val currentId = Prefs.userId
val cookies = loadFbCookiesSync()
if (it.isCancelled) return@suspendCancellableCoroutine
if (!isActive) return@withContext
val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
var notifCount = 0
for (cookie in cookies) {
if (it.isCancelled) break
if (!isActive) break
val current = cookie.id == currentId
if (Prefs.notificationsGeneral &&
(current || Prefs.notificationAllAccounts)

View File

@ -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>

View File

@ -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()
"<EFBFBD>PNG\n\u001A\nIDA<EFBFBD>c<EFBFBD><EFBFBD><EFBFBD><EFBFBD>?\u0000\u0006<EFBFBD>\u0002<EFBFBD><EFBFBD>p<EFBFBD>\u0000\u0000\u0000\u0000IEND<EFBFBD>B`<60>"
println("Hello")
println(s)
}
}

View File

@ -40,6 +40,10 @@ COMMONS_TEXT=1.4
DBFLOW=4.2.4
# https://github.com/brianwernick/ExoMedia/releases
EXOMEDIA=4.3.0
# https://github.com/InsertKoinIO/koin/blob/master/CHANGELOG.md
KOIN=1.0.2
# https://github.com/mockk/mockk/releases
MOCKK=1.8.13.kotlin13
# https://github.com/FasterXML/jackson-core/releases
JACKSON=2.9.8