package com.pitchedapps.frost.activities import android.content.Context import android.content.Intent import android.content.res.ColorStateList import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.Drawable import android.net.Uri import android.os.Bundle import android.os.Environment import android.support.design.widget.FloatingActionButton import android.view.View import android.view.ViewGroup import android.widget.ProgressBar import android.widget.TextView import ca.allanwang.kau.internal.KauBaseActivity import ca.allanwang.kau.logging.KauLoggerExtension 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.* import com.bumptech.glide.Glide import com.bumptech.glide.RequestManager import com.bumptech.glide.request.target.BaseTarget import com.bumptech.glide.request.target.SizeReadyCallback import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.transition.Transition import com.davemorrissey.labs.subscaleview.ImageSource import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.mikepenz.google_material_typeface_library.GoogleMaterial import com.mikepenz.iconics.typeface.IIcon import com.pitchedapps.frost.R import com.pitchedapps.frost.facebook.requests.call import com.pitchedapps.frost.utils.* import com.sothree.slidinguppanel.SlidingUpPanelLayout import okhttp3.Request import org.jetbrains.anko.activityUiThreadWithContext import org.jetbrains.anko.doAsync import java.io.File import java.io.IOException import java.text.SimpleDateFormat import java.util.* /** * Created by Allan Wang on 2017-07-15. */ class ImageActivity : KauBaseActivity() { val progress: ProgressBar by bindView(R.id.image_progress) val container: ViewGroup by bindView(R.id.image_container) val panel: SlidingUpPanelLayout? by bindOptionalView(R.id.image_panel) val photo: SubsamplingScaleImageView by bindView(R.id.image_photo) val caption: TextView? by bindOptionalView(R.id.image_text) val fab: FloatingActionButton by bindView(R.id.image_fab) var errorRef: Throwable? = null private val tempDir: File by lazy { File(cacheDir, IMAGE_FOLDER) } /** * Reference to the temporary file path * Should be nonnull if the image is successfully loaded * As this is temporary, the image is deleted upon exit */ internal var tempFile: File? = null /** * Reference to path for downloaded image * Nonnull once the image is downloaded by the user */ internal var savedFile: File? = null /** * Indicator for fab's click result */ internal var fabAction: FabStates = FabStates.NOTHING set(value) { if (field == value) return field = value value.update(fab) } 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" private const val IMG_EXTENSION = ".png" private val L = KauLoggerExtension("Image", com.pitchedapps.frost.utils.L) } val imageUrl: String by lazy { intent.getStringExtra(ARG_IMAGE_URL).trim('"') } val text: String? by lazy { intent.getStringExtra(ARG_TEXT) } private val glide: RequestManager by lazy { Glide.with(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) intent?.extras ?: return finish() L.i { "Displaying image" } val layout = if (!text.isNullOrBlank()) R.layout.activity_image else R.layout.activity_image_textless setContentView(layout) container.setBackgroundColor(Prefs.bgColor.withMinAlpha(222)) caption?.setTextColor(Prefs.textColor) caption?.setBackgroundColor(Prefs.bgColor.colorToForeground(0.2f).withAlpha(255)) caption?.text = text progress.tint(Prefs.accentColor) panel?.addPanelSlideListener(object : SlidingUpPanelLayout.SimplePanelSlideListener() { override fun onPanelSlide(panel: View, slideOffset: Float) { if (slideOffset == 0f && !fab.isShown) fab.show() else if (slideOffset != 0f && fab.isShown) fab.hide() caption?.alpha = slideOffset / 2 + 0.5f } }) fab.setOnClickListener { fabAction.onClick(this) } photo.setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() { override fun onImageLoadError(e: Exception?) { errorRef = e e.logFrostAnswers("Image load error") imageCallback(null, false) } }) glide.asBitmap().load(imageUrl).into(PhotoTarget(this::imageCallback)) setFrostColors { themeWindow = false } } /** * Callback to add image to view * [resource] is guaranteed to be nonnull when [success] is true * and null when it is false */ private fun imageCallback(resource: Bitmap?, success: Boolean) { if (progress.isVisible) progress.fadeOut() if (success) { saveTempImage(resource!!, { if (it == null) { imageCallback(null, false) } else { photo.setImage(ImageSource.uri(it)) fabAction = FabStates.DOWNLOAD photo.animate().alpha(1f).scaleXY(1f).withEndAction(fab::show).start() } }) } else { fabAction = FabStates.ERROR fab.show() } } /** * Bitmap load handler */ class PhotoTarget(val callback: (resource: Bitmap?, success: Boolean) -> Unit) : BaseTarget() { override fun removeCallback(cb: SizeReadyCallback?) {} override fun onResourceReady(resource: Bitmap, transition: Transition?) = callback(resource, true) override fun onLoadFailed(errorDrawable: Drawable?) = callback(null, false) override fun getSize(cb: SizeReadyCallback) = cb.onSizeReady(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL) } private fun saveTempImage(resource: Bitmap, callback: (uri: Uri?) -> Unit) { var photoFile: File? = null try { photoFile = createPrivateMediaFile() } catch (e: IOException) { errorRef = e logImage(e) } finally { if (photoFile == null) { callback(null) } else { tempFile = photoFile L.d { "Temp image path ${tempFile?.absolutePath}" } // File created; proceed with request val photoURI = frostUriFromFile(photoFile) photoFile.outputStream().use { resource.compress(Bitmap.CompressFormat.PNG, 100, it) } callback(photoURI) } } } private fun logImage(e: Exception?) { if (!Prefs.analytics) return val error = e ?: IOException("$imageUrl failed to load") L.e(error) { "$imageUrl failed to load" } } @Throws(IOException::class) private fun createPrivateMediaFile(): File { val timeStamp = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()).format(Date()) val imageFileName = "${IMG_TAG}_${timeStamp}_" if (!tempDir.exists()) tempDir.mkdirs() return File.createTempFile(imageFileName, IMG_EXTENSION, tempDir) } @Throws(IOException::class) private fun createPublicMediaFile(): File { val timeStamp = SimpleDateFormat(TIME_FORMAT, Locale.getDefault()).format(Date()) val imageFileName = "${IMG_TAG}_${timeStamp}_" val storageDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES) val frostDir = File(storageDir, IMG_TAG) if (!frostDir.exists()) frostDir.mkdirs() return File.createTempFile(imageFileName, IMG_EXTENSION, frostDir) } @Throws(IOException::class) private fun downloadImageTo(file: File) { val body = Request.Builder() .url(imageUrl) .get() .call() .execute() .body() ?: throw IOException("Failed to retrieve image body") body.byteStream().use { input -> file.outputStream().use { output -> input.copyTo(output) } } } internal fun saveImage() { kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ -> L.d { "Download image callback granted: $granted" } if (granted) { doAsync { 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 } } } } } } override fun onDestroy() { tempFile = null tempDir.deleteRecursively() L.d { "Closing $localClassName" } super.onDestroy() } } internal enum class FabStates(val iicon: IIcon, val iconColor: Int = Prefs.iconColor, val backgroundTint: Int = Int.MAX_VALUE) { ERROR(GoogleMaterial.Icon.gmd_error, Color.WHITE, Color.RED) { override fun onClick(activity: ImageActivity) { activity.materialDialogThemed { title(R.string.kau_error) content(R.string.bad_image_overlay) positiveText(R.string.kau_yes) onPositive { _, _ -> if (activity.errorRef != null) L.e(activity.errorRef) { "ImageActivity error report" } activity.sendFrostEmail(R.string.debug_image_link_subject) { addItem("Url", activity.imageUrl) addItem("Message", activity.errorRef?.message ?: "Null") } } negativeText(R.string.kau_no) } } }, NOTHING(GoogleMaterial.Icon.gmd_adjust) { override fun onClick(activity: ImageActivity) {} }, DOWNLOAD(GoogleMaterial.Icon.gmd_file_download) { override fun onClick(activity: ImageActivity) = activity.saveImage() }, SHARE(GoogleMaterial.Icon.gmd_share) { override fun onClick(activity: ImageActivity) { try { val photoURI = activity.frostUriFromFile(activity.savedFile!!) 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.logFrostAnswers("Image share failed") activity.frostSnackbar(R.string.image_share_failed) } } }; /** * Change the fab look * If it's in view, give it some animations */ fun update(fab: FloatingActionButton) { val tint = if (backgroundTint != Int.MAX_VALUE) backgroundTint else Prefs.accentColor if (fab.isHidden) { fab.setIcon(iicon, color = iconColor) fab.backgroundTintList = ColorStateList.valueOf(tint) fab.show() } else { fab.fadeScaleTransition { setIcon(iicon, color = iconColor) backgroundTintList = ColorStateList.valueOf(tint) } } } abstract fun onClick(activity: ImageActivity) }