diff options
author | Allan Wang <me@allanwang.ca> | 2020-03-01 22:54:38 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2020-03-01 22:54:38 -0800 |
commit | 6d29dcd17be1d5d2ddb99bf3befea125ca955420 (patch) | |
tree | 5a2a0669effbd22e0bdd7062193da42afdb6be7e | |
parent | e732e30d97babca49ba5f7d97aea1620ba14024b (diff) | |
parent | 23acc1b70887dc7b1a9600a8cdef1e2c9676665d (diff) | |
download | frost-6d29dcd17be1d5d2ddb99bf3befea125ca955420.tar.gz frost-6d29dcd17be1d5d2ddb99bf3befea125ca955420.tar.bz2 frost-6d29dcd17be1d5d2ddb99bf3befea125ca955420.zip |
Merge pull request #1658 from AllanWang/save-image
Save image
-rw-r--r-- | app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt | 197 | ||||
-rw-r--r-- | app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt | 13 | ||||
-rw-r--r-- | app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt | 2 | ||||
-rw-r--r-- | app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt | 2 | ||||
-rw-r--r-- | app/src/main/play/en-US/whatsnew | 3 | ||||
-rw-r--r-- | app/src/main/res/layout/activity_image.xml | 39 | ||||
-rw-r--r-- | app/src/main/res/values/styles.xml | 9 | ||||
-rw-r--r-- | app/src/main/res/xml/frost_changelog.xml | 2 | ||||
-rw-r--r-- | docs/Changelog.md | 1 |
9 files changed, 121 insertions, 147 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 6ae7622d..f8ba32c4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/activities/ImageActivity.kt @@ -21,19 +21,18 @@ import android.content.Intent import android.content.res.ColorStateList import android.graphics.Color import android.os.Bundle -import android.os.Environment 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.mediapicker.scanMedia -import ca.allanwang.kau.permissions.PERMISSION_WRITE_EXTERNAL_STORAGE -import ca.allanwang.kau.permissions.kauRequestPermissions 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 @@ -61,20 +60,13 @@ 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.Prefs +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 com.pitchedapps.frost.utils.sendFrostEmail import com.pitchedapps.frost.utils.setFrostColors -import java.io.File -import java.io.FileNotFoundException -import java.io.IOException -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import kotlin.math.abs -import kotlin.math.max import kotlinx.coroutines.CoroutineExceptionHandler import kotlinx.coroutines.Deferred import kotlinx.coroutines.Dispatchers @@ -82,7 +74,11 @@ import kotlinx.coroutines.async import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koin.android.ext.android.inject -import org.koin.core.inject +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import kotlin.math.abs +import kotlin.math.max /** * Created by Allan Wang on 2017-07-15. @@ -97,26 +93,10 @@ class ImageActivity : KauBaseActivity() { /** * Reference to the temporary file path */ - internal lateinit var tempFile: File - /** - * 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(binding.imageFab, prefs) - } + internal var tempFile: File? = null private lateinit var dragHelper: ViewDragHelper - private var imgExtension: String = ".jpg" - companion object { /** * Cache folder to store images @@ -145,7 +125,7 @@ class ImageActivity : KauBaseActivity() { "${abs(FB_IMAGE_ID_MATCHER.find(imageUrl)[1]?.hashCode() ?: 0)}_${abs(imageUrl.hashCode())}" } - private lateinit var binding: ActivityImageBinding + lateinit var binding: ActivityImageBinding private var bottomBehavior: BottomSheetBehavior<View>? = null private val baseBackgroundColor = if (prefs.blackMediaBg) Color.BLACK @@ -163,8 +143,8 @@ class ImageActivity : KauBaseActivity() { if (imageProgress.isVisible) imageProgress.fadeOut() } - tempFile.delete() - fabAction = FabStates.ERROR + tempFile?.delete() + binding.error.fadeIn() } override fun onCreate(savedInstanceState: Bundle?) { @@ -182,34 +162,31 @@ class ImageActivity : KauBaseActivity() { } binding = ActivityImageBinding.inflate(layoutInflater) setContentView(binding.root) - binding.onCreate() - tempFile = File(cacheDir(this), imageHash) + binding.init() launch(CoroutineExceptionHandler { _, throwable -> loadError(throwable) }) { - downloadImageTo(tempFile) + val tempFile = downloadTempImage() + this@ImageActivity.tempFile = tempFile binding.imageProgress.fadeOut() binding.imagePhoto.setImage(ImageSource.uri(frostUriFromFile(tempFile))) - fabAction = FabStates.DOWNLOAD binding.imagePhoto.animate().alpha(1f).scaleXY(1f).start() } } - private fun ActivityImageBinding.onCreate() { + private fun ActivityImageBinding.init() { imageContainer.setBackgroundColor(baseBackgroundColor) + toolbar.setBackgroundColor(baseBackgroundColor) this@ImageActivity.imageText.also { text -> if (text.isNullOrBlank()) { imageText.gone() } else { imageText.setTextColor(if (prefs.blackMediaBg) Color.WHITE else prefs.textColor) imageText.setBackgroundColor( - (if (prefs.blackMediaBg) Color.BLACK else prefs.bgColor) - .colorToForeground(0.2f).withAlpha(255) + baseBackgroundColor.colorToForeground(0.2f).withAlpha(255) ) imageText.text = text bottomBehavior = BottomSheetBehavior.from<View>(imageText).apply { - setBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { + addBottomSheetCallback(object : BottomSheetBehavior.BottomSheetCallback() { override fun onSlide(bottomSheet: View, slideOffset: Float) { - if (slideOffset == 0f && !imageFab.isShown) imageFab.show() - else if (slideOffset != 0f && imageFab.isShown) imageFab.hide() imageText.alpha = slideOffset / 2 + 0.5f } @@ -221,8 +198,24 @@ class ImageActivity : KauBaseActivity() { imageText.bringToFront() } } - imageProgress.tint(if (prefs.blackMediaBg) Color.WHITE else prefs.accentColor) - imageFab.setOnClickListener { fabAction.onClick(this@ImageActivity) } + val foregroundTint = if (prefs.blackMediaBg) Color.WHITE else prefs.accentColor + + fun ImageView.setState(state: FabStates) { + setIcon(state.iicon, color = foregroundTint, sizeDp = 24) + setOnClickListener { state.onClick(this@ImageActivity) } + } + + imageProgress.tint(foregroundTint) + error.apply { + invisible() + setState(FabStates.ERROR) + } + download.apply { + setState(FabStates.DOWNLOAD) + } + share.apply { + setState(FabStates.SHARE) + } imagePhoto.setOnImageEventListener(object : SubsamplingScaleImageView.DefaultOnImageEventListener() { override fun onImageLoadError(e: Exception) { @@ -267,7 +260,7 @@ class ImageActivity : KauBaseActivity() { scrollToTop = top < 0 val multiplier = max(1f - scrollPercent, 0f) - imageFab.alpha = multiplier + toolbar.alpha = multiplier bottomBehavior?.also { imageText.alpha = multiplier * (if (it.state == BottomSheetBehavior.STATE_COLLAPSED) 0.5f else 1f) @@ -305,100 +298,38 @@ class ImageActivity : KauBaseActivity() { return null } return when (type.substring(6)) { - "jpeg" -> ".jpg" - "png" -> ".png" - "gif" -> ".gif" + "jpeg" -> "jpg" + "png" -> "png" + "gif" -> "gif" else -> null } } @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, imgExtension, frostDir) - } - - /** - * 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 suspend fun downloadImageTo(file: File): Boolean { - val exceptionHandler = CoroutineExceptionHandler { _, err -> - if (file.isFile && file.length() == 0L) { - file.delete() - } - throw err + private suspend fun downloadTempImage(): File = withContext(Dispatchers.IO) { + val response = cookie.requestBuilder() + .url(trueImageUrl.await()) + .get() + .call() + .execute() + + if (!response.isSuccessful) { + throw IOException("Unsuccessful response for image: ${response.peekBody(128).string()}") } - return withContext(Dispatchers.IO + exceptionHandler) { - 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 false - } - // Fast route, image is already downloaded - if (tempFile.isFile && tempFile.length() > 0) { - if (tempFile == file) { - return@withContext false - } - tempFile.copyTo(file, true) - return@withContext true - } + val imgExtension = getImageExtension(response.header("Content-Type")) ?: "jpg" - // No temp file, download ourselves - val response = cookie.requestBuilder() - .url(trueImageUrl.await()) - .get() - .call() - .execute() + val body = response.body ?: throw IOException("Failed to retrieve image body") - if (!response.isSuccessful) { - throw IOException("Unsuccessful response for image: ${response.peekBody(128).string()}") - } - - imgExtension = getImageExtension(response.header("Content-Type")) ?: ".jpg" - - val body = response.body ?: throw IOException("Failed to retrieve image body") - - file.copyFromInputStream(body.byteStream()) - - return@withContext true + val tempFile = File(cacheDir(this@ImageActivity), "$imageHash.$imgExtension") + if (!tempFile.exists() || tempFile.length() == 0L) { + tempFile.copyFromInputStream(body.byteStream()) } + tempFile } - internal fun saveImage() { - kauRequestPermissions(PERMISSION_WRITE_EXTERNAL_STORAGE) { granted, _ -> - L.d { "Download image callback granted: $granted" } - if (granted) { - val errorHandler = CoroutineExceptionHandler { _, throwable -> - loadError(throwable) - frostSnackbar(R.string.image_download_fail) - } - launch(errorHandler) { - val destination = createPublicMediaFile() - downloadImageTo(destination) - L.d { "Download image async finished" } - scanMedia(destination) - savedFile = destination - frostSnackbar(R.string.image_download_success) - fabAction = FabStates.SHARE - } - } - } + internal suspend fun saveImage() { + frostDownload(cookie = cookie, url = trueImageUrl.await()) } override fun onDestroy() { @@ -435,12 +366,18 @@ internal enum class FabStates( override fun onClick(activity: ImageActivity) {} }, DOWNLOAD(GoogleMaterial.Icon.gmd_file_download) { - override fun onClick(activity: ImageActivity) = activity.saveImage() + override fun onClick(activity: ImageActivity) { + activity.launch { + activity.binding.download.fadeOut() + activity.saveImage() + } + } }, SHARE(GoogleMaterial.Icon.gmd_share) { override fun onClick(activity: ImageActivity) { + val file = activity.tempFile ?: return try { - val photoURI = activity.frostUriFromFile(activity.savedFile!!) + val photoURI = activity.frostUriFromFile(file) val intent = Intent(Intent.ACTION_SEND).apply { flags = Intent.FLAG_ACTIVITY_NEW_TASK putExtra(Intent.EXTRA_STREAM, photoURI) diff --git a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt index 5e909b03..60c642f4 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/utils/Downloader.kt @@ -22,6 +22,7 @@ import android.content.Context.DOWNLOAD_SERVICE import android.net.Uri import android.os.Environment import android.webkit.URLUtil +import androidx.core.content.getSystemService import ca.allanwang.kau.permissions.PERMISSION_WRITE_EXTERNAL_STORAGE import ca.allanwang.kau.permissions.kauRequestPermissions import ca.allanwang.kau.utils.isAppEnabled @@ -39,7 +40,7 @@ import com.pitchedapps.frost.facebook.USER_AGENT * With reference to <a href="https://stackoverflow.com/questions/33434532/android-webview-download-files-like-browsers-do">Stack Overflow</a> */ fun Context.frostDownload( - cookie: CookieEntity, + cookie: String?, url: String?, userAgent: String = USER_AGENT, contentDisposition: String? = null, @@ -51,7 +52,7 @@ fun Context.frostDownload( } fun Context.frostDownload( - cookie: CookieEntity, + cookie: String?, uri: Uri?, userAgent: String = USER_AGENT, contentDisposition: String? = null, @@ -64,7 +65,8 @@ fun Context.frostDownload( toast(R.string.error_invalid_download) return L.e { "Invalid download $uri" } } - if (!isAppEnabled(DOWNLOAD_MANAGER_PACKAGE)) { + val dm = getSystemService<DownloadManager>() + if (dm == null || !isAppEnabled(DOWNLOAD_MANAGER_PACKAGE)) { materialDialog { title(R.string.no_download_manager) message(R.string.no_download_manager_desc) @@ -80,14 +82,15 @@ fun Context.frostDownload( val request = DownloadManager.Request(uri) request.setMimeType(mimeType) val title = URLUtil.guessFileName(uri.toString(), contentDisposition, mimeType) - request.addRequestHeader("Cookie", cookie.cookie) + if (cookie != null) { + request.addRequestHeader("Cookie", cookie) + } request.addRequestHeader("User-Agent", userAgent) request.setDescription(string(R.string.downloading)) request.setTitle(title) request.allowScanningByMediaScanner() request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "Frost/$title") - val dm = getSystemService(DOWNLOAD_SERVICE) as DownloadManager try { dm.enqueue(request) } catch (e: Exception) { diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt index ec822bfa..20480d10 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostVideoViewer.kt @@ -118,7 +118,7 @@ class FrostVideoViewer @JvmOverloads constructor( R.id.action_pip -> video.isExpanded = false R.id.action_download -> context.ctxCoroutine.launchMain { val cookie = cookieDao.currentCookie(prefs) ?: return@launchMain - context.frostDownload(cookie, video.videoUri) + context.frostDownload(cookie.cookie, video.videoUri) } } true diff --git a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt index 9ca622a3..b04b8855 100644 --- a/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt +++ b/app/src/main/kotlin/com/pitchedapps/frost/views/FrostWebView.kt @@ -98,7 +98,7 @@ class FrostWebView @JvmOverloads constructor( context.ctxCoroutine.launchMain { val cookie = cookieDao.currentCookie(prefs) ?: return@launchMain context.frostDownload( - cookie, + cookie.cookie, url, userAgent, contentDisposition, diff --git a/app/src/main/play/en-US/whatsnew b/app/src/main/play/en-US/whatsnew index 4474eebc..cd456c04 100644 --- a/app/src/main/play/en-US/whatsnew +++ b/app/src/main/play/en-US/whatsnew @@ -1,4 +1,5 @@ v2.4.4 * Lots of under the hood fixes -* Fixed sharing
\ No newline at end of file +* Fixed sharing +* Fix photo downloads for Android Q+
\ No newline at end of file diff --git a/app/src/main/res/layout/activity_image.xml b/app/src/main/res/layout/activity_image.xml index 7d79cb74..6530dc4a 100644 --- a/app/src/main/res/layout/activity_image.xml +++ b/app/src/main/res/layout/activity_image.xml @@ -27,6 +27,37 @@ </com.pitchedapps.frost.views.DragFrame> + <LinearLayout + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_gravity="top" + android:gravity="end" + android:orientation="horizontal" + android:paddingStart="8dp" + android:paddingTop="10dp" + android:paddingEnd="8dp" + android:paddingBottom="10dp"> + + <ImageView + android:id="@+id/error" + style="@style/Image.Icon" /> + + <Space + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_weight="1" /> + + <ImageView + android:id="@+id/download" + style="@style/Image.Icon" /> + + <ImageView + android:id="@+id/share" + style="@style/Image.Icon" /> + + </LinearLayout> + <TextView android:id="@+id/image_text" android:layout_width="match_parent" @@ -36,12 +67,4 @@ app:behavior_peekHeight="44dp" app:layout_behavior="@string/bottom_sheet_behavior" /> - <com.google.android.material.floatingactionbutton.FloatingActionButton - android:id="@+id/image_fab" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="end|bottom" - android:layout_margin="@dimen/kau_fab_margin" - android:visibility="invisible" /> - </androidx.coordinatorlayout.widget.CoordinatorLayout> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index b94f754f..fcacacc3 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -182,4 +182,13 @@ <item name="android:paddingEnd">@dimen/drawer_nav_horizontal_margins</item> </style> + <style name="Image.Icon" parent=""> + <item name="android:layout_width">36dp</item> + <item name="android:layout_height">36dp</item> + <item name="android:layout_marginStart">8dp</item> + <item name="android:padding">6dp</item> + <item name="android:layout_marginEnd">8dp</item> + <item name="android:background">?selectableItemBackgroundBorderless</item> + </style> + </resources> diff --git a/app/src/main/res/xml/frost_changelog.xml b/app/src/main/res/xml/frost_changelog.xml index 99a48160..28ac4402 100644 --- a/app/src/main/res/xml/frost_changelog.xml +++ b/app/src/main/res/xml/frost_changelog.xml @@ -9,7 +9,7 @@ <version title="v2.4.4" /> <item text="Lots of under the hood fixes" /> <item text="Fixed sharing" /> - <item text="" /> + <item text="Fix photo downloads for Android Q+" /> <item text="" /> <version title="v2.4.3" /> diff --git a/docs/Changelog.md b/docs/Changelog.md index 4280780a..51ef89f8 100644 --- a/docs/Changelog.md +++ b/docs/Changelog.md @@ -3,6 +3,7 @@ ## v2.4.4 * Lots of under the hood fixes * Fixed sharing +* Fix photo downloads for Android Q+ ## v2.4.3 * Fix Android theme |