Start post submission activity

This commit is contained in:
Marie 2022-10-30 16:24:46 +01:00 committed by Matthieu
parent 2d712ed395
commit 851d95bf0f
10 changed files with 812 additions and 155 deletions

View File

@ -8,6 +8,7 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'jacoco'
apply plugin: "kotlin-parcelize"
// Force latest version of Jacoco, initially done to resolve https://github.com/jacoco/jacoco/issues/1155

View File

@ -72,6 +72,9 @@
<data android:mimeType="video/*" />
</intent-filter>
</activity>
<activity
android:name=".postCreation.PostSubmissionActivity">
</activity>
<activity
android:name=".profile.FollowsActivity"
android:screenOrientation="sensorPortrait"

View File

@ -19,6 +19,7 @@ import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.core.widget.doAfterTextChanged
@ -32,6 +33,7 @@ import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.carousel.CarouselItem
import org.pixeldroid.app.searchDiscover.TrendingActivity
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
@ -112,37 +114,14 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
binding.uploadProgressBar.visibility =
if (uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE
binding.uploadProgressBar.progress = uiState.uploadProgress
binding.uploadCompletedTextview.visibility =
if (uiState.uploadCompletedTextviewVisible) VISIBLE else INVISIBLE
binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled
binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled
binding.uploadError.visibility =
if (uiState.uploadErrorVisible) VISIBLE else INVISIBLE
binding.uploadErrorTextExplanation.visibility =
if (uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE
binding.toolbarPostCreation.visibility =
if (uiState.isCarousel) VISIBLE else INVISIBLE
binding.carousel.layoutCarousel = uiState.isCarousel
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
}
}
}
binding.newPostDescriptionInputField.doAfterTextChanged {
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
}
val existingDescription: String? = intent.getStringExtra(PICTURE_DESCRIPTION)
binding.newPostDescriptionInputField.setText(
// Set description from redraft if any, otherwise from the template
existingDescription ?: model.uiState.value.newPostDescriptionText
)
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
binding.carousel.apply {
layoutCarouselCallback = { model.becameCarousel(it)}
@ -156,13 +135,9 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
}
// get the description and send the post
binding.postCreationSendButton.setOnClickListener {
if (validatePost() && model.isNotEmpty()) model.upload()
}
// Button to retry image upload when it fails
binding.retryUploadButton.setOnClickListener {
model.resetUploadStatus()
model.upload()
if (validatePost() && model.isNotEmpty()) {
model.upload(it.context, binding.root.context)
}
}
binding.editPhotoButton.setOnClickListener {
@ -181,7 +156,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
}
}
binding.removePhotoButton.setOnClickListener {
binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition ->
model.removeAt(currentPosition)
@ -300,14 +274,6 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
private fun validatePost(): Boolean {
binding.postTextInputLayout.run {
val content = editText?.length() ?: 0
if (content > counterMaxLength) {
// error, too many characters
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
return false
}
}
if(model.getPhotoData().value?.all { it.videoEncodeProgress == null } == false){
AlertDialog.Builder(this).apply {
setMessage(R.string.still_encoding)
@ -321,10 +287,10 @@ class PostCreationActivity : BaseThemedWithoutBarActivity() {
private fun enableButton(enable: Boolean = true){
binding.postCreationSendButton.isEnabled = enable
if(enable){
binding.postingProgressBar.visibility = GONE
binding.submittingProgressBar.visibility = GONE
binding.postCreationSendButton.visibility = VISIBLE
} else {
binding.postingProgressBar.visibility = VISIBLE
binding.submittingProgressBar.visibility = VISIBLE
binding.postCreationSendButton.visibility = GONE
}

View File

@ -2,12 +2,15 @@ package org.pixeldroid.app.postCreation
import android.app.Application
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Parcelable
import android.provider.OpenableColumns
import android.text.Editable
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface
@ -22,6 +25,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
import okhttp3.MultipartBody
import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R
@ -37,6 +41,7 @@ import retrofit2.HttpException
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.io.Serializable
import java.net.URI
import javax.inject.Inject
import kotlin.math.ceil
@ -61,8 +66,9 @@ data class PostCreationActivityUiState(
val uploadErrorVisible: Boolean = false,
val uploadErrorExplanationText: String = "",
val uploadErrorExplanationVisible: Boolean = false,
)
)
@Parcelize
data class PhotoData(
var imageUri: Uri,
var size: Long,
@ -74,12 +80,12 @@ data class PhotoData(
var videoEncodeStabilizationFirstPass: Boolean? = null,
var videoEncodeComplete: Boolean = false,
var videoEncodeError: Boolean = false,
)
) : Parcelable
class PostCreationViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) {
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
MutableLiveData<MutableList<PhotoData>>().also {
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
}
}
@ -238,7 +244,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
* [PhotoData.uploadId] (for the list of ids of the uploads).
*/
@OptIn(ExperimentalUnsignedTypes::class)
fun upload() {
fun upload(context: Context, bindingContext: Context) {
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = false,
@ -251,6 +257,10 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
)
}
val intent = Intent(context, PostSubmissionActivity::class.java)
intent.putExtra(PostSubmissionActivity.PHOTO_DATA, getPhotoData().value?.let { ArrayList(it) })
ContextCompat.startActivity(bindingContext, intent, null)
for (data: PhotoData in getPhotoData().value ?: emptyList()) {
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver)
@ -409,7 +419,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
}
fun modifyAt(position: Int, data: Intent): Unit? {
val result: PhotoData = photoData.value?.getOrNull(position)?.run {
val result: PhotoData = photoData.value?.getOrNull(position)?.run {
if (video) {
val modified: Boolean = data.getBooleanExtra(VideoEditActivity.MODIFIED, false)
if(modified){
@ -446,10 +456,6 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
return Unit
}
fun newPostDescriptionChanged(text: Editable?) {
_uiState.update { it.copy(newPostDescriptionText = text.toString()) }
}
/**
* @param originalUri the Uri of the file you sent to be edited
* @param progress percentage of (this pass of) encoding that is done
@ -520,11 +526,10 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null
)
}
}
}
class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java).newInstance(application, clipdata, instance)
}
}
}

View File

@ -0,0 +1,194 @@
package org.pixeldroid.app.postCreation
import android.app.Activity
import android.app.AlertDialog
import android.content.ContentResolver
import android.content.ContentValues
import android.content.Intent
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.*
import android.provider.MediaStore
import android.util.Log
import android.view.View
import android.view.View.INVISIBLE
import android.view.View.VISIBLE
import android.view.View.GONE
import android.widget.Toast
import androidx.activity.result.ActivityResult
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.launch
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ActivityPostCreationBinding
import org.pixeldroid.app.databinding.ActivityPostSubmissionBinding
import org.pixeldroid.app.postCreation.camera.CameraActivity
import org.pixeldroid.app.postCreation.carousel.CarouselItem
import org.pixeldroid.app.utils.BaseThemedWithoutBarActivity
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType
import org.pixeldroid.app.utils.setSquareImageFromURL
import java.io.File
import java.io.OutputStream
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.ArrayList
class PostSubmissionActivity : BaseThemedWithoutBarActivity() {
companion object {
internal const val PICTURE_DESCRIPTION = "picture_description"
internal const val TEMP_FILES = "temp_files"
internal const val POST_REDRAFT = "post_redraft"
internal const val PHOTO_DATA = "photo_data"
}
private var user: UserDatabaseEntity? = null
private lateinit var instance: InstanceDatabaseEntity
private lateinit var binding: ActivityPostSubmissionBinding
private lateinit var model: PostSubmissionViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityPostSubmissionBinding.inflate(layoutInflater)
setContentView(binding.root)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
user = db.userDao().getActiveUser()
instance = user?.run {
db.instanceDao().getAll().first { instanceDatabaseEntity ->
instanceDatabaseEntity.uri.contains(instance_uri)
}
} ?: InstanceDatabaseEntity("", "")
val photoData = intent.getParcelableArrayListExtra<PhotoData>(PHOTO_DATA) as ArrayList<PhotoData>?
val _model: PostSubmissionViewModel by viewModels {
PostSubmissionViewModelFactory(
application,
photoData!!,
instance
)
}
model = _model
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
model.uiState.collect { uiState ->
uiState.userMessage?.let {
AlertDialog.Builder(binding.root.context).apply {
setMessage(it)
setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
// Notify the ViewModel the message is displayed
model.userMessageShown()
}
enableButton(uiState.postCreationSendButtonEnabled)
binding.uploadProgressBar.visibility =
if (uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE
binding.uploadProgressBar.progress = uiState.uploadProgress
binding.uploadCompletedTextview.visibility =
if (uiState.uploadCompletedTextviewVisible) VISIBLE else INVISIBLE
binding.uploadError.visibility =
if (uiState.uploadErrorVisible) VISIBLE else INVISIBLE
binding.uploadErrorTextExplanation.visibility =
if (uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE
binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText
}
}
}
binding.newPostDescriptionInputField.doAfterTextChanged {
model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text)
}
val existingDescription: String? = intent.getStringExtra(PICTURE_DESCRIPTION)
binding.newPostDescriptionInputField.setText(
// Set description from redraft if any, otherwise from the template
existingDescription ?: model.uiState.value.newPostDescriptionText
)
binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars
val firstPostImage = photoData!![0]
setSquareImageFromURL(View(applicationContext), firstPostImage.imageUri.toString(), binding.postPreview)
// get the description and send the post
binding.postCreationSendButton.setOnClickListener {
if (validatePost()) model.upload()
}
// Button to retry image upload when it fails
binding.retryUploadButton.setOnClickListener {
model.resetUploadStatus()
model.upload()
}
// Clean up temporary files, if any
val tempFiles = intent.getStringArrayExtra(TEMP_FILES)
tempFiles?.asList()?.forEach {
val file = File(binding.root.context.cacheDir, it)
model.trackTempFile(file)
}
}
override fun onBackPressed() {
val redraft = intent.getBooleanExtra(POST_REDRAFT, false)
if (redraft) {
val builder = AlertDialog.Builder(binding.root.context)
builder.apply {
setMessage(R.string.redraft_dialog_cancel)
setPositiveButton(android.R.string.ok) { _, _ ->
super.onBackPressed()
}
setNegativeButton(android.R.string.cancel) { _, _ -> }
show()
}
} else {
super.onBackPressed()
}
}
private fun validatePost(): Boolean {
binding.postTextInputLayout.run {
val content = editText?.length() ?: 0
if (content > counterMaxLength) {
// error, too many characters
error = resources.getQuantityString(R.plurals.description_max_characters, counterMaxLength, counterMaxLength)
return false
}
}
return true
}
private fun enableButton(enable: Boolean = true){
binding.postCreationSendButton.isEnabled = enable
if(enable){
binding.postingProgressBar.visibility = GONE
binding.postCreationSendButton.visibility = VISIBLE
} else {
binding.postingProgressBar.visibility = VISIBLE
binding.postCreationSendButton.visibility = GONE
}
}
}

View File

@ -0,0 +1,415 @@
package org.pixeldroid.app.postCreation
import android.app.Application
import android.content.ClipData
import android.content.Intent
import android.net.Uri
import android.provider.OpenableColumns
import android.text.Editable
import android.util.Log
import android.widget.Toast
import androidx.core.net.toFile
import androidx.core.net.toUri
import androidx.exifinterface.media.ExifInterface
import androidx.lifecycle.*
import androidx.preference.PreferenceManager
import com.jarsilio.android.scrambler.exceptions.UnsupportedFileFormatException
import com.jarsilio.android.scrambler.stripMetadata
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
import io.reactivex.rxjava3.disposables.Disposable
import io.reactivex.rxjava3.schedulers.Schedulers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import okhttp3.MultipartBody
import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity
import org.pixeldroid.media_editor.photoEdit.VideoEditActivity.RelativeCropPosition
import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.api.objects.Attachment
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity
import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import org.pixeldroid.app.utils.fileExtension
import org.pixeldroid.app.utils.getMimeType
import retrofit2.HttpException
import java.io.File
import java.io.FileNotFoundException
import java.io.IOException
import java.net.URI
import javax.inject.Inject
import kotlin.math.ceil
// Models the UI state for the PostCreationActivity
data class PostSubmissionActivityUiState(
val userMessage: String? = null,
val postCreationSendButtonEnabled: Boolean = true,
val newPostDescriptionText: String = "",
val uploadProgressBarVisible: Boolean = false,
val uploadProgress: Int = 0,
val uploadCompletedTextviewVisible: Boolean = false,
val uploadErrorVisible: Boolean = false,
val uploadErrorExplanationText: String = "",
val uploadErrorExplanationVisible: Boolean = false,
)
class PostSubmissionViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) {
private val photoData: MutableLiveData<MutableList<PhotoData>> by lazy {
MutableLiveData<MutableList<PhotoData>>().also {
it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
}
}
@Inject
lateinit var apiHolder: PixelfedAPIHolder
private val _uiState: MutableStateFlow<PostCreationActivityUiState>
init {
(application as PixelDroidApplication).getAppComponent().inject(this)
val sharedPreferences =
PreferenceManager.getDefaultSharedPreferences(application)
val initialDescription = sharedPreferences.getString("prefill_description", "") ?: ""
_uiState = MutableStateFlow(PostCreationActivityUiState(newPostDescriptionText = initialDescription))
}
val uiState: StateFlow<PostCreationActivityUiState> = _uiState
// Map photoData indexes to FFmpeg Session IDs
private val sessionMap: MutableMap<Uri, Long> = mutableMapOf()
// Keep track of temporary files to delete them (avoids filling cache super fast with videos)
private val tempFiles: java.util.ArrayList<File> = java.util.ArrayList()
fun userMessageShown() {
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = null)
}
}
fun getPhotoData(): LiveData<MutableList<PhotoData>> = photoData
/**
* Will add as many images as possible to [photoData], from the [clipData], and if
* ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images
* as are legal (if any) and a dialog will be shown to the user alerting them of this fact.
*/
fun addPossibleImages(clipData: ClipData, previousList: MutableList<PhotoData>? = photoData.value): MutableList<PhotoData> {
val dataToAdd: ArrayList<PhotoData> = arrayListOf()
var count = clipData.itemCount
if(count + (previousList?.size ?: 0) > instance!!.albumLimit){
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.total_exceeds_album_limit).format(instance.albumLimit))
}
count = count.coerceAtMost(instance.albumLimit - (previousList?.size ?: 0))
}
if (count + (previousList?.size ?: 0) >= instance.albumLimit) {
// Disable buttons to add more images
_uiState.update { currentUiState ->
currentUiState.copy(addPhotoButtonEnabled = false)
}
}
for (i in 0 until count) {
clipData.getItemAt(i).let {
val sizeAndVideoPair: Pair<Long, Boolean> =
getSizeAndVideoValidate(it.uri, (previousList?.size ?: 0) + dataToAdd.size + 1)
dataToAdd.add(PhotoData(imageUri = it.uri, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second, imageDescription = it.text?.toString()))
}
}
return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf()
}
fun setImages(addPossibleImages: MutableList<PhotoData>) {
photoData.value = addPossibleImages
}
/**
* Returns the size of the file of the Uri, and whether it is a video,
* and opens a dialog in case it is too big or in case the file is unsupported.
*/
fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair<Long, Boolean> {
val size: Long =
if (uri.scheme =="content") {
getApplication<PixelDroidApplication>().contentResolver.query(uri, null, null, null, null)
?.use { cursor ->
/* Get the column indexes of the data in the Cursor,
* move to the first row in the Cursor, get the data,
* and display it.
*/
val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE)
cursor.moveToFirst()
cursor.getLong(sizeIndex)
} ?: 0
} else {
uri.toFile().length()
}
val sizeInkBytes = ceil(size.toDouble() / 1000).toLong()
val type = uri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
val isVideo = type.startsWith("video/")
if(isVideo && !instance!!.videoEnabled){
_uiState.update { currentUiState ->
currentUiState.copy(userMessage = getApplication<PixelDroidApplication>().getString(R.string.video_not_supported))
}
}
if (sizeInkBytes > instance!!.maxPhotoSize || sizeInkBytes > instance.maxVideoSize) {
val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
_uiState.update { currentUiState ->
currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)
)
}
}
return Pair(size, isVideo)
}
fun isNotEmpty(): Boolean = photoData.value?.isNotEmpty() ?: false
fun updateDescription(position: Int, description: String) {
photoData.value?.getOrNull(position)?.imageDescription = description
photoData.value = photoData.value
}
fun resetUploadStatus() {
photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList()
}
fun setVideoEncodeAtPosition(uri: Uri, progress: Int?, stabilizationFirstPass: Boolean = false, error: Boolean = false) {
photoData.value?.indexOfFirst { it.imageUri == uri }?.let { position ->
photoData.value?.set(position,
photoData.value!![position].copy(
videoEncodeProgress = progress,
videoEncodeStabilizationFirstPass = stabilizationFirstPass,
videoEncodeError = error,
)
)
photoData.value = photoData.value
}
}
fun setUriAtPosition(uri: Uri, position: Int) {
photoData.value?.set(position, photoData.value!![position].copy(imageUri = uri))
photoData.value = photoData.value
}
fun setSizeAtPosition(imageSize: Long, position: Int) {
photoData.value?.set(position, photoData.value!![position].copy(size = imageSize))
photoData.value = photoData.value
}
fun removeAt(currentPosition: Int) {
photoData.value?.removeAt(currentPosition)
_uiState.update {
it.copy(
addPhotoButtonEnabled = true
)
}
photoData.value = photoData.value
}
/**
* Uploads the images that are in the [photoData] array.
* Keeps track of them in the [PhotoData.progress] (for the upload progress), and the
* [PhotoData.uploadId] (for the list of ids of the uploads).
*/
@OptIn(ExperimentalUnsignedTypes::class)
fun upload() {
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = false,
addPhotoButtonEnabled = false,
editPhotoButtonEnabled = false,
removePhotoButtonEnabled = false,
uploadCompletedTextviewVisible = false,
uploadErrorVisible = false,
uploadProgressBarVisible = true
)
}
for (data: PhotoData in getPhotoData().value ?: emptyList()) {
val extension = data.imageUri.fileExtension(getApplication<PixelDroidApplication>().contentResolver)
val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication<PixelDroidApplication>().cacheDir)
val imageUri = data.imageUri
val (strippedOrNot, size) = try {
val orientation = ExifInterface(getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
stripMetadata(imageUri, strippedImage, getApplication<PixelDroidApplication>().contentResolver)
// Restore EXIF orientation
val exifInterface = ExifInterface(strippedImage)
exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, orientation.toString())
exifInterface.saveAttributes()
Pair(strippedImage.inputStream(), strippedImage.length())
} catch (e: UnsupportedFileFormatException){
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
val imageInputStream = try {
getApplication<PixelDroidApplication>().contentResolver.openInputStream(imageUri)!!
} catch (e: FileNotFoundException){
_uiState.update { currentUiState ->
currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
data.imageUri)
)
}
return
}
Pair(imageInputStream, data.size)
} catch (e: IOException){
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
_uiState.update { currentUiState ->
currentUiState.copy(
userMessage = getApplication<PixelDroidApplication>().getString(R.string.file_not_found,
data.imageUri)
)
}
return
}
val type = data.imageUri.getMimeType(getApplication<PixelDroidApplication>().contentResolver)
val imagePart = ProgressRequestBody(strippedOrNot, size, type)
val requestBody = MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", System.currentTimeMillis().toString(), imagePart)
.build()
val sub = imagePart.progressSubject
.subscribeOn(Schedulers.io())
.subscribe { percentage ->
data.progress = percentage.toInt()
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgress = getPhotoData().value!!.sumOf { it.progress ?: 0 } / getPhotoData().value!!.size
)
}
}
var postSub: Disposable? = null
val description = data.imageDescription?.let { MultipartBody.Part.createFormData("description", it) }
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
val inter = api.mediaUpload(description, requestBody.parts[0])
postSub = inter
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ attachment: Attachment ->
data.progress = 0
data.uploadId = attachment.id!!
},
{ e: Throwable ->
_uiState.update { currentUiState ->
currentUiState.copy(
uploadErrorVisible = true,
uploadErrorExplanationText = if(e is HttpException){
getApplication<PixelDroidApplication>().getString(R.string.upload_error, e.code())
} else "",
uploadErrorExplanationVisible = e is HttpException,
)
}
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
e.printStackTrace()
postSub?.dispose()
sub.dispose()
},
{
strippedImage.delete()
if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete()
data.progress = 100
if (getPhotoData().value!!.all { it.progress == 100 && it.uploadId != null }) {
_uiState.update { currentUiState ->
currentUiState.copy(
uploadProgressBarVisible = false,
uploadCompletedTextviewVisible = true
)
}
post()
}
postSub?.dispose()
sub.dispose()
}
)
}
}
private fun post() {
val description = uiState.value.newPostDescriptionText
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = false
)
}
viewModelScope.launch {
try {
val api = apiHolder.api ?: apiHolder.setToCurrentUser()
api.postStatus(
statusText = description,
media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList()
)
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_success),
Toast.LENGTH_SHORT).show()
val intent = Intent(getApplication(), MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
//TODO make the activity launch this instead (and surrounding toasts too)
getApplication<PixelDroidApplication>().startActivity(intent)
} catch (exception: IOException) {
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_error),
Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.toString())
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = true
)
}
} catch (exception: HttpException) {
Toast.makeText(getApplication(), getApplication<PixelDroidApplication>().getString(R.string.upload_post_failed),
Toast.LENGTH_SHORT).show()
Log.e(TAG, exception.response().toString() + exception.message().toString())
_uiState.update { currentUiState ->
currentUiState.copy(
postCreationSendButtonEnabled = true
)
}
}
}
}
fun newPostDescriptionChanged(text: Editable?) {
_uiState.update { it.copy(newPostDescriptionText = text.toString()) }
}
fun trackTempFile(file: File) {
tempFiles.add(file)
}
override fun onCleared() {
super.onCleared()
VideoEditActivity.cancelEncoding()
tempFiles.forEach {
it.delete()
}
}
}
class PostSubmissionViewModelFactory(val application: Application, val photoData: ArrayList<PhotoData>, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return modelClass.getConstructor(Application::class.java, ArrayList::class.java, InstanceDatabaseEntity::class.java).newInstance(application, photoData, instance)
}
}

