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