aboutsummaryrefslogtreecommitdiff
path: root/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivityOrig.kt
diff options
context:
space:
mode:
Diffstat (limited to 'app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivityOrig.kt')
-rw-r--r--app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivityOrig.kt438
1 files changed, 438 insertions, 0 deletions
diff --git a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivityOrig.kt b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivityOrig.kt
new file mode 100644
index 00000000..3f6a90fe
--- /dev/null
+++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivityOrig.kt
@@ -0,0 +1,438 @@
+/*
+ * 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.Context
+import android.content.Intent
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.os.Bundle
+import android.view.View
+import android.widget.ImageView
+import androidx.customview.widget.ViewDragHelper
+import ca.allanwang.kau.internal.KauBaseActivity
+import ca.allanwang.kau.logging.KauLoggerExtension
+import ca.allanwang.kau.utils.adjustAlpha
+import ca.allanwang.kau.utils.colorToForeground
+import ca.allanwang.kau.utils.copyFromInputStream
+import ca.allanwang.kau.utils.fadeIn
+import ca.allanwang.kau.utils.fadeOut
+import ca.allanwang.kau.utils.gone
+import ca.allanwang.kau.utils.invisible
+import ca.allanwang.kau.utils.isHidden
+import ca.allanwang.kau.utils.isVisible
+import ca.allanwang.kau.utils.materialDialog
+import ca.allanwang.kau.utils.scaleXY
+import ca.allanwang.kau.utils.setIcon
+import ca.allanwang.kau.utils.tint
+import ca.allanwang.kau.utils.toast
+import ca.allanwang.kau.utils.withAlpha
+import ca.allanwang.kau.utils.withMinAlpha
+import com.davemorrissey.labs.subscaleview.ImageSource
+import com.google.android.material.bottomsheet.BottomSheetBehavior
+import com.google.android.material.floatingactionbutton.FloatingActionButton
+import com.mikepenz.iconics.typeface.IIcon
+import com.mikepenz.iconics.typeface.library.googlematerial.GoogleMaterial
+import com.pitchedapps.frost.R
+import com.pitchedapps.frost.databinding.ActivityImageBinding
+import com.pitchedapps.frost.facebook.FB_IMAGE_ID_MATCHER
+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.injectors.ThemeProvider
+import com.pitchedapps.frost.prefs.Prefs
+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.ActivityThemer
+import com.pitchedapps.frost.utils.frostDownload
+import com.pitchedapps.frost.utils.frostSnackbar
+import com.pitchedapps.frost.utils.frostUriFromFile
+import com.pitchedapps.frost.utils.isIndirectImageUrl
+import com.pitchedapps.frost.utils.logFrostEvent
+import dagger.hilt.android.AndroidEntryPoint
+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.FileNotFoundException
+import java.io.IOException
+import javax.inject.Inject
+import kotlin.math.abs
+import kotlin.math.max
+
+/**
+ * Created by Allan Wang on 2017-07-15.
+ */
+@AndroidEntryPoint
+class ImageActivityOrig : KauBaseActivity() {
+
+ @Inject
+ lateinit var activityThemer: ActivityThemer
+
+ @Inject
+ lateinit var prefs: Prefs
+
+ @Inject
+ lateinit var themeProvider: ThemeProvider
+
+ @Volatile
+ internal var errorRef: Throwable? = null
+
+ /**
+ * Reference to the temporary file path
+ */
+ internal var tempFile: File? = null
+
+ private lateinit var dragHelper: ViewDragHelper
+
+ companion object {
+ /**
+ * Cache folder to store images
+ * Linked to the uri provider
+ */
+ private const val IMAGE_FOLDER = "images"
+ private const val TIME_FORMAT = "yyyyMMdd_HHmmss"
+ private const val IMG_TAG = "Frost"
+ 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 lateinit var trueImageUrl: Deferred<String>
+
+ private val imageText: String? by lazy { intent.getStringExtra(ARG_TEXT) }
+
+ // a unique image identifier based on the id (if it exists), and its hash
+ private val imageHash: String by lazy {
+ "${abs(FB_IMAGE_ID_MATCHER.find(imageUrl)[1]?.hashCode() ?: 0)}_${abs(imageUrl.hashCode())}"
+ }
+
+ lateinit var binding: ActivityImageBinding
+ private var bottomBehavior: BottomSheetBehavior<View>? = null
+
+ private val baseBackgroundColor: Int
+ get() = if (prefs.blackMediaBg) Color.BLACK
+ else themeProvider.bgColor.withMinAlpha(235)
+
+ private fun loadError(e: Throwable) {
+ if (e.message?.contains("<!DOCTYPE html>") == true) {
+ applicationContext.toast(R.string.image_not_found)
+ finish()
+ return
+ }
+ errorRef = e
+ e.logFrostEvent("Image load error")
+ with(binding) {
+ if (imageProgress.isVisible)
+ imageProgress.fadeOut()
+ }
+ tempFile?.delete()
+ binding.error.fadeIn()
+ }
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ if (imageUrl.isEmpty()) {
+ return finish()
+ }
+ L.i { "Displaying image" }
+ trueImageUrl = async(Dispatchers.IO) {
+ val result = if (!imageUrl.isIndirectImageUrl) imageUrl
+ else cookie?.getFullSizedImageUrl(imageUrl) ?: imageUrl
+ if (result != imageUrl)
+ L.v { "Launching with true url $result" }
+ result
+ }
+ binding = ActivityImageBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+ binding.init()
+ launch(CoroutineExceptionHandler { _, throwable -> loadError(throwable) }) {
+ val tempFile = downloadTempImage()
+ this@ImageActivityOrig.tempFile = tempFile
+ binding.imageProgress.fadeOut()
+// binding.imagePhoto.setImageURI(frostUriFromFile(tempFile))
+ binding.imagePhoto.setImage(ImageSource.uri(frostUriFromFile(tempFile)))
+ binding.imagePhoto.animate().alpha(1f).scaleXY(1f).start()
+ }
+ }
+
+ private fun ActivityImageBinding.init() {
+ imageContainer.setBackgroundColor(baseBackgroundColor)
+ toolbar.setBackgroundColor(baseBackgroundColor)
+ this@ImageActivityOrig.imageText.also { text ->
+ if (text.isNullOrBlank()) {
+ imageText.gone()
+ } else {
+ imageText.setTextColor(if (prefs.blackMediaBg) Color.WHITE else themeProvider.textColor)
+ imageText.setBackgroundColor(
+ baseBackgroundColor.colorToForeground(0.2f).withAlpha(255)
+ )
+ imageText.text = text
+ bottomBehavior = BottomSheetBehavior.from<View>(imageText).apply {
+ addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() {
+ override fun onSlide(bottomSheet: View, slideOffset: Float) {
+ imageText.alpha = slideOffset / 2 + 0.5f
+ }
+
+ override fun onStateChanged(bottomSheet: View, newState: Int) {
+ // No op
+ }
+ })
+ }
+ imageText.bringToFront()
+ }
+ }
+ val foregroundTint = if (prefs.blackMediaBg) Color.WHITE else themeProvider.accentColor
+
+ fun ImageView.setState(state: FabStatesOrig) {
+ setIcon(state.iicon, color = foregroundTint, sizeDp = 24)
+ setOnClickListener { state.onClick(this@ImageActivityOrig) }
+ }
+
+ imageProgress.tint(foregroundTint)
+ error.apply {
+ invisible()
+ setState(FabStatesOrig.ERROR)
+ }
+ download.apply {
+ setState(FabStatesOrig.DOWNLOAD)
+ }
+ share.apply {
+ setState(FabStatesOrig.SHARE)
+ }
+// imagePhoto.setOnImageEventListener(object :
+// SubsamplingScaleImageView.DefaultOnImageEventListener() {
+// override fun onImageLoadError(e: Exception) {
+// loadError(e)
+// }
+// })
+ activityThemer.setFrostColors {
+ themeWindow = false
+ }
+ dragHelper = ViewDragHelper.create(imageDrag, ViewDragCallback()).apply {
+ setEdgeTrackingEnabled(ViewDragHelper.EDGE_TOP or ViewDragHelper.EDGE_BOTTOM)
+ }
+ imageDrag.dragHelper = dragHelper
+ imageDrag.viewToIgnore = imageText
+ }
+
+ private inner class ViewDragCallback : ViewDragHelper.Callback() {
+ private var scrollPercent: Float = 0f
+ private var scrollThreshold = 0.5f
+ private var scrollToTop = false
+
+ override fun tryCaptureView(view: View, i: Int): Boolean {
+ L.d { "Try capture ${view.id} $i ${binding.imagePhoto.id} ${binding.imageText.id}" }
+ return view === binding.imagePhoto
+ }
+
+ override fun getViewHorizontalDragRange(child: View): Int = 0
+
+ override fun getViewVerticalDragRange(child: View): Int = child.height
+
+ override fun onViewPositionChanged(
+ changedView: View,
+ left: Int,
+ top: Int,
+ dx: Int,
+ dy: Int
+ ) {
+ super.onViewPositionChanged(changedView, left, top, dx, dy)
+ with(binding) {
+ // make sure that we are using the proper axis
+ scrollPercent = abs(top.toFloat() / imageContainer.height)
+ scrollToTop = top < 0
+ val multiplier = max(1f - scrollPercent, 0f)
+
+ toolbar.alpha = multiplier
+ bottomBehavior?.also {
+ imageText.alpha =
+ multiplier * (if (it.state == BottomSheetBehavior.STATE_COLLAPSED) 0.5f else 1f)
+ }
+ imageContainer.setBackgroundColor(baseBackgroundColor.adjustAlpha(multiplier))
+
+ if (scrollPercent >= 1) {
+ if (!isFinishing) {
+ finish()
+ overridePendingTransition(0, 0)
+ }
+ }
+ }
+ }
+
+ override fun onViewReleased(releasedChild: View, xvel: Float, yvel: Float) {
+ val overScrolled = scrollPercent > scrollThreshold
+ val maxOffset = releasedChild.height + 10
+ val finalTop = when {
+ scrollToTop && (overScrolled || yvel < -dragHelper.minVelocity) -> -maxOffset
+ !scrollToTop && (overScrolled || yvel > dragHelper.minVelocity) -> maxOffset
+ else -> 0
+ }
+ dragHelper.settleCapturedViewAt(0, finalTop)
+ binding.imageDrag.invalidate()
+ }
+
+ override fun clampViewPositionHorizontal(child: View, left: Int, dx: Int): Int = 0
+
+ override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int = top
+ }
+
+ private fun getImageExtension(type: String?): String? {
+ if (type?.startsWith("image/") != true) {
+ return null
+ }
+ return when (type.substring(6)) {
+ "jpeg" -> "jpg"
+ "png" -> "png"
+ "gif" -> "gif"
+ else -> null
+ }
+ }
+
+ @Throws(IOException::class)
+ private suspend fun downloadTempImage(): File = withContext(Dispatchers.IO) {
+
+ // We assume all images are jpg
+ // Activity launcher may be able to provide specifics, but this beats sending a request
+ // just to get the content header
+ val file = File(cacheDir(this@ImageActivityOrig), "$imageHash.jpg")
+
+ if (!file.isFile) {
+ file.parentFile?.mkdirs()
+ file.createNewFile()
+ } else {
+ file.setLastModified(System.currentTimeMillis())
+ }
+
+ // Forbid overwrites
+ if (file.isFile && file.length() > 0) {
+ L.i { "Forbid image overwrite" }
+ return@withContext file
+ }
+
+ 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())
+ file
+ }
+
+ internal suspend fun saveImage() {
+ frostDownload(cookie = cookie, url = trueImageUrl.await())
+ }
+
+ override fun onDestroy() {
+ LocalService.schedule(this, LocalService.Flag.PURGE_IMAGE)
+ super.onDestroy()
+ }
+}
+
+internal enum class FabStatesOrig(
+ val iicon: IIcon,
+ val iconColorProvider: (ThemeProvider) -> Int = { it.iconColor },
+ val backgroundTint: Int = Int.MAX_VALUE
+) {
+ ERROR(GoogleMaterial.Icon.gmd_error, { Color.WHITE }, Color.RED) {
+ override fun onClick(activity: ImageActivityOrig) {
+ val err =
+ activity.errorRef?.takeIf { it !is FileNotFoundException && it.message != "Image failed to decode using JPEG decoder" }
+ ?: return
+ activity.materialDialog {
+ title(R.string.kau_error)
+ message(text = err.message ?: err.javaClass.name)
+ }
+ }
+ },
+ NOTHING(GoogleMaterial.Icon.gmd_adjust) {
+ override fun onClick(activity: ImageActivityOrig) {}
+ },
+ DOWNLOAD(GoogleMaterial.Icon.gmd_file_download) {
+ override fun onClick(activity: ImageActivityOrig) {
+ activity.launch {
+ activity.binding.download.fadeOut()
+ activity.saveImage()
+ }
+ }
+ },
+ SHARE(GoogleMaterial.Icon.gmd_share) {
+ override fun onClick(activity: ImageActivityOrig) {
+ val file = activity.tempFile ?: return
+ try {
+ val photoURI = activity.frostUriFromFile(file)
+ val intent = Intent(Intent.ACTION_SEND).apply {
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ putExtra(Intent.EXTRA_STREAM, photoURI)
+ type = "image/png"
+ }
+ activity.startActivity(intent)
+ } catch (e: Exception) {
+ activity.errorRef = e
+ e.logFrostEvent("Image share failed")
+ activity.frostSnackbar(R.string.image_share_failed, activity.themeProvider)
+ }
+ }
+ };
+
+ /**
+ * Change the fab look
+ * If it's in view, give it some animations
+ *
+ * TODO investigate what is wrong with fadeScaleTransition
+ *
+ * https://github.com/AllanWang/KAU/issues/184
+ *
+ */
+ fun update(fab: FloatingActionButton, themeProvider: ThemeProvider) {
+ val tint =
+ if (backgroundTint != Int.MAX_VALUE) backgroundTint else themeProvider.accentColor
+ val iconColor = iconColorProvider(themeProvider)
+ if (fab.isHidden) {
+ fab.setIcon(iicon, color = iconColor)
+ fab.backgroundTintList = ColorStateList.valueOf(tint)
+ fab.show()
+ } else {
+ fab.hide(object : FloatingActionButton.OnVisibilityChangedListener() {
+ override fun onHidden(fab: FloatingActionButton) {
+ fab.setIcon(iicon, color = iconColor)
+ fab.show()
+ }
+ })
+ }
+ }
+
+ abstract fun onClick(activity: ImageActivityOrig)
+}