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:
parent
697e457da4
commit
49a67bc7c6
@ -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}"
|
||||
|
||||
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
BIN
app/src/androidTest/resources/bayer-pattern.jpg
Normal file
BIN
app/src/androidTest/resources/bayer-pattern.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 6.0 KiB |
BIN
app/src/androidTest/resources/magenta.png
Normal file
BIN
app/src/androidTest/resources/magenta.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 165 B |
@ -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"
|
||||
|
@ -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) {
|
||||
L.d { "Download image async finished" }
|
||||
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
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
@ -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
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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>
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user