aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2018-12-25 22:14:56 -0500
committerAllan Wang <me@allanwang.ca>2018-12-25 22:14:56 -0500
commit49a67bc7c6d0ea38c88d8b424a2f188941dc609e (patch)
treefb01f3a4c2e17236bd229f7846b8f813e94c2a78 /app/src/main/kotlin/com
parent697e457da453568ca703c2b655a2dd490157b443 (diff)
downloadfrost-49a67bc7c6d0ea38c88d8b424a2f188941dc609e.tar.gz
frost-49a67bc7c6d0ea38c88d8b424a2f188941dc609e.tar.bz2
frost-49a67bc7c6d0ea38c88d8b424a2f188941dc609e.zip
Update imageactivity and add tests, resolves #1107
Diffstat (limited to 'app/src/main/kotlin/com')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt110
-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.kt32
6 files changed, 212 insertions, 86 deletions
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 bbd0463a..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
@@ -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()
}
}
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 3470ca07..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,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)