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:
parent
697e457da4
commit
49a67bc7c6
@ -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}"
|
||||||
|
|
||||||
|
@ -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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -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: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"
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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())
|
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)
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
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)
|
||||||
|
@ -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>
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user