update Android Image Cropper and get rid of deprecated onActivityResult (#2351)
* update Android Image Cropper and get rid of deprecated onActivityResult * add comment why skipping caches is necessary * inject application into EditProfileViewModel instead of passing it everytime
This commit is contained in:
parent
4dee5c2774
commit
a6335e6bcd
4 changed files with 104 additions and 323 deletions
|
@ -168,7 +168,7 @@ dependencies {
|
||||||
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
implementation "com.mikepenz:materialdrawer-iconics:$materialdrawerVersion"
|
||||||
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
implementation 'com.mikepenz:google-material-typeface:4.0.0.2-kotlin@aar'
|
||||||
|
|
||||||
implementation "com.github.CanHub:Android-Image-Cropper:3.1.0"
|
implementation "com.github.CanHub:Android-Image-Cropper:4.1.0"
|
||||||
|
|
||||||
implementation "de.c1710:filemojicompat:1.0.18"
|
implementation "de.c1710:filemojicompat:1.0.18"
|
||||||
|
|
||||||
|
|
|
@ -15,28 +15,26 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky
|
package com.keylesspalace.tusky
|
||||||
|
|
||||||
import android.Manifest
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
|
||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.util.Log
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.ImageView
|
import android.widget.ImageView
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.app.ActivityCompat
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import androidx.lifecycle.LiveData
|
import androidx.lifecycle.LiveData
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
import com.bumptech.glide.Glide
|
import com.bumptech.glide.Glide
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||||
import com.canhub.cropper.CropImage
|
import com.canhub.cropper.CropImageContract
|
||||||
|
import com.canhub.cropper.options
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
import com.keylesspalace.tusky.adapter.AccountFieldEditAdapter
|
||||||
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
|
import com.keylesspalace.tusky.databinding.ActivityEditProfileBinding
|
||||||
|
@ -44,9 +42,7 @@ import com.keylesspalace.tusky.di.Injectable
|
||||||
import com.keylesspalace.tusky.di.ViewModelFactory
|
import com.keylesspalace.tusky.di.ViewModelFactory
|
||||||
import com.keylesspalace.tusky.util.Error
|
import com.keylesspalace.tusky.util.Error
|
||||||
import com.keylesspalace.tusky.util.Loading
|
import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
import com.keylesspalace.tusky.util.hide
|
|
||||||
import com.keylesspalace.tusky.util.show
|
import com.keylesspalace.tusky.util.show
|
||||||
import com.keylesspalace.tusky.util.viewBinding
|
import com.keylesspalace.tusky.util.viewBinding
|
||||||
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
import com.keylesspalace.tusky.viewmodel.EditProfileViewModel
|
||||||
|
@ -63,12 +59,7 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
const val HEADER_WIDTH = 1500
|
const val HEADER_WIDTH = 1500
|
||||||
const val HEADER_HEIGHT = 500
|
const val HEADER_HEIGHT = 500
|
||||||
|
|
||||||
private const val AVATAR_PICK_RESULT = 1
|
|
||||||
private const val HEADER_PICK_RESULT = 2
|
|
||||||
private const val PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE = 1
|
|
||||||
private const val MAX_ACCOUNT_FIELDS = 4
|
private const val MAX_ACCOUNT_FIELDS = 4
|
||||||
|
|
||||||
private const val BUNDLE_CURRENTLY_PICKING = "BUNDLE_CURRENTLY_PICKING"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
|
@ -78,23 +69,28 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
|
|
||||||
private val binding by viewBinding(ActivityEditProfileBinding::inflate)
|
private val binding by viewBinding(ActivityEditProfileBinding::inflate)
|
||||||
|
|
||||||
private var currentlyPicking: PickType = PickType.NOTHING
|
|
||||||
|
|
||||||
private val accountFieldEditAdapter = AccountFieldEditAdapter()
|
private val accountFieldEditAdapter = AccountFieldEditAdapter()
|
||||||
|
|
||||||
private enum class PickType {
|
private enum class PickType {
|
||||||
NOTHING,
|
|
||||||
AVATAR,
|
AVATAR,
|
||||||
HEADER
|
HEADER
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val cropImage = registerForActivityResult(CropImageContract()) { result ->
|
||||||
|
if (result.isSuccessful) {
|
||||||
|
if (result.uriContent == viewModel.getAvatarUri()) {
|
||||||
|
viewModel.newAvatarPicked()
|
||||||
|
} else {
|
||||||
|
viewModel.newHeaderPicked()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onPickFailure(result.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
savedInstanceState?.getString(BUNDLE_CURRENTLY_PICKING)?.let {
|
|
||||||
currentlyPicking = PickType.valueOf(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
|
|
||||||
setSupportActionBar(binding.includedToolbar.toolbar)
|
setSupportActionBar(binding.includedToolbar.toolbar)
|
||||||
|
@ -104,8 +100,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
setDisplayShowHomeEnabled(true)
|
setDisplayShowHomeEnabled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.avatarButton.setOnClickListener { onMediaPick(PickType.AVATAR) }
|
binding.avatarButton.setOnClickListener { pickMedia(PickType.AVATAR) }
|
||||||
binding.headerButton.setOnClickListener { onMediaPick(PickType.HEADER) }
|
binding.headerButton.setOnClickListener { pickMedia(PickType.HEADER) }
|
||||||
|
|
||||||
binding.fieldList.layoutManager = LinearLayoutManager(this)
|
binding.fieldList.layoutManager = LinearLayoutManager(this)
|
||||||
binding.fieldList.adapter = accountFieldEditAdapter
|
binding.fieldList.adapter = accountFieldEditAdapter
|
||||||
|
@ -159,11 +155,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is Error -> {
|
is Error -> {
|
||||||
val snackbar = Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG)
|
Snackbar.make(binding.avatarButton, R.string.error_generic, Snackbar.LENGTH_LONG)
|
||||||
snackbar.setAction(R.string.action_retry) {
|
.setAction(R.string.action_retry) {
|
||||||
viewModel.obtainProfile()
|
viewModel.obtainProfile()
|
||||||
}
|
}
|
||||||
snackbar.show()
|
.show()
|
||||||
}
|
}
|
||||||
is Loading -> { }
|
is Loading -> { }
|
||||||
}
|
}
|
||||||
|
@ -179,30 +175,24 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
observeImage(viewModel.avatarData, binding.avatarPreview, binding.avatarProgressBar, true)
|
observeImage(viewModel.avatarData, binding.avatarPreview, true)
|
||||||
observeImage(viewModel.headerData, binding.headerPreview, binding.headerProgressBar, false)
|
observeImage(viewModel.headerData, binding.headerPreview, false)
|
||||||
|
|
||||||
viewModel.saveData.observe(
|
viewModel.saveData.observe(
|
||||||
this,
|
this
|
||||||
{
|
) {
|
||||||
when (it) {
|
when (it) {
|
||||||
is Success -> {
|
is Success -> {
|
||||||
finish()
|
finish()
|
||||||
}
|
}
|
||||||
is Loading -> {
|
is Loading -> {
|
||||||
binding.saveProgressBar.visibility = View.VISIBLE
|
binding.saveProgressBar.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
is Error -> {
|
is Error -> {
|
||||||
onSaveFailure(it.errorMessage)
|
onSaveFailure(it.errorMessage)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle) {
|
|
||||||
super.onSaveInstanceState(outState)
|
|
||||||
outState.putString(BUNDLE_CURRENTLY_PICKING, currentlyPicking.toString())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
|
@ -218,90 +208,60 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun observeImage(
|
private fun observeImage(
|
||||||
liveData: LiveData<Resource<Bitmap>>,
|
liveData: LiveData<Uri>,
|
||||||
imageView: ImageView,
|
imageView: ImageView,
|
||||||
progressBar: View,
|
|
||||||
roundedCorners: Boolean
|
roundedCorners: Boolean
|
||||||
) {
|
) {
|
||||||
liveData.observe(
|
liveData.observe(
|
||||||
this,
|
this
|
||||||
{
|
) { imageUri ->
|
||||||
|
|
||||||
when (it) {
|
// skipping all caches so we can always reuse the same uri
|
||||||
is Success -> {
|
val glide = Glide.with(imageView)
|
||||||
val glide = Glide.with(imageView)
|
.load(imageUri)
|
||||||
.load(it.data)
|
.skipMemoryCache(true)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
|
||||||
if (roundedCorners) {
|
if (roundedCorners) {
|
||||||
glide.transform(
|
glide.transform(
|
||||||
FitCenter(),
|
FitCenter(),
|
||||||
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
RoundedCorners(resources.getDimensionPixelSize(R.dimen.avatar_radius_80dp))
|
||||||
)
|
).into(imageView)
|
||||||
}
|
} else {
|
||||||
|
glide.into(imageView)
|
||||||
glide.into(imageView)
|
|
||||||
|
|
||||||
imageView.show()
|
|
||||||
progressBar.hide()
|
|
||||||
}
|
|
||||||
is Loading -> {
|
|
||||||
progressBar.show()
|
|
||||||
}
|
|
||||||
is Error -> {
|
|
||||||
progressBar.hide()
|
|
||||||
if (!it.consumed) {
|
|
||||||
onResizeFailure()
|
|
||||||
it.consumed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onMediaPick(pickType: PickType) {
|
imageView.show()
|
||||||
if (currentlyPicking != PickType.NOTHING) {
|
|
||||||
// Ignore inputs if another pick operation is still occurring.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
currentlyPicking = pickType
|
|
||||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
|
||||||
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE)
|
|
||||||
} else {
|
|
||||||
initiateMediaPicking()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onRequestPermissionsResult(
|
private fun pickMedia(pickType: PickType) {
|
||||||
requestCode: Int,
|
|
||||||
permissions: Array<String>,
|
|
||||||
grantResults: IntArray
|
|
||||||
) {
|
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
|
||||||
when (requestCode) {
|
|
||||||
PERMISSIONS_REQUEST_READ_EXTERNAL_STORAGE -> {
|
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
|
||||||
initiateMediaPicking()
|
|
||||||
} else {
|
|
||||||
endMediaPicking()
|
|
||||||
Snackbar.make(binding.avatarButton, R.string.error_media_upload_permission, Snackbar.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initiateMediaPicking() {
|
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
intent.type = "image/*"
|
intent.type = "image/*"
|
||||||
when (currentlyPicking) {
|
when (pickType) {
|
||||||
PickType.AVATAR -> {
|
PickType.AVATAR -> {
|
||||||
startActivityForResult(intent, AVATAR_PICK_RESULT)
|
cropImage.launch(
|
||||||
|
options {
|
||||||
|
setRequestedSize(AVATAR_SIZE, AVATAR_SIZE)
|
||||||
|
setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
|
||||||
|
setImageSource(includeGallery = true, includeCamera = false)
|
||||||
|
setOutputUri(viewModel.getAvatarUri())
|
||||||
|
setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
PickType.HEADER -> {
|
PickType.HEADER -> {
|
||||||
startActivityForResult(intent, HEADER_PICK_RESULT)
|
cropImage.launch(
|
||||||
|
options {
|
||||||
|
setRequestedSize(HEADER_WIDTH, HEADER_HEIGHT)
|
||||||
|
setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
|
||||||
|
setImageSource(includeGallery = true, includeCamera = false)
|
||||||
|
setOutputUri(viewModel.getHeaderUri())
|
||||||
|
setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
PickType.NOTHING -> { /* do nothing */ }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -321,16 +281,11 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun save() {
|
private fun save() {
|
||||||
if (currentlyPicking != PickType.NOTHING) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.save(
|
viewModel.save(
|
||||||
binding.displayNameEditText.text.toString(),
|
binding.displayNameEditText.text.toString(),
|
||||||
binding.noteEditText.text.toString(),
|
binding.noteEditText.text.toString(),
|
||||||
binding.lockedCheckBox.isChecked,
|
binding.lockedCheckBox.isChecked,
|
||||||
accountFieldEditAdapter.getFieldData(),
|
accountFieldEditAdapter.getFieldData()
|
||||||
this
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -340,90 +295,8 @@ class EditProfileActivity : BaseActivity(), Injectable {
|
||||||
binding.saveProgressBar.visibility = View.GONE
|
binding.saveProgressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun beginMediaPicking() {
|
private fun onPickFailure(throwable: Throwable?) {
|
||||||
when (currentlyPicking) {
|
Log.w("EditProfileActivity", "failed to pick media", throwable)
|
||||||
PickType.AVATAR -> {
|
|
||||||
binding.avatarProgressBar.visibility = View.VISIBLE
|
|
||||||
binding.avatarPreview.visibility = View.INVISIBLE
|
|
||||||
binding.avatarButton.setImageDrawable(null)
|
|
||||||
}
|
|
||||||
PickType.HEADER -> {
|
|
||||||
binding.headerProgressBar.visibility = View.VISIBLE
|
|
||||||
binding.headerPreview.visibility = View.INVISIBLE
|
|
||||||
binding.headerButton.setImageDrawable(null)
|
|
||||||
}
|
|
||||||
PickType.NOTHING -> { /* do nothing */ }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun endMediaPicking() {
|
|
||||||
binding.avatarProgressBar.visibility = View.GONE
|
|
||||||
binding.headerProgressBar.visibility = View.GONE
|
|
||||||
|
|
||||||
currentlyPicking = PickType.NOTHING
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
|
||||||
super.onActivityResult(requestCode, resultCode, data)
|
|
||||||
when (requestCode) {
|
|
||||||
AVATAR_PICK_RESULT -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
|
||||||
CropImage.activity(data.data)
|
|
||||||
.setInitialCropWindowPaddingRatio(0f)
|
|
||||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
|
||||||
.setAspectRatio(AVATAR_SIZE, AVATAR_SIZE)
|
|
||||||
.start(this)
|
|
||||||
} else {
|
|
||||||
endMediaPicking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
HEADER_PICK_RESULT -> {
|
|
||||||
if (resultCode == Activity.RESULT_OK && data != null) {
|
|
||||||
CropImage.activity(data.data)
|
|
||||||
.setInitialCropWindowPaddingRatio(0f)
|
|
||||||
.setOutputCompressFormat(Bitmap.CompressFormat.PNG)
|
|
||||||
.setAspectRatio(HEADER_WIDTH, HEADER_HEIGHT)
|
|
||||||
.start(this)
|
|
||||||
} else {
|
|
||||||
endMediaPicking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CropImage.CROP_IMAGE_ACTIVITY_REQUEST_CODE -> {
|
|
||||||
val result = CropImage.getActivityResult(data)
|
|
||||||
when (resultCode) {
|
|
||||||
Activity.RESULT_OK -> beginResize(result?.uriContent)
|
|
||||||
CropImage.CROP_IMAGE_ACTIVITY_RESULT_ERROR_CODE -> onResizeFailure()
|
|
||||||
else -> endMediaPicking()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun beginResize(uri: Uri?) {
|
|
||||||
if (uri == null) {
|
|
||||||
currentlyPicking = PickType.NOTHING
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
beginMediaPicking()
|
|
||||||
|
|
||||||
when (currentlyPicking) {
|
|
||||||
PickType.AVATAR -> {
|
|
||||||
viewModel.newAvatar(uri, this)
|
|
||||||
}
|
|
||||||
PickType.HEADER -> {
|
|
||||||
viewModel.newHeader(uri, this)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
throw AssertionError("PickType not set.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
currentlyPicking = PickType.NOTHING
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onResizeFailure() {
|
|
||||||
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
Snackbar.make(binding.avatarButton, R.string.error_media_upload_sending, Snackbar.LENGTH_LONG).show()
|
||||||
endMediaPicking()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,15 +15,11 @@
|
||||||
|
|
||||||
package com.keylesspalace.tusky.viewmodel
|
package com.keylesspalace.tusky.viewmodel
|
||||||
|
|
||||||
import android.content.Context
|
import android.app.Application
|
||||||
import android.graphics.Bitmap
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.util.Log
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import com.keylesspalace.tusky.EditProfileActivity.Companion.AVATAR_SIZE
|
|
||||||
import com.keylesspalace.tusky.EditProfileActivity.Companion.HEADER_HEIGHT
|
|
||||||
import com.keylesspalace.tusky.EditProfileActivity.Companion.HEADER_WIDTH
|
|
||||||
import com.keylesspalace.tusky.appstore.EventHub
|
import com.keylesspalace.tusky.appstore.EventHub
|
||||||
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
import com.keylesspalace.tusky.appstore.ProfileEditedEvent
|
||||||
import com.keylesspalace.tusky.entity.Account
|
import com.keylesspalace.tusky.entity.Account
|
||||||
|
@ -31,16 +27,12 @@ import com.keylesspalace.tusky.entity.Instance
|
||||||
import com.keylesspalace.tusky.entity.StringField
|
import com.keylesspalace.tusky.entity.StringField
|
||||||
import com.keylesspalace.tusky.network.MastodonApi
|
import com.keylesspalace.tusky.network.MastodonApi
|
||||||
import com.keylesspalace.tusky.util.Error
|
import com.keylesspalace.tusky.util.Error
|
||||||
import com.keylesspalace.tusky.util.IOUtils
|
|
||||||
import com.keylesspalace.tusky.util.Loading
|
import com.keylesspalace.tusky.util.Loading
|
||||||
import com.keylesspalace.tusky.util.Resource
|
import com.keylesspalace.tusky.util.Resource
|
||||||
import com.keylesspalace.tusky.util.Success
|
import com.keylesspalace.tusky.util.Success
|
||||||
import com.keylesspalace.tusky.util.getSampledBitmap
|
|
||||||
import com.keylesspalace.tusky.util.randomAlphanumericString
|
import com.keylesspalace.tusky.util.randomAlphanumericString
|
||||||
import io.reactivex.rxjava3.core.Single
|
|
||||||
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
import io.reactivex.rxjava3.kotlin.addTo
|
import io.reactivex.rxjava3.kotlin.addTo
|
||||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.MultipartBody
|
import okhttp3.MultipartBody
|
||||||
import okhttp3.RequestBody
|
import okhttp3.RequestBody
|
||||||
|
@ -52,30 +44,26 @@ import retrofit2.Call
|
||||||
import retrofit2.Callback
|
import retrofit2.Callback
|
||||||
import retrofit2.Response
|
import retrofit2.Response
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.OutputStream
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val HEADER_FILE_NAME = "header.png"
|
private const val HEADER_FILE_NAME = "header.png"
|
||||||
private const val AVATAR_FILE_NAME = "avatar.png"
|
private const val AVATAR_FILE_NAME = "avatar.png"
|
||||||
|
|
||||||
private const val TAG = "EditProfileViewModel"
|
|
||||||
|
|
||||||
class EditProfileViewModel @Inject constructor(
|
class EditProfileViewModel @Inject constructor(
|
||||||
private val mastodonApi: MastodonApi,
|
private val mastodonApi: MastodonApi,
|
||||||
private val eventHub: EventHub
|
private val eventHub: EventHub,
|
||||||
|
private val application: Application
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
|
||||||
val profileData = MutableLiveData<Resource<Account>>()
|
val profileData = MutableLiveData<Resource<Account>>()
|
||||||
val avatarData = MutableLiveData<Resource<Bitmap>>()
|
val avatarData = MutableLiveData<Uri>()
|
||||||
val headerData = MutableLiveData<Resource<Bitmap>>()
|
val headerData = MutableLiveData<Uri>()
|
||||||
val saveData = MutableLiveData<Resource<Nothing>>()
|
val saveData = MutableLiveData<Resource<Nothing>>()
|
||||||
val instanceData = MutableLiveData<Resource<Instance>>()
|
val instanceData = MutableLiveData<Resource<Instance>>()
|
||||||
|
|
||||||
private var oldProfileData: Account? = null
|
private var oldProfileData: Account? = null
|
||||||
|
|
||||||
private val disposeables = CompositeDisposable()
|
private val disposables = CompositeDisposable()
|
||||||
|
|
||||||
fun obtainProfile() {
|
fun obtainProfile() {
|
||||||
if (profileData.value == null || profileData.value is Error) {
|
if (profileData.value == null || profileData.value is Error) {
|
||||||
|
@ -92,70 +80,30 @@ class EditProfileViewModel @Inject constructor(
|
||||||
profileData.postValue(Error())
|
profileData.postValue(Error())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.addTo(disposeables)
|
.addTo(disposables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newAvatar(uri: Uri, context: Context) {
|
fun getAvatarUri() = getCacheFileForName(AVATAR_FILE_NAME).toUri()
|
||||||
val cacheFile = getCacheFileForName(context, AVATAR_FILE_NAME)
|
|
||||||
|
|
||||||
resizeImage(uri, context, AVATAR_SIZE, AVATAR_SIZE, cacheFile, avatarData)
|
fun getHeaderUri() = getCacheFileForName(HEADER_FILE_NAME).toUri()
|
||||||
|
|
||||||
|
fun newAvatarPicked() {
|
||||||
|
avatarData.value = getAvatarUri()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun newHeader(uri: Uri, context: Context) {
|
fun newHeaderPicked() {
|
||||||
val cacheFile = getCacheFileForName(context, HEADER_FILE_NAME)
|
headerData.value = getHeaderUri()
|
||||||
|
|
||||||
resizeImage(uri, context, HEADER_WIDTH, HEADER_HEIGHT, cacheFile, headerData)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun resizeImage(
|
fun save(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List<StringField>) {
|
||||||
uri: Uri,
|
|
||||||
context: Context,
|
|
||||||
resizeWidth: Int,
|
|
||||||
resizeHeight: Int,
|
|
||||||
cacheFile: File,
|
|
||||||
imageLiveData: MutableLiveData<Resource<Bitmap>>
|
|
||||||
) {
|
|
||||||
|
|
||||||
Single.fromCallable {
|
|
||||||
val contentResolver = context.contentResolver
|
|
||||||
val sourceBitmap = getSampledBitmap(contentResolver, uri, resizeWidth, resizeHeight)
|
|
||||||
|
|
||||||
if (sourceBitmap == null) {
|
|
||||||
throw Exception()
|
|
||||||
}
|
|
||||||
|
|
||||||
// dont upscale image if its smaller than the desired size
|
|
||||||
val bitmap =
|
|
||||||
if (sourceBitmap.width <= resizeWidth && sourceBitmap.height <= resizeHeight) {
|
|
||||||
sourceBitmap
|
|
||||||
} else {
|
|
||||||
Bitmap.createScaledBitmap(sourceBitmap, resizeWidth, resizeHeight, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!saveBitmapToFile(bitmap, cacheFile)) {
|
|
||||||
throw Exception()
|
|
||||||
}
|
|
||||||
|
|
||||||
bitmap
|
|
||||||
}.subscribeOn(Schedulers.io())
|
|
||||||
.subscribe(
|
|
||||||
{
|
|
||||||
imageLiveData.postValue(Success(it))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
imageLiveData.postValue(Error())
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.addTo(disposeables)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun save(newDisplayName: String, newNote: String, newLocked: Boolean, newFields: List<StringField>, context: Context) {
|
|
||||||
|
|
||||||
if (saveData.value is Loading || profileData.value !is Success) {
|
if (saveData.value is Loading || profileData.value !is Success) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
saveData.value = Loading()
|
||||||
|
|
||||||
val displayName = if (oldProfileData?.displayName == newDisplayName) {
|
val displayName = if (oldProfileData?.displayName == newDisplayName) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
|
@ -174,15 +122,15 @@ class EditProfileViewModel @Inject constructor(
|
||||||
newLocked.toString().toRequestBody(MultipartBody.FORM)
|
newLocked.toString().toRequestBody(MultipartBody.FORM)
|
||||||
}
|
}
|
||||||
|
|
||||||
val avatar = if (avatarData.value is Success && avatarData.value?.data != null) {
|
val avatar = if (avatarData.value != null) {
|
||||||
val avatarBody = getCacheFileForName(context, AVATAR_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull())
|
val avatarBody = getCacheFileForName(AVATAR_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull())
|
||||||
MultipartBody.Part.createFormData("avatar", randomAlphanumericString(12), avatarBody)
|
MultipartBody.Part.createFormData("avatar", randomAlphanumericString(12), avatarBody)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
|
||||||
val header = if (headerData.value is Success && headerData.value?.data != null) {
|
val header = if (headerData.value != null) {
|
||||||
val headerBody = getCacheFileForName(context, HEADER_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull())
|
val headerBody = getCacheFileForName(HEADER_FILE_NAME).asRequestBody("image/png".toMediaTypeOrNull())
|
||||||
MultipartBody.Part.createFormData("header", randomAlphanumericString(12), headerBody)
|
MultipartBody.Part.createFormData("header", randomAlphanumericString(12), headerBody)
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
|
@ -256,29 +204,12 @@ class EditProfileViewModel @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getCacheFileForName(context: Context, filename: String): File {
|
private fun getCacheFileForName(filename: String): File {
|
||||||
return File(context.cacheDir, filename)
|
return File(application.cacheDir, filename)
|
||||||
}
|
|
||||||
|
|
||||||
private fun saveBitmapToFile(bitmap: Bitmap, file: File): Boolean {
|
|
||||||
|
|
||||||
val outputStream: OutputStream
|
|
||||||
|
|
||||||
try {
|
|
||||||
outputStream = FileOutputStream(file)
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
Log.w(TAG, Log.getStackTraceString(e))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
|
|
||||||
IOUtils.closeQuietly(outputStream)
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
disposeables.dispose()
|
disposables.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun obtainInstance() {
|
fun obtainInstance() {
|
||||||
|
@ -293,7 +224,7 @@ class EditProfileViewModel @Inject constructor(
|
||||||
instanceData.postValue(Error())
|
instanceData.postValue(Error())
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.addTo(disposeables)
|
.addTo(disposables)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
tools:context="com.keylesspalace.tusky.EditProfileActivity">
|
tools:context=".EditProfileActivity">
|
||||||
|
|
||||||
<include
|
<include
|
||||||
android:id="@+id/includedToolbar"
|
android:id="@+id/includedToolbar"
|
||||||
|
@ -37,17 +37,6 @@
|
||||||
app:layout_constraintTop_toTopOf="parent"
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
app:srcCompat="@drawable/ic_add_a_photo_32dp" />
|
app:srcCompat="@drawable/ic_add_a_photo_32dp" />
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/headerProgressBar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:indeterminate="true"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/headerPreview"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/headerPreview"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/headerPreview"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/headerPreview" />
|
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/avatarPreview"
|
android:id="@+id/avatarPreview"
|
||||||
android:layout_width="80dp"
|
android:layout_width="80dp"
|
||||||
|
@ -71,18 +60,6 @@
|
||||||
app:layout_constraintTop_toBottomOf="@id/headerPreview"
|
app:layout_constraintTop_toBottomOf="@id/headerPreview"
|
||||||
app:srcCompat="@drawable/ic_add_a_photo_32dp" />
|
app:srcCompat="@drawable/ic_add_a_photo_32dp" />
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/avatarProgressBar"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:indeterminate="true"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:layout_constraintBottom_toBottomOf="@id/avatarPreview"
|
|
||||||
app:layout_constraintEnd_toEndOf="@id/avatarPreview"
|
|
||||||
app:layout_constraintStart_toStartOf="@id/avatarPreview"
|
|
||||||
app:layout_constraintTop_toTopOf="@id/avatarPreview" />
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/contentContainer"
|
android:id="@+id/contentContainer"
|
||||||
android:layout_width="@dimen/timeline_width"
|
android:layout_width="@dimen/timeline_width"
|
||||||
|
|
Loading…
Reference in a new issue