diff --git a/app/build.gradle b/app/build.gradle
index 58c480f3..033aa3f0 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -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
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b77f5f17..8334af4a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -72,6 +72,9 @@
+
+
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
}
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
index ef640f83..f1e35b3d 100644
--- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt
@@ -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> by lazy {
MutableLiveData>().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().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 create(modelClass: Class): T {
return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java).newInstance(application, clipdata, instance)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt
new file mode 100644
index 00000000..4b0fddd6
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt
@@ -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(PHOTO_DATA) as ArrayList?
+
+ 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
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionViewModel.kt
new file mode 100644
index 00000000..91e9e7be
--- /dev/null
+++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionViewModel.kt
@@ -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> by lazy {
+ MutableLiveData>().also {
+ it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) }
+ }
+ }
+
+ @Inject
+ lateinit var apiHolder: PixelfedAPIHolder
+
+ private val _uiState: MutableStateFlow
+
+ 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 = _uiState
+
+ // Map photoData indexes to FFmpeg Session IDs
+ private val sessionMap: MutableMap = mutableMapOf()
+ // Keep track of temporary files to delete them (avoids filling cache super fast with videos)
+ private val tempFiles: java.util.ArrayList = java.util.ArrayList()
+
+ fun userMessageShown() {
+ _uiState.update { currentUiState ->
+ currentUiState.copy(userMessage = null)
+ }
+ }
+
+ fun getPhotoData(): LiveData> = 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.value): MutableList {
+ val dataToAdd: ArrayList = arrayListOf()
+ var count = clipData.itemCount
+ if(count + (previousList?.size ?: 0) > instance!!.albumLimit){
+ _uiState.update { currentUiState ->
+ currentUiState.copy(userMessage = getApplication().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 =
+ 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.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 {
+ val size: Long =
+ if (uri.scheme =="content") {
+ getApplication().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().contentResolver)
+ val isVideo = type.startsWith("video/")
+
+ if(isVideo && !instance!!.videoEnabled){
+ _uiState.update { currentUiState ->
+ currentUiState.copy(userMessage = getApplication().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().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().contentResolver)
+
+ val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication().cacheDir)
+
+ val imageUri = data.imageUri
+
+ val (strippedOrNot, size) = try {
+ val orientation = ExifInterface(getApplication().contentResolver.openInputStream(imageUri)!!).getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
+
+ stripMetadata(imageUri, strippedImage, getApplication().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().contentResolver.openInputStream(imageUri)!!
+ } catch (e: FileNotFoundException){
+ _uiState.update { currentUiState ->
+ currentUiState.copy(
+ userMessage = getApplication().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().getString(R.string.file_not_found,
+ data.imageUri)
+ )
+ }
+ return
+ }
+
+ val type = data.imageUri.getMimeType(getApplication().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().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().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().startActivity(intent)
+ } catch (exception: IOException) {
+ Toast.makeText(getApplication(), getApplication().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().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, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return modelClass.getConstructor(Application::class.java, ArrayList::class.java, InstanceDatabaseEntity::class.java).newInstance(application, photoData, instance)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt
index b9a779bb..54bd1759 100644
--- a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt
+++ b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt
@@ -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?
diff --git a/app/src/main/res/layout/activity_post_creation.xml b/app/src/main/res/layout/activity_post_creation.xml
index 7fce4465..05ed23a0 100644
--- a/app/src/main/res/layout/activity_post_creation.xml
+++ b/app/src/main/res/layout/activity_post_creation.xml
@@ -6,60 +6,6 @@
android:layout_height="match_parent"
tools:context=".postCreation.PostCreationActivity">
-
-
-
-
-
-
-
-
-
-
-
-
+ app:layout_constraintStart_toStartOf="parent" />
+ app:layout_constraintTop_toBottomOf="@id/carousel">
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index d5dfbe29..49e50c2b 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -103,6 +103,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"
Post upload error
Description…
Post
+ Upload and go to next step
Add a photo
One of the images in the post
Switch to grid view