From 43f691e92220a636bfe6d910d6321c6f624e0689 Mon Sep 17 00:00:00 2001 From: Aman Bansal Date: Mon, 11 Mar 2019 15:58:17 +0530 Subject: [PATCH] fix: Selected image get rotate in edit profile --- .../openevent/general/RotateBitmap.kt | 114 ++++++++++++++++++ .../general/auth/EditProfileFragment.kt | 72 ++++++----- .../general/auth/EditProfileViewModel.kt | 20 ++- .../general/auth/ProfileViewModel.kt | 1 - 4 files changed, 175 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/org/fossasia/openevent/general/RotateBitmap.kt diff --git a/app/src/main/java/org/fossasia/openevent/general/RotateBitmap.kt b/app/src/main/java/org/fossasia/openevent/general/RotateBitmap.kt new file mode 100644 index 0000000000..a7344c7a5c --- /dev/null +++ b/app/src/main/java/org/fossasia/openevent/general/RotateBitmap.kt @@ -0,0 +1,114 @@ +package org.fossasia.openevent.general + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Matrix +import android.net.Uri +import androidx.exifinterface.media.ExifInterface +import java.io.IOException + +/** + * Handle the image rotation + */ + +class RotateBitmap { + + private var context: Context? = null + + /** + * This method is responsible for solving the rotation issue if exist. Also scale the images to + * 1024x1024 resolution + * + * @param selectedImage The Image URI + * @return Bitmap image results + * @throws IOException + */ + @Throws(IOException::class) + fun handleSamplingAndRotationBitmap(context: Context, selectedImage: Uri): Bitmap? { + this.context = context + val MAX_HEIGHT = 1024 + val MAX_WIDTH = 1024 + + // First decode with inJustDecodeBounds=true to check dimensions + val options = BitmapFactory.Options() + options.inJustDecodeBounds = true + var imageStream = context.contentResolver.openInputStream(selectedImage) + BitmapFactory.decodeStream(imageStream, null, options) + imageStream?.close() + + // Calculate inSampleSize + options.inSampleSize = calculateInSampleSize(options, MAX_WIDTH, MAX_HEIGHT) + + // Decode bitmap with inSampleSize set + options.inJustDecodeBounds = false + imageStream = context.contentResolver.openInputStream(selectedImage) + var img = BitmapFactory.decodeStream(imageStream, null, options) + + img = rotateImageIfRequired(img, selectedImage) + return img + } + + @Throws(IOException::class) + private fun rotateImageIfRequired(img: Bitmap?, selectedImage: Uri): Bitmap? { + + val input = context?.contentResolver?.openInputStream(selectedImage) + val ei = input?.let { ExifInterface(it) } + val orientation = ei?.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) + return when (orientation) { + ExifInterface.ORIENTATION_ROTATE_90 -> rotateImage(img, 90) + ExifInterface.ORIENTATION_ROTATE_180 -> rotateImage(img, 180) + ExifInterface.ORIENTATION_ROTATE_270 -> rotateImage(img, 270) + else -> img + } + } + + companion object { + + private fun rotateImage(img: Bitmap?, degree: Int): Bitmap? { + val matrix = Matrix() + matrix.postRotate(degree.toFloat()) + val rotatedImg = img?.let { Bitmap.createBitmap(it, 0, 0, img.width, img.height, matrix, true) } + img?.recycle() + return rotatedImg + } + + private fun calculateInSampleSize( + options: BitmapFactory.Options, + reqWidth: Int, + reqHeight: Int + ): Int { + // Raw height and width of image + val height = options.outHeight + val width = options.outWidth + var inSampleSize = 1 + + if (height > reqHeight || width > reqWidth) { + + // Calculate ratios of height and width to requested height and width + val heightRatio = Math.round(height.toFloat() / reqHeight.toFloat()) + val widthRatio = Math.round(width.toFloat() / reqWidth.toFloat()) + + // Choose the smallest ratio as inSampleSize value, this will guarantee a final image + // with both dimensions larger than or equal to the requested height and width. + inSampleSize = if (heightRatio < widthRatio) heightRatio else widthRatio + + // This offers some additional logic in case the image has a strange + // aspect ratio. For example, a panorama may have a much larger + // width than height. In these cases the total pixels might still + // end up being too large to fit comfortably in memory, so we should + // be more aggressive with sample down the image (=larger inSampleSize). + + val totalPixels = (width * height).toFloat() + + // Anything more than 2x the requested pixels we'll sample down further + val totalReqPixelsCap = (reqWidth * reqHeight * 2).toFloat() + + while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) { + inSampleSize++ + } + } + return inSampleSize + } + } +} diff --git a/app/src/main/java/org/fossasia/openevent/general/auth/EditProfileFragment.kt b/app/src/main/java/org/fossasia/openevent/general/auth/EditProfileFragment.kt index dab33d84f1..4e7124255a 100644 --- a/app/src/main/java/org/fossasia/openevent/general/auth/EditProfileFragment.kt +++ b/app/src/main/java/org/fossasia/openevent/general/auth/EditProfileFragment.kt @@ -6,8 +6,6 @@ import androidx.appcompat.app.AlertDialog import android.content.Intent import android.content.pm.PackageManager import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.net.Uri import android.os.Bundle import android.util.Base64 import android.view.LayoutInflater @@ -23,6 +21,7 @@ import com.squareup.picasso.Picasso import kotlinx.android.synthetic.main.fragment_edit_profile.view.editProfileCoordinatorLayout import kotlinx.android.synthetic.main.fragment_edit_profile.view.updateButton import kotlinx.android.synthetic.main.fragment_edit_profile.view.firstName +import com.squareup.picasso.MemoryPolicy import kotlinx.android.synthetic.main.fragment_edit_profile.view.lastName import kotlinx.android.synthetic.main.fragment_edit_profile.view.profilePhoto import kotlinx.android.synthetic.main.fragment_edit_profile.view.progressBar @@ -30,6 +29,7 @@ import kotlinx.android.synthetic.main.fragment_edit_profile.view.profilePhotoFab import org.fossasia.openevent.general.CircleTransform import org.fossasia.openevent.general.MainActivity import org.fossasia.openevent.general.R +import org.fossasia.openevent.general.RotateBitmap import org.fossasia.openevent.general.utils.Utils.hideSoftKeyboard import org.fossasia.openevent.general.utils.Utils.requireDrawable import org.fossasia.openevent.general.utils.extensions.nonNull @@ -37,6 +37,9 @@ import org.fossasia.openevent.general.utils.nullToEmpty import org.koin.androidx.viewmodel.ext.android.viewModel import timber.log.Timber import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream +import java.io.IOException import java.io.FileNotFoundException import org.fossasia.openevent.general.utils.Utils.setToolbar import org.jetbrains.anko.design.snackbar @@ -47,14 +50,12 @@ class EditProfileFragment : Fragment() { private val editProfileViewModel by viewModel() private lateinit var rootView: View private var permissionGranted = false - private var encodedImage: String? = null private val PICK_IMAGE_REQUEST = 100 private val READ_STORAGE = arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE) private val REQUEST_CODE = 1 private lateinit var userFirstName: String private lateinit var userLastName: String - private var avatarUpdated: Boolean = false override fun onCreateView( inflater: LayoutInflater, @@ -75,7 +76,7 @@ class EditProfileFragment : Fragment() { if (rootView.lastName.text.isNullOrBlank()) { rootView.lastName.setText(userLastName) } - if (!imageUrl.isEmpty() && !avatarUpdated) { + if (imageUrl.isNotEmpty() && !editProfileViewModel.avatarUpdated) { val drawable = requireDrawable(requireContext(), R.drawable.ic_account_circle_grey) Picasso.get() .load(imageUrl) @@ -84,16 +85,7 @@ class EditProfileFragment : Fragment() { .into(rootView.profilePhoto) } }) - profileViewModel.avatarPicked.observe(this, Observer { - if (it != null) { - Picasso.get() - .load(Uri.parse(it)) - .placeholder(requireDrawable(requireContext(), R.drawable.ic_account_circle_grey)) - .transform(CircleTransform()) - .into(rootView.profilePhoto) - this.avatarUpdated = true - } - }) + profileViewModel.fetchProfile() editProfileViewModel.progress @@ -102,12 +94,25 @@ class EditProfileFragment : Fragment() { rootView.progressBar.isVisible = it }) + editProfileViewModel.getUpdatedTempFile() + .nonNull() + .observe(viewLifecycleOwner, Observer { file -> + // prevent picasso from storing tempAvatar cache, + // if user select another image picasso will display tempAvatar instead of its own cache + Picasso.get() + .load(file) + .placeholder(requireDrawable(requireContext(), R.drawable.ic_person_black)) + .memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE) + .transform(CircleTransform()) + .into(rootView.profilePhoto) + }) + permissionGranted = (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) rootView.updateButton.setOnClickListener { hideSoftKeyboard(context, rootView) - editProfileViewModel.updateProfile(encodedImage, rootView.firstName.text.toString(), + editProfileViewModel.updateProfile(rootView.firstName.text.toString(), rootView.lastName.text.toString()) } @@ -138,20 +143,12 @@ class EditProfileFragment : Fragment() { val imageUri = intentData.data ?: return try { - val imageStream = activity?.contentResolver?.openInputStream(imageUri) - val selectedImage = BitmapFactory.decodeStream(imageStream) - encodedImage = encodeImage(selectedImage) + val selectedImage = RotateBitmap().handleSamplingAndRotationBitmap(requireContext(), imageUri) + editProfileViewModel.encodedImage = selectedImage?.let { encodeImage(it) } + editProfileViewModel.avatarUpdated = true } catch (e: FileNotFoundException) { Timber.d(e, "File Not Found Exception") } - - Picasso.get() - .load(imageUri) - .placeholder(requireDrawable(requireContext(), R.drawable.ic_person_black)) - .transform(CircleTransform()) - .into(rootView.profilePhoto) - avatarUpdated = true - profileViewModel.avatarPicked.value = imageUri.toString() } } @@ -160,6 +157,23 @@ class EditProfileFragment : Fragment() { bitmap.compress(Bitmap.CompressFormat.JPEG, 100, baos) val bytes = baos.toByteArray() + //create temp file + try { + + val tempAvatar = File(context?.cacheDir, "tempAvatar") + if (tempAvatar.exists()) { + tempAvatar.delete() + } + val fos = FileOutputStream(tempAvatar) + fos.write(bytes) + fos.flush() + fos.close() + + editProfileViewModel.setUpdatedTempFile(tempAvatar) + } catch (e: IOException) { + e.printStackTrace() + } + return "data:image/jpeg;base64," + Base64.encodeToString(bytes, Base64.DEFAULT) } @@ -207,7 +221,7 @@ class EditProfileFragment : Fragment() { */ fun handleBackPress() { val thisActivity = activity - if (!avatarUpdated && rootView.lastName.text.toString() == userLastName && + if (!editProfileViewModel.avatarUpdated && rootView.lastName.text.toString() == userLastName && rootView.firstName.text.toString() == userFirstName) { if (thisActivity is MainActivity) thisActivity.onSuperBackPressed() } else { @@ -218,7 +232,7 @@ class EditProfileFragment : Fragment() { if (thisActivity is MainActivity) thisActivity.onSuperBackPressed() } dialog.setPositiveButton(getString(R.string.save)) { _, _ -> - editProfileViewModel.updateProfile(encodedImage, rootView.firstName.text.toString(), + editProfileViewModel.updateProfile(rootView.firstName.text.toString(), rootView.lastName.text.toString()) } dialog.create().show() diff --git a/app/src/main/java/org/fossasia/openevent/general/auth/EditProfileViewModel.kt b/app/src/main/java/org/fossasia/openevent/general/auth/EditProfileViewModel.kt index 89f9458f23..6cd173c573 100644 --- a/app/src/main/java/org/fossasia/openevent/general/auth/EditProfileViewModel.kt +++ b/app/src/main/java/org/fossasia/openevent/general/auth/EditProfileViewModel.kt @@ -10,6 +10,7 @@ import org.fossasia.openevent.general.R import org.fossasia.openevent.general.common.SingleLiveEvent import org.fossasia.openevent.general.data.Resource import timber.log.Timber +import java.io.File class EditProfileViewModel( private val authService: AuthService, @@ -25,10 +26,17 @@ class EditProfileViewModel( val user: LiveData = mutableUser private val mutableMessage = SingleLiveEvent() val message: LiveData = mutableMessage + private var updatedImageTemp = MutableLiveData() + var avatarUpdated = false + var encodedImage: String? = null fun isLoggedIn() = authService.isLoggedIn() - fun updateProfile(encodedImage: String?, firstName: String, lastName: String) { + /** + * @param firstName updated firstName + * @param lastName updated lastName + */ + fun updateProfile(firstName: String, lastName: String) { if (encodedImage.isNullOrEmpty()) { updateUser(null, firstName, lastName) return @@ -51,7 +59,7 @@ class EditProfileViewModel( } } - fun updateUser(url: String?, firstName: String, lastName: String) { + private fun updateUser(url: String?, firstName: String, lastName: String) { val id = authHolder.getId() if (firstName.isEmpty() || lastName.isEmpty()) { mutableMessage.value = resource.getString(R.string.provide_name_message) @@ -81,6 +89,14 @@ class EditProfileViewModel( } } + fun setUpdatedTempFile(file: File) { + updatedImageTemp.value = file + } + + fun getUpdatedTempFile(): MutableLiveData { + return updatedImageTemp + } + override fun onCleared() { super.onCleared() compositeDisposable.clear() diff --git a/app/src/main/java/org/fossasia/openevent/general/auth/ProfileViewModel.kt b/app/src/main/java/org/fossasia/openevent/general/auth/ProfileViewModel.kt index f14f6d6b36..0ac51f36ea 100644 --- a/app/src/main/java/org/fossasia/openevent/general/auth/ProfileViewModel.kt +++ b/app/src/main/java/org/fossasia/openevent/general/auth/ProfileViewModel.kt @@ -21,7 +21,6 @@ class ProfileViewModel(private val authService: AuthService, private val resourc val user: LiveData = mutableUser private val mutableError = SingleLiveEvent() val error: LiveData = mutableError - val avatarPicked = MutableLiveData() fun isLoggedIn() = authService.isLoggedIn()