diff options
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt')
-rw-r--r-- | app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt | 237 |
1 files changed, 104 insertions, 133 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 83f617ba..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 @@ -28,13 +29,14 @@ import ca.allanwang.kau.mediapicker.scanMedia import ca.allanwang.kau.permissions.PERMISSION_WRITE_EXTERNAL_STORAGE import ca.allanwang.kau.permissions.kauRequestPermissions import ca.allanwang.kau.utils.colorToForeground +import ca.allanwang.kau.utils.copyFromInputStream import ca.allanwang.kau.utils.fadeOut import ca.allanwang.kau.utils.fadeScaleTransition import ca.allanwang.kau.utils.isHidden +import ca.allanwang.kau.utils.isVisible import ca.allanwang.kau.utils.scaleXY import ca.allanwang.kau.utils.setIcon import ca.allanwang.kau.utils.tint -import ca.allanwang.kau.utils.use import ca.allanwang.kau.utils.withAlpha import ca.allanwang.kau.utils.withMinAlpha import com.davemorrissey.labs.subscaleview.ImageSource @@ -48,12 +50,12 @@ 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 import com.pitchedapps.frost.utils.L import com.pitchedapps.frost.utils.Prefs -import com.pitchedapps.frost.utils.createFreshFile import com.pitchedapps.frost.utils.frostSnackbar import com.pitchedapps.frost.utils.frostUriFromFile import com.pitchedapps.frost.utils.isIndirectImageUrl @@ -63,12 +65,13 @@ import com.pitchedapps.frost.utils.sendFrostEmail import com.pitchedapps.frost.utils.setFrostColors import com.sothree.slidinguppanel.SlidingUpPanelLayout import kotlinx.android.synthetic.main.activity_image.* -import okhttp3.Response -import org.jetbrains.anko.activityUiThreadWithContext -import org.jetbrains.anko.doAsync -import org.jetbrains.anko.uiThread +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 java.io.File -import java.io.FileFilter import java.io.IOException import java.text.SimpleDateFormat import java.util.Date @@ -79,14 +82,13 @@ import java.util.Locale */ 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 @@ -94,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 { @@ -112,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) } @@ -138,11 +136,27 @@ class ImageActivity : KauBaseActivity() { )}_${Math.abs(imageUrl.hashCode())}" } + private fun loadError(e: Throwable) { + errorRef = e + e.logFrostEvent("Image load error") + if (image_progress.isVisible) + image_progress.fadeOut() + tempFile.delete() + fabAction = FabStates.ERROR + } + override fun onCreate(savedInstanceState: Bundle?) { 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( @@ -165,82 +179,23 @@ class ImageActivity : KauBaseActivity() { }) image_fab.setOnClickListener { fabAction.onClick(this) } image_photo.setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() { - override fun onImageLoadError(e: Exception?) { - errorRef = e - e.logFrostEvent("Image load error") - L.e { "Failed to load image $imageUrl" } - tempFile?.delete() - fabAction = FabStates.ERROR + override fun onImageLoadError(e: Exception) { + loadError(e) } }) setFrostColors { themeWindow = false } - tempDir = File(cacheDir, IMAGE_FOLDER) - tempFile = File(tempDir, imageHash) - doAsync({ - L.e(it) { "Failed to load image $imageHash" } - errorRef = it - runOnUiThread { image_progress.fadeOut() } - tempFile.delete() - fabAction = FabStates.ERROR - }) { - val loaded = loadImage(tempFile) - uiThread { - image_progress.fadeOut() - if (!loaded) { - fabAction = FabStates.ERROR - } else { - image_photo.setImage(ImageSource.uri(frostUriFromFile(tempFile))) - fabAction = FabStates.DOWNLOAD - image_photo.animate().alpha(1f).scaleXY(1f).start() - } - } + tempFile = File(cacheDir(this), imageHash) + launch(CoroutineExceptionHandler { _, throwable -> loadError(throwable) }) { + downloadImageTo(tempFile) + image_progress.fadeOut() + image_photo.setImage(ImageSource.uri(frostUriFromFile(tempFile))) + fabAction = FabStates.DOWNLOAD + image_photo.animate().alpha(1f).scaleXY(1f).start() } } - /** - * Attempts to load the image to [file] - * Returns true if successful - * Note that this is a long execution and should not be done on the UI thread - */ - private fun loadImage(file: File): Boolean { - if (file.exists() && file.length() > 1) { - file.setLastModified(System.currentTimeMillis()) - L.d { "Loading from local cache ${file.absolutePath}" } - return true - } - val response = getImageResponse() - - if (!response.isSuccessful) { - L.e { "Unsuccessful response for image" } - errorRef = Throwable("Unsuccessful response for image") - return false - } - - if (!file.createFreshFile()) { - L.e { "Could not create temp file" } - return false - } - - var valid = false - - response.body()?.byteStream()?.use { input -> - file.outputStream().use { output -> - input.copyTo(output) - valid = true - } - } - - if (!valid) { - L.e { "Failed to copy file" } - file.delete() - return false - } - - return true - } - @Throws(IOException::class) private fun createPublicMediaFile(): File { val timeStamp = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()).format(Date()) @@ -251,20 +206,56 @@ 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. + * Throws an error if something goes wrong. + */ @Throws(IOException::class) - private fun downloadImageTo(file: File) { - val body = getImageResponse().body() - ?: throw IOException("Failed to retrieve image body") - body.byteStream().use { input -> - file.outputStream().use { output -> - input.copyTo(output) + private suspend fun downloadImageTo(file: File): Boolean { + val exceptionHandler = CoroutineExceptionHandler { _, err -> + if (file.isFile && file.length() == 0L) { + file.delete() } + throw err + } + return withContext(Dispatchers.IO + exceptionHandler) { + if (!file.isFile) { + file.parentFile.mkdirs() + file.createNewFile() + } else { + file.setLastModified(System.currentTimeMillis()) + } + + // Forbid overwrites + if (file.length() > 0) { + L.i { "Forbid image overwrite" } + return@withContext false + } + 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 = cookie.requestBuilder() + .url(trueImageUrl.await()) + .get() + .call() + .execute() + + if (!response.isSuccessful) { + throw IOException("Unsuccessful response for image: ${response.peekBody(128).string()}") + } + + val body = response.body() ?: throw IOException("Failed to retrieve image body") + + file.copyFromInputStream(body.byteStream()) + + return@withContext true } } @@ -272,45 +263,25 @@ class ImageActivity : KauBaseActivity() { kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ -> L.d { "Download image callback granted: $granted" } if (granted) { - doAsync { + val errorHandler = CoroutineExceptionHandler { _, throwable -> + loadError(throwable) + frostSnackbar(R.string.image_download_fail) + } + launch(errorHandler) { val destination = createPublicMediaFile() - var success = true - try { - val temp = tempFile - if (temp != null) - temp.copyTo(destination, true) - else - 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) { - } - } - activityUiThreadWithContext { - 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() } } |