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..220984953 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.* @@ -1050,34 +1048,7 @@ 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) - } - } - } + launchResizeImageDialog(oldPath) } @TargetApi(Build.VERSION_CODES.N) 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..521cb6f76 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 @@ -144,6 +144,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 = selectedItems.all { it.isImage() } 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 +180,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 +288,20 @@ class MediaAdapter( activity.setAs(path) } + private fun resize() { + val paths = getSelectedPaths() + if (isOneItemSelected()) { + val path = paths.first() + activity.launchResizeImageDialog(path) { + finishActMode() + } + } else { + activity.launchResizeMultipleImagesDialog(paths) { + finishActMode() + } + } + } + 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..e1b96f357 --- /dev/null +++ b/app/src/main/kotlin/com/simplemobiletools/gallery/pro/dialogs/ResizeMultipleImagesDialog.kt @@ -0,0 +1,160 @@ +package com.simplemobiletools.gallery.pro.dialogs + +import android.graphics.Point +import android.os.Handler +import android.os.Looper +import android.view.View +import androidx.appcompat.app.AlertDialog +import androidx.core.widget.doAfterTextChanged +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.config +import com.simplemobiletools.gallery.pro.extensions.ensureWriteAccess +import com.simplemobiletools.gallery.pro.extensions.fixDateTaken +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_info +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" +private const val RESIZE_FACTOR_ERROR_DELAY = 800L + +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 { + setupViews(view) + 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() + val resizeFactor = try { + resizeFactorText?.toFloat()?.div(100) + } catch (e: Exception) { + null + } + + if (resizeFactor == null) { + activity.toast(R.string.resize_factor_error) + return@setOnClickListener + } + + alertDialog.setCanceledOnTouchOutside(false) + arrayOf(view.resize_factor_input_layout, view.resize_factor_info, positiveButton, negativeButton).forEach { + it.isEnabled = false + it.alpha = 0.6f + } + resizeImages(resizeFactor) + } + } + } + } + + private fun resizeImages(factor: Float) { + progressView.show() + ensureBackgroundThread { + 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() + + ensureWriteAccess(parentPath) { + for (i in imagePaths.indices) { + val path = imagePaths[i] + val size = newSizes[i] + + try { + resizeImage(path, size) { + if (it) { + pathsToRescan.add(path) + 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(getString(R.string.failed_to_resize_images, failureCount)) + } else { + toast(R.string.images_resized_successfully) + } + + rescanPaths(pathsToRescan) { + fixDateTaken(pathsToRescan, false) + for (path in pathsToRescan) { + val file = File(path) + val lastModified = file.lastModified() + if (config.keepLastModified && lastModified != 0L) { + File(file.absolutePath).setLastModified(lastModified) + updateLastModified(file.absolutePath, lastModified) + } + } + } + activity.runOnUiThread { + dialog?.dismiss() + callback.invoke() + } + } + } + } + } + + private fun setupViews(view: View) { + val handler = Handler(Looper.getMainLooper()) + val resizeFactorInputLayout = view.resize_factor_input_layout + view.resize_factor_edit_text.apply { + setText(DEFAULT_RESIZE_FACTOR) + doAfterTextChanged { + resizeFactorInputLayout.error = null + handler.removeCallbacksAndMessages(null) + handler.postDelayed({ + val factorText = it?.toString() + if (factorText.isNullOrEmpty() || factorText.toInt() !in 10..90) { + resizeFactorInputLayout.error = activity.getString(R.string.resize_factor_error) + } else { + resizeFactorInputLayout.error = null + } + }, RESIZE_FACTOR_ERROR_DELAY) + } + } + + progressView.apply { + max = imagePaths.size + setIndicatorColor(activity.getProperPrimaryColor()) + } + } +} 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..a93311a08 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,124 @@ 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() + } + } +} + +@TargetApi(Build.VERSION_CODES.N) +fun BaseSimpleActivity.launchResizeMultipleImagesDialog(paths: List, callback: (() -> Unit)? = null) { + val imagePaths = mutableListOf() + val imageSizes = mutableListOf() + for (path in paths) { + val size = path.getImageResolution(this) + if (size != null) { + imagePaths.add(path) + imageSizes.add(size) + } + } + + ResizeMultipleImagesDialog(this, imagePaths, imageSizes) { + callback?.invoke() + } +} + +@TargetApi(Build.VERSION_CODES.N) +fun BaseSimpleActivity.launchResizeImageDialog(path: String, callback: (() -> Unit)? = null) { + val originalSize = path.getImageResolution(this) ?: return + ResizeWithPathDialog(this, originalSize, path) { newSize, newPath -> + ensureBackgroundThread { + try { + resizeImage(newPath, newSize) { success -> + if (success) { + toast(R.string.file_saved) + + val file = File(path) + val lastModified = file.lastModified() + val paths = arrayListOf(file.absolutePath) + rescanPaths(paths) { + fixDateTaken(paths, false) + if (config.keepLastModified && lastModified != 0L) { + File(file.absolutePath).setLastModified(lastModified) + updateLastModified(file.absolutePath, lastModified) + } + } + + 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 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..e5792fe47 --- /dev/null +++ b/app/src/main/res/layout/dialog_resize_multiple_images.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + 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" /> +