diff --git a/app/build.gradle b/app/build.gradle index bad43e53..65495e19 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -154,14 +154,14 @@ dependencies { implementation "androidx.annotation:annotation:1.5.0" implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation "androidx.activity:activity-ktx:1.6.1" - implementation 'androidx.fragment:fragment-ktx:1.5.4' + implementation 'androidx.fragment:fragment-ktx:1.5.5' implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'androidx.media2:media2-widget:1.2.1' implementation 'androidx.media2:media2-player:1.2.1' // Use the most recent version of CameraX - def cameraX_version = '1.1.0' + def cameraX_version = '1.2.0' implementation "androidx.camera:camera-core:$cameraX_version" implementation "androidx.camera:camera-camera2:$cameraX_version" // CameraX Lifecycle library @@ -201,7 +201,7 @@ dependencies { implementation 'com.github.connyduck:sparkbutton:4.1.0' - implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.2' + implementation 'org.pixeldroid.pixeldroid:android-media-editor:1.4' implementation project(path: ':scrambler') implementation('com.github.bumptech.glide:glide:4.14.2') { @@ -254,13 +254,13 @@ dependencies { androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test:rules:1.4.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.3' - androidTestImplementation 'androidx.test:runner:1.4.0' - androidTestImplementation 'androidx.test:rules:1.4.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0' + androidTestImplementation 'androidx.test:runner:1.5.1' + androidTestImplementation 'androidx.test:rules:1.5.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.4' + androidTestImplementation 'androidx.test:runner:1.5.1' + androidTestImplementation 'androidx.test:rules:1.5.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.0' + androidTestImplementation 'androidx.test.espresso:espresso-intents:3.5.0' androidTestImplementation 'com.android.support.test.espresso:espresso-contrib:3.0.2' androidTestImplementation 'com.squareup.okhttp3:mockwebserver:4.9.2' diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionViewModel.kt deleted file mode 100644 index 01458907..00000000 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionViewModel.kt +++ /dev/null @@ -1,314 +0,0 @@ -package org.pixeldroid.app.postCreation - -import android.app.Application -import android.content.Intent -import android.net.Uri -import android.text.Editable -import android.util.Log -import android.widget.Toast -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.app.utils.PixelDroidApplication -import org.pixeldroid.app.utils.api.objects.Attachment -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 retrofit2.HttpException -import java.io.File -import java.io.FileNotFoundException -import java.io.IOException -import java.net.URI -import javax.inject.Inject - - -// Models the UI state for the PostCreationActivity -data class PostSubmissionActivityUiState( - val userMessage: String? = null, - - 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, -) - -class PostSubmissionViewModel(application: Application, photodata: ArrayList? = null, val existingDescription: String? = null) : AndroidViewModel(application) { - private val photoData: MutableLiveData> by lazy { - MutableLiveData>().also { - if (photodata != null) { - it.value = photodata.toMutableList() - } - } - } - - @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(PostSubmissionActivityUiState(newPostDescriptionText = existingDescription ?: 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 - - 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: 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 - ) - } - } catch (exception: Exception) { - 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 - ) - } - }finally { - apiHolder.api = null - } - } - } - - fun newPostDescriptionChanged(text: Editable?) { - _uiState.update { it.copy(newPostDescriptionText = text.toString()) } - } - - fun trackTempFile(file: File) { - tempFiles.add(file) - } - - override fun onCleared() { - super.onCleared() - tempFiles.forEach { - it.delete() - } - } - - fun updateNSFW(checked: Boolean) { _uiState.update { it.copy(nsfw = checked) } } - - fun chooseAccount(which: UserDatabaseEntity) { - _uiState.update { it.copy(chosenAccount = which) } - } -} - - -class PostSubmissionViewModelFactory(val application: Application, val photoData: ArrayList, val existingDescription: String?) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return modelClass.getConstructor(Application::class.java, ArrayList::class.java, String::class.java).newInstance(application, photoData, existingDescription) - } -} \ 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 54bd1759..b9a779bb 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,7 +9,6 @@ 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 @@ -23,7 +22,6 @@ 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/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index a4b5fb4e..57c89346 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -5,6 +5,14 @@ false + + + + + + + + @@ -59,6 +67,14 @@ + + + + + + + + @@ -105,6 +121,14 @@ + + + + + + + + @@ -116,6 +140,14 @@ + + + + + + + + @@ -196,6 +228,14 @@ + + + + + + + + @@ -207,6 +247,14 @@ + + + + + + + + @@ -218,6 +266,14 @@ + + + + + + + + @@ -229,6 +285,14 @@ + + + + + + + + @@ -293,6 +357,14 @@ + + + + + + + + @@ -315,6 +387,14 @@ + + + + + + + + @@ -447,6 +527,14 @@ + + + + + + + + @@ -479,6 +567,14 @@ + + + + + + + + @@ -511,6 +607,14 @@ + + + + + + + + @@ -543,6 +647,14 @@ + + + + + + + + @@ -581,6 +693,14 @@ + + + + + + + + @@ -619,6 +739,14 @@ + + + + + + + + @@ -657,6 +785,14 @@ + + + + + + + + @@ -702,6 +838,14 @@ + + + + + + + + @@ -713,6 +857,14 @@ + + + + + + + + @@ -729,6 +881,11 @@ + + + + + @@ -754,6 +911,9 @@ + + + @@ -769,6 +929,14 @@ + + + + + + + + @@ -780,6 +948,14 @@ + + + + + + + + @@ -1013,6 +1189,11 @@ + + + + + @@ -1680,6 +1861,14 @@ + + + + + + + + @@ -1696,6 +1885,14 @@ + + + + + + + + @@ -1749,6 +1946,14 @@ + + + + + + + + @@ -1969,6 +2174,14 @@ + + + + + + + + @@ -2001,6 +2214,14 @@ + + + + + + + + @@ -2033,6 +2254,14 @@ + + + + + + + + @@ -2145,6 +2374,14 @@ + + + + + + + + @@ -2177,6 +2414,14 @@ + + + + + + + + @@ -2241,6 +2486,14 @@ + + + + + + + + @@ -2265,6 +2518,14 @@ + + + + + + + + @@ -2297,6 +2558,14 @@ + + + + + + + + @@ -2329,6 +2598,14 @@ + + + + + + + + @@ -2361,6 +2638,14 @@ + + + + + + + + @@ -2393,6 +2678,14 @@ + + + + + + + + @@ -2425,6 +2718,14 @@ + + + + + + + + @@ -2457,6 +2758,14 @@ + + + + + + + + @@ -2489,6 +2798,14 @@ + + + + + + + + @@ -2521,6 +2838,14 @@ + + + + + + + + @@ -2561,6 +2886,14 @@ + + + + + + + + @@ -2593,6 +2926,14 @@ + + + + + + + + @@ -2625,6 +2966,14 @@ + + + + + + + + @@ -2657,6 +3006,14 @@ + + + + + + + + @@ -2689,6 +3046,14 @@ + + + + + + + + @@ -2721,6 +3086,14 @@ + + + + + + + + @@ -2753,6 +3126,14 @@ + + + + + + + + @@ -2809,6 +3190,14 @@ + + + + + + + + @@ -2841,6 +3230,14 @@ + + + + + + + + @@ -2865,6 +3262,14 @@ + + + + + + + + @@ -2897,6 +3302,14 @@ + + + + + + + + @@ -2953,6 +3366,14 @@ + + + + + + + + @@ -2985,6 +3406,14 @@ + + + + + + + + @@ -3017,6 +3446,14 @@ + + + + + + + + @@ -3049,6 +3486,14 @@ + + + + + + + + @@ -3081,6 +3526,14 @@ + + + + + + + + @@ -3113,6 +3566,14 @@ + + + + + + + + @@ -3145,6 +3606,14 @@ + + + + + + + + @@ -3177,6 +3646,14 @@ + + + + + + + + @@ -3209,6 +3686,14 @@ + + + + + + + + @@ -3241,6 +3726,14 @@ + + + + + + + + @@ -3273,6 +3766,14 @@ + + + + + + + + @@ -3305,6 +3806,14 @@ + + + + + + + + @@ -3337,6 +3846,14 @@ + + + + + + + + @@ -3369,6 +3886,14 @@ + + + + + + + + @@ -3401,6 +3926,14 @@ + + + + + + + + @@ -3409,6 +3942,14 @@ + + + + + + + + @@ -3441,6 +3982,14 @@ + + + + + + + + @@ -3473,6 +4022,14 @@ + + + + + + + + @@ -4496,6 +5053,14 @@ + + + + + + + + @@ -4515,6 +5080,19 @@ + + + + + + + + + + + + + @@ -4561,6 +5139,11 @@ + + + + + @@ -6676,6 +7259,22 @@ + + + + + + + + + + + + + + + +