Skip to content

Commit

Permalink
fix: Selected image get rotate in edit profile
Browse files Browse the repository at this point in the history
  • Loading branch information
iamanbansal committed May 2, 2019
1 parent ff9ce2e commit 43f691e
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 32 deletions.
114 changes: 114 additions & 0 deletions 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
}
}
}
Expand Up @@ -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
Expand All @@ -23,20 +21,25 @@ 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
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
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
Expand All @@ -47,14 +50,12 @@ class EditProfileFragment : Fragment() {
private val editProfileViewModel by viewModel<EditProfileViewModel>()
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,
Expand All @@ -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)
Expand All @@ -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
Expand All @@ -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())
}

Expand Down Expand Up @@ -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()
}
}

Expand All @@ -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)
}

Expand Down Expand Up @@ -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 {
Expand All @@ -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()
Expand Down
Expand Up @@ -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,
Expand All @@ -25,10 +26,17 @@ class EditProfileViewModel(
val user: LiveData<User> = mutableUser
private val mutableMessage = SingleLiveEvent<String>()
val message: LiveData<String> = mutableMessage
private var updatedImageTemp = MutableLiveData<File>()
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
Expand All @@ -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)
Expand Down Expand Up @@ -81,6 +89,14 @@ class EditProfileViewModel(
}
}

fun setUpdatedTempFile(file: File) {
updatedImageTemp.value = file
}

fun getUpdatedTempFile(): MutableLiveData<File> {
return updatedImageTemp
}

override fun onCleared() {
super.onCleared()
compositeDisposable.clear()
Expand Down
Expand Up @@ -21,7 +21,6 @@ class ProfileViewModel(private val authService: AuthService, private val resourc
val user: LiveData<User> = mutableUser
private val mutableError = SingleLiveEvent<String>()
val error: LiveData<String> = mutableError
val avatarPicked = MutableLiveData<String>()

fun isLoggedIn() = authService.isLoggedIn()

Expand Down

0 comments on commit 43f691e

Please sign in to comment.