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:
Konrad Pozniak 2022-03-02 20:39:56 +01:00 committed by GitHub
parent 4dee5c2774
commit a6335e6bcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 104 additions and 323 deletions

View File

@ -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"

View File

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

View File

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

View File

@ -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"