DROID-81 Editor | Enhancement | Share files (#2568)

This commit is contained in:
Mikhail 2022-08-30 11:15:19 +03:00 committed by GitHub
parent a64a23e51c
commit 2acfccb6e2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 408 additions and 79 deletions

View File

@ -95,6 +95,8 @@ import com.anytypeio.anytype.presentation.editor.template.EditorTemplateDelegate
import com.anytypeio.anytype.presentation.editor.toggle.ToggleStateHolder
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
import com.anytypeio.anytype.providers.DefaultUriFileProvider
import com.anytypeio.anytype.test_utils.MockDataFactory
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.emptyFlow
@ -127,9 +129,12 @@ open class EditorTestSetup {
lateinit var updateDetail: UpdateDetail
lateinit var getCompatibleObjectTypes: GetCompatibleObjectTypes
@Mock
lateinit var copyFileToCacheDirectory: CopyFileToCacheDirectory
@Mock
lateinit var middlewareShareDownloader: MiddlewareShareDownloader
@Mock
lateinit var openPage: OpenPage
@Mock
@ -352,6 +357,7 @@ open class EditorTestSetup {
duplicateBlock = duplicateBlock,
updateAlignment = updateAlignment,
downloadFile = downloadFile,
middlewareShareDownloader = middlewareShareDownloader,
mergeBlocks = mergeBlocks,
updateTextColor = updateTextColor,
replaceBlock = replaceBlock,

View File

@ -48,6 +48,15 @@
android:screenOrientation="fullSensor"
android:exported="false"
tools:replace="screenOrientation" />
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
</application>
</manifest>

View File

@ -20,6 +20,7 @@ import dagger.Subcomponent
modules = [
ArchiveModule::class,
EditorUseCaseModule::class,
EditorUseCaseModule.Bindings::class,
EditorSessionModule::class
]
)

View File

@ -48,8 +48,6 @@ import com.anytypeio.anytype.domain.clipboard.Paste
import com.anytypeio.anytype.domain.config.UserSettingsRepository
import com.anytypeio.anytype.domain.cover.SetDocCoverImage
import com.anytypeio.anytype.domain.dataview.interactor.GetCompatibleObjectTypes
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.relations.SetRelationKey
import com.anytypeio.anytype.domain.download.DownloadFile
import com.anytypeio.anytype.domain.download.Downloader
import com.anytypeio.anytype.domain.event.interactor.EventChannel
@ -72,6 +70,8 @@ import com.anytypeio.anytype.domain.page.UpdateTitle
import com.anytypeio.anytype.domain.page.bookmark.CreateBookmarkBlock
import com.anytypeio.anytype.domain.page.bookmark.SetupBookmark
import com.anytypeio.anytype.domain.relations.AddFileToObject
import com.anytypeio.anytype.domain.relations.SetRelationKey
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.sets.FindObjectSetForType
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.status.ThreadStatusChannel
@ -108,14 +108,24 @@ import com.anytypeio.anytype.presentation.relations.providers.ObjectValueProvide
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.DefaultCopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import com.anytypeio.anytype.providers.DefaultCoverImageHashProvider
import com.anytypeio.anytype.providers.DefaultUriFileProvider
import com.anytypeio.anytype.ui.editor.EditorFragment
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.Subcomponent
import kotlinx.coroutines.Dispatchers
@Subcomponent(modules = [EditorSessionModule::class, EditorUseCaseModule::class])
@Subcomponent(
modules = [
EditorSessionModule::class,
EditorUseCaseModule::class,
EditorUseCaseModule.Bindings::class
]
)
@PerScreen
interface EditorSubComponent {
@ -134,23 +144,23 @@ interface EditorSubComponent {
// Relations
fun documentRelationSubComponent(): DocumentRelationSubComponent.Builder
fun relationAddToObjectComponent() : RelationAddToObjectSubComponent.Builder
fun relationCreateFromScratchForObjectComponent() : RelationCreateFromScratchForObjectSubComponent.Builder
fun relationCreateFromScratchForObjectBlockComponent() : RelationCreateFromScratchForObjectBlockSubComponent.Builder
fun relationAddToObjectComponent(): RelationAddToObjectSubComponent.Builder
fun relationCreateFromScratchForObjectComponent(): RelationCreateFromScratchForObjectSubComponent.Builder
fun relationCreateFromScratchForObjectBlockComponent(): RelationCreateFromScratchForObjectBlockSubComponent.Builder
fun relationTextValueComponent(): RelationTextValueSubComponent.Builder
fun editDocRelationComponent() : ObjectObjectRelationValueSubComponent.Builder
fun editDocRelationComponent(): ObjectObjectRelationValueSubComponent.Builder
fun editRelationDateComponent(): RelationDataValueSubComponent.Builder
fun objectCoverComponent() : SelectCoverObjectSubComponent.Builder
fun objectUnsplashComponent() : UnsplashSubComponent.Builder
fun objectMenuComponent() : ObjectMenuComponent.Builder
fun objectCoverComponent(): SelectCoverObjectSubComponent.Builder
fun objectUnsplashComponent(): UnsplashSubComponent.Builder
fun objectMenuComponent(): ObjectMenuComponent.Builder
fun objectLayoutComponent() : ObjectLayoutSubComponent.Builder
fun objectAppearanceSettingComponent() : ObjectAppearanceSettingSubComponent.Builder
fun objectAppearanceIconComponent() : ObjectAppearanceIconSubComponent.Builder
fun objectAppearancePreviewLayoutComponent() : ObjectAppearancePreviewLayoutSubComponent.Builder
fun objectAppearanceCoverComponent() : ObjectAppearanceCoverSubComponent.Builder
fun objectAppearanceChooseDescription() : ObjectAppearanceChooseDescriptionSubComponent.Builder
fun objectLayoutComponent(): ObjectLayoutSubComponent.Builder
fun objectAppearanceSettingComponent(): ObjectAppearanceSettingSubComponent.Builder
fun objectAppearanceIconComponent(): ObjectAppearanceIconSubComponent.Builder
fun objectAppearancePreviewLayoutComponent(): ObjectAppearancePreviewLayoutSubComponent.Builder
fun objectAppearanceCoverComponent(): ObjectAppearanceCoverSubComponent.Builder
fun objectAppearanceChooseDescription(): ObjectAppearanceChooseDescriptionSubComponent.Builder
fun setBlockTextValueComponent(): SetBlockTextValueSubComponent.Builder
}
@ -253,7 +263,7 @@ object EditorSessionModule {
getDefaultEditorType: GetDefaultEditorType,
getTemplates: GetTemplates,
createPage: CreatePage,
) : CreateNewObject = CreateNewObject(
): CreateNewObject = CreateNewObject(
getDefaultEditorType,
getTemplates,
createPage
@ -265,7 +275,7 @@ object EditorSessionModule {
fun provideTemplateDelegate(
getTemplates: GetTemplates,
applyTemplate: ApplyTemplate
) : EditorTemplateDelegate = DefaultEditorTemplateDelegate(
): EditorTemplateDelegate = DefaultEditorTemplateDelegate(
getTemplates = getTemplates,
applyTemplate = applyTemplate
)
@ -274,7 +284,7 @@ object EditorSessionModule {
@Provides
@PerScreen
fun provideSimpleTableDelegate(
) : SimpleTableDelegate = DefaultSimpleTableDelegate()
): SimpleTableDelegate = DefaultSimpleTableDelegate()
@JvmStatic
@Provides
@ -294,7 +304,8 @@ object EditorSessionModule {
@JvmStatic
@Provides
fun provideDocumentExternalEventReducer(): DocumentExternalEventReducer = DocumentExternalEventReducer()
fun provideDocumentExternalEventReducer(): DocumentExternalEventReducer =
DocumentExternalEventReducer()
@JvmStatic
@Provides
@ -349,7 +360,8 @@ object EditorSessionModule {
redo: Redo,
setRelationKey: SetRelationKey,
analytics: Analytics,
updateBlocksMark: UpdateBlocksMark
updateBlocksMark: UpdateBlocksMark,
middlewareShareDownloader: MiddlewareShareDownloader
): Orchestrator = Orchestrator(
stores = storage,
createBlock = createBlock,
@ -369,6 +381,7 @@ object EditorSessionModule {
updateDivider = updateDivider,
memory = memory,
downloadFile = downloadFile,
middlewareShareDownloader = middlewareShareDownloader,
turnIntoDocument = turnIntoDocument,
textInteractor = Interactor.TextInteractor(
proxies = proxer,
@ -389,7 +402,7 @@ object EditorSessionModule {
updateBlocksMark = updateBlocksMark,
setObjectType = setObjectType,
createTable = createTable,
fillTableRow = fillTableRow
fillTableRow = fillTableRow,
)
}
@ -790,21 +803,21 @@ object EditorUseCaseModule {
@PerScreen
fun provideDefaultObjectRelationProvider(
storage: Editor.Storage
) : ObjectRelationProvider = DefaultObjectRelationProvider(storage.relations)
): ObjectRelationProvider = DefaultObjectRelationProvider(storage.relations)
@JvmStatic
@Provides
@PerScreen
fun provideDefaultObjectValueProvider(
storage: Editor.Storage
) : ObjectValueProvider = DefaultObjectValueProvider(storage.details)
): ObjectValueProvider = DefaultObjectValueProvider(storage.details)
@JvmStatic
@Provides
@PerScreen
fun provideObjectTypeProvider(
storage: Editor.Storage
) : ObjectTypeProvider = object : ObjectTypeProvider {
): ObjectTypeProvider = object : ObjectTypeProvider {
override fun provide(): List<ObjectType> = storage.objectTypes.current()
}
@ -813,26 +826,26 @@ object EditorUseCaseModule {
@PerScreen
fun provideObjectDetailProvider(
storage: Editor.Storage
) : ObjectDetailProvider = object : ObjectDetailProvider {
): ObjectDetailProvider = object : ObjectDetailProvider {
override fun provide(): Map<Id, Block.Fields> = storage.details.current().details
}
@JvmStatic
@Provides
@PerScreen
fun providePayloadDispatcher() : Dispatcher<Payload> = Dispatcher.Default()
fun providePayloadDispatcher(): Dispatcher<Payload> = Dispatcher.Default()
@JvmStatic
@Provides
@PerScreen
fun provideDelegator() : Delegator<Action> = Delegator.Default()
fun provideDelegator(): Delegator<Action> = Delegator.Default()
@JvmStatic
@Provides
@PerScreen
fun provideDetailManager(
storage: Editor.Storage
) : DetailModificationManager = InternalDetailModificationManager(
): DetailModificationManager = InternalDetailModificationManager(
store = storage.details
)
@ -846,14 +859,14 @@ object EditorUseCaseModule {
@PerScreen
fun provideUpdateDetailUseCase(
repository: BlockRepository
) : UpdateDetail = UpdateDetail(repository)
): UpdateDetail = UpdateDetail(repository)
@JvmStatic
@Provides
@PerScreen
fun provideGetObjectTypesUseCase(
repository: BlockRepository
) : GetObjectTypes = GetObjectTypes(repository)
): GetObjectTypes = GetObjectTypes(repository)
@JvmStatic
@Provides
@ -934,7 +947,7 @@ object EditorUseCaseModule {
@JvmStatic
@Provides
@PerScreen
fun getTemplates(repo: BlockRepository) : GetTemplates = GetTemplates(
fun getTemplates(repo: BlockRepository): GetTemplates = GetTemplates(
repo = repo,
dispatchers = AppCoroutineDispatchers(
io = Dispatchers.IO,
@ -946,7 +959,7 @@ object EditorUseCaseModule {
@JvmStatic
@Provides
@PerScreen
fun applyTemplates(repo: BlockRepository) : ApplyTemplate = ApplyTemplate(
fun applyTemplates(repo: BlockRepository): ApplyTemplate = ApplyTemplate(
repo = repo,
dispatchers = AppCoroutineDispatchers(
io = Dispatchers.IO,
@ -954,4 +967,32 @@ object EditorUseCaseModule {
main = Dispatchers.Main
)
)
@JvmStatic
@Provides
@PerScreen
fun providesMiddlewareShareDownloader(
repo: BlockRepository,
context: Context,
fileProvider: UriFileProvider
): MiddlewareShareDownloader = MiddlewareShareDownloader(
repo = repo,
dispatchers = AppCoroutineDispatchers(
io = Dispatchers.IO,
computation = Dispatchers.Default,
main = Dispatchers.Main
),
context = context.applicationContext,
uriFileProvider = fileProvider
)
@Module
interface Bindings {
@PerScreen
@Binds
fun bindUriFileProvider(
defaultProvider: DefaultUriFileProvider
): UriFileProvider
}
}

View File

@ -0,0 +1,22 @@
package com.anytypeio.anytype.providers
import android.content.Context
import android.net.Uri
import androidx.core.content.FileProvider
import com.anytypeio.anytype.BuildConfig
import com.anytypeio.anytype.presentation.util.downloader.UriFileProvider
import java.io.File
import javax.inject.Inject
class DefaultUriFileProvider @Inject constructor(
private val context: Context
) : UriFileProvider {
override fun getUriForFile(file: File): Uri = FileProvider.getUriForFile(
context,
BuildConfig.APPLICATION_ID + PROVIDER,
file
)
}
private const val PROVIDER = ".provider"

View File

@ -2,9 +2,13 @@ package com.anytypeio.anytype.ui.editor
import android.animation.ObjectAnimator
import android.app.Activity
import android.app.DownloadManager
import android.content.ActivityNotFoundException
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.graphics.Point
import android.net.Uri
import android.os.Build
@ -22,6 +26,7 @@ import androidx.constraintlayout.widget.ConstraintLayout
import androidx.constraintlayout.widget.ConstraintSet
import androidx.core.animation.doOnEnd
import androidx.core.animation.doOnStart
import androidx.core.content.ContextCompat.getSystemService
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_STOP
@ -1104,7 +1109,11 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
is Command.ShowKeyboard -> {
binding.recycler.findFocus()?.focusAndShowKeyboard()
}
is Command.OpenFileByDefaultApp -> openFileByDefaultApp(command)
is Command.OpenFileByDefaultApp -> {
vm.startSharingFile(command.id) { uri ->
openFileByDefaultApp(uri)
}
}
Command.ShowTextLinkMenu -> {
val urlButton = binding.markupToolbar.findViewById<View>(R.id.url)
val popup = TextLinkPopupMenu(
@ -1149,22 +1158,18 @@ open class EditorFragment : NavigationFragment<FragmentEditorBinding>(R.layout.f
}
}
private fun openFileByDefaultApp(command: Command.OpenFileByDefaultApp) {
private fun openFileByDefaultApp(uri: Uri) {
try {
val uri = Uri.parse(command.uri)
val intent = Intent().apply {
action = Intent.ACTION_VIEW
if (command.mime.isNotEmpty()) {
setDataAndTypeAndNormalize(uri, command.mime)
} else {
data = uri
val intent = Intent(Intent.ACTION_VIEW, uri)
.apply {
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
startActivity(intent)
startActivity(
intent
)
} catch (e: Exception) {
if (e is ActivityNotFoundException) {
toast("No Application found to open the selected file")
toast("No application found to open the selected file")
} else {
toast("Could not open file: ${e.message}")
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<cache-path name="." path="/" />
</paths>

View File

@ -7,6 +7,11 @@ sealed class Command {
val type: Block.Content.File.Type?
)
class DownloadFile(
val path: String,
val hash: Hash
)
/**
* Command for turning simple blocks into documents
* @property context id of the context

View File

@ -5,7 +5,5 @@ import com.anytypeio.anytype.domain.download.Downloader
class DataDownloader(private val device: Device) : Downloader {
override fun download(url: Url, name: String) {
device.download(url, name)
}
override fun download(url: Url, name: String) = device.download(url, name)
}

View File

@ -256,6 +256,10 @@ class BlockDataRepository(
command: Command.UploadFile
): Hash = remote.uploadFile(command)
override suspend fun downloadFile(
command: Command.DownloadFile
): String = remote.downloadFile(command)
override suspend fun getObjectInfoWithLinks(
pageId: String
): ObjectInfoWithLinks = remote.getObjectInfoWithLinks(pageId)

View File

@ -77,6 +77,7 @@ interface BlockDataStore {
suspend fun setRelationKey(command: Command.SetRelationKey): Payload
suspend fun uploadFile(command: Command.UploadFile): String
suspend fun downloadFile(command: Command.DownloadFile): String
suspend fun getObjectInfoWithLinks(pageId: String): ObjectInfoWithLinks

View File

@ -75,6 +75,7 @@ interface BlockRemote {
suspend fun copy(command: Command.Copy) : Response.Clipboard.Copy
suspend fun uploadFile(command: Command.UploadFile): String
suspend fun downloadFile(command: Command.DownloadFile): String
suspend fun getObjectInfoWithLinks(pageId: String): ObjectInfoWithLinks

View File

@ -198,6 +198,10 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore {
command: Command.UploadFile
): String = remote.uploadFile(command)
override suspend fun downloadFile(
command: Command.DownloadFile
): String = remote.downloadFile(command)
override suspend fun getObjectInfoWithLinks(pageId: String): ObjectInfoWithLinks =
remote.getObjectInfoWithLinks(pageId)

View File

@ -6,7 +6,5 @@ import com.anytypeio.anytype.device.download.AndroidDeviceDownloader
class AndroidDevice(
private val downloader: AndroidDeviceDownloader
) : Device {
override fun download(url: String, name: String) {
downloader.download(url = url, name = name)
}
override fun download(url: String, name: String) = downloader.download(url = url, name = name)
}

View File

@ -28,6 +28,7 @@ import com.anytypeio.anytype.domain.page.Undo
interface BlockRepository {
suspend fun uploadFile(command: Command.UploadFile): Hash
suspend fun downloadFile(command: Command.DownloadFile): String
suspend fun move(command: Command.Move): Payload
suspend fun unlink(command: Command.Unlink): Payload

View File

@ -225,6 +225,10 @@ class BlockMiddleware(
command: Command.UploadFile
): String = middleware.fileUpload(command).hash
override suspend fun downloadFile(
command: Command.DownloadFile
): String = middleware.fileDownload(command).localPath
override suspend fun getObjectInfoWithLinks(pageId: String): ObjectInfoWithLinks {
return middleware.navigationGetObjectInfoWithLinks(pageId).toCoreModel()
}

View File

@ -911,6 +911,18 @@ class Middleware(
return Response.Media.Upload(response.hash)
}
@Throws(Exception::class)
fun fileDownload(command: Command.DownloadFile): Rpc.File.Download.Response {
val request = Rpc.File.Download.Request(
hash = command.hash,
path = command.path
)
if (BuildConfig.DEBUG) logRequest(request)
val response = service.fileDownload(request)
if (BuildConfig.DEBUG) logResponse(response)
return response
}
@Throws(Exception::class)
fun getConfig(): Config {
TODO()

View File

@ -156,6 +156,9 @@ interface MiddlewareService {
@Throws(Exception::class)
fun fileUpload(request: Rpc.File.Upload.Request): Rpc.File.Upload.Response
@Throws(Exception::class)
fun fileDownload(request: Rpc.File.Download.Request): Rpc.File.Download.Response
//endregion
//region UNSPLASH commands

View File

@ -583,6 +583,17 @@ class MiddlewareServiceImplementation : MiddlewareService {
}
}
override fun fileDownload(request: Rpc.File.Download.Request): Rpc.File.Download.Response {
val encoded = Service.fileDownload(Rpc.File.Download.Request.ADAPTER.encode(request))
val response = Rpc.File.Download.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.File.Download.Response.Error.Code.NULL) {
throw Exception(error.description)
} else {
return response
}
}
override fun navigationListObjects(request: Rpc.Navigation.ListObjects.Request): Rpc.Navigation.ListObjects.Response {
val encoded =
Service.navigationListObjects(Rpc.Navigation.ListObjects.Request.ADAPTER.encode(request))

View File

@ -194,6 +194,7 @@ import com.anytypeio.anytype.presentation.util.CopyFileStatus
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.OnCopyFileToCacheAction
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow
@ -3882,6 +3883,31 @@ class EditorViewModel(
}
}
fun startSharingFile(id: String, onDownloaded: (Uri) -> Unit = {}) {
Timber.d("startDownloadingFile, id:[$id]")
sendToast("Preparing file to share...")
val block = blocks.firstOrNull { it.id == id }
val content = block?.content
if (content is Content.File && content.state == Content.File.State.DONE) {
viewModelScope.launch {
orchestrator.proxies.intents.send(
Media.ShareFile(
hash = content.hash.orEmpty(),
name = content.name.orEmpty(),
type = content.type,
onDownloaded = onDownloaded
)
)
}
} else {
Timber.e("Block is not File or with wrong state, can't proceed with share!")
}
}
fun startDownloadingFile(id: String) {
Timber.d("startDownloadingFile, id:[$id]")
@ -4524,11 +4550,15 @@ class EditorViewModel(
}
}
private fun getObjectTypes(excluded: List<Id> = emptyList(), action: (List<ObjectType>) -> Unit) {
private fun getObjectTypes(
excluded: List<Id> = emptyList(),
action: (List<ObjectType>) -> Unit
) {
viewModelScope.launch {
getCompatibleObjectTypes.invoke(
GetCompatibleObjectTypes.Params(
smartBlockType = blocks.first { it.id == context }.content<Content.Smart>().type,
smartBlockType = blocks.first { it.id == context }
.content<Content.Smart>().type,
excludedTypes = excluded
)
).proceed(
@ -5115,7 +5145,9 @@ class EditorViewModel(
when (val content = selected.content) {
is Content.Bookmark -> {
val target = content.targetObjectId
if (target != null) { proceedWithOpeningPage(target) }
if (target != null) {
proceedWithOpeningPage(target)
}
}
else -> sendToast("Unexpected object")
}

View File

@ -13,7 +13,6 @@ import com.anytypeio.anytype.domain.block.interactor.UpdateLinkMarks
import com.anytypeio.anytype.domain.block.interactor.sets.CreateObjectSet
import com.anytypeio.anytype.domain.cover.SetDocCoverImage
import com.anytypeio.anytype.domain.dataview.interactor.GetCompatibleObjectTypes
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon
import com.anytypeio.anytype.domain.launch.GetDefaultEditorType
@ -21,15 +20,16 @@ import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.page.CloseBlock
import com.anytypeio.anytype.domain.page.CreateDocument
import com.anytypeio.anytype.domain.page.CreateNewDocument
import com.anytypeio.anytype.domain.page.CreateNewObject
import com.anytypeio.anytype.domain.page.CreateObject
import com.anytypeio.anytype.domain.page.OpenPage
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.sets.FindObjectSetForType
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.presentation.common.Action
import com.anytypeio.anytype.presentation.common.Delegator
import com.anytypeio.anytype.presentation.common.StateReducer
import com.anytypeio.anytype.domain.page.CreateNewObject
import com.anytypeio.anytype.presentation.editor.editor.DetailModificationManager
import com.anytypeio.anytype.presentation.editor.editor.Orchestrator
import com.anytypeio.anytype.presentation.editor.editor.table.SimpleTableDelegate

View File

@ -1,7 +1,9 @@
package com.anytypeio.anytype.presentation.editor.editor
import android.net.Uri
import android.os.Parcelable
import com.anytypeio.anytype.core_models.Block
import com.anytypeio.anytype.core_models.Hash
import com.anytypeio.anytype.core_models.Id
import com.anytypeio.anytype.core_models.Position
import com.anytypeio.anytype.core_utils.ext.Mimetype
@ -174,6 +176,13 @@ sealed class Intent {
val type: Block.Content.File.Type?
) : Media()
class ShareFile(
val hash: Hash,
val name: String,
val type: Block.Content.File.Type?,
val onDownloaded: (Uri) -> Unit
) : Media()
class Upload(
val context: Id,
val description: Description,

View File

@ -35,6 +35,7 @@ import com.anytypeio.anytype.domain.page.bookmark.CreateBookmarkBlock
import com.anytypeio.anytype.domain.page.bookmark.SetupBookmark
import com.anytypeio.anytype.domain.table.CreateTable
import com.anytypeio.anytype.domain.table.FillTableRow
import com.anytypeio.anytype.presentation.dashboard.HomeDashboardStateMachine
import com.anytypeio.anytype.presentation.editor.Editor
import com.anytypeio.anytype.presentation.extension.sendAnalyticsChangeTextBlockStyleEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsCopyBlockEvent
@ -48,6 +49,7 @@ import com.anytypeio.anytype.presentation.extension.sendAnalyticsReorderBlockEve
import com.anytypeio.anytype.presentation.extension.sendAnalyticsSplitBlockEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsUndoEvent
import com.anytypeio.anytype.presentation.extension.sendAnalyticsUploadMediaEvent
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
import timber.log.Timber
class Orchestrator(
@ -64,6 +66,7 @@ class Orchestrator(
private val turnIntoStyle: TurnIntoStyle,
private val updateCheckbox: UpdateCheckbox,
private val downloadFile: DownloadFile,
private val middlewareShareDownloader: MiddlewareShareDownloader,
val updateText: UpdateText,
private val updateAlignment: UpdateAlignment,
private val uploadBlock: UploadBlock,
@ -432,6 +435,20 @@ class Orchestrator(
success = { analytics.sendAnalyticsDownloadMediaEvent(intent.type) }
)
}
is Intent.Media.ShareFile -> {
middlewareShareDownloader.execute(
params = MiddlewareShareDownloader.Params(
hash = intent.hash,
name = intent.name
)
).fold(
onSuccess = { uri ->
intent.onDownloaded(uri)
analytics.sendAnalyticsDownloadMediaEvent(intent.type)
},
onFailure = { e -> Timber.e(e, "Error while sharing a file") }
)
}
is Intent.Media.Upload -> {
uploadBlock(
params = UploadBlock.Params(

View File

@ -0,0 +1,55 @@
package com.anytypeio.anytype.presentation.util.downloader
import android.content.Context
import android.net.Uri
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.core_models.Hash
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import com.anytypeio.anytype.presentation.util.TEMPORARY_DIRECTORY_NAME
import kotlinx.coroutines.withContext
import java.io.File
class MiddlewareShareDownloader(
private val repo: BlockRepository,
private val dispatchers: AppCoroutineDispatchers,
private val context: Context,
private val uriFileProvider: UriFileProvider
) : ResultInteractor<MiddlewareShareDownloader.Params, Uri>() {
data class Params(
val hash: Hash,
val name: String
)
override suspend fun doWork(params: Params) = withContext(dispatchers.io) {
val cacheDir = context.cacheDir
require(cacheDir != null) { "Impossible to cache files!" }
val downloadFolder = File("${cacheDir.path}/${params.hash}").apply { mkdirs() }
val resultFilePath = "${cacheDir.path}/${params.hash}/${params.name}"
val resultFile = File(resultFilePath)
if (!resultFile.exists()) {
val tempFileFolderPath = "${downloadFolder.absolutePath}/tmp"
val tempDir = File(tempFileFolderPath)
if (tempDir.exists()) tempDir.deleteRecursively()
tempDir.mkdirs()
val tempResult = File(
repo.downloadFile(
Command.DownloadFile(
hash = params.hash,
path = tempFileFolderPath
)
)
)
tempResult.renameTo(resultFile)
}
uriFileProvider.getUriForFile(resultFile)
}
}

View File

@ -0,0 +1,10 @@
package com.anytypeio.anytype.presentation.util.downloader
import android.net.Uri
import java.io.File
interface UriFileProvider {
fun getUriForFile(file: File): Uri
}

View File

@ -1,5 +1,6 @@
package com.anytypeio.anytype.presentation.editor
import android.net.Uri
import android.os.Build
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import com.anytypeio.anytype.analytics.base.Analytics
@ -13,7 +14,9 @@ import com.anytypeio.anytype.core_models.SmartBlockType
import com.anytypeio.anytype.core_models.StubFile
import com.anytypeio.anytype.core_models.StubNumbered
import com.anytypeio.anytype.core_models.StubParagraph
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_models.ext.content
import com.anytypeio.anytype.core_models.ext.parseThemeTextColor
import com.anytypeio.anytype.core_models.restrictions.ObjectRestriction
import com.anytypeio.anytype.core_utils.common.EventWrapper
import com.anytypeio.anytype.core_utils.ext.Mimetype
@ -50,8 +53,6 @@ import com.anytypeio.anytype.domain.clipboard.Paste
import com.anytypeio.anytype.domain.config.Gateway
import com.anytypeio.anytype.domain.cover.SetDocCoverImage
import com.anytypeio.anytype.domain.dataview.interactor.GetCompatibleObjectTypes
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.relations.SetRelationKey
import com.anytypeio.anytype.domain.download.DownloadFile
import com.anytypeio.anytype.domain.event.interactor.InterceptEvents
import com.anytypeio.anytype.domain.icon.SetDocumentImageIcon
@ -68,12 +69,14 @@ import com.anytypeio.anytype.domain.page.Undo
import com.anytypeio.anytype.domain.page.UpdateTitle
import com.anytypeio.anytype.domain.page.bookmark.CreateBookmarkBlock
import com.anytypeio.anytype.domain.page.bookmark.SetupBookmark
import com.anytypeio.anytype.domain.relations.SetRelationKey
import com.anytypeio.anytype.domain.search.SearchObjects
import com.anytypeio.anytype.domain.sets.FindObjectSetForType
import com.anytypeio.anytype.domain.status.InterceptThreadStatus
import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.table.CreateTable
import com.anytypeio.anytype.domain.table.FillTableRow
import com.anytypeio.anytype.domain.templates.ApplyTemplate
import com.anytypeio.anytype.domain.templates.GetTemplates
import com.anytypeio.anytype.domain.unsplash.DownloadUnsplashImage
import com.anytypeio.anytype.domain.unsplash.UnsplashRepository
import com.anytypeio.anytype.presentation.BuildConfig
@ -87,8 +90,6 @@ import com.anytypeio.anytype.presentation.editor.editor.Interactor
import com.anytypeio.anytype.presentation.editor.editor.InternalDetailModificationManager
import com.anytypeio.anytype.presentation.editor.editor.Markup
import com.anytypeio.anytype.presentation.editor.editor.Orchestrator
import com.anytypeio.anytype.core_models.ThemeColor
import com.anytypeio.anytype.core_models.ext.parseThemeTextColor
import com.anytypeio.anytype.presentation.editor.editor.ViewState
import com.anytypeio.anytype.presentation.editor.editor.actions.ActionItemType
import com.anytypeio.anytype.presentation.editor.editor.control.ControlPanelState
@ -111,6 +112,7 @@ import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.CoroutinesTestRule
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.TXT
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
import com.anytypeio.anytype.test_utils.MockDataFactory
import com.anytypeio.anytype.test_utils.ValueClassAnswer
import com.jraska.livedata.test
@ -121,6 +123,7 @@ import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runBlockingTest
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@ -132,6 +135,7 @@ import org.mockito.kotlin.argThat
import org.mockito.kotlin.doAnswer
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.given
import org.mockito.kotlin.never
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
@ -223,6 +227,9 @@ open class EditorViewModelTest {
@Mock
lateinit var downloadFile: DownloadFile
@Mock
lateinit var middlewareShareDownloader: MiddlewareShareDownloader
@Mock
lateinit var uploadBlock: UploadBlock
@ -2616,6 +2623,63 @@ open class EditorViewModelTest {
}
}
@Test
fun `should start sharing a file`() {
val root = MockDataFactory.randomUuid()
val file = MockBlockFactory.makeFileBlock()
val title = MockBlockFactory.makeTitleBlock()
val page = listOf(
Block(
id = root,
fields = Block.Fields(emptyMap()),
content = Block.Content.Smart(),
children = listOf(title.id, file.id)
),
title,
file
)
val flow: Flow<List<Event.Command>> = flow {
delay(100)
emit(
listOf(
Event.Command.ShowObject(
root = root,
blocks = page,
context = root
)
)
)
}
stubObserveEvents(flow)
stubOpenPage()
givenViewModel(builder)
givenSharedFile()
vm.onStart(root)
coroutineTestRule.advanceTime(100)
// TESTING
vm.startSharingFile(id = file.id)
runTest {
verify(middlewareShareDownloader, times(1)).execute(
params = eq(
MiddlewareShareDownloader.Params(
name = file.content<Block.Content.File>().name.orEmpty(),
hash = file.content<Block.Content.File>().hash.orEmpty(),
)
)
)
}
}
@Test
fun `should start downloading file`() {
@ -3817,6 +3881,12 @@ open class EditorViewModelTest {
}
}
private fun givenSharedFile() {
middlewareShareDownloader.stub {
onBlocking { execute(any()) } doAnswer ValueClassAnswer(Uri.EMPTY)
}
}
private fun stubUpdateTextColor(root: String) {
updateTextColor.stub {
onBlocking { invoke(any()) } doReturn Either.Right(
@ -3878,7 +3948,9 @@ open class EditorViewModelTest {
vm = EditorViewModel(
openPage = openPage,
closePage = closePage,
createDocument = createDocument,
createObject = createObject,
createNewDocument = createNewDocument,
interceptEvents = interceptEvents,
interceptThreadStatus = interceptThreadStatus,
updateLinkMarks = updateLinkMark,
@ -3890,16 +3962,13 @@ open class EditorViewModelTest {
toggleStateHolder = ToggleStateHolder.Default(),
coverImageHashProvider = coverImageHashProvider
),
createDocument = createDocument,
createNewDocument = createNewDocument,
analytics = analytics,
getDefaultEditorType = getDefaultEditorType,
orchestrator = Orchestrator(
createBlock = createBlock,
replaceBlock = replaceBlock,
updateTextColor = updateTextColor,
duplicateBlock = duplicateBlock,
downloadFile = downloadFile,
middlewareShareDownloader = middlewareShareDownloader,
undo = undo,
redo = redo,
updateText = updateText,
@ -3935,22 +4004,24 @@ open class EditorViewModelTest {
createTable = createTable,
fillTableRow = fillTableRow
),
analytics = analytics,
dispatcher = Dispatcher.Default(),
delegator = delegator,
detailModificationManager = InternalDetailModificationManager(storage.details),
updateDetail = updateDetail,
getCompatibleObjectTypes = getCompatibleObjectTypes,
objectTypesProvider = objectTypesProvider,
searchObjects = searchObjects,
getDefaultEditorType = getDefaultEditorType,
findObjectSetForType = findObjectSetForType,
createObjectSet = createObjectSet,
copyFileToCache = copyFileToCacheDirectory,
downloadUnsplashImage = downloadUnsplashImage,
setDocCoverImage = setDocCoverImage,
setDocImageIcon = setDocImageIcon,
delegator = delegator,
templateDelegate = editorTemplateDelegate,
createNewObject = createNewObject,
simpleTableDelegate = simpleTableDelegate
simpleTableDelegate = simpleTableDelegate,
createNewObject = createNewObject
)
}

View File

@ -83,6 +83,7 @@ import com.anytypeio.anytype.presentation.editor.template.EditorTemplateDelegate
import com.anytypeio.anytype.presentation.editor.toggle.ToggleStateHolder
import com.anytypeio.anytype.presentation.util.CopyFileToCacheDirectory
import com.anytypeio.anytype.presentation.util.Dispatcher
import com.anytypeio.anytype.presentation.util.downloader.MiddlewareShareDownloader
import com.anytypeio.anytype.test_utils.MockDataFactory
import com.anytypeio.anytype.test_utils.ValueClassAnswer
import kotlinx.coroutines.flow.Flow
@ -164,6 +165,9 @@ open class EditorPresentationTestSetup {
@Mock
lateinit var downloadFile: DownloadFile
@Mock
lateinit var middlewareShareDownloader: MiddlewareShareDownloader
@Mock
lateinit var uploadBlock: UploadBlock
@ -298,6 +302,7 @@ open class EditorPresentationTestSetup {
updateTextColor = updateTextColor,
duplicateBlock = duplicateBlock,
downloadFile = downloadFile,
middlewareShareDownloader = middlewareShareDownloader,
undo = undo,
redo = redo,
updateText = updateText,
@ -337,11 +342,13 @@ open class EditorPresentationTestSetup {
return EditorViewModel(
openPage = openPage,
closePage = closePage,
createDocument = createDocument,
createObject = createObject,
createNewDocument = createNewDocument,
interceptEvents = interceptEvents,
interceptThreadStatus = interceptThreadStatus,
updateLinkMarks = updateLinkMark,
removeLinkMark = removeLinkMark,
createObject = createObject,
reducer = DocumentExternalEventReducer(),
urlBuilder = urlBuilder,
renderer = DefaultBlockViewRenderer(
@ -349,11 +356,10 @@ open class EditorPresentationTestSetup {
toggleStateHolder = ToggleStateHolder.Default(),
coverImageHashProvider = coverImageHashProvider
),
createDocument = createDocument,
createNewDocument = createNewDocument,
analytics = analytics,
orchestrator = orchestrator,
analytics = analytics,
dispatcher = Dispatcher.Default(),
delegator = delegator,
detailModificationManager = InternalDetailModificationManager(storage.details),
updateDetail = updateDetail,
getCompatibleObjectTypes = getCompatibleObjectTypes,
@ -363,13 +369,12 @@ open class EditorPresentationTestSetup {
findObjectSetForType = findObjectSetForType,
createObjectSet = createObjectSet,
copyFileToCache = copyFileToCacheDirectory,
delegator = delegator,
downloadUnsplashImage = downloadUnsplashImage,
setDocCoverImage = setDocCoverImage,
setDocImageIcon = setDocImageIcon,
downloadUnsplashImage = downloadUnsplashImage,
templateDelegate = editorTemplateDelegate,
createNewObject = createNewObject,
simpleTableDelegate = simpleTableDelegate
simpleTableDelegate = simpleTableDelegate,
createNewObject = createNewObject
)
}