1
0
Fork 0
mirror of https://github.com/anyproto/anytype-kotlin.git synced 2025-06-08 05:47:05 +09:00

DROID-1760 Tech | Fix | Unable to upload a picture or video (#487)

This commit is contained in:
Konstantin Ivanov 2023-11-01 17:50:25 +01:00 committed by GitHub
parent 2ec651b657
commit 5af9384212
Signed by: github
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 254 additions and 428 deletions

View file

@ -0,0 +1,54 @@
package com.anytypeio.anytype.other
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.fragment.app.Fragment
import com.anytypeio.anytype.core_utils.ext.FilePickerUtils.getPermissionToRequestByMime
import com.anytypeio.anytype.core_utils.ext.FilePickerUtils.getPermissionToRequestForFiles
import com.anytypeio.anytype.core_utils.ext.FilePickerUtils.getPermissionToRequestForImages
import com.anytypeio.anytype.core_utils.ext.FilePickerUtils.getPermissionToRequestForVideos
import com.anytypeio.anytype.core_utils.ext.FilePickerUtils.hasPermission
import com.anytypeio.anytype.core_utils.ext.Mimetype
class MediaPermissionHelper(
private val fragment: Fragment,
private val onPermissionDenied: () -> Unit,
private val onPermissionSuccess: (Mimetype, Int?) -> Unit
) {
private var mimeType: Mimetype? = null
private var requestCode: Int? = null
private val permissionReadStorage: ActivityResultLauncher<Array<String>>
init {
permissionReadStorage = fragment.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grantResults ->
val readResult = when (mimeType) {
Mimetype.MIME_VIDEO_ALL -> grantResults[getPermissionToRequestForVideos()]
Mimetype.MIME_IMAGE_ALL -> grantResults[getPermissionToRequestForImages()]
Mimetype.MIME_FILE_ALL -> grantResults[getPermissionToRequestForFiles()]
null -> false
}
if (readResult == true) {
val type = requireNotNull(mimeType) {
"mimeType should be initialized"
}
onPermissionSuccess(type, requestCode)
} else {
onPermissionDenied()
}
}
}
fun openFilePicker(mimeType: Mimetype, requestCode: Int?) {
this.mimeType = mimeType
this.requestCode = requestCode
val context = fragment.context ?: return
val hasPermission = mimeType.hasPermission(context)
if (hasPermission) {
onPermissionSuccess(mimeType, requestCode)
} else {
val permission = mimeType.getPermissionToRequestByMime()
permissionReadStorage.launch(arrayOf(permission))
}
}
}

View file

