Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

♻️ ⚡ Improve uploading image #375

Merged
merged 8 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -2,68 +2,106 @@ package net.pengcook.android.presentation.core.util

import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.Matrix
import android.media.ExifInterface
import android.net.Uri
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.IOException
import java.io.FileOutputStream
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class ImageUtils(private val context: Context) {
class ImageUtils(
private val context: Context,
) {
private fun createTempImageFile(): File {
val timeStamp: String =
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
SimpleDateFormat(DATA_FORMAT, Locale.getDefault()).format(Date())
val storageDir: File? = context.getExternalFilesDir(null)
return File.createTempFile(
"JPEG_${timeStamp}_",
".jpg",
FILE_SUFFIX,
storageDir,
)
}

fun createImageFile(): File {
val timeStamp: String =
SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
SimpleDateFormat(DATA_FORMAT, Locale.getDefault()).format(Date())
val storageDir: File? = context.getExternalFilesDir(null)
return File
.createTempFile(
"JPEG_${timeStamp}_",
".jpg",
FILE_SUFFIX,
storageDir,
)
}

fun getUriForFile(file: File): Uri {
return FileProvider.getUriForFile(
fun getUriForFile(file: File): Uri =
FileProvider.getUriForFile(
context,
"net.pengcook.android.fileprovider",
file,
)
}

fun processImageUri(uri: Uri): String? {
return try {
fun isPermissionGranted(permissions: Array<String>): Boolean =
permissions.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}

suspend fun compressAndResizeImage(uri: Uri): File =
withContext(Dispatchers.IO) {
val inputStream = context.contentResolver.openInputStream(uri)
if (inputStream != null) {
val tempFile = createTempImageFile()
tempFile.outputStream().use { outputStream ->
inputStream.copyTo(outputStream)
}
tempFile.absolutePath
} else {
null
}
} catch (e: IOException) {
e.printStackTrace()
null
val originalBitmap = BitmapFactory.decodeStream(inputStream)

val resizedBitmap =
adjustImageOrientation(
Bitmap.createScaledBitmap(
originalBitmap,
MAX_WIDTH,
MAX_HEIGHT,
true,
),
uri,
)

val compressedFile = createTempImageFile()
val outputStream = FileOutputStream(compressedFile)
resizedBitmap.compress(Bitmap.CompressFormat.JPEG, COMPRESSED_QUALITY, outputStream)
outputStream.flush()
outputStream.close()

compressedFile
}
}

fun isPermissionGranted(permissions: Array<String>): Boolean {
return permissions.all {
ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
private fun adjustImageOrientation(
bitmap: Bitmap,
uri: Uri,
): Bitmap {
val inputStream = context.contentResolver.openInputStream(uri)
val exif = ExifInterface(inputStream!!)
val orientation =
exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
val matrix = Matrix()
when (orientation) {
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.postRotate(90f)
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.postRotate(180f)
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.postRotate(270f)
}
return Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
}

companion object {
private const val COMPRESSED_QUALITY = 80
private const val MAX_WIDTH = 1080
private const val DATA_FORMAT = "yyyyMMdd_HHmmss"
private const val FILE_SUFFIX = ".jpg"
private const val MAX_HEIGHT = 1080
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.activity.result.contract.ActivityResultContracts
import androidx.datastore.core.IOException
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.pengcook.android.R
import net.pengcook.android.databinding.FragmentRecipeMakingBinding
import net.pengcook.android.presentation.core.util.AnalyticsLogging
Expand Down Expand Up @@ -65,7 +70,7 @@ class RecipeMakingFragment : Fragment() {
if (currentPhotoPath != null) {
viewModel.fetchImageUri(File(currentPhotoPath!!).name)
} else {
processImageUri(photoUri)
compressAndFetchPresignedUrl(photoUri)
}
} ?: run {
showSnackBar(getString(R.string.image_selection_failed))
Expand Down Expand Up @@ -119,12 +124,19 @@ class RecipeMakingFragment : Fragment() {
takePictureLauncher.launch(photoUri)
}

private fun processImageUri(uri: Uri) {
currentPhotoPath = imageUtils.processImageUri(uri)
if (currentPhotoPath != null) {
viewModel.fetchImageUri(File(currentPhotoPath!!).name)
} else {
showSnackBar(getString(R.string.image_selection_failed))
private fun compressAndFetchPresignedUrl(uri: Uri) {
viewLifecycleOwner.lifecycleScope.launch {
try {
val compressedFile = imageUtils.compressAndResizeImage(uri)
currentPhotoPath = compressedFile.absolutePath

viewModel.fetchImageUri(File(currentPhotoPath!!).name)
} catch (e: IOException) {
e.printStackTrace()
withContext(Dispatchers.Main) {
showSnackBar(getString(R.string.image_selection_failed))
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@ import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.datastore.core.IOException
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.navigation.fragment.navArgs
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.pengcook.android.R
import net.pengcook.android.databinding.FragmentMakingStepBinding
import net.pengcook.android.presentation.core.util.AnalyticsLogging
Expand Down Expand Up @@ -72,7 +77,7 @@ class StepMakingFragment : Fragment() {
if (currentPhotoPath != null) {
viewModel.fetchImageUri(File(currentPhotoPath!!).name)
} else {
processImageUri(photoUri)
compressAndFetchPresignedUrl(photoUri)
}
} ?: run {
showToast(getString(R.string.image_selection_failed))
Expand Down Expand Up @@ -181,12 +186,18 @@ class StepMakingFragment : Fragment() {
viewModel.uploadImageToS3(presignedUrl, file)
}

private fun processImageUri(uri: Uri) {
currentPhotoPath = imageUtils.processImageUri(uri)
if (currentPhotoPath != null) {
viewModel.fetchImageUri(File(currentPhotoPath!!).name)
} else {
showToast(getString(R.string.image_selection_failed))
private fun compressAndFetchPresignedUrl(uri: Uri) {
viewLifecycleOwner.lifecycleScope.launch {
try {
val compressedFile = imageUtils.compressAndResizeImage(uri)
currentPhotoPath = compressedFile.absolutePath
viewModel.fetchImageUri(File(currentPhotoPath!!).name)
} catch (e: IOException) {
e.printStackTrace()
withContext(Dispatchers.Main) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이건 인정입니다!

showToast(getString(R.string.image_selection_failed))
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.activity.result.contract.ActivityResultContracts
import androidx.datastore.core.IOException
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
Expand All @@ -15,6 +16,7 @@ import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.pengcook.android.R
import net.pengcook.android.databinding.FragmentEditProfileBinding
import net.pengcook.android.presentation.core.util.AnalyticsLogging
Expand Down Expand Up @@ -43,7 +45,7 @@ class EditProfileFragment : Fragment() {
if (currentPhotoPath != null) {
viewModel.fetchImageUri(File(currentPhotoPath!!).name)
} else {
processImageUri(photoUri)
compressAndFetchPresignedUrl(photoUri)
}
} ?: run {
showSnackBar(getString(R.string.image_selection_failed))
Expand Down Expand Up @@ -86,12 +88,19 @@ class EditProfileFragment : Fragment() {
viewModel.uploadImageToS3(presignedUrl, file)
}

private fun processImageUri(uri: Uri) {
currentPhotoPath = imageUtils.processImageUri(uri)
if (currentPhotoPath != null) {
viewModel.fetchImageUri(File(currentPhotoPath!!).name)
} else {
showSnackBar(getString(R.string.image_selection_failed))
private fun compressAndFetchPresignedUrl(uri: Uri) {
viewLifecycleOwner.lifecycleScope.launch {
try {
val compressedFile = imageUtils.compressAndResizeImage(uri)
currentPhotoPath = compressedFile.absolutePath

viewModel.fetchImageUri(File(currentPhotoPath!!).name)
} catch (e: IOException) {
e.printStackTrace()
withContext(Dispatchers.Main) {
showSnackBar(getString(R.string.image_selection_failed))
}
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import com.google.android.material.snackbar.Snackbar
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import net.pengcook.android.R
import net.pengcook.android.databinding.FragmentSignUpBinding
import net.pengcook.android.presentation.core.util.AnalyticsLogging
Expand All @@ -28,6 +29,7 @@ import net.pengcook.android.presentation.core.util.ImageUtils
import java.io.ByteArrayOutputStream
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import javax.inject.Inject

@AndroidEntryPoint
Expand Down Expand Up @@ -57,7 +59,7 @@ class SignUpFragment : Fragment() {
if (currentPhotoPath != null) {
viewModel.fetchImageUri(File(currentPhotoPath!!).name)
} else {
processImageUri(photoUri)
compressAndFetchPresignedUrl(photoUri)
}
} ?: run {
showSnackBar(getString(R.string.image_selection_failed))
Expand Down Expand Up @@ -100,12 +102,19 @@ class SignUpFragment : Fragment() {
viewModel.uploadImageToS3(presignedUrl, file)
}

private fun processImageUri(uri: Uri) {
currentPhotoPath = imageUtils.processImageUri(uri)
if (currentPhotoPath != null) {
viewModel.fetchImageUri(File(currentPhotoPath!!).name)
} else {
showSnackBar(getString(R.string.image_selection_failed))
private fun compressAndFetchPresignedUrl(uri: Uri) {
viewLifecycleOwner.lifecycleScope.launch {
try {
val compressedFile = imageUtils.compressAndResizeImage(uri)
currentPhotoPath = compressedFile.absolutePath

viewModel.fetchImageUri(File(currentPhotoPath!!).name)
} catch (e: IOException) {
e.printStackTrace()
withContext(Dispatchers.Main) {
showSnackBar(getString(R.string.image_selection_failed))
}
}
}
}

Expand Down
Loading