diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e429dd5af8..5ed9991ea3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -57,6 +57,21 @@ + + + + + + + + + + + + + + + { - SharingFragment.new(command.data).show( + is Command.Sharing.Text -> { + SharingFragment.text(command.data).show( + supportFragmentManager, + SHARE_DIALOG_LABEL + ) + } + is Command.Sharing.Image -> { + SharingFragment.image(command.uri).show( + supportFragmentManager, + SHARE_DIALOG_LABEL + ) + } + is Command.Sharing.Images -> { + SharingFragment.images(command.uris).show( + supportFragmentManager, + SHARE_DIALOG_LABEL + ) + } + is Command.Sharing.Files -> { + SharingFragment.files(command.uris).show( + supportFragmentManager, + SHARE_DIALOG_LABEL + ) + } + is Command.Sharing.File -> { + SharingFragment.file(command.uri).show( supportFragmentManager, SHARE_DIALOG_LABEL ) @@ -121,8 +154,11 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr } } } - if (savedInstanceState == null && intent.action == Intent.ACTION_SEND) { - proceedWithShareIntent(intent) + if (savedInstanceState == null) { + val action = intent.action + if (action == Intent.ACTION_SEND || action == Intent.ACTION_SEND_MULTIPLE) { + proceedWithShareIntent(intent) + } } } @@ -172,6 +208,9 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr Intent.ACTION_SEND -> { proceedWithShareIntent(intent) } + Intent.ACTION_SEND_MULTIPLE -> { + proceedWithShareIntent(intent) + } } } if (BuildConfig.DEBUG) { @@ -180,8 +219,47 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr } private fun proceedWithShareIntent(intent: Intent) { - intent.getStringExtra(Intent.EXTRA_TEXT)?.let { - vm.onIntentShare(it) + if (BuildConfig.DEBUG) Timber.d("Proceeding with share intent: $intent") + when { + intent.type == Mimetype.MIME_TEXT_PLAIN.value -> { + vm.onIntentTextShare(intent.getStringExtra(Intent.EXTRA_TEXT).orEmpty()) + } + intent.type?.startsWith(SHARE_IMAGE_INTENT_PATTERN) == true -> { + proceedWithImageShareIntent(intent) + } + intent.type?.startsWith(SHARE_FILE_INTENT_PATTERN) == true -> { + proceedWithFileShareIntent(intent) + } + intent.type == Mimetype.MIME_FILE_ALL.value -> { + proceedWithFileShareIntent(intent) + } + else -> Timber.e("Unexpected scenario: ${intent.type}") + } + } + + private fun proceedWithFileShareIntent(intent: Intent) { + if (intent.action == Intent.ACTION_SEND_MULTIPLE) { + vm.onIntentMultipleFilesShare(intent.parseActionSendMultipleUris()) + } else { + val uri = intent.parseActionSendUri() + if (uri != null) { + vm.onIntentMultipleFilesShare(listOf(uri)) + } else { + toast("Could not parse URI") + } + } + } + + private fun proceedWithImageShareIntent(intent: Intent) { + if (intent.action == Intent.ACTION_SEND_MULTIPLE) { + vm.onIntentMultipleImageShare(uris = intent.parseActionSendMultipleUris()) + } else { + val uri = intent.parseActionSendUri() + if (uri != null) { + vm.onIntentMultipleImageShare(listOf(uri)) + } else { + toast("Could not parse URI") + } } } @@ -249,5 +327,8 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr companion object { const val AUTO_UPDATE_URL = "https://fra1.digitaloceanspaces.com/anytype-release/latest-android.json" const val SHARE_DIALOG_LABEL = "anytype.dialog.share.label" + const val SHARE_IMAGE_INTENT_PATTERN = "image/" + const val SHARE_FILE_INTENT_PATTERN = "application/" + } } diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sharing/Sharing.kt b/app/src/main/java/com/anytypeio/anytype/ui/sharing/Sharing.kt index 75bf3f8e18..67b89503b0 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/sharing/Sharing.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/sharing/Sharing.kt @@ -63,7 +63,7 @@ fun AddToAnytypeScreenUrlPreview() { @Composable fun AddToAnytypeScreenNotePreview() { AddToAnytypeScreen( - data = SharingData.Raw("The Work of Art in the Age of its Technological Reproducibility"), + data = SharingData.Text("The Work of Art in the Age of its Technological Reproducibility"), onCancelClicked = {}, onDoneClicked = {}, spaces = emptyList(), @@ -80,15 +80,23 @@ fun AddToAnytypeScreen( onSelectSpaceClicked: (SpaceView) -> Unit ) { var isSaveAsMenuExpanded by remember { mutableStateOf(false) } - val items = if (data is SharingData.Url) - listOf(SAVE_AS_NOTE, SAVE_AS_BOOKMARK) - else - listOf(SAVE_AS_NOTE) + val items = when (data) { + is SharingData.Url -> listOf(SAVE_AS_NOTE, SAVE_AS_BOOKMARK) + is SharingData.Image -> listOf(SAVE_AS_IMAGE) + is SharingData.File -> listOf(SAVE_AS_FILE) + is SharingData.Images -> listOf(SAVE_AS_IMAGES) + is SharingData.Files -> listOf(SAVE_AS_FILES) + is SharingData.Text -> listOf(SAVE_AS_NOTE) + } var selectedIndex by remember { mutableStateOf( when(data) { is SharingData.Url -> SAVE_AS_BOOKMARK - else -> SAVE_AS_NOTE + is SharingData.Image -> SAVE_AS_IMAGE + is SharingData.File -> SAVE_AS_FILE + is SharingData.Images -> SAVE_AS_IMAGES + is SharingData.Files -> SAVE_AS_FILES + is SharingData.Text -> SAVE_AS_NOTE } ) } @@ -111,10 +119,14 @@ fun AddToAnytypeScreen( ) Text( - text = if (selectedIndex == SAVE_AS_BOOKMARK) - stringResource(id = R.string.sharing_menu_save_as_bookmark_option) - else - stringResource(id = R.string.sharing_menu_save_as_note_option), + text = when (selectedIndex) { + SAVE_AS_BOOKMARK -> stringResource(id = R.string.sharing_menu_save_as_bookmark_option) + SAVE_AS_IMAGE -> stringResource(id = R.string.sharing_menu_save_as_image_option) + SAVE_AS_FILE -> stringResource(id = R.string.sharing_menu_save_as_file_option) + SAVE_AS_IMAGES -> stringResource(id = R.string.sharing_menu_save_as_images_option) + SAVE_AS_FILES -> stringResource(id = R.string.sharing_menu_save_as_files_option) + else -> stringResource(id = R.string.sharing_menu_save_as_note_option) + }, modifier = Modifier .align(Alignment.BottomStart) .padding(bottom = 14.dp, start = 20.dp), @@ -390,6 +402,10 @@ private fun SmallSpaceIcon( const val SAVE_AS_NOTE = 0 const val SAVE_AS_BOOKMARK = 1 +const val SAVE_AS_IMAGE = 2 +const val SAVE_AS_FILE = 3 +const val SAVE_AS_IMAGES = 4 +const val SAVE_AS_FILES = 5 typealias SaveAsOption = Int sealed class SharingData { @@ -398,8 +414,27 @@ sealed class SharingData { override val data: String get() = url } - data class Raw(val raw: String) : SharingData() { + data class Text(val raw: String) : SharingData() { override val data: String get() = raw } + data class Image(val uri: String) : SharingData() { + override val data: String + get() = uri + } + + data class Images(val uris: List): SharingData() { + override val data: String + get() = uris.toString() + } + + data class Files(val uris: List): SharingData() { + override val data: String + get() = uris.toString() + } + + data class File(val uri: String): SharingData() { + override val data: String + get() = uri + } } \ No newline at end of file diff --git a/app/src/main/java/com/anytypeio/anytype/ui/sharing/SharingFragment.kt b/app/src/main/java/com/anytypeio/anytype/ui/sharing/SharingFragment.kt index 8955ca77f2..c9a5ed457c 100644 --- a/app/src/main/java/com/anytypeio/anytype/ui/sharing/SharingFragment.kt +++ b/app/src/main/java/com/anytypeio/anytype/ui/sharing/SharingFragment.kt @@ -15,6 +15,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.fragment.findNavController import com.anytypeio.anytype.R import com.anytypeio.anytype.core_utils.ext.arg +import com.anytypeio.anytype.core_utils.ext.argStringList import com.anytypeio.anytype.core_utils.ext.toast import com.anytypeio.anytype.core_utils.ui.BaseBottomSheetComposeFragment import com.anytypeio.anytype.di.common.componentManager @@ -22,18 +23,37 @@ import com.anytypeio.anytype.presentation.home.OpenObjectNavigation import com.anytypeio.anytype.presentation.sharing.AddToAnytypeViewModel import com.anytypeio.anytype.ui.editor.EditorFragment import com.anytypeio.anytype.ui.settings.typography +import java.lang.IllegalStateException import javax.inject.Inject class SharingFragment : BaseBottomSheetComposeFragment() { - private val sharedData : SharingData get() { - val result = arg(SHARING_DATE_KEY) - return if (URLUtil.isValidUrl(result)) { - SharingData.Url(result) - } else { - SharingData.Raw(result) + private val sharedData: SharingData + get() { + val args = requireArguments() + return if (args.containsKey(SHARING_TEXT_KEY)) { + val result = arg(SHARING_TEXT_KEY) + if (URLUtil.isValidUrl(result)) { + SharingData.Url(result) + } else { + SharingData.Text(result) + } + } else if (args.containsKey(SHARING_IMAGE_KEY)) { + val result = arg(SHARING_IMAGE_KEY) + SharingData.Image(uri = result) + } else if (args.containsKey(SHARING_FILE_KEY)) { + val result = arg(SHARING_FILE_KEY) + SharingData.File(uri = result) + } else if (args.containsKey(SHARING_MULTIPLE_IMAGES_KEY)) { + val result = argStringList(SHARING_MULTIPLE_IMAGES_KEY) + SharingData.Images(uris = result) + } else if (args.containsKey(SHARING_MULTIPLE_FILES_KEY)) { + val result = argStringList(SHARING_MULTIPLE_FILES_KEY) + SharingData.Files(uris = result) + } else { + throw IllegalStateException("Unexpcted shared data") + } } - } @Inject lateinit var factory: AddToAnytypeViewModel.Factory @@ -56,6 +76,24 @@ class SharingFragment : BaseBottomSheetComposeFragment() { when(option) { SAVE_AS_BOOKMARK -> vm.onCreateBookmark(url = sharedData.data) SAVE_AS_NOTE -> vm.onCreateNote(sharedData.data) + SAVE_AS_IMAGE -> vm.onShareMedia(listOf(sharedData.data)) + SAVE_AS_FILE -> vm.onShareMedia(listOf(sharedData.data)) + SAVE_AS_IMAGES -> { + val data = sharedData + if (data is SharingData.Images) { + vm.onShareMedia(uris = data.uris) + } else { + toast("Unexpected data format") + } + } + SAVE_AS_FILES -> { + val data = sharedData + if (data is SharingData.Files) { + vm.onShareMedia(uris = data.uris) + } else { + toast("Unexpected data format") + } + } } }, onCancelClicked = { @@ -120,9 +158,30 @@ class SharingFragment : BaseBottomSheetComposeFragment() { } companion object { - private const val SHARING_DATE_KEY = "arg.sharing.data-key" - fun new(data: String) : SharingFragment = SharingFragment().apply { - arguments = bundleOf(SHARING_DATE_KEY to data) + private const val SHARING_TEXT_KEY = "arg.sharing.text-key" + private const val SHARING_IMAGE_KEY = "arg.sharing.image-key" + private const val SHARING_FILE_KEY = "arg.sharing.file-key" + private const val SHARING_MULTIPLE_IMAGES_KEY = "arg.sharing.multiple-images-key" + private const val SHARING_MULTIPLE_FILES_KEY = "arg.sharing.multiple-files-key" + + fun text(data: String) : SharingFragment = SharingFragment().apply { + arguments = bundleOf(SHARING_TEXT_KEY to data) + } + + fun image(uri: String) : SharingFragment = SharingFragment().apply { + arguments = bundleOf(SHARING_IMAGE_KEY to uri) + } + + fun images(uris: List) : SharingFragment = SharingFragment().apply { + arguments = bundleOf(SHARING_MULTIPLE_IMAGES_KEY to ArrayList(uris)) + } + + fun files(uris: List) : SharingFragment = SharingFragment().apply { + arguments = bundleOf(SHARING_MULTIPLE_FILES_KEY to ArrayList(uris)) + } + + fun file(uri: String) : SharingFragment = SharingFragment().apply { + arguments = bundleOf(SHARING_FILE_KEY to uri) } } } \ No newline at end of file diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Block.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Block.kt index a2bcba5abd..3828a934a6 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Block.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Block.kt @@ -398,7 +398,8 @@ data class Block( data class File( val type: Content.File.Type, - val state: Content.File.State + val state: Content.File.State, + val targetObjectId: Id? = null ) : Prototype() data class Link( diff --git a/core-models/src/main/java/com/anytypeio/anytype/core_models/Command.kt b/core-models/src/main/java/com/anytypeio/anytype/core_models/Command.kt index c0728f8da3..9613378063 100644 --- a/core-models/src/main/java/com/anytypeio/anytype/core_models/Command.kt +++ b/core-models/src/main/java/com/anytypeio/anytype/core_models/Command.kt @@ -21,6 +21,7 @@ sealed class Command { ) : Command() class UploadFile( + val space: SpaceId? = null, val path: String, val type: Block.Content.File.Type? ) diff --git a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/AndroidExtension.kt b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/AndroidExtension.kt index d252913470..624f4b83e5 100644 --- a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/AndroidExtension.kt +++ b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/AndroidExtension.kt @@ -13,6 +13,7 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Environment +import android.os.Parcelable import android.provider.MediaStore import android.text.Editable import android.text.InputType @@ -377,6 +378,25 @@ fun Fragment.shareFile(uri: Uri) { } } +fun Intent.parseActionSendMultipleUris() : List { + val extras = getParcelableArrayListExtra(Intent.EXTRA_STREAM) ?: arrayListOf() + return extras.mapNotNull { extra -> + if (extra is Uri) + extra.toString() + else + null + } +} + +fun Intent.parseActionSendUri() : String? { + val extra = getParcelableExtra(Intent.EXTRA_STREAM) + return if (extra is Uri) { + extra.toString() + } else { + null + } +} + inline fun Pair.letNotNull(block: (T1, T2) -> R): R? { return if (first != null && second != null) { block(first!!, second!!) diff --git a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/FilePickerUtils.kt b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/FilePickerUtils.kt index 3e9845ffda..ea236d1af8 100644 --- a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/FilePickerUtils.kt +++ b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/FilePickerUtils.kt @@ -14,7 +14,7 @@ object FilePickerUtils { Mimetype.MIME_VIDEO_ALL -> context.isPermissionGranted(getPermissionToRequestForVideos()) Mimetype.MIME_IMAGE_ALL -> context.isPermissionGranted(getPermissionToRequestForImages()) Mimetype.MIME_IMAGE_AND_VIDEO -> context.isPermissionGranted(getPermissionToRequestForImagesAndVideos()) - Mimetype.MIME_FILE_ALL, Mimetype.MIME_YAML -> { + else -> { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { true } else { @@ -37,6 +37,7 @@ object FilePickerUtils { Mimetype.MIME_FILE_ALL -> getPermissionToRequestForFiles() Mimetype.MIME_IMAGE_AND_VIDEO -> getPermissionToRequestForImagesAndVideos() Mimetype.MIME_YAML -> getPermissionToRequestForFiles() + else -> getPermissionToRequestForFiles() } } diff --git a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/FragmentExtensions.kt b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/FragmentExtensions.kt index 10b1181329..d13ae4f65e 100644 --- a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/FragmentExtensions.kt +++ b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/FragmentExtensions.kt @@ -46,6 +46,10 @@ fun Fragment.argList(key: String): ArrayList { return checkNotNull(value) } +fun Fragment.argStringList(key: String): ArrayList { + return requireArguments().getStringArrayList(key) ?: ArrayList() +} + fun CoroutineScope.subscribe(flow: Flow, body: suspend (T) -> Unit): Job = flow.cancellable().onEach { body(it) }.launchIn(this) diff --git a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/Mimetype.kt b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/Mimetype.kt index fd6beffec7..630e4cd610 100644 --- a/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/Mimetype.kt +++ b/core-utils/src/main/java/com/anytypeio/anytype/core_utils/ext/Mimetype.kt @@ -1,9 +1,11 @@ package com.anytypeio.anytype.core_utils.ext enum class Mimetype(val value: String) { + MIME_TEXT_PLAIN("text/plain"), MIME_VIDEO_ALL("video/*"), MIME_IMAGE_ALL("image/*"), MIME_FILE_ALL("*/*"), MIME_IMAGE_AND_VIDEO("image/*,video/*"), - MIME_YAML("application/zip") + MIME_YAML("application/zip"), + MIME_APPLICATION_ALL("application/*") } \ No newline at end of file diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt index f50fce1a08..911d2e3978 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockDataRepository.kt @@ -256,7 +256,7 @@ class BlockDataRepository( override suspend fun uploadFile( command: Command.UploadFile - ): Hash = remote.uploadFile(command) + ): Id = remote.uploadFile(command) override suspend fun downloadFile( command: Command.DownloadFile diff --git a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt index a9e1ed8405..8f8e7e32b2 100644 --- a/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt +++ b/data/src/main/java/com/anytypeio/anytype/data/auth/repo/block/BlockRemote.kt @@ -80,7 +80,7 @@ interface BlockRemote { suspend fun paste(command: Command.Paste): Response.Clipboard.Paste suspend fun copy(command: Command.Copy): Response.Clipboard.Copy - suspend fun uploadFile(command: Command.UploadFile): String + suspend fun uploadFile(command: Command.UploadFile): Id suspend fun downloadFile(command: Command.DownloadFile): String suspend fun setRelationKey(command: Command.SetRelationKey): Payload diff --git a/device/build.gradle b/device/build.gradle index 68a7c164de..0a230d3724 100644 --- a/device/build.gradle +++ b/device/build.gradle @@ -7,6 +7,7 @@ dependencies { implementation project(':data') implementation project(':domain') + implementation project(':localization') implementation libs.kotlin implementation libs.coroutinesAndroid diff --git a/device/src/main/java/com/anytypeio/anytype/device/SharedFileUploader.kt b/device/src/main/java/com/anytypeio/anytype/device/SharedFileUploader.kt new file mode 100644 index 0000000000..43c8dbfa31 --- /dev/null +++ b/device/src/main/java/com/anytypeio/anytype/device/SharedFileUploader.kt @@ -0,0 +1,85 @@ +package com.anytypeio.anytype.device + +import android.content.Context +import android.net.Uri +import android.provider.OpenableColumns +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.device.FileSharer +import com.anytypeio.anytype.localization.R +import java.io.File +import java.io.FileOutputStream +import javax.inject.Inject +import kotlinx.coroutines.withContext +import timber.log.Timber + +class SharedFileUploader @Inject constructor( + private val context: Context, + private val dispatchers: AppCoroutineDispatchers +) : FileSharer { + + override suspend fun getPath(uri: String): String = withContext(dispatchers.io) { + if (BuildConfig.DEBUG) Timber.d("Getting path for: $uri") + val parsed = Uri.parse(uri) + checkNotNull(parsed) + parsePathFromUri(parsed) + } + + override suspend fun clear() { + TODO("Not yet implemented") + } + + private fun parsePathFromUri(extra: Uri) : String { + val name = if (extra.scheme == CONTENT_URI_SCHEME) { + context.contentResolver.query( + extra, + null, + null, + null, + null + ).use { cursor -> + if (cursor != null && cursor.moveToFirst()) { + val idx = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (idx != -1) { + cursor.getString(idx) + } else { + context.resources.getString(R.string.untitled) + } + } else { + context.resources.getString(R.string.untitled) + } + } + } else { + val rawPath = extra.path + if (rawPath != null) { + rawPath.substring(rawPath.lastIndexOf("/")) + } else { + "" + } + } + val inputStream = context.contentResolver.openInputStream(extra) + val cacheDir = context.getExternalFilesDir(null) + if (cacheDir != null && !cacheDir.exists()) { + cacheDir.mkdirs() + } + var path = "" + inputStream?.use { input -> + val newFile = File(cacheDir?.path + "/" + name); + FileOutputStream(newFile).use { output -> + val buffer = ByteArray(1024) + var read: Int = input.read(buffer) + while (read != -1) { + output.write(buffer, 0, read) + read = input.read(buffer) + } + } + path = newFile.path + } + + return path + } + + companion object { + const val CONTENT_URI_SCHEME = "content" + } + +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt index 2b8ffecc1a..002c3d3d9b 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/block/repo/BlockRepository.kt @@ -33,7 +33,7 @@ import com.anytypeio.anytype.domain.page.Undo interface BlockRepository { - suspend fun uploadFile(command: Command.UploadFile): Hash + suspend fun uploadFile(command: Command.UploadFile): Id suspend fun downloadFile(command: Command.DownloadFile): String suspend fun move(command: Command.Move): Payload diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/device/FileSharer.kt b/domain/src/main/java/com/anytypeio/anytype/domain/device/FileSharer.kt new file mode 100644 index 0000000000..898b5832f4 --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/device/FileSharer.kt @@ -0,0 +1,6 @@ +package com.anytypeio.anytype.domain.device + +interface FileSharer { + suspend fun getPath(uri: String) : String? + suspend fun clear() +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/media/UploadFile.kt b/domain/src/main/java/com/anytypeio/anytype/domain/media/UploadFile.kt new file mode 100644 index 0000000000..a268e763ea --- /dev/null +++ b/domain/src/main/java/com/anytypeio/anytype/domain/media/UploadFile.kt @@ -0,0 +1,30 @@ +package com.anytypeio.anytype.domain.media + +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Command +import com.anytypeio.anytype.core_models.Id +import com.anytypeio.anytype.core_models.primitives.SpaceId +import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers +import com.anytypeio.anytype.domain.base.ResultInteractor +import com.anytypeio.anytype.domain.block.repo.BlockRepository +import javax.inject.Inject + +class UploadFile @Inject constructor( + private val repo: BlockRepository, + private val dispatchers: AppCoroutineDispatchers +) : ResultInteractor(dispatchers.io) { + + override suspend fun doWork(params: Params) : Id = repo.uploadFile( + command = Command.UploadFile( + path = params.path, + type = params.type, + space = params.space + ) + ) + + data class Params( + val path: String, + val space: SpaceId, + val type: Block.Content.File.Type = Block.Content.File.Type.FILE, + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/anytypeio/anytype/domain/objects/CreatePrefilledNote.kt b/domain/src/main/java/com/anytypeio/anytype/domain/objects/CreatePrefilledNote.kt index f6967aa83e..8bb58f7176 100644 --- a/domain/src/main/java/com/anytypeio/anytype/domain/objects/CreatePrefilledNote.kt +++ b/domain/src/main/java/com/anytypeio/anytype/domain/objects/CreatePrefilledNote.kt @@ -40,6 +40,19 @@ class CreatePrefilledNote @Inject constructor( target = NO_VALUE ) ) + params.attachments.forEach { attachment -> + repo.create( + command = Command.Create( + context = obj.id, + prototype = Block.Prototype.Link( + target = attachment, + cardStyle = Block.Content.Link.CardStyle.CARD + ), + position = Position.NONE, + target = NO_VALUE + ) + ) + } return obj.id } @@ -50,6 +63,7 @@ class CreatePrefilledNote @Inject constructor( val space: Id, val text: String, val details: Struct, - val customType: TypeKey? = null + val customType: TypeKey? = null, + val attachments: List = emptyList() ) } \ No newline at end of file diff --git a/localization/src/main/res/values/strings.xml b/localization/src/main/res/values/strings.xml index d5d44deb87..729c9d714a 100644 --- a/localization/src/main/res/values/strings.xml +++ b/localization/src/main/res/values/strings.xml @@ -193,7 +193,7 @@ Upload a picture Upload a file Upload an audio - Add a web bookmark + Add bookmark Bulleted list item Numbered list item Toggle block @@ -1192,6 +1192,10 @@ Save as Note Bookmark + Image + File + Images + Files Data Add to Anytype New object is added to the space \'%1$s\' @@ -1255,5 +1259,4 @@ Create option Edit option - \ No newline at end of file diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt index d50f95b584..d60d460f4a 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/Middleware.kt @@ -34,6 +34,8 @@ import com.anytypeio.anytype.core_utils.tools.ThreadInfo import com.anytypeio.anytype.middleware.BuildConfig import com.anytypeio.anytype.middleware.auth.toAccountSetup import com.anytypeio.anytype.middleware.const.Constants +import com.anytypeio.anytype.middleware.mappers.MBFile +import com.anytypeio.anytype.middleware.mappers.MBFileType import com.anytypeio.anytype.middleware.mappers.MDVFilter import com.anytypeio.anytype.middleware.mappers.MNetworkMode import com.anytypeio.anytype.middleware.mappers.MRelationFormat @@ -821,7 +823,8 @@ class Middleware @Inject constructor( val type = command.type.toMiddlewareModel() val request = Rpc.File.Upload.Request( localPath = command.path, - type = type + type = type, + spaceId = command.space?.id.orEmpty() ) if (BuildConfig.DEBUG) logRequest(request) val response = service.fileUpload(request) diff --git a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareFactory.kt b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareFactory.kt index 3d38b0464f..026148abaf 100644 --- a/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareFactory.kt +++ b/middleware/src/main/java/com/anytypeio/anytype/middleware/interactor/MiddlewareFactory.kt @@ -46,7 +46,8 @@ class MiddlewareFactory { is Block.Prototype.File -> { val file = MBFile( state = prototype.state.toMiddlewareModel(), - type = prototype.type.toMiddlewareModel() + type = prototype.type.toMiddlewareModel(), + targetObjectId = prototype.targetObjectId.orEmpty() ) MBlock(file_ = file) } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt index 1a672104e7..110cac58ce 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/editor/EditorViewModel.kt @@ -3109,6 +3109,7 @@ class EditorViewModel( ObjectType.Layout.NOTE, ObjectType.Layout.TODO, ObjectType.Layout.FILE, + ObjectType.Layout.IMAGE, ObjectType.Layout.BOOKMARK -> { proceedWithOpeningObject(target = target) } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModel.kt index bfb8279532..910a12105c 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/main/MainViewModel.kt @@ -164,13 +164,49 @@ class MainViewModel( } } - fun onIntentShare(data: String) { + fun onIntentTextShare(data: String) { viewModelScope.launch { checkAuthorizationStatus(Unit).process( failure = { e -> Timber.e(e, "Error while checking auth status") }, success = { status -> if (status == AuthStatus.AUTHORIZED) { - commands.emit(Command.AddToAnytype(data)) + commands.emit(Command.Sharing.Text(data)) + } + } + ) + } + } + + fun onIntentMultipleFilesShare(uris: List) { + Timber.d("onIntentFileShare: $uris") + viewModelScope.launch { + checkAuthorizationStatus(Unit).process( + failure = { e -> Timber.e(e, "Error while checking auth status") }, + success = { status -> + if (status == AuthStatus.AUTHORIZED) { + if (uris.size == 1) { + commands.emit(Command.Sharing.File(uris.first())) + } else { + commands.emit(Command.Sharing.Files(uris)) + } + } + } + ) + } + } + + fun onIntentMultipleImageShare(uris: List) { + Timber.d("onIntentImageShare: $uris") + viewModelScope.launch { + checkAuthorizationStatus(Unit).process( + failure = { e -> Timber.e(e, "Error while checking auth status") }, + success = { status -> + if (status == AuthStatus.AUTHORIZED) { + if (uris.size == 1) { + commands.emit(Command.Sharing.Image(uris.first())) + } else { + commands.emit(Command.Sharing.Images(uris)) + } } } ) @@ -182,6 +218,12 @@ class MainViewModel( object LogoutDueToAccountDeletion : Command() class OpenCreateNewType(val type: Id) : Command() data class Error(val msg: String) : Command() - data class AddToAnytype(val data: String): Command() + sealed class Sharing : Command() { + data class Text(val data: String) : Sharing() + data class Image(val uri: String): Sharing() + data class Images(val uris: List): Sharing() + data class File(val uri: String): Sharing() + data class Files(val uris: List): Sharing() + } } } \ No newline at end of file diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/sharing/AddToAnytypeViewModel.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/sharing/AddToAnytypeViewModel.kt index 302c06af3d..72ef2778ce 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/sharing/AddToAnytypeViewModel.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/sharing/AddToAnytypeViewModel.kt @@ -11,14 +11,21 @@ import com.anytypeio.anytype.analytics.base.EventsDictionary.CLICK_ONBOARDING_TO import com.anytypeio.anytype.analytics.base.EventsPropertiesKey import com.anytypeio.anytype.analytics.event.EventAnalytics import com.anytypeio.anytype.analytics.props.Props +import com.anytypeio.anytype.core_models.Block +import com.anytypeio.anytype.core_models.Id import com.anytypeio.anytype.core_models.MarketplaceObjectTypeIds import com.anytypeio.anytype.core_models.NO_VALUE import com.anytypeio.anytype.core_models.ObjectOrigin import com.anytypeio.anytype.core_models.ObjectWrapper import com.anytypeio.anytype.core_models.Relations +import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE +import com.anytypeio.anytype.core_models.primitives.SpaceId import com.anytypeio.anytype.core_utils.ext.msg import com.anytypeio.anytype.domain.account.AwaitAccountStartManager import com.anytypeio.anytype.domain.base.fold +import com.anytypeio.anytype.domain.base.onSuccess +import com.anytypeio.anytype.domain.device.FileSharer +import com.anytypeio.anytype.domain.media.UploadFile import com.anytypeio.anytype.domain.misc.UrlBuilder import com.anytypeio.anytype.domain.objects.CreateBookmarkObject import com.anytypeio.anytype.domain.objects.CreatePrefilledNote @@ -31,6 +38,7 @@ import com.anytypeio.anytype.presentation.spaces.SpaceGradientProvider import com.anytypeio.anytype.presentation.spaces.SpaceIconView import com.anytypeio.anytype.presentation.spaces.spaceIcon import javax.inject.Inject +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow @@ -48,7 +56,9 @@ class AddToAnytypeViewModel( private val getSpaceViews: GetSpaceViews, private val urlBuilder: UrlBuilder, private val awaitAccountStartManager: AwaitAccountStartManager, - private val analytics: Analytics + private val analytics: Analytics, + private val uploadFile: UploadFile, + private val fileSharer: FileSharer ) : BaseViewModel() { private val selectedSpaceId = MutableStateFlow(NO_VALUE) @@ -108,6 +118,74 @@ class AddToAnytypeViewModel( } } + fun onShareMedia(uris: List) { + viewModelScope.launch(Dispatchers.IO) { + val targetSpaceView = spaceViews.value.firstOrNull { view -> + view.isSelected + } + val targetSpaceId = targetSpaceView?.obj?.targetSpaceId!! + val paths = uris.mapNotNull { uri -> + fileSharer.getPath(uri) + } + val files = mutableListOf() + paths.forEach { path -> + uploadFile.async( + UploadFile.Params( + path = path, + space = SpaceId(targetSpaceId), + // Temporary workaround to fix issue on the MW side. + type = Block.Content.File.Type.NONE + ) + ).onSuccess { obj -> + files.add(obj) + } + } + if (files.size == 1) { + if (targetSpaceId == spaceManager.get()) { + navigation.emit(OpenObjectNavigation.OpenEditor(files.first())) + } else { + with(commands) { + emit(Command.ObjectAddToSpaceToast(targetSpaceView.obj.name)) + emit(Command.Dismiss) + } + } + } else { + val startTime = System.currentTimeMillis() + createPrefilledNote.async( + CreatePrefilledNote.Params( + text = EMPTY_STRING_VALUE, + space = targetSpaceId, + details = mapOf( + Relations.ORIGIN to ObjectOrigin.SHARING_EXTENSION.code.toDouble() + ), + attachments = files + ) + ).fold( + onSuccess = { result -> + sendAnalyticsObjectCreateEvent( + analytics = analytics, + objType = MarketplaceObjectTypeIds.NOTE, + route = EventsDictionary.Routes.sharingExtension, + startTime = startTime + ) + if (targetSpaceId == spaceManager.get()) { + navigation.emit(OpenObjectNavigation.OpenEditor(result)) + } else { + with(commands) { + emit(Command.ObjectAddToSpaceToast(targetSpaceView.obj.name)) + emit(Command.Dismiss) + } + } + }, + onFailure = { + Timber.d(it, "Error while creating note") + sendToast("Error while creating note: ${it.msg()}") + } + ) + } + } + } + fun onCreateBookmark(url: String) { viewModelScope.launch { val targetSpaceView = spaceViews.value.firstOrNull { view -> @@ -225,7 +303,9 @@ class AddToAnytypeViewModel( private val getSpaceViews: GetSpaceViews, private val urlBuilder: UrlBuilder, private val awaitAccountStartManager: AwaitAccountStartManager, - private val analytics: Analytics + private val analytics: Analytics, + private val uploadFile: UploadFile, + private val fileSharer: FileSharer ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun create(modelClass: Class): T { @@ -236,7 +316,9 @@ class AddToAnytypeViewModel( getSpaceViews = getSpaceViews, urlBuilder = urlBuilder, awaitAccountStartManager = awaitAccountStartManager, - analytics = analytics + analytics = analytics, + uploadFile = uploadFile, + fileSharer = fileSharer ) as T } } diff --git a/presentation/src/main/java/com/anytypeio/anytype/presentation/util/CopyFileToCache.kt b/presentation/src/main/java/com/anytypeio/anytype/presentation/util/CopyFileToCache.kt index 45e60a8b2c..e28d72505c 100644 --- a/presentation/src/main/java/com/anytypeio/anytype/presentation/util/CopyFileToCache.kt +++ b/presentation/src/main/java/com/anytypeio/anytype/presentation/util/CopyFileToCache.kt @@ -308,7 +308,7 @@ private fun Context.deleteTemporaryFolder() { /** * Return /storage/emulated/0/Android/data/package/files/$TEMPORARY_DIRECTORY_NAME directory */ -private fun Context.getExternalFilesDirTemp(): File? = getExternalFilesDir(TEMPORARY_DIRECTORY_NAME) +fun Context.getExternalFilesDirTemp(): File? = getExternalFilesDir(TEMPORARY_DIRECTORY_NAME) /** * Return /storage/emulated/0/Android/data/io.anytype.app/files/networkModeConfig directory