From 4f3020e0bed4461e5507fea97bd0a28d3ba0b57e Mon Sep 17 00:00:00 2001 From: fgerber Date: Tue, 15 Nov 2022 12:31:04 +0100 Subject: [PATCH] Restructure post creation activity into two fragments --- app/src/main/AndroidManifest.xml | 3 - .../app/postCreation/PostCreationActivity.kt | 278 +--------------- .../app/postCreation/PostCreationFragment.kt | 311 ++++++++++++++++++ .../app/postCreation/PostCreationViewModel.kt | 257 ++++++++++++++- .../postCreation/PostSubmissionActivity.kt | 193 ----------- .../postCreation/PostSubmissionFragment.kt | 194 +++++++++++ .../app/postCreation/camera/CameraFragment.kt | 1 + app/src/main/res/drawable/switch_account.xml | 5 + .../res/layout/activity_post_creation.xml | 98 +----- .../res/layout/fragment_post_creation.xml | 102 ++++++ ...ssion.xml => fragment_post_submission.xml} | 28 +- .../res/menu/post_submission_account_menu.xml | 2 +- .../res/navigation/post_creation_graph.xml | 25 ++ 13 files changed, 919 insertions(+), 578 deletions(-) create mode 100644 app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt delete mode 100644 app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt create mode 100644 app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt create mode 100644 app/src/main/res/drawable/switch_account.xml create mode 100644 app/src/main/res/layout/fragment_post_creation.xml rename app/src/main/res/layout/{activity_post_submission.xml => fragment_post_submission.xml} (89%) create mode 100644 app/src/main/res/navigation/post_creation_graph.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8d057857..915bc2c3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -67,9 +67,6 @@ - - - // update UI - binding.carousel.addData( - newPhotoData.map { - CarouselItem( - it.imageUri, it.imageDescription, it.video, - it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass, - it.videoEncodeComplete, it.videoEncodeError, - ) - } - ) - } - - 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() - } - binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled - binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled - binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled - binding.toolbarPostCreation.visibility = - if (uiState.isCarousel) VISIBLE else INVISIBLE - binding.carousel.layoutCarousel = uiState.isCarousel - } - } - } - - binding.carousel.apply { - layoutCarouselCallback = { model.becameCarousel(it)} - maxEntries = instance.albumLimit - addPhotoButtonCallback = { - addPhoto() - } - updateDescriptionCallback = { position: Int, description: String -> - model.updateDescription(position, description) - } - } - // get the description and send the post - binding.postCreationSendButton.setOnClickListener { - if (validatePost() && model.isNotEmpty()) { - model.nextStep(binding.root.context) - } - } - - binding.editPhotoButton.setOnClickListener { - binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> - edit(currentPosition) - } - } - - binding.addPhotoButton.setOnClickListener { - addPhoto() - } - - binding.savePhotoButton.setOnClickListener { - binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> - savePicture(it, currentPosition) - } - } - - binding.removePhotoButton.setOnClickListener { - binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> - model.removeAt(currentPosition) - model.cancelEncode(currentPosition) - } - } - - // 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) - } - - onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - 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) { _, _ -> - finish() - } - setNegativeButton(android.R.string.cancel) { _, _ -> } - show() - } - } else { - finish() - } - } - }) + binding = ActivityPostCreationBinding.inflate(layoutInflater) + setContentView(binding.root) + val navHostFragment = + supportFragmentManager.findFragmentById(R.id.postCreationContainer) as NavHostFragment + navController = navHostFragment.navController + navController.setGraph(R.navigation.post_creation_graph) } - private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> - if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) { - result.data?.clipData?.let { - model.setImages(model.addPossibleImages(it)) - } - } else if (result.resultCode != Activity.RESULT_CANCELED) { - Toast.makeText(applicationContext, R.string.add_images_error, Toast.LENGTH_SHORT).show() - } + override fun onSupportNavigateUp(): Boolean { + return navController.navigateUp() || super.onSupportNavigateUp() } - private fun addPhoto(){ - addPhotoResultContract.launch( - Intent(this, CameraActivity::class.java) - ) - } - - private fun savePicture(button: View, currentPosition: Int) { - val originalUri = model.getPhotoData().value!![currentPosition].imageUri - - val pair = getOutputFile(originalUri) - val outputStream: OutputStream = pair.first - val path: String = pair.second - - contentResolver.openInputStream(originalUri)!!.use { input -> - outputStream.use { output -> - input.copyTo(output) - } - } - - if(path.startsWith("file")) { - MediaScannerConnection.scanFile( - this, - arrayOf(path.toUri().toFile().absolutePath), - null - ) { path, uri -> - if (uri == null) { - Log.e( - "NEW IMAGE SCAN FAILED", - "Tried to scan $path, but it failed" - ) - } - } - } - Snackbar.make( - button, getString(R.string.save_image_success), - Snackbar.LENGTH_LONG - ).show() - } - - private fun getOutputFile(uri: Uri): Pair { - val extension = uri.fileExtension(contentResolver) - - val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US) - .format(System.currentTimeMillis()) + ".$extension" - - val outputStream: OutputStream - val path: String - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - val resolver: ContentResolver = contentResolver - val type = uri.getMimeType(contentResolver) - val contentValues = ContentValues() - contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name) - contentValues.put(MediaStore.MediaColumns.MIME_TYPE, type) - contentValues.put( - MediaStore.MediaColumns.RELATIVE_PATH, - Environment.DIRECTORY_PICTURES - ) - val store = - if (type.startsWith("video")) MediaStore.Video.Media.EXTERNAL_CONTENT_URI - else MediaStore.Images.Media.EXTERNAL_CONTENT_URI - val imageUri: Uri = resolver.insert(store, contentValues)!! - path = imageUri.toString() - outputStream = resolver.openOutputStream(imageUri)!! - } else { - @Suppress("DEPRECATION") val imagesDir = - Environment.getExternalStoragePublicDirectory(getString(R.string.app_name)) - imagesDir.mkdir() - val file = File(imagesDir, name) - path = Uri.fromFile(file).toString() - outputStream = file.outputStream() - } - return Pair(outputStream, path) - } - - - private fun validatePost(): Boolean { - if(model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false){ - AlertDialog.Builder(this).apply { - setMessage(R.string.still_encoding) - setNegativeButton(android.R.string.ok) { _, _ -> } - }.show() - return false - } - return true - } - - private val editResultContract: ActivityResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()){ - result: ActivityResult? -> - if (result?.resultCode == Activity.RESULT_OK && result.data != null) { - val position: Int = result.data!!.getIntExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_POSITION, 0) - model.modifyAt(position, result.data!!) - ?: Toast.makeText(applicationContext, R.string.error_editing, Toast.LENGTH_SHORT).show() - } else if(result?.resultCode != Activity.RESULT_CANCELED){ - Toast.makeText(applicationContext, R.string.error_editing, Toast.LENGTH_SHORT).show() - } - } - - private fun edit(position: Int) { - val intent = Intent( - this, - if(model.getPhotoData().value!![position].video) org.pixeldroid.media_editor.photoEdit.VideoEditActivity::class.java else org.pixeldroid.media_editor.photoEdit.PhotoEditActivity::class.java - ) - .putExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri) - .putExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_POSITION, position) - - editResultContract.launch(intent) - - } } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt new file mode 100644 index 00000000..e0d722b8 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt @@ -0,0 +1,311 @@ +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.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Toast +import androidx.activity.OnBackPressedCallback +import androidx.activity.result.ActivityResult +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.core.net.toFile +import androidx.core.net.toUri +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +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.FragmentPostCreationBinding +import org.pixeldroid.app.postCreation.camera.CameraActivity +import org.pixeldroid.app.postCreation.carousel.CarouselItem +import org.pixeldroid.app.utils.BaseFragment +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.media_editor.photoEdit.PhotoEditActivity +import org.pixeldroid.media_editor.photoEdit.VideoEditActivity +import java.io.File +import java.io.OutputStream +import java.text.SimpleDateFormat +import java.util.* + + +class PostCreationFragment : BaseFragment() { + + private var user: UserDatabaseEntity? = null + private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "") + + private lateinit var binding: FragmentPostCreationBinding + private lateinit var model: PostCreationViewModel + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + super.onCreateView(inflater, container, savedInstanceState) + + // Inflate the layout for this fragment + binding = FragmentPostCreationBinding.inflate(layoutInflater) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + user = db.userDao().getActiveUser() + + instance = user?.run { + db.instanceDao().getAll().first { instanceDatabaseEntity -> + instanceDatabaseEntity.uri.contains(instance_uri) + } + } ?: InstanceDatabaseEntity("", "") + + val _model: PostCreationViewModel by activityViewModels { + PostCreationViewModelFactory( + requireActivity().application, + requireActivity().intent.clipData!!, + instance, + requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION), + requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false) + ) + } + model = _model + + model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData -> + // update UI + binding.carousel.addData( + newPhotoData.map { + CarouselItem( + it.imageUri, it.imageDescription, it.video, + it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass, + it.videoEncodeComplete, it.videoEncodeError, + ) + } + ) + } + + lifecycleScope.launch { + viewLifecycleOwner.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() + } + binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled + binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled + binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled + binding.toolbarPostCreation.visibility = + if (uiState.isCarousel) View.VISIBLE else View.INVISIBLE + binding.carousel.layoutCarousel = uiState.isCarousel + } + } + } + + binding.carousel.apply { + layoutCarouselCallback = { model.becameCarousel(it)} + maxEntries = instance.albumLimit + addPhotoButtonCallback = { + addPhoto() + } + updateDescriptionCallback = { position: Int, description: String -> + model.updateDescription(position, description) + } + } + // get the description and send the post + binding.postCreationSendButton.setOnClickListener { + if (validatePost() && model.isNotEmpty()) { + findNavController().navigate(R.id.action_postCreationFragment_to_postSubmissionFragment) + } + } + + binding.editPhotoButton.setOnClickListener { + binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> + edit(currentPosition) + } + } + + binding.addPhotoButton.setOnClickListener { + addPhoto() + } + + binding.savePhotoButton.setOnClickListener { + binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> + savePicture(it, currentPosition) + } + } + + binding.removePhotoButton.setOnClickListener { + binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> + model.removeAt(currentPosition) + model.cancelEncode(currentPosition) + } + } + + // Clean up temporary files, if any + val tempFiles = requireActivity().intent.getStringArrayExtra(PostCreationActivity.TEMP_FILES) + tempFiles?.asList()?.forEach { + val file = File(binding.root.context.cacheDir, it) + model.trackTempFile(file) + } + + // Handle back pressed button + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + val redraft = requireActivity().intent.getBooleanExtra(PostCreationActivity.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) { _, _ -> + requireActivity().finish() + } + setNegativeButton(android.R.string.cancel) { _, _ -> } + show() + } + } else { + requireActivity().finish() + } + } + }) + } + + private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) { + result.data?.clipData?.let { + model.setImages(model.addPossibleImages(it)) + } + } else if (result.resultCode != Activity.RESULT_CANCELED) { + Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show() + } + } + + private fun addPhoto(){ + addPhotoResultContract.launch( + Intent(requireActivity(), CameraActivity::class.java) + ) + } + + private fun savePicture(button: View, currentPosition: Int) { + val originalUri = model.getPhotoData().value!![currentPosition].imageUri + + val pair = getOutputFile(originalUri) + val outputStream: OutputStream = pair.first + val path: String = pair.second + + requireActivity().contentResolver.openInputStream(originalUri)!!.use { input -> + outputStream.use { output -> + input.copyTo(output) + } + } + + if(path.startsWith("file")) { + MediaScannerConnection.scanFile( + requireActivity(), + arrayOf(path.toUri().toFile().absolutePath), + null + ) { path, uri -> + if (uri == null) { + Log.e( + "NEW IMAGE SCAN FAILED", + "Tried to scan $path, but it failed" + ) + } + } + } + Snackbar.make( + button, getString(R.string.save_image_success), + Snackbar.LENGTH_LONG + ).show() + } + + private fun getOutputFile(uri: Uri): Pair { + val extension = uri.fileExtension(requireActivity().contentResolver) + + val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US) + .format(System.currentTimeMillis()) + ".$extension" + + val outputStream: OutputStream + val path: String + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + val resolver: ContentResolver = requireActivity().contentResolver + val type = uri.getMimeType(requireActivity().contentResolver) + val contentValues = ContentValues() + contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name) + contentValues.put(MediaStore.MediaColumns.MIME_TYPE, type) + contentValues.put( + MediaStore.MediaColumns.RELATIVE_PATH, + Environment.DIRECTORY_PICTURES + ) + val store = + if (type.startsWith("video")) MediaStore.Video.Media.EXTERNAL_CONTENT_URI + else MediaStore.Images.Media.EXTERNAL_CONTENT_URI + val imageUri: Uri = resolver.insert(store, contentValues)!! + path = imageUri.toString() + outputStream = resolver.openOutputStream(imageUri)!! + } else { + @Suppress("DEPRECATION") val imagesDir = + Environment.getExternalStoragePublicDirectory(getString(R.string.app_name)) + imagesDir.mkdir() + val file = File(imagesDir, name) + path = Uri.fromFile(file).toString() + outputStream = file.outputStream() + } + return Pair(outputStream, path) + } + + + private fun validatePost(): Boolean { + if (model.getPhotoData().value?.all { !it.video || it.videoEncodeComplete } == false) { + AlertDialog.Builder(requireActivity()).apply { + setMessage(R.string.still_encoding) + setNegativeButton(android.R.string.ok) { _, _ -> } + }.show() + return false + } + return true + } + + private val editResultContract: ActivityResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult()){ + result: ActivityResult? -> + if (result?.resultCode == Activity.RESULT_OK && result.data != null) { + val position: Int = result.data!!.getIntExtra(org.pixeldroid.media_editor.photoEdit.PhotoEditActivity.PICTURE_POSITION, 0) + model.modifyAt(position, result.data!!) + ?: Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show() + } else if(result?.resultCode != Activity.RESULT_CANCELED){ + Toast.makeText(requireActivity(), R.string.error_editing, Toast.LENGTH_SHORT).show() + } + } + + private fun edit(position: Int) { + val intent = Intent( + requireActivity(), + if (model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java + ) + .putExtra(PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri) + .putExtra(PhotoEditActivity.PICTURE_POSITION, position) + + editResultContract.launch(intent) + } +} + 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 dcb5c6a7..4f9c214f 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -2,14 +2,16 @@ 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 androidx.core.content.ContextCompat +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.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -17,18 +19,32 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope 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 kotlinx.parcelize.Parcelize +import okhttp3.MultipartBody +import org.pixeldroid.app.MainActivity import org.pixeldroid.app.R 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.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.di.PixelfedAPIHolder +import org.pixeldroid.app.utils.fileExtension import org.pixeldroid.app.utils.getMimeType import org.pixeldroid.media_editor.photoEdit.VideoEditActivity +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.collections.ArrayList import kotlin.collections.MutableList @@ -57,7 +73,19 @@ data class PostCreationActivityUiState( val isCarousel: Boolean = true, + val postCreationSendButtonEnabled: Boolean = true, + val newPostDescriptionText: String = "", + val nsfw: Boolean = false, + + val chosenAccount: UserDatabaseEntity? = null, + + val uploadProgressBarVisible: Boolean = false, + val uploadProgress: Int = 0, + val uploadCompletedTextviewVisible: Boolean = false, + val uploadErrorVisible: Boolean = false, + val uploadErrorExplanationText: String = "", + val uploadErrorExplanationVisible: Boolean = false, ) @Parcelize @@ -92,7 +120,10 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null PreferenceManager.getDefaultSharedPreferences(application) val initialDescription = sharedPreferences.getString("prefill_description", "") ?: "" - _uiState = MutableStateFlow(PostCreationActivityUiState(newPostDescriptionText = existingDescription ?: initialDescription)) + _uiState = MutableStateFlow(PostCreationActivityUiState( + newPostDescriptionText = existingDescription ?: initialDescription, + nsfw = existingNSFW + )) } val uiState: StateFlow = _uiState @@ -169,7 +200,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null val type = uri.getMimeType(getApplication().contentResolver) val isVideo = type.startsWith("video/") - if(isVideo && !instance!!.videoEnabled){ + if (isVideo && !instance!!.videoEnabled) { _uiState.update { currentUiState -> currentUiState.copy(userMessage = getApplication().getString(R.string.video_not_supported)) } @@ -203,17 +234,6 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null photoData.value = photoData.value } - /** - * Next step - */ - fun nextStep(context: Context) { - val intent = Intent(context, PostSubmissionActivity::class.java) - intent.putExtra(PostSubmissionActivity.PHOTO_DATA, getPhotoData().value?.let { ArrayList(it) }) - intent.putExtra(PostSubmissionActivity.PICTURE_DESCRIPTION, existingDescription) - intent.putExtra(PostSubmissionActivity.POST_NSFW, existingNSFW) - ContextCompat.startActivity(context, intent, null) - } - fun modifyAt(position: Int, data: Intent): Unit? { val result: PhotoData = photoData.value?.getOrNull(position)?.run { if (video) { @@ -262,7 +282,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null private fun videoEncodeProgress(originalUri: Uri, progress: Int, firstPass: Boolean, outputVideoPath: Uri?, error: Boolean){ photoData.value?.indexOfFirst { it.imageUri == originalUri }?.let { position -> - if(outputVideoPath != null){ + if (outputVideoPath != null) { // If outputVideoPath is not null, it means the video is done and we can change Uris val (size, _) = getSizeAndVideoValidate(outputVideoPath, position) @@ -310,7 +330,7 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null } } - fun registerNewFFmpegSession(position: Uri, sessionId: Long) { + private fun registerNewFFmpegSession(position: Uri, sessionId: Long) { sessionMap[position] = sessionId } @@ -321,6 +341,209 @@ class PostCreationViewModel(application: Application, clipdata: ClipData? = null ) } } + + fun resetUploadStatus() { + photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList() + } + + /** + * 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, + 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) } + + // Ugly temporary account switching, but it works well enough for now + val api = uiState.value.chosenAccount?.let { + apiHolder.setToCurrentUser(it) + } ?: apiHolder.api ?: apiHolder.setToCurrentUser() + + val inter = api.mediaUpload(description, requestBody.parts[0]) + + apiHolder.api = null + 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 + + // TODO: investigate why this works but booleans don't + val nsfw = if (uiState.value.nsfw) 1 else 0 + + _uiState.update { currentUiState -> + currentUiState.copy( + postCreationSendButtonEnabled = false + ) + } + viewModelScope.launch { + try { + //Ugly temporary account switching, but it works well enough for now + val api = uiState.value.chosenAccount?.let { + apiHolder.setToCurrentUser(it) + } ?: apiHolder.api ?: apiHolder.setToCurrentUser() + + api.postStatus( + statusText = description, + media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList(), + sensitive = nsfw + ) + 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 + ) + } + } finally { + apiHolder.api = null + } + } + } + + fun newPostDescriptionChanged(text: Editable?) { + _uiState.update { it.copy(newPostDescriptionText = text.toString()) } + } + + fun updateNSFW(checked: Boolean) { _uiState.update { it.copy(nsfw = checked) } } + + fun chooseAccount(which: UserDatabaseEntity) { + _uiState.update { it.copy(chosenAccount = which) } + } } class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean) : ViewModelProvider.Factory { diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt deleted file mode 100644 index 76ce02f3..00000000 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionActivity.kt +++ /dev/null @@ -1,193 +0,0 @@ -package org.pixeldroid.app.postCreation - -import android.app.AlertDialog -import android.os.Bundle -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.view.View.GONE -import android.view.View.INVISIBLE -import android.view.View.VISIBLE -import androidx.activity.viewModels -import androidx.core.widget.doAfterTextChanged -import androidx.lifecycle.Lifecycle -import androidx.lifecycle.lifecycleScope -import androidx.lifecycle.repeatOnLifecycle -import kotlinx.coroutines.launch -import org.pixeldroid.app.R -import org.pixeldroid.app.databinding.ActivityPostSubmissionBinding -import org.pixeldroid.app.postCreation.PostCreationActivity.Companion.TEMP_FILES -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.setSquareImageFromURL -import java.io.File - - -class PostSubmissionActivity : BaseThemedWithoutBarActivity() { - - companion object { - internal const val PICTURE_DESCRIPTION = "picture_description" - internal const val PHOTO_DATA = "photo_data" - internal const val POST_NSFW = "post_nsfw" -} - - private lateinit var accounts: List - private var selectedAccount: Int = -1 - private lateinit var menu: Menu - 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) - supportActionBar?.setTitle(R.string.add_details) - - user = db.userDao().getActiveUser() - accounts = db.userDao().getAll() - - 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!!, - intent.getStringExtra(PICTURE_DESCRIPTION) - ) - } - model = _model - - val sensitive = intent.getBooleanExtra(POST_NSFW, false) - model.updateNSFW(sensitive) - binding.nsfwSwitch.isChecked = sensitive - - 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 - - selectedAccount = accounts.indexOf(uiState.chosenAccount) - - binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText - } - } - } - binding.newPostDescriptionInputField.doAfterTextChanged { - model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text) - } - - binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked -> - model.updateNSFW(isChecked) - } - - 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 - - setSquareImageFromURL(View(applicationContext), photoData!![0].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 onCreateOptionsMenu(newMenu: Menu): Boolean { - menuInflater.inflate(R.menu.post_submission_account_menu, newMenu) - menu = newMenu - return true - } - - override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId){ - R.id.action_switch_accounts -> { - AlertDialog.Builder(this).apply { - setIcon(R.drawable.material_drawer_ico_account) - setTitle(R.string.switch_accounts) - setSingleChoiceItems(accounts.map { it.username + " (${it.fullHandle})" }.toTypedArray(), selectedAccount) { dialog, which -> - if(selectedAccount != which){ - model.chooseAccount(accounts[which]) - } - dialog.dismiss() - } - setNegativeButton(android.R.string.cancel) { _, _ -> } - }.show() - return true - } - } - return super.onOptionsItemSelected(item) - } - - 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/PostSubmissionFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt new file mode 100644 index 00000000..371f41e3 --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt @@ -0,0 +1,194 @@ +package org.pixeldroid.app.postCreation + +import android.app.AlertDialog +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.activity.OnBackPressedCallback +import androidx.core.view.MenuProvider +import androidx.core.widget.doAfterTextChanged +import androidx.fragment.app.activityViewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle +import androidx.navigation.fragment.findNavController +import androidx.navigation.ui.setupWithNavController +import kotlinx.coroutines.launch +import org.pixeldroid.app.R +import org.pixeldroid.app.databinding.FragmentPostSubmissionBinding +import org.pixeldroid.app.utils.BaseFragment +import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity +import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity +import org.pixeldroid.app.utils.setSquareImageFromURL + + +class PostSubmissionFragment : BaseFragment() { + + private lateinit var accounts: List + private var selectedAccount: Int = -1 +// private lateinit var menu: Menu + + private var user: UserDatabaseEntity? = null + private lateinit var instance: InstanceDatabaseEntity + + private lateinit var binding: FragmentPostSubmissionBinding + private lateinit var model: PostCreationViewModel + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + super.onCreateView(inflater, container, savedInstanceState) + + // Inflate the layout for this fragment + binding = FragmentPostSubmissionBinding.inflate(layoutInflater) +// setHasOptionsMenu(true) + return binding.root + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + binding.topBar.setupWithNavController(findNavController()) + + user = db.userDao().getActiveUser() + accounts = db.userDao().getAll() + + instance = user?.run { + db.instanceDao().getAll().first { instanceDatabaseEntity -> + instanceDatabaseEntity.uri.contains(instance_uri) + } + } ?: InstanceDatabaseEntity("", "") + + val _model: PostCreationViewModel by activityViewModels { + PostCreationViewModelFactory( + requireActivity().application, + requireActivity().intent.clipData!!, + instance, + requireActivity().intent.getStringExtra(PostCreationActivity.PICTURE_DESCRIPTION), + requireActivity().intent.getBooleanExtra(PostCreationActivity.POST_NSFW, false) + ) + } + model = _model + + // Display the values from the view model + binding.nsfwSwitch.isChecked = model.uiState.value.nsfw + binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText) + + lifecycleScope.launch { + viewLifecycleOwner.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) View.VISIBLE else View.INVISIBLE + binding.uploadProgressBar.progress = uiState.uploadProgress + binding.uploadCompletedTextview.visibility = + if (uiState.uploadCompletedTextviewVisible) View.VISIBLE else View.INVISIBLE + binding.uploadError.visibility = + if (uiState.uploadErrorVisible) View.VISIBLE else View.INVISIBLE + binding.uploadErrorTextExplanation.visibility = + if (uiState.uploadErrorExplanationVisible) View.VISIBLE else View.INVISIBLE + + selectedAccount = accounts.indexOf(uiState.chosenAccount) + + binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText + } + } + } + + binding.newPostDescriptionInputField.doAfterTextChanged { + model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text) + } + + binding.nsfwSwitch.setOnCheckedChangeListener { _, isChecked -> + model.updateNSFW(isChecked) + } + + binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars + + setSquareImageFromURL(View(requireActivity()), model.getPhotoData()!!.value?.get(0)?.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() + } + + // Handle back pressed button + requireActivity().onBackPressedDispatcher.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + findNavController().navigate(R.id.action_postSubmissionFragment_to_postCreationFragment) + } + }) + + binding.topBar.addMenuProvider(object: MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + // Add menu items here + menuInflater.inflate(R.menu.post_submission_account_menu, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean { + // Handle the menu selection + return when (menuItem.itemId) { + R.id.action_switch_accounts -> { + AlertDialog.Builder(requireActivity()).apply { + setIcon(R.drawable.switch_account) + setTitle(R.string.switch_accounts) + setSingleChoiceItems(accounts.map { it.username + " (${it.fullHandle})" }.toTypedArray(), selectedAccount) { dialog, which -> + if (selectedAccount != which) { + model.chooseAccount(accounts[which]) + } + dialog.dismiss() + } + setNegativeButton(android.R.string.cancel) { _, _ -> } + }.show() + return true + } + else -> false + } + } + }, viewLifecycleOwner, Lifecycle.State.RESUMED) + } + + 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 = View.GONE + binding.postCreationSendButton.visibility = View.VISIBLE + } else { + binding.postingProgressBar.visibility = View.VISIBLE + binding.postCreationSendButton.visibility = View.GONE + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt index 51bf07b0..715c4ea1 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/camera/CameraFragment.kt @@ -81,6 +81,7 @@ class CameraFragment : BaseFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View { + super.onCreateView(inflater, container, savedInstanceState) inActivity = arguments?.getBoolean("CameraActivity") ?: false binding = FragmentCameraBinding.inflate(layoutInflater) diff --git a/app/src/main/res/drawable/switch_account.xml b/app/src/main/res/drawable/switch_account.xml new file mode 100644 index 00000000..ace366b2 --- /dev/null +++ b/app/src/main/res/drawable/switch_account.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_post_creation.xml b/app/src/main/res/layout/activity_post_creation.xml index 57d5def9..98f621cc 100644 --- a/app/src/main/res/layout/activity_post_creation.xml +++ b/app/src/main/res/layout/activity_post_creation.xml @@ -6,96 +6,16 @@ android:layout_height="match_parent" tools:context=".postCreation.PostCreationActivity"> - - - - -