diff --git a/app/build.gradle b/app/build.gradle index 7e3793eb..6e29fd3e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,10 +1,13 @@ import com.android.build.api.dsl.ManagedVirtualDevice -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'jacoco' -apply plugin: "kotlin-parcelize" -apply plugin: 'com.google.devtools.ksp' +plugins { + id("com.android.application") + id("com.google.dagger.hilt.android") + id("kotlin-android") + id("jacoco") + id("kotlin-parcelize") + id("com.google.devtools.ksp") +} android { @@ -184,6 +187,9 @@ dependencies { implementation 'com.google.dagger:dagger:2.50' ksp 'com.google.dagger:dagger-compiler:2.50' + implementation("com.google.dagger:hilt-android:2.50") + ksp "com.google.dagger:hilt-compiler:2.50" + implementation 'com.squareup.okhttp3:okhttp:4.12.0' implementation 'com.squareup.retrofit2:retrofit:2.9.0' implementation 'com.squareup.retrofit2:converter-gson:2.9.0' diff --git a/app/src/main/java/org/pixeldroid/app/MainActivity.kt b/app/src/main/java/org/pixeldroid/app/MainActivity.kt index 5e7d46ca..6912fbe2 100644 --- a/app/src/main/java/org/pixeldroid/app/MainActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/MainActivity.kt @@ -78,7 +78,7 @@ class MainActivity : BaseActivity() { private lateinit var header: AccountHeaderView private var user: UserDatabaseEntity? = null - private lateinit var model: MainActivityViewModel + private val model: MainActivityViewModel by viewModels() companion object { const val ADD_ACCOUNT_IDENTIFIER: Long = -13 @@ -111,12 +111,6 @@ class MainActivity : BaseActivity() { } else { sendTraceDroidStackTracesIfExist("contact@pixeldroid.org", this) - val _model: MainActivityViewModel by viewModels { - MainActivityViewModelFactory(application) - } - model = _model - - setupDrawer() val tabs: List<() -> Fragment> = listOf( { @@ -280,13 +274,13 @@ class MainActivity : BaseActivity() { val remainingUsers = db.userDao().getAll() if (remainingUsers.isEmpty()){ - //no more users, start first-time login flow + // No more users, start first-time login flow launchActivity(LoginActivity(), firstTime = true) } else { val newActive = remainingUsers.first() db.userDao().activateUser(newActive.user_id, newActive.instance_uri) apiHolder.setToCurrentUser() - //relaunch the app + // Relaunch the app launchActivity(MainActivity(), firstTime = true) } } @@ -334,9 +328,11 @@ class MainActivity : BaseActivity() { } private fun switchUser(userId: String, instance_uri: String) { - db.userDao().deActivateActiveUsers() - db.userDao().activateUser(userId, instance_uri) - apiHolder.setToCurrentUser() + db.runInTransaction{ + db.userDao().deActivateActiveUsers() + db.userDao().activateUser(userId, instance_uri) + apiHolder.setToCurrentUser() + } } private inline fun primaryDrawerItem(block: PrimaryDrawerItem.() -> Unit): PrimaryDrawerItem { diff --git a/app/src/main/java/org/pixeldroid/app/MainActivityViewModel.kt b/app/src/main/java/org/pixeldroid/app/MainActivityViewModel.kt index 77d3ce6b..60d41a03 100644 --- a/app/src/main/java/org/pixeldroid/app/MainActivityViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/MainActivityViewModel.kt @@ -1,25 +1,22 @@ package org.pixeldroid.app -import android.app.Application -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch -import org.pixeldroid.app.utils.PixelDroidApplication import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import javax.inject.Inject -class MainActivityViewModel(application: Application) : AndroidViewModel(application) { - - @Inject - lateinit var db: AppDatabase +@HiltViewModel +class MainActivityViewModel @Inject constructor( + private val db: AppDatabase +): ViewModel() { // Mutable state flow that will be used internally in the ViewModel, empty list is given as initial value. private val _users = MutableStateFlow(emptyList()) @@ -29,7 +26,6 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica init { - (application as PixelDroidApplication).getAppComponent().inject(this) getUsers() } @@ -41,12 +37,4 @@ class MainActivityViewModel(application: Application) : AndroidViewModel(applica } } } -} - -class MainActivityViewModelFactory( - val application: Application, -) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return modelClass.getConstructor(Application::class.java).newInstance(application) - } -} +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt index 932b2e4f..bef490c0 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt @@ -1,43 +1,28 @@ package org.pixeldroid.app.postCreation -import android.os.* +import android.os.Bundle import androidx.navigation.NavController import androidx.navigation.fragment.NavHostFragment import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityPostCreationBinding import org.pixeldroid.app.utils.BaseActivity -import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity -import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity - -const val TAG = "Post Creation Activity" class PostCreationActivity : BaseActivity() { companion object { - internal const val PICTURE_DESCRIPTION = "picture_description" + internal const val POST_DESCRIPTION = "post_description" + internal const val PICTURE_DESCRIPTIONS = "picture_descriptions" internal const val POST_REDRAFT = "post_redraft" internal const val POST_NSFW = "post_nsfw" internal const val TEMP_FILES = "temp_files" } - private var user: UserDatabaseEntity? = null - private lateinit var instance: InstanceDatabaseEntity - private lateinit var binding: ActivityPostCreationBinding - private lateinit var navController: NavController override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - user = db.userDao().getActiveUser() - - instance = user?.run { - db.instanceDao().getAll().first { instanceDatabaseEntity -> - instanceDatabaseEntity.uri.contains(instance_uri) - } - } ?: InstanceDatabaseEntity("", "") - binding = ActivityPostCreationBinding.inflate(layoutInflater) setContentView(binding.root) val navHostFragment = @@ -46,8 +31,5 @@ class PostCreationActivity : BaseActivity() { navController.setGraph(R.navigation.post_creation_graph) } - override fun onSupportNavigateUp(): Boolean { - return navController.navigateUp() || super.onSupportNavigateUp() - } - + override fun onSupportNavigateUp() = navController.navigateUp() || super.onSupportNavigateUp() } \ 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 index 552d4b01..8e53629a 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationFragment.kt @@ -33,12 +33,10 @@ 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.camera.CameraFragment import org.pixeldroid.app.postCreation.carousel.CarouselItem import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.bindingLifecycleAware 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 @@ -48,14 +46,10 @@ import java.io.OutputStream import java.text.SimpleDateFormat import java.util.Locale - class PostCreationFragment : BaseFragment() { - private var user: UserDatabaseEntity? = null - private var instance: InstanceDatabaseEntity = InstanceDatabaseEntity("", "") - private var binding: FragmentPostCreationBinding by bindingLifecycleAware() - private lateinit var model: PostCreationViewModel + private val model: PostCreationViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -72,30 +66,16 @@ class PostCreationFragment : BaseFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - user = db.userDao().getActiveUser() + val user = db.userDao().getActiveUser() - instance = user?.run { - db.instanceDao().getAll().first { instanceDatabaseEntity -> - instanceDatabaseEntity.uri.contains(instance_uri) - } + val instance = user?.run { + db.instanceDao().getInstance(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), - requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false), - ) - } - model = _model - - model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData -> + model.getPhotoData().observe(viewLifecycleOwner) { newPhotoData: MutableList? -> // update UI binding.carousel.addData( - newPhotoData.map { + newPhotoData.orEmpty().map { CarouselItem( it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress, it.videoEncodeStabilizationFirstPass, @@ -103,7 +83,7 @@ class PostCreationFragment : BaseFragment() { ) } ) - binding.postCreationNextButton.isEnabled = newPhotoData.isNotEmpty() + binding.postCreationNextButton.isEnabled = newPhotoData?.isNotEmpty() ?: false } lifecycleScope.launch { @@ -227,10 +207,9 @@ class PostCreationFragment : BaseFragment() { } 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)) - } + val uris = result.data?.extras?.getParcelableArrayList(Intent.EXTRA_STREAM) + if (result.resultCode == Activity.RESULT_OK && uris != null) { + model.setImages(model.addPossibleImages(uris, emptyList())) } else if (result.resultCode != Activity.RESULT_CANCELED) { Toast.makeText(requireActivity(), R.string.add_images_error, Toast.LENGTH_SHORT).show() } 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 3569ce8c..08d05f01 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -1,7 +1,6 @@ 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 @@ -12,15 +11,16 @@ 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 +import androidx.lifecycle.SavedStateHandle 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 dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.disposables.Disposable @@ -33,10 +33,10 @@ 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.postCreation.camera.CameraFragment + import org.pixeldroid.app.utils.api.objects.Attachment + import org.pixeldroid.app.utils.db.AppDatabase + 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 @@ -47,21 +47,10 @@ import java.io.FileNotFoundException import java.io.IOException import java.net.URI import javax.inject.Inject -import kotlin.collections.ArrayList -import kotlin.collections.MutableList -import kotlin.collections.MutableMap -import kotlin.collections.arrayListOf -import kotlin.collections.forEach -import kotlin.collections.get -import kotlin.collections.getOrNull -import kotlin.collections.indexOfFirst -import kotlin.collections.mutableListOf -import kotlin.collections.mutableMapOf -import kotlin.collections.plus import kotlin.collections.set -import kotlin.collections.toMutableList import kotlin.math.ceil +const val TAG = "Post Creation ViewModel" // Models the UI state for the PostCreationActivity data class PostCreationActivityUiState( @@ -108,35 +97,40 @@ data class PhotoData( var videoEncodeError: Boolean = false, ) : Parcelable -class PostCreationViewModel( - application: Application, - clipdata: ClipData? = null, - val instance: InstanceDatabaseEntity? = null, - existingDescription: String? = null, - existingNSFW: Boolean = false, - storyCreation: Boolean = false, -) : AndroidViewModel(application) { +@HiltViewModel +class PostCreationViewModel @Inject constructor( + private val state: SavedStateHandle, + @ApplicationContext private val applicationContext: Context, + db: AppDatabase +): ViewModel() { private var storyPhotoDataBackup: MutableList? = null private val photoData: MutableLiveData> by lazy { - MutableLiveData>().also { - it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) } - } + MutableLiveData>( + addPossibleImages( + state.get>(Intent.EXTRA_STREAM), + state.get>(PostCreationActivity.PICTURE_DESCRIPTIONS), + mutableListOf() + ) + ) } + private val instance = db.instanceDao().getActiveInstance() + @Inject lateinit var apiHolder: PixelfedAPIHolder private val _uiState: MutableStateFlow init { - (application as PixelDroidApplication).getAppComponent().inject(this) val sharedPreferences = - PreferenceManager.getDefaultSharedPreferences(application) + PreferenceManager.getDefaultSharedPreferences(applicationContext) val templateDescription = sharedPreferences.getString("prefill_description", "") ?: "" + val storyCreation: Boolean = state[CameraFragment.CAMERA_ACTIVITY_STORY] ?: false + _uiState = MutableStateFlow(PostCreationActivityUiState( - newPostDescriptionText = existingDescription ?: templateDescription, - nsfw = existingNSFW, + newPostDescriptionText = state[PostCreationActivity.POST_DESCRIPTION] ?: templateDescription, + nsfw = state[PostCreationActivity.POST_NSFW] ?: false, maxEntries = if(storyCreation) 1 else instance?.albumLimit, storyCreation = storyCreation )) @@ -161,32 +155,41 @@ class PostCreationViewModel( fun getPhotoData(): LiveData> = photoData /** - * Will add as many images as possible to [photoData], from the [clipData], and if - * ([photoData].size + [clipData].itemCount) > uiState.value.maxEntries then it will only add as many images + * Will add as many images as possible to [photoData], from the [uris], and if + * ([photoData].size + [uris].size) > uiState.value.maxEntries 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 { + fun addPossibleImages( + uris: ArrayList?, + descriptions: List?, + previousList: MutableList? = photoData.value, + ): MutableList { val dataToAdd: ArrayList = arrayListOf() - var count = clipData.itemCount - uiState.value.maxEntries?.let { - if(count + (previousList?.size ?: 0) > it){ + var count = uris?.size ?: 0 + uiState.value.maxEntries?.let { maxEntries -> + if(count + (previousList?.size ?: 0) > maxEntries){ _uiState.update { currentUiState -> - currentUiState.copy(userMessage = getApplication().getString(R.string.total_exceeds_album_limit).format(it)) + currentUiState.copy(userMessage = applicationContext.getString(R.string.total_exceeds_album_limit).format(maxEntries)) } - count = count.coerceAtMost(it - (previousList?.size ?: 0)) + count = count.coerceAtMost(maxEntries - (previousList?.size ?: 0)) } - if (count + (previousList?.size ?: 0) >= it) { + if (count + (previousList?.size ?: 0) >= maxEntries) { // 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())) - } + for ((i, uri) in uris.orEmpty().withIndex()) { + val sizeAndVideoPair: Pair = + getSizeAndVideoValidate(uri, (previousList?.size ?: 0) + dataToAdd.size + 1) + dataToAdd.add( + PhotoData( + imageUri = uri, + size = sizeAndVideoPair.first, + video = sizeAndVideoPair.second, + imageDescription = descriptions?.getOrNull(i) + ) + ) } } @@ -204,7 +207,7 @@ class PostCreationViewModel( private fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair { val size: Long = if (uri.scheme =="content") { - getApplication().contentResolver.query(uri, null, null, null, null) + applicationContext.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, @@ -221,12 +224,12 @@ class PostCreationViewModel( } val sizeInkBytes = ceil(size.toDouble() / 1000).toLong() - val type = uri.getMimeType(getApplication().contentResolver) + val type = uri.getMimeType(applicationContext.contentResolver) val isVideo = type.startsWith("video/") if (isVideo && !instance!!.videoEnabled) { _uiState.update { currentUiState -> - currentUiState.copy(userMessage = getApplication().getString(R.string.video_not_supported)) + currentUiState.copy(userMessage = applicationContext.getString(R.string.video_not_supported)) } } @@ -235,7 +238,7 @@ class PostCreationViewModel( 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) + userMessage = applicationContext.getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize) ) } } @@ -272,7 +275,7 @@ class PostCreationViewModel( videoEncodeComplete = false VideoEditActivity.startEncoding(imageUri, null, it, - context = getApplication(), + context = applicationContext, registerNewFFmpegSession = ::registerNewFFmpegSession, trackTempFile = ::trackTempFile, videoEncodeProgress = ::videoEncodeProgress @@ -387,17 +390,17 @@ class PostCreationViewModel( } for (data: PhotoData in getPhotoData().value ?: emptyList()) { - val extension = data.imageUri.fileExtension(getApplication().contentResolver) + val extension = data.imageUri.fileExtension(applicationContext.contentResolver) - val strippedImage = File.createTempFile("temp_img", ".$extension", getApplication().cacheDir) + val strippedImage = File.createTempFile("temp_img", ".$extension", applicationContext.cacheDir) val imageUri = data.imageUri val (strippedOrNot, size) = try { - val orientation = ExifInterface(getApplication().contentResolver.openInputStream(imageUri)!!).getAttributeInt( + val orientation = ExifInterface(applicationContext.contentResolver.openInputStream(imageUri)!!).getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) - stripMetadata(imageUri, strippedImage, getApplication().contentResolver) + stripMetadata(imageUri, strippedImage, applicationContext.contentResolver) // Restore EXIF orientation val exifInterface = ExifInterface(strippedImage) @@ -409,11 +412,11 @@ class PostCreationViewModel( strippedImage.delete() if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete() val imageInputStream = try { - getApplication().contentResolver.openInputStream(imageUri)!! + applicationContext.contentResolver.openInputStream(imageUri)!! } catch (e: FileNotFoundException){ _uiState.update { currentUiState -> currentUiState.copy( - userMessage = getApplication().getString(R.string.file_not_found, + userMessage = applicationContext.getString(R.string.file_not_found, data.imageUri) ) } @@ -425,14 +428,14 @@ class PostCreationViewModel( if(imageUri != data.imageUri) File(URI(imageUri.toString())).delete() _uiState.update { currentUiState -> currentUiState.copy( - userMessage = getApplication().getString(R.string.file_not_found, + userMessage = applicationContext.getString(R.string.file_not_found, data.imageUri) ) } return } - val type = data.imageUri.getMimeType(getApplication().contentResolver) + val type = data.imageUri.getMimeType(applicationContext.contentResolver) val imagePart = ProgressRequestBody(strippedOrNot, size, type) val requestBody = MultipartBody.Builder() .setType(MultipartBody.FORM) @@ -482,7 +485,7 @@ class PostCreationViewModel( currentUiState.copy( uploadErrorVisible = true, uploadErrorExplanationText = if(e is HttpException){ - getApplication().getString(R.string.upload_error, e.code()) + applicationContext.getString(R.string.upload_error, e.code()) } else "", uploadErrorExplanationVisible = e is HttpException, ) @@ -548,14 +551,14 @@ class PostCreationViewModel( sensitive = nsfw ) } - Toast.makeText(getApplication(), getApplication().getString(R.string.upload_post_success), + Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_success), Toast.LENGTH_SHORT).show() - val intent = Intent(getApplication(), MainActivity::class.java) + val intent = Intent(applicationContext, 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) + applicationContext.startActivity(intent) } catch (exception: IOException) { - Toast.makeText(getApplication(), getApplication().getString(R.string.upload_post_error), + Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_error), Toast.LENGTH_SHORT).show() Log.e(TAG, exception.toString()) _uiState.update { currentUiState -> @@ -564,7 +567,7 @@ class PostCreationViewModel( ) } } catch (exception: HttpException) { - Toast.makeText(getApplication(), getApplication().getString(R.string.upload_post_failed), + Toast.makeText(applicationContext, applicationContext.getString(R.string.upload_post_failed), Toast.LENGTH_SHORT).show() Log.e(TAG, exception.response().toString() + exception.message().toString()) _uiState.update { currentUiState -> @@ -609,7 +612,7 @@ class PostCreationViewModel( //Show message saying extraneous pictures were removed but can be restored newUiState = newUiState.copy( - userMessage = getApplication().getString(R.string.extraneous_pictures_stories) + userMessage = applicationContext.getString(R.string.extraneous_pictures_stories) ) } // Restore if backup not null and first value is unchanged @@ -629,10 +632,4 @@ class PostCreationViewModel( fun updateStoryReactions(checked: Boolean) { _uiState.update { it.copy(storyReactions = checked) } } fun updateStoryReplies(checked: Boolean) { _uiState.update { it.copy(storyReplies = checked) } } -} - -class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity, val existingDescription: String?, val existingNSFW: Boolean, val storyCreation: Boolean) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java, String::class.java, Boolean::class.java, Boolean::class.java).newInstance(application, clipdata, instance, existingDescription, existingNSFW, storyCreation) - } -} +} \ 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 index 3fadc832..bb9cfdb9 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostSubmissionFragment.kt @@ -38,7 +38,7 @@ class PostSubmissionFragment : BaseFragment() { private lateinit var instance: InstanceDatabaseEntity private var binding: FragmentPostSubmissionBinding by bindingLifecycleAware() - private lateinit var model: PostCreationViewModel + private val model: PostCreationViewModel by activityViewModels() override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, @@ -60,23 +60,9 @@ class PostSubmissionFragment : BaseFragment() { accounts = db.userDao().getAll() instance = user?.run { - db.instanceDao().getAll().first { instanceDatabaseEntity -> - instanceDatabaseEntity.uri.contains(instance_uri) - } + db.instanceDao().getInstance(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), - requireActivity().intent.getBooleanExtra(CameraFragment.CAMERA_ACTIVITY_STORY, false) - ) - } - model = _model - // Display the values from the view model binding.nsfwSwitch.isChecked = model.uiState.value.nsfw binding.newPostDescriptionInputField.setText(model.uiState.value.newPostDescriptionText) 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 8ee524a7..76f8d230 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 @@ -38,6 +38,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import org.pixeldroid.app.databinding.FragmentCameraBinding import org.pixeldroid.app.postCreation.PostCreationActivity +import org.pixeldroid.app.posts.fromHtml import org.pixeldroid.app.utils.BaseFragment import java.io.File import java.util.concurrent.ExecutorService @@ -326,7 +327,7 @@ class CameraFragment : BaseFragment() { } private fun setupUploadImage() { - val videoEnabled: Boolean = db.instanceDao().getInstance(db.userDao().getActiveUser()!!.instance_uri).videoEnabled + val videoEnabled: Boolean = db.instanceDao().getActiveInstance().videoEnabled var mimeTypes: Array = arrayOf("image/*") if(videoEnabled) mimeTypes += "video/*" @@ -449,21 +450,16 @@ class CameraFragment : BaseFragment() { private fun startAlbumCreation(uris: ArrayList) { - val intent = Intent(requireActivity(), PostCreationActivity::class.java) - .apply { - uris.forEach{ - //Why are we using ClipData here? Because the FLAG_GRANT_READ_URI_PERMISSION - //needs to be applied to the URIs, and this flag only applies to the - //Intent's data and any URIs specified in its ClipData. - if(clipData == null){ - clipData = ClipData("", emptyArray(), ClipData.Item(it.toUri())) - } else { - clipData!!.addItem(ClipData.Item(it.toUri())) - } - } - addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } + val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply { + // Pass downloaded images to new post creation activity + putParcelableArrayListExtra( + Intent.EXTRA_STREAM, ArrayList(uris.map { it.toUri() }) + ) + setClass(requireContext(), PostCreationActivity::class.java) + + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + } if(inActivity && !addToStory){ requireActivity().setResult(Activity.RESULT_OK, intent) diff --git a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt index b77cf62c..d07c0f69 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/StatusViewHolder.kt @@ -2,10 +2,8 @@ package org.pixeldroid.app.posts import android.annotation.SuppressLint import android.app.Activity -import android.content.ClipData import android.content.Intent import android.content.pm.PackageManager.PERMISSION_DENIED -import android.content.pm.PackageManager.PERMISSION_GRANTED import android.graphics.Typeface import android.graphics.drawable.AnimatedVectorDrawable import android.graphics.drawable.Drawable @@ -77,7 +75,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold fun bind( status: Status?, pixelfedAPI: PixelfedAPIHolder, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope, displayDimensionsInPx: Pair, - requestPermissionDownloadPic: ActivityResultLauncher, isActivity: Boolean = false + requestPermissionDownloadPic: ActivityResultLauncher, isActivity: Boolean = false, ) { this.itemView.visibility = View.VISIBLE @@ -371,7 +369,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold apiHolder: PixelfedAPIHolder, db: AppDatabase, lifecycleScope: LifecycleCoroutineScope, - requestPermissionDownloadPic: ActivityResultLauncher + requestPermissionDownloadPic: ActivityResultLauncher, ){ var bookmarked: Boolean? = null binding.statusMore.setOnClickListener { @@ -449,178 +447,7 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold true } - R.id.post_more_menu_redraft -> { - MaterialAlertDialogBuilder(binding.root.context).apply { - setMessage(R.string.redraft_dialog_launch) - setPositiveButton(android.R.string.ok) { _, _ -> - - lifecycleScope.launch { - try { - // Create new post creation activity - val intent = - Intent(context, PostCreationActivity::class.java) - - // Get descriptions and images from original post - val postDescription = status?.content ?: "" - val postAttachments = - status?.media_attachments!! // Catch possible exception from !! (?) - val postNSFW = status?.sensitive - - val imageUriStrings = postAttachments.map { postAttachment -> - postAttachment.url ?: "" - } - val imageNames = imageUriStrings.map { imageUriString -> - Uri.parse(imageUriString).lastPathSegment.toString() - } - val downloadedFiles = imageNames.map { imageName -> - File(context.cacheDir, imageName) - } - val imageUris = downloadedFiles.map { downloadedFile -> - Uri.fromFile(downloadedFile) - } - val imageDescriptions = postAttachments.map { postAttachment -> - fromHtml(postAttachment.description ?: "").toString() - } - val downloadRequests: List = imageUriStrings.map { imageUriString -> - Request.Builder().url(imageUriString).build() - } - - val counter = AtomicInteger(0) - - // Define callback function for after downloading the images - fun continuation() { - // Wait for all outstanding downloads to finish - if (counter.incrementAndGet() == imageUris.size) { - if (allFilesExist(imageNames)) { - // Delete original post - lifecycleScope.launch { - deletePost(apiHolder.api ?: apiHolder.setToCurrentUser(), db) - } - - val counterInt = counter.get() - Toast.makeText( - binding.root.context, - binding.root.context.resources.getQuantityString( - R.plurals.items_load_success, - counterInt, - counterInt - ), - Toast.LENGTH_SHORT - ).show() - // Pass downloaded images to new post creation activity - intent.apply { - imageUris.zip(imageDescriptions).map { (imageUri, imageDescription) -> - ClipData.Item(imageDescription, null, imageUri) - }.forEach { imageItem -> - if (clipData == null) { - clipData = ClipData( - "", - emptyArray(), - imageItem - ) - } else { - clipData!!.addItem(imageItem) - } - } - addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) - addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } - - // Pass post description of existing post to new post creation activity - intent.putExtra( - PostCreationActivity.PICTURE_DESCRIPTION, - fromHtml(postDescription).toString() - ) - if (imageNames.isNotEmpty()) { - intent.putExtra( - PostCreationActivity.TEMP_FILES, - imageNames.toTypedArray() - ) - } - intent.putExtra( - PostCreationActivity.POST_REDRAFT, - true - ) - intent.putExtra( - PostCreationActivity.POST_NSFW, - postNSFW - ) - - // Launch post creation activity - binding.root.context.startActivity(intent) - } - } - } - - if (!allFilesExist(imageNames)) { - // Track download progress - Toast.makeText( - binding.root.context, - binding.root.context.getString(R.string.image_download_downloading), - Toast.LENGTH_SHORT - ).show() - } - - // Iterate through all pictures of the original post - downloadRequests.zip(downloadedFiles).forEach { (downloadRequest, downloadedFile) -> - // Check whether image is in cache directory already (maybe rather do so using Glide in the future?) - if (!downloadedFile.exists()) { - OkHttpClient().newCall(downloadRequest) - .enqueue(object : Callback { - override fun onFailure( - call: Call, - e: IOException - ) { - Looper.prepare() - downloadedFile.delete() - Toast.makeText( - binding.root.context, - binding.root.context.getString(R.string.redraft_post_failed_io_except), - Toast.LENGTH_SHORT - ).show() - } - - @Throws(IOException::class) - override fun onResponse( - call: Call, - response: Response - ) { - val sink: BufferedSink = - downloadedFile.sink().buffer() - sink.writeAll(response.body!!.source()) - sink.close() - Looper.prepare() - continuation() - } - }) - } else { - continuation() - } - } - } catch (exception: HttpException) { - Toast.makeText( - binding.root.context, - binding.root.context.getString( - R.string.redraft_post_failed_error, - exception.code() - ), - Toast.LENGTH_SHORT - ).show() - } catch (exception: IOException) { - Toast.makeText( - binding.root.context, - binding.root.context.getString(R.string.redraft_post_failed_io_except), - Toast.LENGTH_SHORT - ).show() - } - } - } - setNegativeButton(android.R.string.cancel) { _, _ -> } - show() - } - - true - } + R.id.post_more_menu_redraft -> launchRedraftDialog(lifecycleScope, apiHolder, db) else -> false } } @@ -645,6 +472,176 @@ class StatusViewHolder(val binding: PostFragmentBinding) : RecyclerView.ViewHold } } + private fun launchRedraftDialog( + lifecycleScope: LifecycleCoroutineScope, + apiHolder: PixelfedAPIHolder, + db: AppDatabase + ): Boolean { + MaterialAlertDialogBuilder(binding.root.context).apply { + setMessage(R.string.redraft_dialog_launch) + setPositiveButton(android.R.string.ok) { _, _ -> + + lifecycleScope.launch { + try { + // Get descriptions and images from original post + val postDescription = status?.content ?: "" + val postAttachments = + status?.media_attachments!! // TODO Catch possible exception from !! (?) + val postNSFW = status?.sensitive + + val imageUriStrings = postAttachments.map { postAttachment -> + postAttachment.url ?: "" + } + val imageNames = imageUriStrings.map { imageUriString -> + Uri.parse(imageUriString).lastPathSegment.toString() + } + val downloadedFiles = imageNames.map { imageName -> + File(context.cacheDir, imageName) + } + val imageDescriptions = postAttachments.map { postAttachment -> + fromHtml( + postAttachment.description ?: "" + ).toString() + } + val downloadRequests: List = + imageUriStrings.map { imageUriString -> + Request.Builder().url(imageUriString).build() + } + + val imageUris = downloadedFiles.map { downloadedFile -> + Uri.fromFile(downloadedFile) + } + + val counter = AtomicInteger(0) + + // Define callback function for after downloading the images + fun continuation() { + // Wait for all outstanding downloads to finish + if (counter.incrementAndGet() == imageUris.size) { + if (allFilesExist(imageNames)) { + // Delete original post + lifecycleScope.launch { + deletePost( + apiHolder.api ?: apiHolder.setToCurrentUser(), db + ) + } + + val counterInt = counter.get() + Toast.makeText( + binding.root.context, + binding.root.context.resources.getQuantityString( + R.plurals.items_load_success, counterInt, counterInt + ), + Toast.LENGTH_SHORT + ).show() + + // Create new post creation activity + + //TODO use this instead of clipdata (everywhere) + val intent = Intent(Intent.ACTION_SEND_MULTIPLE).apply { + // Pass downloaded images to new post creation activity + putParcelableArrayListExtra( + Intent.EXTRA_STREAM, ArrayList(imageUris) + ) + setClass(context, PostCreationActivity::class.java) + + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + + putExtra( + PostCreationActivity.PICTURE_DESCRIPTIONS, + ArrayList(imageDescriptions) + ) + // Pass post description of existing post to new post creation activity + putExtra( + PostCreationActivity.POST_DESCRIPTION, + fromHtml(postDescription).toString() + ) + if (imageNames.isNotEmpty()) { + putExtra( + PostCreationActivity.TEMP_FILES, + imageNames.toTypedArray() + ) + } + putExtra(PostCreationActivity.POST_REDRAFT, true) + putExtra(PostCreationActivity.POST_NSFW, postNSFW) + } + + // Launch post creation activity + binding.root.context.startActivity(intent) + } + } + } + + if (!allFilesExist(imageNames)) { + // Track download progress + Toast.makeText( + binding.root.context, + binding.root.context.getString(R.string.image_download_downloading), + Toast.LENGTH_SHORT + ).show() + } + + // Iterate through all pictures of the original post + downloadRequests.zip(downloadedFiles) + .forEach { (downloadRequest, downloadedFile) -> + // Check whether image is in cache directory already (maybe rather do so using Glide in the future?) + if (!downloadedFile.exists()) { + OkHttpClient().newCall(downloadRequest) + .enqueue(object : Callback { + override fun onFailure( + call: Call, + e: IOException, + ) { + Looper.prepare() + downloadedFile.delete() + Toast.makeText( + binding.root.context, + binding.root.context.getString( + R.string.redraft_post_failed_io_except + ), + Toast.LENGTH_SHORT + ).show() + } + + @Throws(IOException::class) + override fun onResponse( + call: Call, + response: Response, + ) { + val sink: BufferedSink = + downloadedFile.sink().buffer() + sink.writeAll(response.body!!.source()) + sink.close() + Looper.prepare() + continuation() + } + }) + } else { + continuation() + } + } + } catch (exception: HttpException) { + Toast.makeText( + binding.root.context, binding.root.context.getString( + R.string.redraft_post_failed_error, exception.code() + ), Toast.LENGTH_SHORT + ).show() + } catch (exception: IOException) { + Toast.makeText( + binding.root.context, + binding.root.context.getString(R.string.redraft_post_failed_io_except), + Toast.LENGTH_SHORT + ).show() + } + } + } + setNegativeButton(android.R.string.cancel) { _, _ -> } + show() + } + return true + } + private fun activateLiker( apiHolder: PixelfedAPIHolder, isLiked: Boolean, diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/FeedContentRepository.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/FeedContentRepository.kt index ea8699ae..bf8d47f6 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/FeedContentRepository.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/FeedContentRepository.kt @@ -16,18 +16,20 @@ package org.pixeldroid.app.posts.feeds.cachedFeeds -import androidx.paging.* +import androidx.paging.ExperimentalPagingApi +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.RemoteMediator +import kotlinx.coroutines.flow.Flow +import org.pixeldroid.app.utils.api.objects.FeedContentDatabase import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.dao.feedContent.FeedContentDao -import org.pixeldroid.app.utils.api.objects.FeedContentDatabase -import kotlinx.coroutines.flow.Flow -import javax.inject.Inject /** * Repository class that works with local and remote data sources. */ -class FeedContentRepository @ExperimentalPagingApi -@Inject constructor( +class FeedContentRepository @ExperimentalPagingApi constructor( private val db: AppDatabase, private val dao: FeedContentDao, private val mediator: RemoteMediator diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt index c0b974ba..ebd31b3a 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/HomeFeedRemoteMediator.kt @@ -1,12 +1,14 @@ package org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds -import androidx.paging.* +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingSource +import androidx.paging.PagingState +import androidx.paging.RemoteMediator import androidx.room.withTransaction import org.pixeldroid.app.utils.db.AppDatabase -import org.pixeldroid.app.utils.di.PixelfedAPIHolder import org.pixeldroid.app.utils.db.entities.HomeStatusDatabaseEntity -import java.lang.NullPointerException -import javax.inject.Inject +import org.pixeldroid.app.utils.di.PixelfedAPIHolder /** @@ -17,7 +19,7 @@ import javax.inject.Inject * a local db cache. */ @OptIn(ExperimentalPagingApi::class) -class HomeFeedRemoteMediator @Inject constructor( +class HomeFeedRemoteMediator( private val apiHolder: PixelfedAPIHolder, private val db: AppDatabase, ) : RemoteMediator() { diff --git a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt index 9651ff0e..0502cc58 100644 --- a/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt +++ b/app/src/main/java/org/pixeldroid/app/posts/feeds/cachedFeeds/postFeeds/PublicFeedRemoteMediator.kt @@ -16,13 +16,15 @@ package org.pixeldroid.app.posts.feeds.cachedFeeds.postFeeds -import androidx.paging.* +import androidx.paging.ExperimentalPagingApi +import androidx.paging.LoadType +import androidx.paging.PagingSource +import androidx.paging.PagingState +import androidx.paging.RemoteMediator import androidx.room.withTransaction import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.entities.PublicFeedStatusDatabaseEntity import org.pixeldroid.app.utils.di.PixelfedAPIHolder -import java.lang.NullPointerException -import javax.inject.Inject /** * RemoteMediator for the public feed. @@ -32,7 +34,7 @@ import javax.inject.Inject * a local db cache. */ @OptIn(ExperimentalPagingApi::class) -class PublicFeedRemoteMediator @Inject constructor( +class PublicFeedRemoteMediator( private val apiHolder: PixelfedAPIHolder, private val db: AppDatabase ) : RemoteMediator() { diff --git a/app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt b/app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt index c06c4789..e7113268 100644 --- a/app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/profile/EditProfileActivity.kt @@ -25,7 +25,7 @@ import org.pixeldroid.app.utils.openUrl class EditProfileActivity : BaseActivity() { - private lateinit var model: EditProfileViewModel + private val model: EditProfileViewModel by viewModels() private lateinit var binding: ActivityEditProfileBinding override fun onCreate(savedInstanceState: Bundle?) { @@ -35,9 +35,6 @@ class EditProfileActivity : BaseActivity() { setSupportActionBar(binding.topBar) supportActionBar?.setDisplayHomeAsUpEnabled(true) - val _model: EditProfileViewModel by viewModels { EditProfileViewModelFactory(application) } - model = _model - onBackPressedDispatcher.addCallback(this) { // Handle the back button event if(model.madeChanges()){ diff --git a/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt b/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt index 5b63f3e4..a3735d3d 100644 --- a/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/profile/EditProfileViewModel.kt @@ -1,16 +1,16 @@ package org.pixeldroid.app.profile -import android.app.Application +import android.content.Context import android.net.Uri import android.provider.OpenableColumns import android.text.Editable import android.util.Log import androidx.core.net.toFile import androidx.core.net.toUri -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers @@ -21,7 +21,6 @@ import kotlinx.coroutines.launch import okhttp3.MultipartBody import org.pixeldroid.app.postCreation.ProgressRequestBody import org.pixeldroid.app.posts.fromHtml -import org.pixeldroid.app.utils.PixelDroidApplication import org.pixeldroid.app.utils.api.objects.Account import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.updateUserInfoDb @@ -29,7 +28,10 @@ import org.pixeldroid.app.utils.di.PixelfedAPIHolder import retrofit2.HttpException import javax.inject.Inject -class EditProfileViewModel(application: Application) : AndroidViewModel(application) { +@HiltViewModel +class EditProfileViewModel @Inject constructor( + @ApplicationContext private val applicationContext: Context +): ViewModel() { @Inject lateinit var apiHolder: PixelfedAPIHolder @@ -46,7 +48,6 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat private set init { - (application as PixelDroidApplication).getAppComponent().inject(this) loadProfile() } @@ -197,12 +198,12 @@ class EditProfileViewModel(application: Application) : AndroidViewModel(applicat val image = uiState.value.profilePictureUri!! val inputStream = - getApplication().contentResolver.openInputStream(image) + applicationContext.contentResolver.openInputStream(image) ?: return val size: Long = if (image.scheme == "content") { - getApplication().contentResolver.query( + applicationContext.contentResolver.query( image, null, null, @@ -303,10 +304,4 @@ data class EditProfileActivityUiState( val error: Boolean = false, val uploadingPicture: Boolean = false, val uploadProgress: Int = 0, -) - -class EditProfileViewModelFactory(val application: Application) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return modelClass.getConstructor(Application::class.java).newInstance(application) - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt index 8ca480e5..5aed36a6 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesActivity.kt @@ -25,9 +25,6 @@ import org.pixeldroid.app.databinding.ActivityStoriesBinding import org.pixeldroid.app.posts.setTextViewFromISO8601 import org.pixeldroid.app.utils.BaseActivity import org.pixeldroid.app.utils.api.objects.Account -import org.pixeldroid.app.utils.api.objects.Story -import org.pixeldroid.app.utils.api.objects.StoryCarousel - class StoriesActivity: BaseActivity() { @@ -37,12 +34,11 @@ class StoriesActivity: BaseActivity() { const val STORY_CAROUSEL_USER_ID = "LaunchStoryUserId" } - private lateinit var binding: ActivityStoriesBinding private lateinit var storyProgress: StoryProgress - private lateinit var model: StoriesViewModel + private val model: StoriesViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { //force night mode always @@ -50,18 +46,9 @@ class StoriesActivity: BaseActivity() { super.onCreate(savedInstanceState) - val carousel = intent.getSerializableExtra(STORY_CAROUSEL) as? StoryCarousel - val userId = intent.getStringExtra(STORY_CAROUSEL_USER_ID) - val selfCarousel: Array? = intent.getSerializableExtra(STORY_CAROUSEL_SELF) as? Array - binding = ActivityStoriesBinding.inflate(layoutInflater) setContentView(binding.root) - val _model: StoriesViewModel by viewModels { - StoriesViewModelFactory(application, carousel, userId, selfCarousel?.asList()) - } - model = _model - storyProgress = StoryProgress(model.uiState.value.imageList.size) binding.storyProgressImage.setImageDrawable(storyProgress) diff --git a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt index 0d42af37..4bd2802c 100644 --- a/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/stories/StoriesViewModel.kt @@ -1,20 +1,18 @@ package org.pixeldroid.app.stories -import android.app.Application import android.os.CountDownTimer import android.text.Editable import androidx.annotation.StringRes -import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.pixeldroid.app.R -import org.pixeldroid.app.utils.PixelDroidApplication import org.pixeldroid.app.utils.api.objects.CarouselUserContainer import org.pixeldroid.app.utils.api.objects.Story import org.pixeldroid.app.utils.api.objects.StoryCarousel @@ -37,18 +35,13 @@ data class StoriesUiState( val snackBar: Int? = null, val reply: String = "" ) - -class StoriesViewModel( - application: Application, - val carousel: StoryCarousel?, - userId: String?, - val selfCarousel: List? -) : AndroidViewModel(application) { - - @Inject - lateinit var apiHolder: PixelfedAPIHolder - @Inject - lateinit var db: AppDatabase +@HiltViewModel +class StoriesViewModel @Inject constructor(state: SavedStateHandle, + db: AppDatabase, + private val apiHolder: PixelfedAPIHolder) : ViewModel() { + private val carousel: StoryCarousel? = state[StoriesActivity.STORY_CAROUSEL] + private val userId: String? = state[StoriesActivity.STORY_CAROUSEL_USER_ID] + private val selfCarousel: Array? = state[StoriesActivity.STORY_CAROUSEL_SELF] private var currentAccount: CarouselUserContainer? @@ -61,10 +54,9 @@ class StoriesViewModel( private var timer: CountDownTimer? = null init { - (application as PixelDroidApplication).getAppComponent().inject(this) currentAccount = if (selfCarousel != null) { - db.userDao().getActiveUser()?.let { CarouselUserContainer(it, selfCarousel) } + db.userDao().getActiveUser()?.let { CarouselUserContainer(it, selfCarousel.toList()) } } else carousel?.nodes?.firstOrNull { it?.user?.id == userId } _uiState = MutableStateFlow(newUiStateFromCurrentAccount()) @@ -216,14 +208,3 @@ class StoriesViewModel( fun currentProfileId(): String? = currentAccount?.user?.id } - -class StoriesViewModelFactory( - val application: Application, - val carousel: StoryCarousel?, - val userId: String?, - val selfCarousel: List? -) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return modelClass.getConstructor(Application::class.java, StoryCarousel::class.java, String::class.java, List::class.java).newInstance(application, carousel, userId, selfCarousel) - } -} diff --git a/app/src/main/java/org/pixeldroid/app/utils/BaseActivity.kt b/app/src/main/java/org/pixeldroid/app/utils/BaseActivity.kt index 4508f14c..fc7f281e 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/BaseActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/BaseActivity.kt @@ -1,10 +1,11 @@ package org.pixeldroid.app.utils -import android.os.Bundle +import dagger.hilt.android.AndroidEntryPoint import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.di.PixelfedAPIHolder import javax.inject.Inject +@AndroidEntryPoint open class BaseActivity : org.pixeldroid.common.ThemedActivity() { @Inject @@ -12,11 +13,6 @@ open class BaseActivity : org.pixeldroid.common.ThemedActivity() { @Inject lateinit var apiHolder: PixelfedAPIHolder - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - (this.application as PixelDroidApplication).getAppComponent().inject(this) - } - override fun onSupportNavigateUp(): Boolean { onBackPressedDispatcher.onBackPressed() return true diff --git a/app/src/main/java/org/pixeldroid/app/utils/BaseFragment.kt b/app/src/main/java/org/pixeldroid/app/utils/BaseFragment.kt index e415ccb7..a5a43c43 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/BaseFragment.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/BaseFragment.kt @@ -1,9 +1,9 @@ package org.pixeldroid.app.utils -import android.os.Bundle import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import com.google.android.material.dialog.MaterialAlertDialogBuilder +import dagger.hilt.android.AndroidEntryPoint import org.pixeldroid.app.R import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.di.PixelfedAPIHolder @@ -12,6 +12,7 @@ import javax.inject.Inject /** * Base Fragment, for dependency injection and other things common to a lot of the fragments */ +@AndroidEntryPoint open class BaseFragment: Fragment() { @Inject @@ -20,11 +21,6 @@ open class BaseFragment: Fragment() { @Inject lateinit var db: AppDatabase - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - (requireActivity().application as PixelDroidApplication).getAppComponent().inject(this) - } - internal val requestPermissionDownloadPic = registerForActivityResult( ActivityResultContracts.RequestPermission() diff --git a/app/src/main/java/org/pixeldroid/app/utils/PixelDroidApplication.kt b/app/src/main/java/org/pixeldroid/app/utils/PixelDroidApplication.kt index 560fbbba..89ad951c 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/PixelDroidApplication.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/PixelDroidApplication.kt @@ -3,14 +3,12 @@ package org.pixeldroid.app.utils import android.app.Application import androidx.preference.PreferenceManager import com.google.android.material.color.DynamicColors +import dagger.hilt.android.HiltAndroidApp import org.ligi.tracedroid.TraceDroid -import org.pixeldroid.app.utils.di.* - +@HiltAndroidApp class PixelDroidApplication: Application() { - private lateinit var mApplicationComponent: ApplicationComponent - override fun onCreate() { super.onCreate() @@ -19,18 +17,7 @@ class PixelDroidApplication: Application() { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) setThemeFromPreferences(sharedPreferences, resources) - mApplicationComponent = DaggerApplicationComponent - .builder() - .applicationModule(ApplicationModule(this)) - .databaseModule(DatabaseModule(applicationContext)) - .aPIModule(APIModule()) - .build() - mApplicationComponent.inject(this) DynamicColors.applyToActivitiesIfAvailable(this) } - - fun getAppComponent(): ApplicationComponent { - return mApplicationComponent - } } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/DBUtils.kt b/app/src/main/java/org/pixeldroid/app/utils/db/DBUtils.kt index 0eb9ecb2..f7ad4c42 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/DBUtils.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/DBUtils.kt @@ -44,7 +44,7 @@ suspend fun updateUserInfoDb(db: AppDatabase, account: Account) { ) } -fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) { +suspend fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) { val dbInstance: InstanceDatabaseEntity = nodeInfo?.run { InstanceDatabaseEntity( uri = normalizeDomain(metadata?.config?.site?.url!!), diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/dao/InstanceDao.kt b/app/src/main/java/org/pixeldroid/app/utils/db/dao/InstanceDao.kt index 4b894645..50a2ca65 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/dao/InstanceDao.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/dao/InstanceDao.kt @@ -1,13 +1,15 @@ package org.pixeldroid.app.utils.db.dao -import androidx.room.* +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity @Dao interface InstanceDao { - @Query("SELECT * FROM instances") - fun getAll(): List - @Query("SELECT * FROM instances WHERE uri=:instanceUri") fun getInstance(instanceUri: String): InstanceDatabaseEntity @@ -19,13 +21,13 @@ interface InstanceDao { * Insert an instance, if it already exists return -1 */ @Insert(onConflict = OnConflictStrategy.IGNORE) - fun insertInstance(instance: InstanceDatabaseEntity): Long + suspend fun insertInstance(instance: InstanceDatabaseEntity): Long @Update - fun updateInstance(instance: InstanceDatabaseEntity) + suspend fun updateInstance(instance: InstanceDatabaseEntity) @Transaction - fun insertOrUpdate(instance: InstanceDatabaseEntity) { + suspend fun insertOrUpdate(instance: InstanceDatabaseEntity) { if (insertInstance(instance) == -1L) { updateInstance(instance) } diff --git a/app/src/main/java/org/pixeldroid/app/utils/db/dao/UserDao.kt b/app/src/main/java/org/pixeldroid/app/utils/db/dao/UserDao.kt index bf1afd55..d03ee3db 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/db/dao/UserDao.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/db/dao/UserDao.kt @@ -1,6 +1,11 @@ package org.pixeldroid.app.utils.db.dao -import androidx.room.* +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import androidx.room.Transaction +import androidx.room.Update import kotlinx.coroutines.flow.Flow import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity diff --git a/app/src/main/java/org/pixeldroid/app/utils/di/APIModule.kt b/app/src/main/java/org/pixeldroid/app/utils/di/APIModule.kt index 813356d9..26ea78d3 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/di/APIModule.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/di/APIModule.kt @@ -6,13 +6,16 @@ import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import dagger.Module import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.runBlocking import okhttp3.* import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser import javax.inject.Singleton @Module -class APIModule{ +@InstallIn(SingletonComponent::class) +class APIModule { @Provides @Singleton @@ -54,7 +57,7 @@ class TokenAuthenticator(val user: UserDatabaseEntity, val db: AppDatabase, val client_secret = user.clientSecret ) } - }catch (e: Exception){ + } catch (e: Exception){ return null } 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 deleted file mode 100644 index d1862c86..00000000 --- a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.pixeldroid.app.utils.di - -import android.app.Application -import android.content.Context -import org.pixeldroid.app.utils.BaseActivity -import org.pixeldroid.app.utils.PixelDroidApplication -import org.pixeldroid.app.utils.db.AppDatabase -import org.pixeldroid.app.utils.BaseFragment -import dagger.Component -import org.pixeldroid.app.MainActivityViewModel -import org.pixeldroid.app.postCreation.PostCreationViewModel -import org.pixeldroid.app.profile.EditProfileViewModel -import org.pixeldroid.app.stories.StoriesViewModel -import org.pixeldroid.app.stories.StoryCarouselViewHolder -import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker -import javax.inject.Singleton - - -@Singleton -@Component(modules = [ApplicationModule::class, DatabaseModule::class, APIModule::class]) -interface ApplicationComponent { - fun inject(application: PixelDroidApplication?) - fun inject(activity: BaseActivity?) - fun inject(feedFragment: BaseFragment) - fun inject(notificationsWorker: NotificationsWorker) - fun inject(postCreationViewModel: PostCreationViewModel) - fun inject(editProfileViewModel: EditProfileViewModel) - fun inject(storiesViewModel: StoriesViewModel) - fun inject(mainActivityViewModel: MainActivityViewModel) - - val context: Context? - val application: Application? - val database: AppDatabase -} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationModule.kt b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationModule.kt deleted file mode 100644 index 88b8008e..00000000 --- a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationModule.kt +++ /dev/null @@ -1,27 +0,0 @@ -package org.pixeldroid.app.utils.di - -import android.app.Application -import android.content.Context -import dagger.Module -import dagger.Provides - -import javax.inject.Singleton - - -@Module -class ApplicationModule(app: Application) { - private val mApplication: Application = app - - @Singleton - @Provides - fun provideContext(): Context { - return mApplication - } - - @Singleton - @Provides - fun provideApplication(): Application { - return mApplication - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/di/DatabaseModule.kt b/app/src/main/java/org/pixeldroid/app/utils/di/DatabaseModule.kt index 9e505afe..0e97e693 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/di/DatabaseModule.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/di/DatabaseModule.kt @@ -5,19 +5,25 @@ import androidx.room.Room import org.pixeldroid.app.utils.db.AppDatabase import dagger.Module import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent import org.pixeldroid.app.utils.db.MIGRATION_3_4 import org.pixeldroid.app.utils.db.MIGRATION_4_5 import org.pixeldroid.app.utils.db.MIGRATION_5_6 import javax.inject.Singleton +@InstallIn(SingletonComponent::class) @Module -class DatabaseModule(private val context: Context) { +class DatabaseModule { @Provides @Singleton - fun providesDatabase(): AppDatabase { + fun providesDatabase( + @ApplicationContext applicationContext: Context + ): AppDatabase { return Room.databaseBuilder( - context, + applicationContext, AppDatabase::class.java, "pixeldroid" ).addMigrations(MIGRATION_3_4, MIGRATION_4_5, MIGRATION_5_6) .allowMainThreadQueries().build() diff --git a/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt b/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt index ad49936e..f4646b54 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/notificationsWorker/NotificationsWorker.kt @@ -32,9 +32,6 @@ import java.io.IOException import java.time.Instant import javax.inject.Inject - - - class NotificationsWorker( context: Context, params: WorkerParameters @@ -46,9 +43,6 @@ class NotificationsWorker( lateinit var apiHolder: PixelfedAPIHolder override suspend fun doWork(): Result { - - (applicationContext as PixelDroidApplication).getAppComponent().inject(this) - val users: List = db.userDao().getAll() for (user in users){ @@ -306,8 +300,7 @@ fun removeNotificationChannelsFromAccount(context: Context, user: UserDatabaseEn if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { notificationManager.deleteNotificationChannelGroup(channelGroupId.hashCode().toString()) } else { - val types: MutableList = - Notification.NotificationType.values().toMutableList() + val types: MutableList = entries.toMutableList() types += null types.forEach { diff --git a/build.gradle b/build.gradle index c6ca818c..b72f7232 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.2.1' + classpath 'com.android.tools.build:gradle:8.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong @@ -16,6 +16,7 @@ buildscript { plugins { id 'com.google.devtools.ksp' version '1.9.20-1.0.14' apply false + id("com.google.dagger.hilt.android") version "2.50" apply false } allprojects { diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8daa1609..df147285 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ #Fri Oct 14 13:37:44 GMT 2022 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=38f66cd6eef217b4c35855bb11ea4e9fbc53594ccccb5fb82dfd317ef8c2c5a3 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +distributionSha256Sum=3e1af3ae886920c3ac87f7a91f816c0c7c436f276a6eefdb3da152100fef72ae +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists