diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/ViewPagerActivity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/ViewPagerActivity.kt index d00343110..64365f414 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/ViewPagerActivity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/activities/ViewPagerActivity.kt @@ -16,7 +16,6 @@ import android.graphics.Bitmap import android.graphics.Color import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Icon -import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler @@ -47,7 +46,6 @@ import com.simplemobiletools.gallery.pro.R import com.simplemobiletools.gallery.pro.adapters.MyPagerAdapter import com.simplemobiletools.gallery.pro.asynctasks.GetMediaAsynctask import com.simplemobiletools.gallery.pro.dialogs.DeleteWithRememberDialog -import com.simplemobiletools.gallery.pro.dialogs.ResizeWithPathDialog import com.simplemobiletools.gallery.pro.dialogs.SaveAsDialog import com.simplemobiletools.gallery.pro.dialogs.SlideshowDialog import com.simplemobiletools.gallery.pro.extensions.* @@ -1047,62 +1045,9 @@ class ViewPagerActivity : SimpleActivity(), ViewPager.OnPageChangeListener, View } } - @TargetApi(Build.VERSION_CODES.N) private fun resizeImage() { val oldPath = getCurrentPath() - val originalSize = oldPath.getImageResolution(this) ?: return - ResizeWithPathDialog(this, originalSize, oldPath) { newSize, newPath -> - ensureBackgroundThread { - try { - var oldExif: ExifInterface? = null - if (isNougatPlus()) { - val inputStream = contentResolver.openInputStream(Uri.fromFile(File(oldPath))) - oldExif = ExifInterface(inputStream!!) - } - - val newBitmap = Glide.with(applicationContext).asBitmap().load(oldPath).submit(newSize.x, newSize.y).get() - - val newFile = File(newPath) - val newFileDirItem = FileDirItem(newPath, newPath.getFilenameFromPath()) - getFileOutputStream(newFileDirItem, true) { - if (it != null) { - saveBitmap(newFile, newBitmap, it, oldExif, File(oldPath).lastModified()) - } else { - toast(R.string.image_editing_failed) - } - } - } catch (e: OutOfMemoryError) { - toast(R.string.out_of_memory_error) - } catch (e: Exception) { - showErrorToast(e) - } - } - } - } - - @TargetApi(Build.VERSION_CODES.N) - private fun saveBitmap(file: File, bitmap: Bitmap, out: OutputStream, oldExif: ExifInterface?, lastModified: Long) { - try { - bitmap.compress(file.absolutePath.getCompressionFormat(), 90, out) - - if (isNougatPlus()) { - val newExif = ExifInterface(file.absolutePath) - oldExif?.copyNonDimensionAttributesTo(newExif) - } - } catch (e: Exception) { - } - - toast(R.string.file_saved) - val paths = arrayListOf(file.absolutePath) - rescanPaths(paths) { - fixDateTaken(paths, false) - - if (config.keepLastModified && lastModified != 0L) { - File(file.absolutePath).setLastModified(lastModified) - updateLastModified(file.absolutePath, lastModified) - } - } - out.close() + launchResizeImageDialog(oldPath) } private fun checkDeleteConfirmation() { diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/adapters/MediaAdapter.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/adapters/MediaAdapter.kt index b784dc0f2..a19d09394 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/adapters/MediaAdapter.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/adapters/MediaAdapter.kt @@ -30,14 +30,9 @@ import com.simplemobiletools.gallery.pro.interfaces.MediaOperationsListener import com.simplemobiletools.gallery.pro.models.Medium import com.simplemobiletools.gallery.pro.models.ThumbnailItem import com.simplemobiletools.gallery.pro.models.ThumbnailSection -import kotlinx.android.synthetic.main.photo_item_grid.view.* -import kotlinx.android.synthetic.main.thumbnail_section.view.* +import kotlinx.android.synthetic.main.photo_item_grid.view.file_type +import kotlinx.android.synthetic.main.thumbnail_section.view.thumbnail_section import kotlinx.android.synthetic.main.video_item_grid.view.* -import kotlinx.android.synthetic.main.video_item_grid.view.favorite -import kotlinx.android.synthetic.main.video_item_grid.view.media_item_holder -import kotlinx.android.synthetic.main.video_item_grid.view.medium_check -import kotlinx.android.synthetic.main.video_item_grid.view.medium_name -import kotlinx.android.synthetic.main.video_item_grid.view.medium_thumbnail class MediaAdapter( activity: BaseSimpleActivity, var media: ArrayList, val listener: MediaOperationsListener?, val isAGetIntent: Boolean, @@ -144,6 +139,7 @@ class MediaAdapter( findItem(R.id.cab_open_with).isVisible = isOneItemSelected findItem(R.id.cab_edit).isVisible = isOneItemSelected findItem(R.id.cab_set_as).isVisible = isOneItemSelected + findItem(R.id.cab_resize).isVisible = canResize(selectedItems) findItem(R.id.cab_confirm_selection).isVisible = isAGetIntent && allowMultiplePicks && selectedKeys.isNotEmpty() findItem(R.id.cab_restore_recycle_bin_files).isVisible = selectedPaths.all { it.startsWith(activity.recycleBinPath) } findItem(R.id.cab_create_shortcut).isVisible = isOreoPlus() && isOneItemSelected @@ -179,6 +175,7 @@ class MediaAdapter( R.id.cab_open_with -> openPath() R.id.cab_fix_date_taken -> fixDateTaken() R.id.cab_set_as -> setAs() + R.id.cab_resize -> resize() R.id.cab_delete -> checkDeleteConfirmation() } } @@ -286,6 +283,34 @@ class MediaAdapter( activity.setAs(path) } + private fun resize() { + val paths = getSelectedItems().filter { it.isImage() }.map { it.path } + if (isOneItemSelected()) { + val path = paths.first() + activity.launchResizeImageDialog(path) { + finishActMode() + listener?.refreshItems() + } + } else { + activity.launchResizeMultipleImagesDialog(paths) { + finishActMode() + listener?.refreshItems() + } + } + } + + private fun canResize(selectedItems: ArrayList): Boolean { + val selectionContainsImages = selectedItems.any { it.isImage() } + if (!selectionContainsImages) { + return false + } + + val parentPath = selectedItems.first { it.isImage() }.parentPath + val isCommonParent = selectedItems.all { parentPath == it.parentPath } + val isRestrictedDir = activity.isRestrictedWithSAFSdk30(parentPath) + return isExternalStorageManager() || (isCommonParent && !isRestrictedDir) + } + private fun toggleFileVisibility(hide: Boolean) { ensureBackgroundThread { getSelectedItems().forEach { diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/ResizeMultipleImagesDialog.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/ResizeMultipleImagesDialog.kt new file mode 100644 index 000000000..6066accde --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/ResizeMultipleImagesDialog.kt @@ -0,0 +1,123 @@ +package com.simplemobiletools.gallery.pro.dialogs + +import android.graphics.Point +import androidx.appcompat.app.AlertDialog +import com.simplemobiletools.commons.activities.BaseSimpleActivity +import com.simplemobiletools.commons.extensions.* +import com.simplemobiletools.commons.helpers.ensureBackgroundThread +import com.simplemobiletools.gallery.pro.R +import com.simplemobiletools.gallery.pro.extensions.ensureWriteAccess +import com.simplemobiletools.gallery.pro.extensions.rescanPathsAndUpdateLastModified +import com.simplemobiletools.gallery.pro.extensions.resizeImage +import kotlinx.android.synthetic.main.dialog_resize_multiple_images.view.resize_factor_edit_text +import kotlinx.android.synthetic.main.dialog_resize_multiple_images.view.resize_factor_input_layout +import kotlinx.android.synthetic.main.dialog_resize_multiple_images.view.resize_progress +import java.io.File +import kotlin.math.roundToInt + +private const val DEFAULT_RESIZE_FACTOR = "75" + +class ResizeMultipleImagesDialog( + private val activity: BaseSimpleActivity, + private val imagePaths: List, + private val imageSizes: List, + private val callback: () -> Unit +) { + + private var dialog: AlertDialog? = null + private val view = activity.layoutInflater.inflate(R.layout.dialog_resize_multiple_images, null) + private val progressView = view.resize_progress + private val resizeFactorEditText = view.resize_factor_edit_text + + init { + resizeFactorEditText.setText(DEFAULT_RESIZE_FACTOR) + progressView.apply { + max = imagePaths.size + setIndicatorColor(activity.getProperPrimaryColor()) + } + + activity.getAlertDialogBuilder() + .setPositiveButton(R.string.ok, null) + .setNegativeButton(R.string.cancel, null) + .apply { + activity.setupDialogStuff(view, this, R.string.resize_multiple_images) { alertDialog -> + dialog = alertDialog + alertDialog.showKeyboard(resizeFactorEditText) + + val positiveButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE) + val negativeButton = alertDialog.getButton(AlertDialog.BUTTON_NEGATIVE) + positiveButton.setOnClickListener { + val resizeFactorText = resizeFactorEditText.text?.toString() + if (resizeFactorText.isNullOrEmpty() || resizeFactorText.toInt() !in 10..90) { + activity.toast(R.string.resize_factor_error) + return@setOnClickListener + } + + val resizeFactor = resizeFactorText.toFloat().div(100) + + alertDialog.setCanceledOnTouchOutside(false) + arrayOf(view.resize_factor_input_layout, positiveButton, negativeButton).forEach { + it.isEnabled = false + it.alpha = 0.6f + } + resizeImages(resizeFactor) + } + } + } + } + + private fun resizeImages(factor: Float) { + progressView.show() + with(activity) { + val newSizes = imageSizes.map { + val width = (it.x * factor).roundToInt() + val height = (it.y * factor).roundToInt() + Point(width, height) + } + + val parentPath = imagePaths.first().getParentPath() + val pathsToRescan = arrayListOf() + val pathLastModifiedMap = mutableMapOf() + + ensureWriteAccess(parentPath) { + ensureBackgroundThread { + for (i in imagePaths.indices) { + val path = imagePaths[i] + val size = newSizes[i] + val lastModified = File(path).lastModified() + + try { + resizeImage(path, size) { + if (it) { + pathsToRescan.add(path) + pathLastModifiedMap[path] = lastModified + runOnUiThread { + progressView.progress = i + 1 + } + } + } + } catch (e: OutOfMemoryError) { + toast(R.string.out_of_memory_error) + } catch (e: Exception) { + showErrorToast(e) + } + } + + val failureCount = imagePaths.size - pathsToRescan.size + if (failureCount > 0) { + toast(resources.getQuantityString(R.plurals.failed_to_resize_images, failureCount, failureCount)) + } else { + toast(R.string.images_resized_successfully) + } + + rescanPathsAndUpdateLastModified(pathsToRescan, pathLastModifiedMap) { + activity.runOnUiThread { + dialog?.dismiss() + callback.invoke() + } + } + } + } + } + } +} diff --git a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/extensions/Activity.kt b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/extensions/Activity.kt index fe096ba67..c6519fcbc 100644 --- a/app/src/main/kotlin/com/simplemobiletools/gallery/pro/extensions/Activity.kt +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/extensions/Activity.kt @@ -8,6 +8,7 @@ import android.content.Intent import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.Matrix +import android.graphics.Point import android.graphics.drawable.Drawable import android.graphics.drawable.LayerDrawable import android.net.Uri @@ -39,6 +40,8 @@ import com.simplemobiletools.gallery.pro.activities.SettingsActivity import com.simplemobiletools.gallery.pro.activities.SimpleActivity import com.simplemobiletools.gallery.pro.dialogs.AllFilesPermissionDialog import com.simplemobiletools.gallery.pro.dialogs.PickDirectoryDialog +import com.simplemobiletools.gallery.pro.dialogs.ResizeMultipleImagesDialog +import com.simplemobiletools.gallery.pro.dialogs.ResizeWithPathDialog import com.simplemobiletools.gallery.pro.helpers.DIRECTORY import com.simplemobiletools.gallery.pro.helpers.RECYCLE_BIN import com.simplemobiletools.gallery.pro.models.DateTaken @@ -734,6 +737,136 @@ fun BaseSimpleActivity.copyFile(source: String, destination: String) { } } +fun BaseSimpleActivity.ensureWriteAccess(path: String, callback: () -> Unit) { + when { + isRestrictedSAFOnlyRoot(path) -> { + handleAndroidSAFDialog(path) { + if (!it) { + return@handleAndroidSAFDialog + } + callback.invoke() + } + } + + needsStupidWritePermissions(path) -> { + handleSAFDialog(path) { + if (!it) { + return@handleSAFDialog + } + callback() + } + } + + isAccessibleWithSAFSdk30(path) -> { + handleSAFDialogSdk30(path) { + if (!it) { + return@handleSAFDialogSdk30 + } + callback() + } + } + + else -> { + callback() + } + } +} + +fun BaseSimpleActivity.launchResizeMultipleImagesDialog(paths: List, callback: (() -> Unit)? = null) { + ensureBackgroundThread { + val imagePaths = mutableListOf() + val imageSizes = mutableListOf() + for (path in paths) { + val size = path.getImageResolution(this) + if (size != null) { + imagePaths.add(path) + imageSizes.add(size) + } + } + + runOnUiThread { + ResizeMultipleImagesDialog(this, imagePaths, imageSizes) { + callback?.invoke() + } + } + } +} + +fun BaseSimpleActivity.launchResizeImageDialog(path: String, callback: (() -> Unit)? = null) { + val originalSize = path.getImageResolution(this) ?: return + ResizeWithPathDialog(this, originalSize, path) { newSize, newPath -> + ensureBackgroundThread { + val file = File(newPath) + val pathLastModifiedMap = mapOf(file.absolutePath to file.lastModified()) + try { + resizeImage(newPath, newSize) { success -> + if (success) { + toast(R.string.file_saved) + + val paths = arrayListOf(file.absolutePath) + rescanPathsAndUpdateLastModified(paths, pathLastModifiedMap) { + runOnUiThread { + callback?.invoke() + } + } + } else { + toast(R.string.image_editing_failed) + } + } + } catch (e: OutOfMemoryError) { + toast(R.string.out_of_memory_error) + } catch (e: Exception) { + showErrorToast(e) + } + } + } +} + +fun BaseSimpleActivity.resizeImage(path: String, size: Point, callback: (success: Boolean) -> Unit) { + var oldExif: ExifInterface? = null + if (isNougatPlus()) { + val inputStream = contentResolver.openInputStream(Uri.fromFile(File(path))) + oldExif = ExifInterface(inputStream!!) + } + + val newBitmap = Glide.with(applicationContext).asBitmap().load(path).submit(size.x, size.y).get() + + val newFile = File(path) + val newFileDirItem = FileDirItem(path, path.getFilenameFromPath()) + getFileOutputStream(newFileDirItem, true) { out -> + if (out != null) { + out.use { + try { + newBitmap.compress(newFile.absolutePath.getCompressionFormat(), 90, out) + + if (isNougatPlus()) { + val newExif = ExifInterface(newFile.absolutePath) + oldExif?.copyNonDimensionAttributesTo(newExif) + } + } catch (ignored: Exception) { + } + + callback(true) + } + } else { + callback(false) + } + } +} + +fun BaseSimpleActivity.rescanPathsAndUpdateLastModified(paths: ArrayList, pathLastModifiedMap: Map, callback: () -> Unit) { + fixDateTaken(paths, false) + for (path in paths) { + val file = File(path) + val lastModified = pathLastModifiedMap[path] + if (config.keepLastModified && lastModified != null && lastModified != 0L) { + File(file.absolutePath).setLastModified(lastModified) + updateLastModified(file.absolutePath, lastModified) + } + } + rescanPaths(paths, callback) +} + fun saveFile(path: String, bitmap: Bitmap, out: FileOutputStream, degrees: Int) { val matrix = Matrix() matrix.postRotate(degrees.toFloat()) diff --git a/app/src/main/res/layout/dialog_resize_multiple_images.xml b/app/src/main/res/layout/dialog_resize_multiple_images.xml new file mode 100644 index 000000000..a9aa638fe --- /dev/null +++ b/app/src/main/res/layout/dialog_resize_multiple_images.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/menu/cab_media.xml b/app/src/main/res/menu/cab_media.xml index f861bac02..9be65e89d 100644 --- a/app/src/main/res/menu/cab_media.xml +++ b/app/src/main/res/menu/cab_media.xml @@ -83,6 +83,11 @@ android:showAsAction="never" android:title="@string/set_as" app:showAsAction="never" /> +