@ -11,22 +11,19 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.fragment.app.viewModels
import androidx.lifecycle.Observer
import com.anytypeio.anytype.BuildConfig
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_ui.extensions.toast
import com.anytypeio.anytype.core_utils.const.FileConstants.getPermissionToRequestForImages
import com.anytypeio.anytype.core_utils.ext.*
import com.anytypeio.anytype.databinding.FragmentCreateAccountBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.other.MediaPermissionHelper
import com.anytypeio.anytype.presentation.auth.account.CreateAccountViewModel
import com.anytypeio.anytype.presentation.auth.account.CreateAccountViewModelFactory
import com.anytypeio.anytype.ui.base.NavigationFragment
import com.bumptech.glide.Glide
import com.google.android.material.snackbar.Snackbar
import timber.log.Timber
import javax.inject.Inject
@ -38,6 +35,17 @@ class CreateAccountFragment : NavigationFragment<FragmentCreateAccountBinding>(R
private val vm : CreateAccountViewModel by viewModels { factory }
private lateinit var permissionHelper: MediaPermissionHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
permissionHelper = MediaPermissionHelper(
fragment = this,
onPermissionDenied = { toast(R.string.permission_read_denied) },
onPermissionSuccess = { _, _ -> openGallery() }
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.createProfileButton.setOnClickListener {
@ -46,7 +54,9 @@ class CreateAccountFragment : NavigationFragment<FragmentCreateAccountBinding>(R
invitationCode = getCode()
)
}
binding.profileIconPlaceholder.setOnClickListener { proceedWithImagePick() }
binding.profileIconPlaceholder.setOnClickListener {
permissionHelper.openFilePicker(Mimetype.MIME_IMAGE_ALL, null)
}
binding.backButton.setOnClickListener { vm.onBackButtonClicked() }
setupNavigation()
vm.error.observe(viewLifecycleOwner, Observer(this::showError))
@ -123,28 +133,6 @@ class CreateAccountFragment : NavigationFragment<FragmentCreateAccountBinding>(R
getContent.launch(SELECT_IMAGE_CODE)
}
private fun proceedWithImagePick() {
if (!hasExternalStoragePermission())
permissionReadStorage.launch(arrayOf(getPermissionToRequestForImages()))
else
openGallery()
}
private fun hasExternalStoragePermission() = ContextCompat.checkSelfPermission(
requireActivity(),
getPermissionToRequestForImages()
).let { result -> result == PackageManager.PERMISSION_GRANTED }
private val permissionReadStorage =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grantResults ->
val readResult = grantResults[getPermissionToRequestForImages()]
if (readResult == true) {
openGallery()
} else {
binding.root.showSnackbar(R.string.permission_read_denied, Snackbar.LENGTH_SHORT)
}
}
val getContent = registerForActivityResult(GetImageContract()) { uri: Uri? ->
if (uri != null) {
try {

View file

@ -34,7 +34,6 @@ import androidx.core.view.updateLayoutParams
import androidx.fragment.app.viewModels
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleEventObserver
import androidx.lifecycle.distinctUntilChanged
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.DefaultItemAnimator
@ -45,7 +44,6 @@ import androidx.transition.Fade
import androidx.transition.Slide
import androidx.transition.TransitionManager
import androidx.transition.TransitionSet
import androidx.viewbinding.ViewBinding
import com.anytypeio.anytype.BuildConfig
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
@ -99,7 +97,6 @@ import com.anytypeio.anytype.core_utils.ext.syncTranslationWithImeVisibility
import com.anytypeio.anytype.core_utils.ext.throttleFirst
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ext.visible
import com.anytypeio.anytype.core_utils.ui.BaseFragment
import com.anytypeio.anytype.core_utils.ui.showActionableSnackBar
import com.anytypeio.anytype.databinding.FragmentEditorBinding
import com.anytypeio.anytype.di.common.componentManager
@ -414,7 +411,22 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
}
}
private val pickerDelegate = PickerDelegate.Impl(this as BaseFragment<ViewBinding>)
private val pickerDelegate = PickerDelegate.Impl(this) { actions ->
when (actions) {
PickerDelegate.Actions.OnCancelCopyFileToCacheDir -> {
vm.onCancelCopyFileToCacheDir()
}
is PickerDelegate.Actions.OnPickedDocImageFromDevice -> {
vm.onPickedDocImageFromDevice(actions.ctx, actions.filePath)
}
is PickerDelegate.Actions.OnProceedWithFilePath -> {
vm.onProceedWithFilePath(filePath = actions.filePath)
}
is PickerDelegate.Actions.OnStartCopyFileToCacheDir -> {
vm.onStartCopyFileToCacheDir(actions.uri)
}
}
}
private val dndDelegate = DragAndDropDelegate()
@Inject
@ -422,7 +434,7 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pickerDelegate.initPicker(vm, ctx)
pickerDelegate.initPicker(ctx)
setupOnBackPressedDispatcher()
getEditorSettings()
}

View file

@ -1,27 +1,23 @@
package com.anytypeio.anytype.ui.editor
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.app.ProgressDialog
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.view.LayoutInflater
import android.widget.Button
import android.widget.ProgressBar
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.viewbinding.ViewBinding
import androidx.fragment.app.Fragment
import com.anytypeio.anytype.BuildConfig
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_utils.const.FileConstants
import com.anytypeio.anytype.core_utils.ext.Mimetype
import com.anytypeio.anytype.core_utils.ext.isPermissionGranted
import com.anytypeio.anytype.core_utils.ext.shouldShowRequestPermissionRationaleCompat
import com.anytypeio.anytype.core_utils.ext.showSnackbar
import com.anytypeio.anytype.core_utils.ext.startFilePicker
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ui.BaseFragment
import com.anytypeio.anytype.presentation.editor.EditorViewModel
import com.anytypeio.anytype.other.MediaPermissionHelper
import com.anytypeio.anytype.presentation.util.CopyFileStatus
import com.google.android.material.snackbar.Snackbar
import com.hbisoft.pickit.PickiT
@ -30,7 +26,7 @@ import timber.log.Timber
interface PickerDelegate : PickiTCallbacks {
fun initPicker(vm: EditorViewModel, ctx: Id)
fun initPicker(ctx: Id)
fun resolveActivityResult(requestCode: Int, resultCode: Int, data: Intent?)
@ -44,20 +40,27 @@ interface PickerDelegate : PickiTCallbacks {
fun clearOnCopyFile()
sealed class Actions {
data class OnStartCopyFileToCacheDir(val uri: Uri) : Actions()
object OnCancelCopyFileToCacheDir : Actions()
data class OnProceedWithFilePath(val filePath: String) : Actions()
data class OnPickedDocImageFromDevice(val ctx: String, val filePath: String) : Actions()
}
class Impl(
private val fragment: BaseFragment<ViewBinding>
private val fragment: Fragment,
private val actions: (Actions) -> Unit
) : PickerDelegate {
private lateinit var vm: EditorViewModel
private lateinit var ctx: Id
private lateinit var pickiT: PickiT
private lateinit var permissionHelper: MediaPermissionHelper
private var pickitProgressDialog: ProgressDialog? = null
private var pickitProgressBar: ProgressBar? = null
private var pickitAlertDialog: AlertDialog? = null
private var snackbar: Snackbar? = null
private var mimeType: Mimetype? = null
private var requestCode: Int? = null
override fun resolveActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
@ -70,7 +73,7 @@ interface PickerDelegate : PickiTCallbacks {
}
FileConstants.REQUEST_FILE_SAF_CODE -> {
data?.data?.let { uri ->
vm.onStartCopyFileToCacheDir(uri)
actions(Actions.OnStartCopyFileToCacheDir(uri))
} ?: run {
Timber.e("onActivityResult error, data is null")
fragment.toast("Error while getting file")
@ -89,19 +92,17 @@ interface PickerDelegate : PickiTCallbacks {
}
override fun openFilePicker(mimeType: Mimetype, requestCode: Int?) {
this.mimeType = mimeType
this.requestCode = requestCode
if (fragment.requireContext().isPermissionGranted(mimeType)) {
fragment.startFilePicker(mimeType, requestCode)
} else {
takeReadStoragePermission()
}
permissionHelper.openFilePicker(mimeType, requestCode)
}
override fun initPicker(vm: EditorViewModel, ctx: Id) {
this.vm = vm
override fun initPicker(ctx: Id) {
this.ctx = ctx
pickiT = PickiT(fragment.requireContext(), this, fragment.requireActivity())
permissionHelper = MediaPermissionHelper(
fragment = fragment,
onPermissionDenied = { fragment.toast(R.string.permission_read_denied) },
onPermissionSuccess = { mimetype, code -> fragment.startFilePicker(mimetype, code) }
)
}
override fun clearPickit() {
@ -138,60 +139,25 @@ interface PickerDelegate : PickiTCallbacks {
onFilePathReady(command.result)
}
CopyFileStatus.Started -> {
snackbar = fragment.binding.root.showSnackbar(
R.string.loading_file,
Snackbar.LENGTH_INDEFINITE,
R.string.cancel
) {
vm.onCancelCopyFileToCacheDir()
fragment.view?.rootView?.let {
snackbar = it.showSnackbar(
R.string.loading_file,
Snackbar.LENGTH_INDEFINITE,
R.string.cancel
) {
actions(Actions.OnCancelCopyFileToCacheDir)
}
}
}
}
}
override fun clearOnCopyFile() {
vm.onCancelCopyFileToCacheDir()
actions(Actions.OnCancelCopyFileToCacheDir)
snackbar?.dismiss()
snackbar = null
}
private fun takeReadStoragePermission() {
try {
if (fragment.requireActivity()
.shouldShowRequestPermissionRationaleCompat(READ_EXTERNAL_STORAGE)
) {
snackbar = fragment.binding.root.showSnackbar(
R.string.permission_read_rationale,
Snackbar.LENGTH_INDEFINITE,
R.string.button_ok
) {
permissionReadStorage.launch(arrayOf(READ_EXTERNAL_STORAGE))
}
} else {
permissionReadStorage.launch(arrayOf(READ_EXTERNAL_STORAGE))
}
} catch (e: Exception) {
Timber.e(e, "Error while requesting permission")
}
}
private val permissionReadStorage =
fragment.registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions())
{ grantResults ->
val readResult = grantResults[READ_EXTERNAL_STORAGE]
if (readResult == true) {
val type = requireNotNull(mimeType) {
"mimeType should be initialized"
}
fragment.startFilePicker(type, requestCode)
} else {
snackbar = fragment.binding.root.showSnackbar(
R.string.permission_read_denied,
Snackbar.LENGTH_SHORT
)
}
}
override fun PickiTonUriReturned() {
Timber.d("PickiTonUriReturned")
if (pickitProgressDialog == null || pickitProgressDialog?.isShowing == false) {
@ -269,9 +235,9 @@ interface PickerDelegate : PickiTCallbacks {
private fun onFilePathReady(filePath: String?) {
if (filePath != null) {
if (requestCode == FileConstants.REQUEST_PROFILE_IMAGE_CODE) {
vm.onPickedDocImageFromDevice(ctx, filePath)
actions(Actions.OnPickedDocImageFromDevice(ctx, filePath))
} else {
vm.onProceedWithFilePath(filePath = filePath)
actions(Actions.OnProceedWithFilePath(filePath))
}
} else {
Timber.e("onFilePathReady, filePath is null")

View file

@ -1,15 +1,11 @@
package com.anytypeio.anytype.ui.editor.cover
import android.Manifest
import android.content.pm.PackageManager
import android.graphics.Rect
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
@ -21,23 +17,23 @@ import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_ui.features.editor.modal.DocCoverGalleryAdapter
import com.anytypeio.anytype.core_ui.reactive.clicks
import com.anytypeio.anytype.core_utils.ext.GetImageContract
import com.anytypeio.anytype.core_utils.ext.Mimetype
import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.dimen
import com.anytypeio.anytype.core_utils.ext.parseImagePath
import com.anytypeio.anytype.core_utils.ext.showSnackbar
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetFragment
import com.anytypeio.anytype.databinding.FragmentDocCoverGalleryBinding
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.other.MediaPermissionHelper
import com.anytypeio.anytype.presentation.editor.cover.SelectCoverObjectSetViewModel
import com.anytypeio.anytype.presentation.editor.cover.SelectCoverObjectViewModel
import com.anytypeio.anytype.presentation.editor.cover.SelectCoverViewModel
import com.google.android.material.snackbar.Snackbar
import javax.inject.Inject
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import timber.log.Timber
import javax.inject.Inject
abstract class SelectCoverGalleryFragment :
BaseBottomSheetFragment<FragmentDocCoverGalleryBinding>() {
@ -57,6 +53,8 @@ abstract class SelectCoverGalleryFragment :
null
}
private lateinit var permissionHelper: MediaPermissionHelper
private fun getContentLauncher() = registerForActivityResult(GetImageContract()) { uri: Uri? ->
if (uri != null) {
try {
@ -71,14 +69,14 @@ abstract class SelectCoverGalleryFragment :
}
}
private val permissionReadStorage = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grantResults ->
val readResult = grantResults[Manifest.permission.READ_EXTERNAL_STORAGE]
if (readResult == true) {
openGallery()
} else {
binding.root.showSnackbar(R.string.permission_read_denied, Snackbar.LENGTH_SHORT)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
permissionHelper = MediaPermissionHelper(
fragment = this,
onPermissionDenied = { toast(R.string.permission_read_denied) },
onPermissionSuccess = { _, _ -> openGallery() }
)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@ -99,7 +97,7 @@ abstract class SelectCoverGalleryFragment :
.launchIn(lifecycleScope)
binding.btnUpload.clicks()
.onEach { proceedWithImagePick() }
.onEach { permissionHelper.openFilePicker(Mimetype.MIME_IMAGE_ALL, null) }
.launchIn(lifecycleScope)
val spacing = requireContext().dimen(R.dimen.cover_gallery_item_spacing).toInt()
@ -153,13 +151,6 @@ abstract class SelectCoverGalleryFragment :
expand()
}
private fun proceedWithImagePick() {
if (!hasReadStoragePermission())
permissionReadStorage.launch(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE))
else
openGallery()
}
private fun openGallery() {
try {
getContent?.launch(SELECT_IMAGE_CODE)
@ -169,13 +160,6 @@ abstract class SelectCoverGalleryFragment :
}
}
private fun hasReadStoragePermission(): Boolean = ContextCompat.checkSelfPermission(
requireActivity(),
Manifest.permission.READ_EXTERNAL_STORAGE
).let { result ->
result == PackageManager.PERMISSION_GRANTED
}
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?
@ -187,7 +171,6 @@ abstract class SelectCoverGalleryFragment :
companion object {
private const val SELECT_IMAGE_CODE = 1
private const val REQUEST_PERMISSION_CODE = 2
}
}

View file

@ -1,36 +1,32 @@
package com.anytypeio.anytype.ui.editor.modals
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.EditText
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import androidx.core.widget.doAfterTextChanged
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_utils.const.FileConstants.getPermissionToRequestForImages
import com.anytypeio.anytype.core_utils.ext.GetImageContract
import com.anytypeio.anytype.core_utils.ext.Mimetype
import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.invisible
import com.anytypeio.anytype.core_utils.ext.parseImagePath
import com.anytypeio.anytype.core_utils.ext.showSnackbar
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ext.visible
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetTextInputFragment
import com.anytypeio.anytype.databinding.FragmentPageIconPickerBinding
import com.anytypeio.anytype.library_page_icon_picker_widget.ui.DocumentEmojiIconPickerAdapter
import com.anytypeio.anytype.other.MediaPermissionHelper
import com.anytypeio.anytype.presentation.editor.picker.EmojiPickerView.Companion.HOLDER_EMOJI_CATEGORY_HEADER
import com.anytypeio.anytype.presentation.editor.picker.EmojiPickerView.Companion.HOLDER_EMOJI_ITEM
import com.anytypeio.anytype.presentation.picker.IconPickerViewModel
import com.anytypeio.anytype.presentation.picker.IconPickerViewModel.ViewState
import com.google.android.material.snackbar.Snackbar
import timber.log.Timber
abstract class IconPickerFragmentBase<T> :
@ -59,6 +55,17 @@ abstract class IconPickerFragmentBase<T> :
)
}
private lateinit var permissionHelper: MediaPermissionHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
permissionHelper = MediaPermissionHelper(
fragment = this,
onPermissionDenied = { toast(R.string.permission_read_denied) },
onPermissionSuccess = { _, _ -> openGallery() }
)
}
override val textInput: EditText get() = binding.filterInputField
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
@ -72,7 +79,7 @@ abstract class IconPickerFragmentBase<T> :
filterInputField.doAfterTextChanged { vm.onQueryChanged(it.toString()) }
btnRemoveIcon.setOnClickListener { vm.onRemoveClicked(target) }
tvTabRandom.setOnClickListener { vm.onRandomEmoji(target) }
tvTabUpload.setOnClickListener { proceedWithImagePick() }
tvTabUpload.setOnClickListener { permissionHelper.openFilePicker(Mimetype.MIME_IMAGE_ALL, 0) }
}
skipCollapsed()
expand()
@ -132,29 +139,6 @@ abstract class IconPickerFragmentBase<T> :
super.onDestroyView()
}
private fun proceedWithImagePick() {
if (!hasExternalStoragePermission()) {
permissionReadStorage.launch(arrayOf(getPermissionToRequestForImages()))
} else {
openGallery()
}
}
private fun hasExternalStoragePermission() = ContextCompat.checkSelfPermission(
requireActivity(),
getPermissionToRequestForImages()
).let { result -> result == PackageManager.PERMISSION_GRANTED }
private val permissionReadStorage =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grantResults ->
val readResult = grantResults[getPermissionToRequestForImages()]
if (readResult == true) {
openGallery()
} else {
binding.root.showSnackbar(R.string.permission_read_denied, Snackbar.LENGTH_SHORT)
}
}
private val getContent = registerForActivityResult(GetImageContract()) { uri: Uri? ->
if (uri != null) {
try {

View file

@ -18,7 +18,6 @@ import com.anytypeio.anytype.core_ui.reactive.clicks
import com.anytypeio.anytype.core_ui.tools.DefaultDividerItemDecoration
import com.anytypeio.anytype.core_utils.ext.drawable
import com.anytypeio.anytype.core_utils.ext.gone
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ext.visible
import com.anytypeio.anytype.core_utils.ui.proceed
import com.anytypeio.anytype.databinding.FragmentRelationStatusValueBinding
@ -104,14 +103,6 @@ class RelationStatusValueFragment :
)
}
override fun PickiTonMultipleCompleteListener(
paths: ArrayList<String>?,
wasSuccessful: Boolean,
Reason: String?
) {
toast("Not implemented yet")
}
override fun observeCommands(command: RelationValueBaseViewModel.ObjectRelationValueCommand) {
when (command) {
RelationValueBaseViewModel.ObjectRelationValueCommand.ShowAddStatusOrTagScreen -> {

View file

@ -1,40 +1,26 @@
package com.anytypeio.anytype.ui.relations
import android.Manifest.permission.READ_EXTERNAL_STORAGE
import android.app.Activity
import android.app.ProgressDialog
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.widget.ProgressBar
import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.os.bundleOf
import androidx.lifecycle.lifecycleScope
import androidx.navigation.fragment.findNavController
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.anytypeio.anytype.BuildConfig
import com.anytypeio.anytype.R
import com.anytypeio.anytype.core_ui.features.sets.RelationValueAdapter
import com.anytypeio.anytype.core_ui.tools.DefaultDragAndDropBehavior
import com.anytypeio.anytype.core_utils.const.FileConstants.REQUEST_FILE_SAF_CODE
import com.anytypeio.anytype.core_utils.const.FileConstants.REQUEST_MEDIA_CODE
import com.anytypeio.anytype.core_utils.ext.Mimetype
import com.anytypeio.anytype.core_utils.ext.arg
import com.anytypeio.anytype.core_utils.ext.argString
import com.anytypeio.anytype.core_utils.ext.gone
import com.anytypeio.anytype.core_utils.ext.invisible
import com.anytypeio.anytype.core_utils.ext.isPermissionGranted
import com.anytypeio.anytype.core_utils.ext.shouldShowRequestPermissionRationaleCompat
import com.anytypeio.anytype.core_utils.ext.showSnackbar
import com.anytypeio.anytype.core_utils.ext.startFilePicker
import com.anytypeio.anytype.core_utils.ext.subscribe
import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.ext.visible
@ -44,19 +30,15 @@ import com.anytypeio.anytype.core_utils.ui.OnStartDragListener
import com.anytypeio.anytype.presentation.navigation.AppNavigation
import com.anytypeio.anytype.presentation.relations.RelationValueView
import com.anytypeio.anytype.presentation.sets.RelationValueBaseViewModel
import com.anytypeio.anytype.presentation.util.CopyFileStatus
import com.anytypeio.anytype.ui.editor.EditorFragment
import com.anytypeio.anytype.ui.editor.PickerDelegate
import com.anytypeio.anytype.ui.relations.add.AddObjectRelationFragment
import com.anytypeio.anytype.ui.sets.ObjectSetFragment
import com.google.android.material.snackbar.Snackbar
import com.hbisoft.pickit.PickiT
import com.hbisoft.pickit.PickiTCallbacks
import timber.log.Timber
abstract class RelationValueBaseFragment<T: ViewBinding> : BaseBottomSheetFragment<T>(),
OnStartDragListener,
AddObjectRelationFragment.ObjectValueAddReceiver,
PickiTCallbacks {
AddObjectRelationFragment.ObjectValueAddReceiver {
protected val ctx get() = argString(CTX_KEY)
protected val relationKey get() = argString(RELATION_KEY)
@ -161,6 +143,11 @@ abstract class RelationValueBaseFragment<T: ViewBinding> : BaseBottomSheetFragm
)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pickerDelegate.initPicker(ctx)
}
override fun onStart() {
jobs += lifecycleScope.subscribe(vm.toasts) { toast(it) }
jobs += lifecycleScope.subscribe(vm.commands) { observeCommands(it) }
@ -170,7 +157,9 @@ abstract class RelationValueBaseFragment<T: ViewBinding> : BaseBottomSheetFragm
jobs += lifecycleScope.subscribe(vm.name) { tvRelationHeader.text = it }
jobs += lifecycleScope.subscribe(vm.navigation) { command -> navigate(command) }
jobs += lifecycleScope.subscribe(vm.isLoading) { isLoading -> observeLoading(isLoading) }
jobs += lifecycleScope.subscribe(vm.copyFileStatus) { command -> onCopyFileCommand(command) }
jobs += lifecycleScope.subscribe(vm.copyFileStatus) { command ->
pickerDelegate.onCopyFileCommand(command)
}
super.onStart()
vm.onStart(
ctx = ctx,
@ -210,6 +199,18 @@ abstract class RelationValueBaseFragment<T: ViewBinding> : BaseBottomSheetFragm
override fun onStop() {
super.onStop()
vm.onStop()
pickerDelegate.onStop()
}
override fun onDestroyView() {
pickerDelegate.clearPickit()
super.onDestroyView()
}
override fun onDestroy() {
pickerDelegate.deleteTemporaryFile()
pickerDelegate.clearOnCopyFile()
super.onDestroy()
}
override fun onStartDrag(viewHolder: RecyclerView.ViewHolder) {
@ -260,185 +261,38 @@ abstract class RelationValueBaseFragment<T: ViewBinding> : BaseBottomSheetFragm
}
}
private lateinit var pickiT: PickiT
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
pickiT = PickiT(requireContext(), this, requireActivity())
}
private var pickitProgressDialog: ProgressDialog? = null
private var pickitProgressBar: ProgressBar? = null
private var pickitAlertDialog: AlertDialog? = null
override fun PickiTonUriReturned() {
if (pickitProgressDialog == null || pickitProgressDialog?.isShowing == false) {
pickitProgressDialog = ProgressDialog(requireContext()).apply {
setMessage(getString(R.string.pickit_waiting))
setCancelable(false)
private val pickerDelegate = PickerDelegate.Impl(this) { actions ->
when (actions) {
PickerDelegate.Actions.OnCancelCopyFileToCacheDir -> {
vm.onCancelCopyFileToCacheDir()
}
pickitProgressDialog?.show()
}
}
override fun PickiTonStartListener() {
if (pickitProgressDialog?.isShowing == true) {
pickitProgressDialog?.cancel()
}
pickitAlertDialog =
AlertDialog.Builder(requireContext(), R.style.SyncFromCloudDialog).apply {
val view =
LayoutInflater.from(requireContext()).inflate(R.layout.dialog_layout, null)
setView(view)
view.findViewById<Button>(R.id.btnCancel).setOnClickListener {
pickiT.cancelTask()
if (pickitAlertDialog?.isShowing == true) {
pickitAlertDialog?.cancel()
}
}
pickitProgressBar = view.findViewById(R.id.mProgressBar)
}.create()
pickitAlertDialog?.show()
Timber.d("PickiTonStartListener")
}
override fun PickiTonProgressUpdate(progress: Int) {
Timber.d("PickiTonProgressUpdate progress:$progress")
pickitProgressBar?.progress = progress
}
override fun PickiTonCompleteListener(
path: String?,
wasDriveFile: Boolean,
wasUnknownProvider: Boolean,
wasSuccessful: Boolean,
Reason: String?
) {
Timber.d("PickiTonCompleteListener path:$path, wasDriveFile:$wasDriveFile, wasUnknownProvider:$wasUnknownProvider, wasSuccessful:$wasSuccessful, reason:$Reason")
if (pickitAlertDialog?.isShowing == true) {
pickitAlertDialog?.cancel()
}
if (pickitProgressDialog?.isShowing == true) {
pickitProgressDialog?.cancel()
}
if (BuildConfig.DEBUG) {
when {
wasDriveFile -> toast(getString(R.string.pickit_drive))
wasUnknownProvider -> toast(getString(R.string.pickit_file_selected))
else -> toast(getString(R.string.pickit_local_file))
is PickerDelegate.Actions.OnPickedDocImageFromDevice -> {
onFilePathReady(actions.filePath)
}
is PickerDelegate.Actions.OnProceedWithFilePath -> {
onFilePathReady(actions.filePath)
}
is PickerDelegate.Actions.OnStartCopyFileToCacheDir -> {
vm.onStartCopyFileToCacheDir(actions.uri)
}
}
when {
wasSuccessful -> onFilePathReady(path)
else -> toast("Error: $Reason")
}
}
private fun clearPickit() {
val ctx = context
if (ctx != null) {
pickiT.deleteTemporaryFile(ctx)
}
pickiT.cancelTask()
pickitAlertDialog?.dismiss()
pickitProgressDialog?.dismiss()
}
//endregion
//region READ PERMISSION
private fun takeReadStoragePermission() {
if (requireActivity().shouldShowRequestPermissionRationaleCompat(READ_EXTERNAL_STORAGE)) {
root.showSnackbar(
R.string.permission_read_rationale,
Snackbar.LENGTH_INDEFINITE,
R.string.button_ok
) {
permissionReadStorage.launch(arrayOf(READ_EXTERNAL_STORAGE))
}
} else {
permissionReadStorage.launch(arrayOf(READ_EXTERNAL_STORAGE))
}
}
private val permissionReadStorage =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grantResults ->
val readResult = grantResults[READ_EXTERNAL_STORAGE]
if (readResult == true) {
startFilePicker(Mimetype.MIME_FILE_ALL)
} else {
root.showSnackbar(R.string.permission_read_denied, Snackbar.LENGTH_SHORT)
}
}
//endregion
//region UPLOAD FILE LOGIC
private var mSnackbar: Snackbar? = null
protected fun openFilePicker() {
if (requireContext().isPermissionGranted(Mimetype.MIME_FILE_ALL)) {
startFilePicker(Mimetype.MIME_FILE_ALL)
} else {
takeReadStoragePermission()
}
pickerDelegate.openFilePicker(Mimetype.MIME_FILE_ALL)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (resultCode == Activity.RESULT_OK) {
when (requestCode) {
REQUEST_MEDIA_CODE -> {
data?.data?.let { uri ->
pickiT.getPath(uri, Build.VERSION.SDK_INT)
}
}
REQUEST_FILE_SAF_CODE -> {
data?.data?.let { uri ->
vm.onStartCopyFileToCacheDir(uri)
} ?: run {
toast("Error while getting file")
}
}
else -> toast("Unknown Request Code:$requestCode")
}
pickerDelegate.resolveActivityResult(requestCode, resultCode, data)
} else {
super.onActivityResult(requestCode, resultCode, data)
}
}
private fun onCopyFileCommand(command: CopyFileStatus) {
when (command) {
is CopyFileStatus.Error -> {
mSnackbar?.dismiss()
activity?.toast("Error while loading file:${command.msg}")
}
is CopyFileStatus.Completed -> {
mSnackbar?.dismiss()
onFilePathReady(command.result)
}
CopyFileStatus.Started -> {
mSnackbar = root.showSnackbar(
R.string.loading_file,
Snackbar.LENGTH_INDEFINITE,
R.string.cancel
) {
vm.onCancelCopyFileToCacheDir()
}
}
}
}
private fun clearOnCopyFile() {
vm.onCancelCopyFileToCacheDir()
mSnackbar?.dismiss()
mSnackbar = null
}
//endregion
override fun onDestroyView() {
clearPickit()
clearOnCopyFile()
super.onDestroyView()
}
abstract fun observeCommands(command: RelationValueBaseViewModel.ObjectRelationValueCommand)
companion object {

View file

@ -113,8 +113,6 @@ open class RelationValueDVFragment : RelationValueBaseFragment<FragmentRelationV
showAddFileScreen()
}
RelationValueBaseViewModel.ObjectRelationValueCommand.ShowFileValueActionScreen -> {
//turn off for now https://app.clickup.com/t/h59z1j
//FileActionsFragment().showChildFragment()
openFilePicker()
}
}
@ -164,14 +162,6 @@ open class RelationValueDVFragment : RelationValueBaseFragment<FragmentRelationV
vm.onFileValueActionUploadFromStorageClicked()
}
override fun PickiTonMultipleCompleteListener(
paths: ArrayList<String>?,
wasSuccessful: Boolean,
Reason: String?
) {
toast("Not implemented yet")
}
override fun inflateBinding(
inflater: LayoutInflater,
container: ViewGroup?

View file

@ -98,14 +98,6 @@ class RelationValueFragment : RelationValueBaseFragment<FragmentRelationValueBin
)
}
override fun PickiTonMultipleCompleteListener(
paths: ArrayList<String>?,
wasSuccessful: Boolean,
Reason: String?
) {
toast("Not implemented yet")
}
override fun observeCommands(command: RelationValueBaseViewModel.ObjectRelationValueCommand) {
when (command) {
RelationValueBaseViewModel.ObjectRelationValueCommand.ShowAddObjectScreen -> {
@ -118,8 +110,6 @@ class RelationValueFragment : RelationValueBaseFragment<FragmentRelationValueBin
showAddFileScreen()
}
RelationValueBaseViewModel.ObjectRelationValueCommand.ShowFileValueActionScreen -> {
//turn off for now https://app.clickup.com/t/h59z1j
//FileActionsFragment().showChildFragment()
openFilePicker()
}
}

View file

@ -1,16 +1,13 @@
package com.anytypeio.anytype.ui.settings
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.viewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -18,8 +15,8 @@ import androidx.lifecycle.lifecycleScope
import com.anytypeio.anytype.R
import com.anytypeio.anytype.analytics.base.EventsDictionary
import com.anytypeio.anytype.core_ui.common.ComposeDialogView
import com.anytypeio.anytype.core_utils.const.FileConstants.getPermissionToRequestForImages
import com.anytypeio.anytype.core_utils.ext.GetImageContract
import com.anytypeio.anytype.core_utils.ext.Mimetype
import com.anytypeio.anytype.core_utils.ext.parseImagePath
import com.anytypeio.anytype.core_utils.ext.setupBottomSheetBehavior
import com.anytypeio.anytype.core_utils.ext.shareFile
@ -28,6 +25,7 @@ import com.anytypeio.anytype.core_utils.ext.toast
import com.anytypeio.anytype.core_utils.tools.FeatureToggles
import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment
import com.anytypeio.anytype.di.common.componentManager
import com.anytypeio.anytype.other.MediaPermissionHelper
import com.anytypeio.anytype.ui.auth.account.DeleteAccountWarning
import com.anytypeio.anytype.ui.profile.KeychainPhraseDialog
import com.anytypeio.anytype.ui_settings.account.ProfileScreen
@ -55,6 +53,17 @@ class ProfileFragment : BaseBottomSheetComposeFragment() {
safeNavigate(R.id.logoutWarningScreen)
}
private lateinit var permissionHelper: MediaPermissionHelper
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
permissionHelper = MediaPermissionHelper(
fragment = this,
onPermissionDenied = { toast(R.string.permission_read_denied) },
onPermissionSuccess = { _, _ -> openGallery() }
)
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
@ -102,28 +111,9 @@ class ProfileFragment : BaseBottomSheetComposeFragment() {
}
private fun proceedWithIconClick() {
if (!hasExternalStoragePermission()) {
permissionReadStorage.launch(arrayOf(getPermissionToRequestForImages()))
} else {
openGallery()
}
permissionHelper.openFilePicker(Mimetype.MIME_IMAGE_ALL, null)
}
private fun hasExternalStoragePermission() = ContextCompat.checkSelfPermission(
requireActivity(),
getPermissionToRequestForImages()
).let { result -> result == PackageManager.PERMISSION_GRANTED }
private val permissionReadStorage =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { grantResults ->
val readResult = grantResults[getPermissionToRequestForImages()]
if (readResult == true) {
openGallery()
} else {
toast(R.string.permission_read_denied)
}
}
private fun openGallery() {
getContent.launch(SELECT_IMAGE_CODE)
}

View file

@ -1,24 +1,8 @@
package com.anytypeio.anytype.core_utils.const
import android.Manifest
import android.os.Build
object FileConstants {
const val REQUEST_FILE_SAF_CODE = 2211
const val REQUEST_MEDIA_CODE = 2212
const val REQUEST_PROFILE_IMAGE_CODE = 2213
fun getPermissionToRequestForImages() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
fun getPermissionToRequestForVideos() =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_VIDEO
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
}

View file

@ -276,21 +276,6 @@ fun View.focusAndShowKeyboard() {
fun String.normalizeUrl(): String =
if (!startsWith("http://") && !startsWith("https://")) "https://$this" else this
fun Context.isPermissionGranted(mimeType: Mimetype): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && mimeType == Mimetype.MIME_FILE_ALL) {
true
} else {
val readExternalStorage: Int = ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
)
readExternalStorage == PackageManager.PERMISSION_GRANTED
}
}
fun Activity.shouldShowRequestPermissionRationaleCompat(permission: String) =
ActivityCompat.shouldShowRequestPermissionRationale(this, permission)
/**
* [requestCode] is used only for picking media.
* Should be refactored here:

View file

@ -0,0 +1,56 @@
package com.anytypeio.anytype.core_utils.ext
import android.Manifest
import android.content.Context
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
import timber.log.Timber
object FilePickerUtils {
fun Mimetype.hasPermission(context: Context): Boolean {
return when (this) {
Mimetype.MIME_VIDEO_ALL -> context.isPermissionGranted(getPermissionToRequestForVideos())
Mimetype.MIME_IMAGE_ALL -> context.isPermissionGranted(getPermissionToRequestForImages())
Mimetype.MIME_FILE_ALL -> {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
true
} else {
context.isPermissionGranted(Manifest.permission.READ_EXTERNAL_STORAGE)
}
}
}
}
fun Context.isPermissionGranted(permission: String): Boolean {
val hasPermission = ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
Timber.d("hasExternalStoragePermission, hasPermission:$hasPermission for permission:$permission")
return hasPermission
}
fun Mimetype.getPermissionToRequestByMime(): String {
return when (this) {
Mimetype.MIME_VIDEO_ALL -> getPermissionToRequestForVideos()
Mimetype.MIME_IMAGE_ALL -> getPermissionToRequestForImages()
Mimetype.MIME_FILE_ALL -> getPermissionToRequestForFiles()
}
}
fun getPermissionToRequestForImages(): String =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_IMAGES
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
fun getPermissionToRequestForVideos(): String =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
Manifest.permission.READ_MEDIA_VIDEO
} else {
Manifest.permission.READ_EXTERNAL_STORAGE
}
fun getPermissionToRequestForFiles(): String =
Manifest.permission.READ_EXTERNAL_STORAGE
}

View file

@ -56,7 +56,7 @@ coroutineTestingVersion = '1.6.4'
liveDataTestingVersion = '1.2.0'
espressoVersion = '3.5.1'
disableAnimationVersion = '2.0.0'
leakCanaryVersion = '1.5'
leakCanaryVersion = '2.12'
timberVersion = '5.0.1'
protobufJavaVersion = '3.9.2'
protocVersion = '3.9.0'
@ -134,7 +134,6 @@ liveDataTesting = { module = "com.jraska.livedata:testing-ktx", version.ref = "l
androidXTestCore = { module = "androidx.test:core", version.ref = "androidxTestCoreVersion" }
androidxSecurityCrypto = { module = "androidx.security:security-crypto", version.ref = "androidxSecurityCryptoVersion" }
preference = { module = "androidx.preference:preference", version.ref = "androidxPreferenceVersion" }
leakCanaryAndroid = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakCanaryVersion" }
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectricLatestVersion" }
kluent = { module = "org.amshove.kluent:kluent-android", version.ref = "kluentVersion" }
timberJUnit = { module = "net.lachlanmckee:timber-junit-rule", version.ref = "timberJunitVersion" }