View File

@ -9,6 +9,7 @@ import org.pixeldroid.app.utils.BaseFragment
import dagger.Component
import org.pixeldroid.app.postCreation.PostCreationViewModel
import org.pixeldroid.app.profile.EditProfileViewModel
import org.pixeldroid.app.postCreation.PostSubmissionViewModel
import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker
import javax.inject.Singleton
@ -22,6 +23,7 @@ interface ApplicationComponent {
fun inject(notificationsWorker: NotificationsWorker)
fun inject(postCreationViewModel: PostCreationViewModel)
fun inject(editProfileViewModel: EditProfileViewModel)
fun inject(postSubmissionViewModel: PostSubmissionViewModel)
val context: Context?
val application: Application?

View File

@ -6,60 +6,6 @@
android:layout_height="match_parent"
tools:context=".postCreation.PostCreationActivity">
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/upload_error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="2dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<TextView
android:id="@+id/upload_error_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/colorError"
android:text="@string/media_upload_failed"
android:textColor="?attr/colorOnError"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:drawableStartCompat="@drawable/cloud_off_24"
app:drawableTint="?attr/colorOnError" />
<TextView
android:id="@+id/upload_error_text_explanation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/colorError"
tools:text="Error code returned by server: 413"
android:textColor="?attr/colorOnError"
android:textSize="20sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_view"
tools:visibility="visible" />
<Button
android:id="@+id/retry_upload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/retry"
app:layout_constraintEnd_toEndOf="@id/upload_error_text_view"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="@id/upload_error_text_view"
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" />
</androidx.constraintlayout.widget.ConstraintLayout>
<org.pixeldroid.app.postCreation.carousel.ImageCarousel
android:id="@+id/carousel"
android:layout_width="match_parent"
@ -68,20 +14,15 @@
app:layout_constraintBottom_toBottomOf="@+id/uploadProgressBar"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/upload_completed_textview"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginEnd="8dp"
android:text="@string/media_upload_completed"
android:textColor="@android:color/holo_green_light"
android:textSize="16sp"
<ProgressBar
android:id="@+id/uploadProgressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="invisible"
app:drawableStartCompat="@drawable/cloud_done_24"
app:drawableTint="@android:color/holo_green_light"
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
app:layout_constraintBottom_toTopOf="@id/buttonConstraints"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
app:layout_constraintStart_toStartOf="parent" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/buttonConstraints"
@ -90,14 +31,14 @@
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/postTextInputLayout">
app:layout_constraintTop_toBottomOf="@id/carousel">
<Button
android:id="@+id/post_creation_send_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:enabled="true"
android:text="@string/post"
android:text="@string/upload_next_step"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
@ -105,7 +46,7 @@
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/posting_progress_bar"
android:id="@+id/submitting_progress_bar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -116,39 +57,6 @@
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:id="@+id/uploadProgressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/postTextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/description"
app:counterEnabled="true"
android:paddingStart="15dp"
android:paddingEnd="15dp"
app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/buttonConstraints"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/new_post_description_input_field"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ems="10"
android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/toolbarPostCreation"
android:layout_width="match_parent"
@ -198,7 +106,6 @@
app:layout_constraintStart_toEndOf="@+id/removePhotoButton"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageButton
android:id="@+id/addPhotoButton"
android:layout_width="wrap_content"

View File

@ -0,0 +1,163 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".postCreation.PostSubmissionActivity">
<ImageView
android:id="@+id/profilePictureImageView"
android:layout_width="88dp"
android:layout_height="88dp"
android:layout_marginStart="20dp"
android:layout_marginTop="6dp"
android:contentDescription="@string/profile_picture"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/upload_error"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="2dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<TextView
android:id="@+id/upload_error_text_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/colorError"
android:text="@string/media_upload_failed"
android:textColor="?attr/colorOnError"
android:textSize="20sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:drawableStartCompat="@drawable/cloud_off_24"
app:drawableTint="?attr/colorOnError" />
<TextView
android:id="@+id/upload_error_text_explanation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/colorError"
tools:text="Error code returned by server: 413"
android:textColor="?attr/colorOnError"
android:textSize="20sp"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_view"
tools:visibility="visible" />
<Button
android:id="@+id/retry_upload_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/retry"
app:layout_constraintEnd_toEndOf="@id/upload_error_text_view"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="@id/upload_error_text_view"
app:layout_constraintTop_toBottomOf="@+id/upload_error_text_explanation" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ImageView
android:id="@+id/post_preview"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@id/buttonConstraints"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"/>
<TextView
android:id="@+id/upload_completed_textview"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginEnd="8dp"
android:text="@string/media_upload_completed"
android:textColor="@android:color/holo_green_light"
android:textSize="16sp"
android:visibility="invisible"
app:drawableStartCompat="@drawable/cloud_done_24"
app:drawableTint="@android:color/holo_green_light"
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
app:layout_constraintEnd_toEndOf="parent"
tools:visibility="visible" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/buttonConstraints"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/postTextInputLayout">
<Button
android:id="@+id/post_creation_send_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:enabled="true"
android:text="@string/post"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/posting_progress_bar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<ProgressBar
android:id="@+id/uploadProgressBar"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@id/postTextInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/postTextInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:hint="@string/description"
app:counterEnabled="true"
android:paddingStart="15dp"
android:paddingEnd="15dp"
app:errorEnabled="true"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/buttonConstraints"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/new_post_description_input_field"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ems="10"
android:inputType="textMultiLine" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -103,6 +103,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="upload_post_error">Post upload error</string>
<string name="description">Description…</string>
<string name="post">Post</string>
<string name="upload_next_step">Upload and go to next step</string>
<string name="add_photo">Add a photo</string>
<string name="post_image">One of the images in the post</string>
<string name="switch_to_grid">Switch to grid view</string>