1
0
mirror of https://github.com/AllanWang/Frost-for-Facebook.git synced 2024-11-10 04:52:38 +01: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 // TODO temp
implementation "org.jetbrains.anko:anko-commons:0.10.8" 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.jetbrains.kotlinx:kotlinx-coroutines-android:${COROUTINES}"
implementation "org.apache.commons:commons-text:${COMMONS_TEXT}" implementation "org.apache.commons:commons-text:${COMMONS_TEXT}"
@ -220,6 +226,8 @@ dependencies {
implementation "com.squareup.okhttp3:okhttp:${OKHTTP}" implementation "com.squareup.okhttp3:okhttp:${OKHTTP}"
implementation "com.squareup.okhttp3:logging-interceptor:${OKHTTP}" implementation "com.squareup.okhttp3:logging-interceptor:${OKHTTP}"
androidTestImplementation "com.squareup.okhttp3:mockwebserver:${OKHTTP}"
implementation "co.zsmb:materialdrawer-kt:${MATERIAL_DRAWER_KT}" 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 package com.pitchedapps.frost.activities
import android.content.Intent
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.rule.ActivityTestRule import androidx.test.rule.ActivityTestRule
import org.junit.Rule import com.pitchedapps.frost.helper.getResource
import org.junit.runner.RunWith
import android.content.Intent
import com.pitchedapps.frost.utils.ARG_COOKIE import com.pitchedapps.frost.utils.ARG_COOKIE
import com.pitchedapps.frost.utils.ARG_IMAGE_URL import com.pitchedapps.frost.utils.ARG_IMAGE_URL
import com.pitchedapps.frost.utils.ARG_TEXT 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.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) @RunWith(AndroidJUnit4::class)
class ImageActivityTest { class ImageActivityTest {
@ -16,7 +45,14 @@ class ImageActivityTest {
@get:Rule @get:Rule
val activity: ActivityTestRule<ImageActivity> = ActivityTestRule(ImageActivity::class.java, true, false) 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) { 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 { val intent = Intent().apply {
putExtra(ARG_IMAGE_URL, imageUrl) putExtra(ARG_IMAGE_URL, imageUrl)
putExtra(ARG_TEXT, text) putExtra(ARG_TEXT, text)
@ -25,8 +61,64 @@ class ImageActivityTest {
activity.launchActivity(intent) activity.launchActivity(intent)
} }
@Test private val mockServer: MockWebServer by lazy {
fun intent() { 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:enabled="true"
android:label="@string/frost_requests" android:label="@string/frost_requests"
android:permission="android.permission.BIND_JOB_SERVICE" /> 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 <receiver
android:name=".services.UpdateReceiver" android:name=".services.UpdateReceiver"

View File

@ -16,6 +16,7 @@
*/ */
package com.pitchedapps.frost.activities package com.pitchedapps.frost.activities
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Color 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.call
import com.pitchedapps.frost.facebook.requests.getFullSizedImageUrl import com.pitchedapps.frost.facebook.requests.getFullSizedImageUrl
import com.pitchedapps.frost.facebook.requests.requestBuilder 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_COOKIE
import com.pitchedapps.frost.utils.ARG_IMAGE_URL import com.pitchedapps.frost.utils.ARG_IMAGE_URL
import com.pitchedapps.frost.utils.ARG_TEXT import com.pitchedapps.frost.utils.ARG_TEXT
@ -64,12 +66,12 @@ import com.pitchedapps.frost.utils.setFrostColors
import com.sothree.slidinguppanel.SlidingUpPanelLayout import com.sothree.slidinguppanel.SlidingUpPanelLayout
import kotlinx.android.synthetic.main.activity_image.* import kotlinx.android.synthetic.main.activity_image.*
import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import okhttp3.Response
import java.io.File import java.io.File
import java.io.FileFilter
import java.io.IOException import java.io.IOException
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@ -83,12 +85,10 @@ class ImageActivity : KauBaseActivity() {
@Volatile @Volatile
internal var errorRef: Throwable? = null internal var errorRef: Throwable? = null
private lateinit var tempDir: File
/** /**
* Reference to the temporary file path * Reference to the temporary file path
*/ */
private lateinit var tempFile: File internal lateinit var tempFile: File
/** /**
* Reference to path for downloaded image * Reference to path for downloaded image
* Nonnull once the image is downloaded by the user * Nonnull once the image is downloaded by the user
@ -96,13 +96,12 @@ class ImageActivity : KauBaseActivity() {
internal var savedFile: File? = null internal var savedFile: File? = null
/** /**
* Indicator for fab's click result * Indicator for fab's click result
* Can be called from any thread
*/ */
internal var fabAction: FabStates = FabStates.NOTHING internal var fabAction: FabStates = FabStates.NOTHING
set(value) { set(value) {
if (field == value) return if (field == value) return
field = value field = value
runOnUiThread { value.update(image_fab) } value.update(image_fab)
} }
companion object { companion object {
@ -114,21 +113,18 @@ class ImageActivity : KauBaseActivity() {
private const val TIME_FORMAT = "yyyyMMdd_HHmmss" private const val TIME_FORMAT = "yyyyMMdd_HHmmss"
private const val IMG_TAG = "Frost" private const val IMG_TAG = "Frost"
private const val IMG_EXTENSION = ".png" 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) 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) } private val cookie: String? by lazy { intent.getStringExtra(ARG_COOKIE) }
val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') } val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') }
private val trueImageUrl: String by lazy { private lateinit var trueImageUrl: Deferred<String>
val result = if (!imageUrl.isIndirectImageUrl) imageUrl
else cookie?.getFullSizedImageUrl(imageUrl)?.blockingGet() ?: imageUrl
if (result != imageUrl)
L.v { "Launching with true url $result" }
result
}
private val imageText: String? by lazy { intent.getStringExtra(ARG_TEXT) } private val imageText: String? by lazy { intent.getStringExtra(ARG_TEXT) }
@ -143,7 +139,6 @@ class ImageActivity : KauBaseActivity() {
private fun loadError(e: Throwable) { private fun loadError(e: Throwable) {
errorRef = e errorRef = e
e.logFrostEvent("Image load error") e.logFrostEvent("Image load error")
L.e { "Failed to load image $imageHash" }
if (image_progress.isVisible) if (image_progress.isVisible)
image_progress.fadeOut() image_progress.fadeOut()
tempFile.delete() tempFile.delete()
@ -154,7 +149,14 @@ class ImageActivity : KauBaseActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
intent?.extras ?: return finish() intent?.extras ?: return finish()
L.i { "Displaying image" } 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 val layout = if (!imageText.isNullOrBlank()) R.layout.activity_image else R.layout.activity_image_textless
setContentView(layout) setContentView(layout)
image_container.setBackgroundColor( image_container.setBackgroundColor(
@ -184,9 +186,8 @@ class ImageActivity : KauBaseActivity() {
setFrostColors { setFrostColors {
themeWindow = false themeWindow = false
} }
tempDir = File(cacheDir, IMAGE_FOLDER) tempFile = File(cacheDir(this), imageHash)
tempFile = File(tempDir, imageHash) launch(CoroutineExceptionHandler { _, throwable -> loadError(throwable) }) {
launch(CoroutineExceptionHandler { _, err -> loadError(err) }) {
downloadImageTo(tempFile) downloadImageTo(tempFile)
image_progress.fadeOut() image_progress.fadeOut()
image_photo.setImage(ImageSource.uri(frostUriFromFile(tempFile))) image_photo.setImage(ImageSource.uri(frostUriFromFile(tempFile)))
@ -205,12 +206,6 @@ class ImageActivity : KauBaseActivity() {
return File.createTempFile(imageFileName, IMG_EXTENSION, frostDir) 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. * Saves the image to the specified file, creating it if it doesn't exist.
* Returns true if a change is made, false otherwise. * Returns true if a change is made, false otherwise.
@ -218,30 +213,39 @@ class ImageActivity : KauBaseActivity() {
*/ */
@Throws(IOException::class) @Throws(IOException::class)
private suspend fun downloadImageTo(file: File): Boolean { private suspend fun downloadImageTo(file: File): Boolean {
val exceptionHandler = CoroutineExceptionHandler { _, _ -> val exceptionHandler = CoroutineExceptionHandler { _, err ->
if (file.isFile && file.length() == 0L) { if (file.isFile && file.length() == 0L) {
file.delete() file.delete()
} }
throw err
} }
return withContext(Dispatchers.IO + exceptionHandler) { return withContext(Dispatchers.IO + exceptionHandler) {
if (!file.isFile) { if (!file.isFile) {
file.mkdirs() file.parentFile.mkdirs()
file.createNewFile() file.createNewFile()
} else {
file.setLastModified(System.currentTimeMillis())
} }
file.setLastModified(System.currentTimeMillis())
// Forbid overwrites // Forbid overwrites
if (file.length() > 1) if (file.length() > 0) {
L.i { "Forbid image overwrite" }
return@withContext false return@withContext false
if (tempFile.isFile && tempFile.length() > 1) { }
if (tempFile == file) if (tempFile.isFile && tempFile.length() > 0) {
if (tempFile == file) {
return@withContext false return@withContext false
}
tempFile.copyTo(file) tempFile.copyTo(file)
return@withContext true return@withContext true
} }
// No temp file, download ourselves // No temp file, download ourselves
val response = getImageResponse() val response = cookie.requestBuilder()
.url(trueImageUrl.await())
.get()
.call()
.execute()
if (!response.isSuccessful) { if (!response.isSuccessful) {
throw IOException("Unsuccessful response for image: ${response.peekBody(128).string()}") throw IOException("Unsuccessful response for image: ${response.peekBody(128).string()}")
@ -259,39 +263,25 @@ class ImageActivity : KauBaseActivity() {
kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ -> kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ ->
L.d { "Download image callback granted: $granted" } L.d { "Download image callback granted: $granted" }
if (granted) { if (granted) {
launch { val errorHandler = CoroutineExceptionHandler { _, throwable ->
loadError(throwable)
frostSnackbar(R.string.image_download_fail)
}
launch(errorHandler) {
val destination = createPublicMediaFile() val destination = createPublicMediaFile()
var success = true
try {
downloadImageTo(destination) downloadImageTo(destination)
} catch (e: Exception) { L.d { "Download image async finished" }
errorRef = e
success = false
} finally {
L.d { "Download image async finished: $success" }
if (success) {
scanMedia(destination) scanMedia(destination)
savedFile = destination savedFile = destination
} else { frostSnackbar(R.string.image_download_success)
try { fabAction = FabStates.SHARE
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
}
} }
} }
} }
} }
override fun onDestroy() { override fun onDestroy() {
val purge = System.currentTimeMillis() - PURGE_TIME LocalService.schedule(this, LocalService.Flag.PURGE_IMAGE)
tempDir.listFiles(FileFilter { it.isFile && it.lastModified() < purge })?.forEach {
it.delete()
}
super.onDestroy() 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()) NotificationManagerCompat.from(context).notify(tag, id, notif.build())
} }
const val NOTIFICATION_PERIODIC_JOB = 7
fun Context.scheduleNotifications(minutes: Long): Boolean = fun Context.scheduleNotifications(minutes: Long): Boolean =
scheduleJob<NotificationService>(NOTIFICATION_PERIODIC_JOB, minutes) scheduleJob<NotificationService>(NOTIFICATION_PERIODIC_JOB, minutes)
const val NOTIFICATION_JOB_NOW = 6
fun Context.fetchNotifications(): Boolean = fun Context.fetchNotifications(): Boolean =
fetchJob<NotificationService>(NOTIFICATION_JOB_NOW) 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_1 = "frost_request_arg_1"
private const val ARG_2 = "frost_request_arg_2" private const val ARG_2 = "frost_request_arg_2"
private const val ARG_3 = "frost_request_arg_3" 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.getCookie() = getString(ARG_COOKIE)
private fun BaseBundle.putCookie(cookie: String) = putString(ARG_COOKIE, cookie) private fun BaseBundle.putCookie(cookie: String) = putString(ARG_COOKIE, cookie)
@ -145,7 +144,7 @@ object FrostRunnable {
return false return false
} }
val builder = JobInfo.Builder(JOB_REQUEST_BASE + command.ordinal, serviceComponent) val builder = JobInfo.Builder(REQUEST_SERVICE_BASE + command.ordinal, serviceComponent)
.setMinimumLatency(0L) .setMinimumLatency(0L)
.setExtras(bundle) .setExtras(bundle)
.setOverrideDeadline(2000L) .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 package com.pitchedapps.frost.services
import android.app.job.JobParameters import android.app.job.JobParameters
import android.app.job.JobService
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import ca.allanwang.kau.utils.string import ca.allanwang.kau.utils.string
import com.pitchedapps.frost.BuildConfig 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.L
import com.pitchedapps.frost.utils.Prefs import com.pitchedapps.frost.utils.Prefs
import com.pitchedapps.frost.utils.frostEvent import com.pitchedapps.frost.utils.frostEvent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.withContext
import kotlin.coroutines.CoroutineContext
/** /**
* Created by Allan Wang on 2017-06-14. * Created by Allan Wang on 2017-06-14.
@ -44,22 +39,20 @@ import kotlin.coroutines.CoroutineContext
* *
* All fetching is done through parsers * All fetching is done through parsers
*/ */
class NotificationService : JobService(), CoroutineScope { class NotificationService : BaseJobService() {
private lateinit var job: Job
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
private val startTime = System.currentTimeMillis()
override fun onStopJob(params: JobParameters?): Boolean { override fun onStopJob(params: JobParameters?): Boolean {
super.onStopJob(params)
prepareFinish(true) prepareFinish(true)
return false return false
} }
private var preparedFinish = false
private fun prepareFinish(abrupt: Boolean) { private fun prepareFinish(abrupt: Boolean) {
if (job.isCancelled) if (preparedFinish)
return return
preparedFinish = true
val time = System.currentTimeMillis() - startTime val time = System.currentTimeMillis() - startTime
L.i { "Notification service has ${if (abrupt) "finished abruptly" else "finished"} in $time ms" } L.i { "Notification service has ${if (abrupt) "finished abruptly" else "finished"} in $time ms" }
frostEvent( frostEvent(
@ -68,15 +61,14 @@ class NotificationService : JobService(), CoroutineScope {
"IM Included" to Prefs.notificationsInstantMessages, "IM Included" to Prefs.notificationsInstantMessages,
"Duration" to time "Duration" to time
) )
job.cancel()
} }
override fun onStartJob(params: JobParameters?): Boolean { override fun onStartJob(params: JobParameters?): Boolean {
super.onStartJob(params)
L.i { "Fetching notifications" } L.i { "Fetching notifications" }
job = Job()
launch { launch {
try { try {
async { sendNotifications(params) }.await() sendNotifications(params)
} finally { } finally {
if (!isActive) if (!isActive)
prepareFinish(false) prepareFinish(false)
@ -86,14 +78,14 @@ class NotificationService : JobService(), CoroutineScope {
return true 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 currentId = Prefs.userId
val cookies = loadFbCookiesSync() val cookies = loadFbCookiesSync()
if (it.isCancelled) return@suspendCancellableCoroutine if (!isActive) return@withContext
val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1 val jobId = params?.extras?.getInt(NOTIFICATION_PARAM_ID, -1) ?: -1
var notifCount = 0 var notifCount = 0
for (cookie in cookies) { for (cookie in cookies) {
if (it.isCancelled) break if (!isActive) break
val current = cookie.id == currentId val current = cookie.id == currentId
if (Prefs.notificationsGeneral && if (Prefs.notificationsGeneral &&
(current || Prefs.notificationAllAccounts) (current || Prefs.notificationAllAccounts)

View File

@ -3,6 +3,7 @@
<string name="share_link">Share Link</string> <string name="share_link">Share Link</string>
<string name="debug_link">Debug 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_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_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> <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 package com.pitchedapps.frost
import com.pitchedapps.frost.facebook.requests.call
import com.pitchedapps.frost.facebook.requests.zip import com.pitchedapps.frost.facebook.requests.zip
import okhttp3.Request
import org.junit.Test import org.junit.Test
import kotlin.test.assertTrue import kotlin.test.assertTrue
@ -45,4 +47,15 @@ class MiscTest {
"zip did not seem to work on different threads" "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 DBFLOW=4.2.4
# https://github.com/brianwernick/ExoMedia/releases # https://github.com/brianwernick/ExoMedia/releases
EXOMEDIA=4.3.0 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 # https://github.com/FasterXML/jackson-core/releases
JACKSON=2.9.8 JACKSON=2.9.8