From 98367451ffc61512304779d48eb059b62d526790 Mon Sep 17 00:00:00 2001 From: Evgenii Kozlov Date: Mon, 1 Jun 2020 17:04:43 +0300 Subject: [PATCH] Feature/copy paste inside anytype (#473) --- CHANGELOG.md | 6 +- app/build.gradle | 1 + .../agileburo/anytype/di/feature/PageDI.kt | 29 ++- .../anytype/di/main/ClipboardModule.kt | 63 +++++ .../agileburo/anytype/di/main/DeviceModule.kt | 6 +- .../anytype/di/main/MainComponent.kt | 3 +- .../agileburo/anytype/ui/page/PageFragment.kt | 31 ++- app/src/main/res/layout/fragment_page.xml | 1 + clipboard/.gitignore | 1 + clipboard/build.gradle | 62 +++++ clipboard/consumer-rules.pro | 0 clipboard/gradle.properties | 6 + clipboard/proguard-rules.pro | 21 ++ clipboard/src/main/AndroidManifest.xml | 1 + .../anytype/clipboard/AnytypeClipboard.kt | 56 +++++ .../clipboard/AnytypeClipboardStorage.kt | 30 +++ .../anytype/clipboard/AnytypeUriMatcher.kt | 9 + .../anytype/clipboard/AndroidClipboardTest.kt | 77 ++++++ .../anytype/clipboard/MockDataFactory.kt | 63 +++++ core-ui/build.gradle | 1 + .../core_ui/features/page/BlockAdapter.kt | 7 +- .../core_ui/tools/ClipboardInterceptor.kt | 11 + .../core_ui/widgets/text/TextInputWidget.kt | 40 ++- .../toolbar/MultiSelectBottomToolbarWidget.kt | 3 + .../layout_bottom_multi_select_toolbar.xml | 8 +- core-ui/src/main/res/values/strings.xml | 1 + .../anytype/core_ui/BlockAdapterTest.kt | 6 +- .../data/auth/mapper/MapperExtension.kt | 45 ++-- .../anytype/data/auth/mapper/Serializer.kt | 8 + .../anytype/data/auth/model/BlockEntity.kt | 8 - .../anytype/data/auth/model/ClipEntity.kt | 12 + .../anytype/data/auth/model/CommandEntity.kt | 6 + .../anytype/data/auth/model/Response.kt | 8 +- .../auth/other/ClipboardDataUriMatcher.kt | 12 + .../data/auth/other/ClipboardUriMatcher.kt | 5 + .../auth/repo/block/BlockDataRepository.kt | 9 +- .../data/auth/repo/block/BlockDataStore.kt | 1 + .../data/auth/repo/block/BlockRemote.kt | 1 + .../auth/repo/block/BlockRemoteDataStore.kt | 4 + .../repo/clipboard/ClipboardDataRepository.kt | 28 +++ .../auth/repo/clipboard/ClipboardDataStore.kt | 35 +++ device/build.gradle | 2 + .../anytype/device/base/AndroidDevice.kt | 7 +- ...wnloader.kt => AndroidDeviceDownloader.kt} | 2 +- .../domain/block/interactor/Clipboard.kt | 65 ----- .../anytype/domain/block/model/Block.kt | 11 - .../anytype/domain/block/model/Command.kt | 14 +- .../domain/block/repo/BlockRepository.kt | 7 +- .../anytype/domain/clipboard/Clip.kt | 13 + .../anytype/domain/clipboard/Clipboard.kt | 29 +++ .../anytype/domain/clipboard/Copy.kt | 51 ++++ .../anytype/domain/clipboard/Paste.kt | 69 ++++++ .../agileburo/anytype/domain/ext/BlockExt.kt | 1 - .../anytype/domain/ext/BlockExtensionTest.kt | 5 +- .../anytype/ExampleInstrumentedTest.java | 26 -- .../anytype/middleware/auth/AuthMiddleware.kt | 2 +- .../middleware/block/BlockMiddleware.kt | 8 +- .../converters/ClipboardSerializer.kt | 18 ++ .../{ => converters}/MapperExtension.kt | 67 +---- .../middleware/converters/ToMiddleware.kt | 234 ++++++++++++++++++ .../middleware/interactor/Middleware.java | 46 ++++ .../interactor/MiddlewareEventMapper.kt | 8 +- .../interactor/MiddlewareFactory.kt | 8 +- .../middleware/interactor/MiddlewareMapper.kt | 12 +- .../service/DefaultMiddlewareService.java | 11 + .../middleware/service/MiddlewareService.java | 2 + .../presentation/page/PageViewModel.kt | 35 ++- .../presentation/page/editor/Intent.kt | 8 +- .../presentation/page/editor/Orchestrator.kt | 27 +- .../presentation/page/PageViewModelTest.kt | 10 +- protobuf/src/main/proto/clipboard.proto | 8 + sample/build.gradle | 6 + sample/src/main/AndroidManifest.xml | 8 +- .../anytype/sample/ClipboardActivity.kt | 85 +++++++ .../com/agileburo/anytype/sample/SampleApp.kt | 4 + .../anytype/sample/helpers/MockDataFactory.kt | 64 +++++ .../main/res/layout/activity_clipboard.xml | 69 ++++++ settings.gradle | 1 + 78 files changed, 1474 insertions(+), 294 deletions(-) create mode 100644 app/src/main/java/com/agileburo/anytype/di/main/ClipboardModule.kt create mode 100644 clipboard/.gitignore create mode 100644 clipboard/build.gradle create mode 100644 clipboard/consumer-rules.pro create mode 100644 clipboard/gradle.properties create mode 100644 clipboard/proguard-rules.pro create mode 100644 clipboard/src/main/AndroidManifest.xml create mode 100644 clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboard.kt create mode 100644 clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboardStorage.kt create mode 100644 clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeUriMatcher.kt create mode 100644 clipboard/src/test/java/com/agileburo/anytype/clipboard/AndroidClipboardTest.kt create mode 100644 clipboard/src/test/java/com/agileburo/anytype/clipboard/MockDataFactory.kt create mode 100644 core-ui/src/main/java/com/agileburo/anytype/core_ui/tools/ClipboardInterceptor.kt create mode 100644 data/src/main/java/com/agileburo/anytype/data/auth/mapper/Serializer.kt create mode 100644 data/src/main/java/com/agileburo/anytype/data/auth/model/ClipEntity.kt create mode 100644 data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardDataUriMatcher.kt create mode 100644 data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardUriMatcher.kt create mode 100644 data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataRepository.kt create mode 100644 data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataStore.kt rename device/src/main/java/com/agileburo/anytype/device/download/{DeviceDownloader.kt => AndroidDeviceDownloader.kt} (94%) delete mode 100644 domain/src/main/java/com/agileburo/anytype/domain/block/interactor/Clipboard.kt create mode 100644 domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clip.kt create mode 100644 domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clipboard.kt create mode 100644 domain/src/main/java/com/agileburo/anytype/domain/clipboard/Copy.kt create mode 100644 domain/src/main/java/com/agileburo/anytype/domain/clipboard/Paste.kt delete mode 100644 middleware/src/androidTest/java/com/agileburo/anytype/ExampleInstrumentedTest.java create mode 100644 middleware/src/main/java/com/agileburo/anytype/middleware/converters/ClipboardSerializer.kt rename middleware/src/main/java/com/agileburo/anytype/middleware/{ => converters}/MapperExtension.kt (85%) create mode 100644 middleware/src/main/java/com/agileburo/anytype/middleware/converters/ToMiddleware.kt create mode 100644 protobuf/src/main/proto/clipboard.proto create mode 100644 sample/src/main/java/com/agileburo/anytype/sample/ClipboardActivity.kt create mode 100644 sample/src/main/java/com/agileburo/anytype/sample/helpers/MockDataFactory.kt create mode 100644 sample/src/main/res/layout/activity_clipboard.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index 187ff88f66..8825951a2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,8 @@ ### New features 🚀 -* +* Select text and copy-paste inside Anytype. First iteration (#467) +* Copy and paste multiple blocks in multi-select mode. First iteration (#467) ### Design & UX 🔳 @@ -12,7 +13,8 @@ ### Fixes & tech 🚒 -* +* Resolve race conditions on split and merge (#463, #448) +* Turn-into code block in edit-mode and multi-select mode does not work (#468) ### Middleware ⚙️ diff --git a/app/build.gradle b/app/build.gradle index 87b9361ac1..0fa2807e63 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -79,6 +79,7 @@ dependencies { implementation project(':persistence') implementation project(':middleware') implementation project(':presentation') + implementation project(':clipboard') implementation project(':core-utils') implementation project(':core-ui') implementation project(':library-kanban-widget') diff --git a/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt b/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt index efbef142ca..7e608fc460 100644 --- a/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt +++ b/app/src/main/java/com/agileburo/anytype/di/feature/PageDI.kt @@ -5,6 +5,9 @@ import com.agileburo.anytype.core_utils.di.scope.PerScreen import com.agileburo.anytype.core_utils.tools.Counter import com.agileburo.anytype.domain.block.interactor.* import com.agileburo.anytype.domain.block.repo.BlockRepository +import com.agileburo.anytype.domain.clipboard.Clipboard +import com.agileburo.anytype.domain.clipboard.Copy +import com.agileburo.anytype.domain.clipboard.Paste import com.agileburo.anytype.domain.download.DownloadFile import com.agileburo.anytype.domain.download.Downloader import com.agileburo.anytype.domain.event.interactor.EventChannel @@ -323,7 +326,8 @@ class PageModule { updateAlignment: UpdateAlignment, textInteractor: Interactor.TextInteractor, setupBookmark: SetupBookmark, - paste: Clipboard.Paste, + copy: Copy, + paste: Paste, undo: Undo, redo: Redo ): Orchestrator = Orchestrator( @@ -348,7 +352,8 @@ class PageModule { updateText = updateText, updateAlignment = updateAlignment, setupBookmark = setupBookmark, - paste = paste + paste = paste, + copy = copy ) @Provides @@ -390,8 +395,22 @@ class PageModule { @Provides @PerScreen fun provideClipboardPasteUseCase( - repo: BlockRepository - ) : Clipboard.Paste = Clipboard.Paste( - repo = repo + repo: BlockRepository, + clipboard: Clipboard, + matcher: Clipboard.UriMatcher + ) : Paste = Paste( + repo = repo, + clipboard = clipboard, + matcher = matcher + ) + + @Provides + @PerScreen + fun provideCopyUseCase( + repo: BlockRepository, + clipboard: Clipboard + ) : Copy = Copy( + repo = repo, + clipboard = clipboard ) } \ No newline at end of file diff --git a/app/src/main/java/com/agileburo/anytype/di/main/ClipboardModule.kt b/app/src/main/java/com/agileburo/anytype/di/main/ClipboardModule.kt new file mode 100644 index 0000000000..cd67cfa0d9 --- /dev/null +++ b/app/src/main/java/com/agileburo/anytype/di/main/ClipboardModule.kt @@ -0,0 +1,63 @@ +package com.agileburo.anytype.di.main + +import android.content.ClipboardManager +import android.content.Context +import android.content.Context.CLIPBOARD_SERVICE +import com.agileburo.anytype.clipboard.AnytypeClipboard +import com.agileburo.anytype.clipboard.AnytypeClipboardStorage +import com.agileburo.anytype.clipboard.AnytypeUriMatcher +import com.agileburo.anytype.data.auth.mapper.Serializer +import com.agileburo.anytype.data.auth.other.ClipboardDataUriMatcher +import com.agileburo.anytype.data.auth.repo.clipboard.ClipboardDataRepository +import com.agileburo.anytype.data.auth.repo.clipboard.ClipboardDataStore +import com.agileburo.anytype.domain.clipboard.Clipboard +import com.agileburo.anytype.middleware.converters.ClipboardSerializer +import dagger.Module +import dagger.Provides +import javax.inject.Singleton + +@Module +class ClipboardModule { + + @Provides + @Singleton + fun provideClipboardRepository( + factory: ClipboardDataStore.Factory + ) : Clipboard = ClipboardDataRepository(factory) + + @Provides + @Singleton + fun provideClipboardDataStoreFactory( + storage: ClipboardDataStore.Storage, + system: ClipboardDataStore.System + ) : ClipboardDataStore.Factory = ClipboardDataStore.Factory(storage, system) + + @Provides + @Singleton + fun provideClipboardStorage( + context: Context, + serializer: Serializer + ) : ClipboardDataStore.Storage = AnytypeClipboardStorage(context, serializer) + + @Provides + @Singleton + fun provideClipboardSystem( + cm: ClipboardManager + ) : ClipboardDataStore.System = AnytypeClipboard(cm) + + @Provides + @Singleton + fun provideClipboardManager( + context: Context + ) : ClipboardManager = context.getSystemService(CLIPBOARD_SERVICE) as ClipboardManager + + @Provides + @Singleton + fun provideUriMatcher() : Clipboard.UriMatcher = ClipboardDataUriMatcher( + matcher = AnytypeUriMatcher() + ) + + @Provides + @Singleton + fun provideSerializer() : Serializer = ClipboardSerializer() +} \ No newline at end of file diff --git a/app/src/main/java/com/agileburo/anytype/di/main/DeviceModule.kt b/app/src/main/java/com/agileburo/anytype/di/main/DeviceModule.kt index da5cf561ac..4d82095bba 100644 --- a/app/src/main/java/com/agileburo/anytype/di/main/DeviceModule.kt +++ b/app/src/main/java/com/agileburo/anytype/di/main/DeviceModule.kt @@ -4,7 +4,7 @@ import android.content.Context import com.agileburo.anytype.data.auth.other.DataDownloader import com.agileburo.anytype.data.auth.other.Device import com.agileburo.anytype.device.base.AndroidDevice -import com.agileburo.anytype.device.download.DeviceDownloader +import com.agileburo.anytype.device.download.AndroidDeviceDownloader import com.agileburo.anytype.domain.download.Downloader import dagger.Module import dagger.Provides @@ -22,13 +22,13 @@ class DeviceModule { @Provides @Singleton fun provideDevice( - downloader: DeviceDownloader + downloader: AndroidDeviceDownloader ): Device = AndroidDevice(downloader = downloader) @Provides @Singleton fun provideDeviceDownloader( context: Context - ): DeviceDownloader = DeviceDownloader(context = context) + ): AndroidDeviceDownloader = AndroidDeviceDownloader(context = context) } \ No newline at end of file diff --git a/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt b/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt index dc71dc7ce1..561ae9b365 100644 --- a/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt +++ b/app/src/main/java/com/agileburo/anytype/di/main/MainComponent.kt @@ -13,7 +13,8 @@ import javax.inject.Singleton ConfigModule::class, DeviceModule::class, UtilModule::class, - EmojiModule::class + EmojiModule::class, + ClipboardModule::class ] ) interface MainComponent { diff --git a/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt b/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt index 21c3010aec..f0654cd50f 100644 --- a/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt +++ b/app/src/main/java/com/agileburo/anytype/ui/page/PageFragment.kt @@ -30,6 +30,7 @@ import com.agileburo.anytype.core_ui.menu.DocumentPopUpMenu import com.agileburo.anytype.core_ui.model.UiBlock import com.agileburo.anytype.core_ui.reactive.clicks import com.agileburo.anytype.core_ui.state.ControlPanelState +import com.agileburo.anytype.core_ui.tools.ClipboardInterceptor import com.agileburo.anytype.core_ui.tools.FirstItemInvisibilityDetector import com.agileburo.anytype.core_ui.tools.OutsideClickDetector import com.agileburo.anytype.core_ui.widgets.ActionItemType @@ -70,6 +71,7 @@ open class PageFragment : OnFragmentInteractionListener, AddBlockFragment.AddBlockActionReceiver, TurnIntoActionReceiver, + ClipboardInterceptor, PickiTCallbacks { private val vm by lazy { @@ -139,22 +141,7 @@ open class PageFragment : onLongClickListener = vm::onBlockLongPressedClicked, onTitleTextInputClicked = vm::onTitleTextInputClicked, onClickListener = vm::onClickListener, - clipboardDetector = { range -> - // TODO this logic should be moved to device module - clipboard().primaryClip?.let { clip -> - if (clip.itemCount > 0) { - val item = clip.getItemAt(0) - vm.onPaste( - plain = item.text.toString(), - html = if (item.htmlText != null) - item.htmlText - else - null, - range = range - ) - } - } - } + clipboardInterceptor = this ) } @@ -330,6 +317,11 @@ open class PageFragment : .onEach { vm.onMultiSelectModeDeleteClicked() } .launchIn(lifecycleScope) + bottomMenu + .copyClicks() + .onEach { vm.onMultiSelectCopyClicked() } + .launchIn(lifecycleScope) + bottomMenu .turnIntoClicks() .onEach { vm.onMultiSelectTurnIntoButtonClicked() } @@ -641,6 +633,13 @@ open class PageFragment : } } + override fun onClipboardAction(action: ClipboardInterceptor.Action) { + when(action) { + is ClipboardInterceptor.Action.Copy -> vm.onCopy(action.selection) + is ClipboardInterceptor.Action.Paste -> vm.onPaste(action.selection) + } + } + private fun showSelectButton() { ObjectAnimator.ofFloat( select, diff --git a/app/src/main/res/layout/fragment_page.xml b/app/src/main/res/layout/fragment_page.xml index cf3fb86805..29fd8b2f0b 100644 --- a/app/src/main/res/layout/fragment_page.xml +++ b/app/src/main/res/layout/fragment_page.xml @@ -24,6 +24,7 @@ app:layout_behavior="@string/bottom_sheet_behavior"> diff --git a/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboard.kt b/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboard.kt new file mode 100644 index 0000000000..8a11e64be9 --- /dev/null +++ b/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboard.kt @@ -0,0 +1,56 @@ +package com.agileburo.anytype.clipboard + +import android.content.ClipData +import android.content.ClipboardManager +import android.net.Uri +import com.agileburo.anytype.clipboard.BuildConfig.ANYTYPE_CLIPBOARD_LABEL +import com.agileburo.anytype.clipboard.BuildConfig.ANYTYPE_CLIPBOARD_URI + +import com.agileburo.anytype.data.auth.model.ClipEntity +import com.agileburo.anytype.data.auth.repo.clipboard.ClipboardDataStore + +class AnytypeClipboard( + private val cm: ClipboardManager +) : ClipboardDataStore.System { + + override suspend fun put(text: String, html: String?) { + + val uri = Uri.parse(ANYTYPE_CLIPBOARD_URI) + if (html != null) + cm.setPrimaryClip( + ClipData.newHtmlText(ANYTYPE_CLIPBOARD_LABEL, text, html).apply { + addItem(ClipData.Item(uri)) + } + ) + else + cm.setPrimaryClip( + ClipData.newPlainText(ANYTYPE_CLIPBOARD_LABEL, text).apply { + addItem(ClipData.Item(uri)) + } + ) + } + + override suspend fun clip(): ClipEntity? { + return cm.primaryClip?.let { clip -> + when { + clip.itemCount > 1 -> { + ClipEntity( + text = clip.getItemAt(0).text.toString(), + html = clip.getItemAt(0).htmlText, + uri = clip.getItemAt(1).uri.toString() + ) + } + clip.itemCount == 1 -> { + ClipEntity( + text = clip.getItemAt(0).text.toString(), + html = clip.getItemAt(0).htmlText, + uri = null + ) + } + else -> { + null + } + } + } + } +} \ No newline at end of file diff --git a/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboardStorage.kt b/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboardStorage.kt new file mode 100644 index 0000000000..e6be261ab8 --- /dev/null +++ b/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeClipboardStorage.kt @@ -0,0 +1,30 @@ +package com.agileburo.anytype.clipboard + +import android.content.Context +import com.agileburo.anytype.data.auth.mapper.Serializer +import com.agileburo.anytype.data.auth.model.BlockEntity +import com.agileburo.anytype.data.auth.repo.clipboard.ClipboardDataStore + +class AnytypeClipboardStorage( + private val context: Context, + private val serializer: Serializer +) : ClipboardDataStore.Storage { + + override fun persist(blocks: List) { + val serialized = serializer.serialize(blocks) + context.openFileOutput(CLIPBOARD_FILE_NAME, Context.MODE_PRIVATE).use { + it.write(serialized) + it.flush() + } + } + + override fun fetch(): List { + val stream = context.openFileInput(CLIPBOARD_FILE_NAME) + val blob = stream.use { it.readBytes() } + return serializer.deserialize(blob) + } + + companion object { + const val CLIPBOARD_FILE_NAME = "anytype_clipboard" + } +} \ No newline at end of file diff --git a/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeUriMatcher.kt b/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeUriMatcher.kt new file mode 100644 index 0000000000..99dc165bed --- /dev/null +++ b/clipboard/src/main/java/com/agileburo/anytype/clipboard/AnytypeUriMatcher.kt @@ -0,0 +1,9 @@ +package com.agileburo.anytype.clipboard + +import com.agileburo.anytype.data.auth.other.ClipboardUriMatcher + +class AnytypeUriMatcher : ClipboardUriMatcher { + override fun isAnytypeUri(uri: String): Boolean { + return uri == BuildConfig.ANYTYPE_CLIPBOARD_URI + } +} \ No newline at end of file diff --git a/clipboard/src/test/java/com/agileburo/anytype/clipboard/AndroidClipboardTest.kt b/clipboard/src/test/java/com/agileburo/anytype/clipboard/AndroidClipboardTest.kt new file mode 100644 index 0000000000..088d14299e --- /dev/null +++ b/clipboard/src/test/java/com/agileburo/anytype/clipboard/AndroidClipboardTest.kt @@ -0,0 +1,77 @@ +package com.agileburo.anytype.clipboard + +import android.content.ClipboardManager +import android.content.Context +import android.os.Build +import androidx.test.core.app.ApplicationProvider +import kotlinx.coroutines.runBlocking +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +@Config(sdk = [Build.VERSION_CODES.P]) +@RunWith(RobolectricTestRunner::class) +class AndroidClipboardTest { + + private lateinit var clipboard : AnytypeClipboard + + private lateinit var cm: ClipboardManager + + @Before + fun setup() { + val context = ApplicationProvider.getApplicationContext() + cm = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + clipboard = AnytypeClipboard(cm = cm) + } + + @Test + fun `should put only text and uri`() { + val text = MockDataFactory.randomString() + + runBlocking { + clipboard.put( + text = text, + html = null + ) + } + + assertEquals( + expected = text, + actual = cm.primaryClip?.getItemAt(0)?.text + ) + + assertNull(cm.primaryClip?.getItemAt(0)?.htmlText) + assertNotNull(cm.primaryClip?.getItemAt(1)?.uri) + } + + @Test + fun `should put text, html as first item and uri as second item`() { + val text = MockDataFactory.randomString() + val html = MockDataFactory.randomString() + + runBlocking { + clipboard.put( + text = text, + html = html + ) + } + + assertEquals( + expected = text, + actual = cm.primaryClip?.getItemAt(0)?.text + ) + + assertEquals( + expected = html, + actual = cm.primaryClip?.getItemAt(0)?.htmlText + ) + + assertNull(cm.primaryClip?.getItemAt(0)?.uri) + assertNotNull(cm.primaryClip?.getItemAt(1)?.uri) + } +} \ No newline at end of file diff --git a/clipboard/src/test/java/com/agileburo/anytype/clipboard/MockDataFactory.kt b/clipboard/src/test/java/com/agileburo/anytype/clipboard/MockDataFactory.kt new file mode 100644 index 0000000000..4e8c3536c0 --- /dev/null +++ b/clipboard/src/test/java/com/agileburo/anytype/clipboard/MockDataFactory.kt @@ -0,0 +1,63 @@ +package com.agileburo.anytype.clipboard + +import java.util.* +import java.util.concurrent.ThreadLocalRandom + +object MockDataFactory { + + fun randomUuid(): String { + return UUID.randomUUID().toString() + } + + fun randomString(): String { + return randomUuid() + } + + fun randomInt(): Int { + return ThreadLocalRandom.current().nextInt(0, 1000 + 1) + } + + fun randomInt(max: Int): Int { + return ThreadLocalRandom.current().nextInt(0, max) + } + + fun randomLong(): Long { + return randomInt().toLong() + } + + fun randomFloat(): Float { + return randomInt().toFloat() + } + + fun randomDouble(): Double { + return randomInt().toDouble() + } + + fun randomBoolean(): Boolean { + return Math.random() < 0.5 + } + + fun makeIntList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomInt()) + } + return items + } + + fun makeStringList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomUuid()) + } + return items + } + + fun makeDoubleList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomDouble()) + } + return items + } +} \ No newline at end of file diff --git a/core-ui/build.gradle b/core-ui/build.gradle index 9531127d6d..15c7322219 100644 --- a/core-ui/build.gradle +++ b/core-ui/build.gradle @@ -62,4 +62,5 @@ dependencies { testImplementation unitTestDependencies.kotlinTest testImplementation unitTestDependencies.robolectric testImplementation unitTestDependencies.androidXTestCore + testImplementation unitTestDependencies.mockitoKotlin } \ No newline at end of file diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockAdapter.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockAdapter.kt index d61fd04e10..f2ad3384f5 100644 --- a/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockAdapter.kt +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/features/page/BlockAdapter.kt @@ -38,6 +38,7 @@ import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOL import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO_ERROR import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO_PLACEHOLDER import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.HOLDER_VIDEO_UPLOAD +import com.agileburo.anytype.core_ui.tools.ClipboardInterceptor import com.agileburo.anytype.core_utils.ext.typeOf import timber.log.Timber @@ -72,7 +73,7 @@ class BlockAdapter( private val onToggleClicked: (String) -> Unit, private val onMarkupActionClicked: (Markup.Type) -> Unit, private val onLongClickListener: (String) -> Unit, - private val clipboardDetector: (IntRange) -> Unit + private val clipboardInterceptor: ClipboardInterceptor ) : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BlockViewHolder { @@ -844,9 +845,7 @@ class BlockAdapter( else holder.setOnClickListener { onTextInputClicked(blocks[holder.adapterPosition].id) } - holder.content.clipboardDetector = { - clipboardDetector(holder.content.selectionStart..holder.content.selectionEnd) - } + holder.content.clipboardInterceptor = clipboardInterceptor } } diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/tools/ClipboardInterceptor.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/tools/ClipboardInterceptor.kt new file mode 100644 index 0000000000..b4d3b55bbe --- /dev/null +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/tools/ClipboardInterceptor.kt @@ -0,0 +1,11 @@ +package com.agileburo.anytype.core_ui.tools + +interface ClipboardInterceptor { + + fun onClipboardAction(action: Action) + + sealed class Action { + data class Copy(val selection: IntRange) : Action() + data class Paste(val selection: IntRange) : Action() + } +} \ No newline at end of file diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextInputWidget.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextInputWidget.kt index 3c0024b95c..ff05049b44 100644 --- a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextInputWidget.kt +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/text/TextInputWidget.kt @@ -13,6 +13,7 @@ import android.view.inputmethod.EditorInfo import androidx.appcompat.widget.AppCompatEditText import androidx.core.graphics.withTranslation import com.agileburo.anytype.core_ui.extensions.toast +import com.agileburo.anytype.core_ui.tools.ClipboardInterceptor import com.agileburo.anytype.core_ui.tools.DefaultTextWatcher import com.agileburo.anytype.core_ui.widgets.text.highlight.HighlightAttributeReader import com.agileburo.anytype.core_ui.widgets.text.highlight.HighlightDrawer @@ -28,10 +29,12 @@ class TextInputWidget : AppCompatEditText { } private val watchers: MutableList = mutableListOf() + private var highlightDrawer: HighlightDrawer? = null var selectionDetector: ((IntRange) -> Unit)? = null - var clipboardDetector: (() -> Unit)? = null + + var clipboardInterceptor: ClipboardInterceptor? = null constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { @@ -121,19 +124,40 @@ class TextInputWidget : AppCompatEditText { } override fun onTextContextMenuItem(id: Int): Boolean { - var consumed = true + if (clipboardInterceptor == null) { + return super.onTextContextMenuItem(id) + } + + var consumed = false + when(id) { R.id.paste -> { - clipboardDetector?.invoke() - } - R.id.cut -> { - consumed = super.onTextContextMenuItem(id) + if (clipboardInterceptor != null) { + clipboardInterceptor?.onClipboardAction( + ClipboardInterceptor.Action.Paste( + selection = selectionStart..selectionEnd + ) + ) + consumed = true + } } R.id.copy -> { - consumed = super.onTextContextMenuItem(id) + if (clipboardInterceptor != null) { + clipboardInterceptor?.onClipboardAction( + ClipboardInterceptor.Action.Copy( + selection = selectionStart..selectionEnd + ) + ) + consumed = true + } } } - return consumed + + return if (!consumed) { + super.onTextContextMenuItem(id) + } else { + consumed + } } override fun onDraw(canvas: Canvas?) { diff --git a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/toolbar/MultiSelectBottomToolbarWidget.kt b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/toolbar/MultiSelectBottomToolbarWidget.kt index 655f2007a8..82fb551002 100644 --- a/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/toolbar/MultiSelectBottomToolbarWidget.kt +++ b/core-ui/src/main/java/com/agileburo/anytype/core_ui/widgets/toolbar/MultiSelectBottomToolbarWidget.kt @@ -39,6 +39,9 @@ class MultiSelectBottomToolbarWidget : ConstraintLayout { fun deleteClicks() = delete.clicks() fun turnIntoClicks() = convert.clicks() + // Temporary button usage for copying. + fun copyClicks() = more.clicks() + fun showWithAnimation() { ObjectAnimator.ofFloat(this, ANIMATED_PROPERTY, 0f).apply { duration = ANIMATION_DURATION diff --git a/core-ui/src/main/res/layout/layout_bottom_multi_select_toolbar.xml b/core-ui/src/main/res/layout/layout_bottom_multi_select_toolbar.xml index 3e77311260..8c0eb834a3 100644 --- a/core-ui/src/main/res/layout/layout_bottom_multi_select_toolbar.xml +++ b/core-ui/src/main/res/layout/layout_bottom_multi_select_toolbar.xml @@ -3,10 +3,10 @@ xmlns:app="http://schemas.android.com/apk/res-auto"> + android:text="@string/copy" /> @@ -88,12 +90,12 @@ diff --git a/core-ui/src/main/res/values/strings.xml b/core-ui/src/main/res/values/strings.xml index c1ece95a76..78ad980ca6 100644 --- a/core-ui/src/main/res/values/strings.xml +++ b/core-ui/src/main/res/values/strings.xml @@ -197,5 +197,6 @@ Undo Redo + Copy diff --git a/core-ui/src/test/java/com/agileburo/anytype/core_ui/BlockAdapterTest.kt b/core-ui/src/test/java/com/agileburo/anytype/core_ui/BlockAdapterTest.kt index c5e0ca724a..3d875072cf 100644 --- a/core-ui/src/test/java/com/agileburo/anytype/core_ui/BlockAdapterTest.kt +++ b/core-ui/src/test/java/com/agileburo/anytype/core_ui/BlockAdapterTest.kt @@ -23,9 +23,11 @@ import com.agileburo.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.T import com.agileburo.anytype.core_ui.features.page.BlockViewDiffUtil.Companion.TEXT_COLOR_CHANGED import com.agileburo.anytype.core_ui.features.page.BlockViewHolder import com.agileburo.anytype.core_ui.features.page.BlockViewHolder.Companion.FOCUS_TIMEOUT_MILLIS +import com.agileburo.anytype.core_ui.tools.ClipboardInterceptor import com.agileburo.anytype.core_ui.widgets.text.TextInputWidget.Companion.TEXT_INPUT_WIDGET_INPUT_TYPE import com.agileburo.anytype.core_utils.ext.dimen import com.agileburo.anytype.core_utils.ext.hexColorCode +import com.nhaarman.mockitokotlin2.mock import kotlinx.android.synthetic.main.item_block_bookmark_placeholder.view.* import kotlinx.android.synthetic.main.item_block_checkbox.view.* import kotlinx.android.synthetic.main.item_block_page.view.* @@ -44,6 +46,8 @@ class BlockAdapterTest { private val context: Context = ApplicationProvider.getApplicationContext() + private val clipboardInterceptor : ClipboardInterceptor = mock() + @Test fun `should return transparent hex code when int color value is zero`() { @@ -3263,7 +3267,7 @@ class BlockAdapterTest { onLongClickListener = {}, onTitleTextInputClicked = {}, onClickListener = {}, - clipboardDetector = {} + clipboardInterceptor = clipboardInterceptor ) } } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt b/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt index 6d1043681a..63f241c266 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/mapper/MapperExtension.kt @@ -3,10 +3,11 @@ package com.agileburo.anytype.data.auth.mapper import com.agileburo.anytype.data.auth.model.* import com.agileburo.anytype.domain.auth.model.Account import com.agileburo.anytype.domain.auth.model.Wallet -import com.agileburo.anytype.domain.block.interactor.Clipboard import com.agileburo.anytype.domain.block.model.Block import com.agileburo.anytype.domain.block.model.Command import com.agileburo.anytype.domain.block.model.Position +import com.agileburo.anytype.domain.clipboard.Copy +import com.agileburo.anytype.domain.clipboard.Paste import com.agileburo.anytype.domain.config.Config import com.agileburo.anytype.domain.event.model.Event import com.agileburo.anytype.domain.event.model.Payload @@ -49,10 +50,8 @@ fun BlockEntity.Details.toDomain(): Block.Details = Block.Details( fun BlockEntity.Content.toDomain(): Block.Content = when (this) { is BlockEntity.Content.Text -> toDomain() - is BlockEntity.Content.Dashboard -> toDomain() is BlockEntity.Content.Page -> toDomain() is BlockEntity.Content.Layout -> toDomain() - is BlockEntity.Content.Image -> toDomain() is BlockEntity.Content.Link -> toDomain() is BlockEntity.Content.Divider -> toDomain() is BlockEntity.Content.File -> toDomain() @@ -116,12 +115,6 @@ fun BlockEntity.Content.Text.toDomain(): Block.Content.Text { ) } -fun BlockEntity.Content.Dashboard.toDomain(): Block.Content.Dashboard { - return Block.Content.Dashboard( - type = Block.Content.Dashboard.Type.valueOf(type.name) - ) -} - fun BlockEntity.Content.Page.toDomain(): Block.Content.Page { return Block.Content.Page( style = Block.Content.Page.Style.valueOf(style.name) @@ -148,12 +141,6 @@ fun Block.Content.Layout.toEntity(): BlockEntity.Content.Layout { ) } -fun BlockEntity.Content.Image.toDomain(): Block.Content.Image { - return Block.Content.Image( - path = path - ) -} - fun BlockEntity.Content.Divider.toDomain() = Block.Content.Divider @@ -161,12 +148,6 @@ fun BlockEntity.Content.Smart.toDomain() = Block.Content.Smart( type = Block.Content.Smart.Type.valueOf(type.name) ) -fun Block.Content.Image.toEntity(): BlockEntity.Content.Image { - return BlockEntity.Content.Image( - path = path - ) -} - fun BlockEntity.Content.Text.Mark.toDomain(): Block.Content.Text.Mark { return Block.Content.Text.Mark( range = range, @@ -186,10 +167,8 @@ fun Block.toEntity(): BlockEntity { fun Block.Content.toEntity(): BlockEntity.Content = when (this) { is Block.Content.Text -> toEntity() - is Block.Content.Dashboard -> toEntity() is Block.Content.Page -> toEntity() is Block.Content.Layout -> toEntity() - is Block.Content.Image -> toEntity() is Block.Content.Link -> toEntity() is Block.Content.Divider -> toEntity() is Block.Content.File -> toEntity() @@ -249,12 +228,6 @@ fun Block.Content.Text.toEntity(): BlockEntity.Content.Text { ) } -fun Block.Content.Dashboard.toEntity(): BlockEntity.Content.Dashboard { - return BlockEntity.Content.Dashboard( - type = BlockEntity.Content.Dashboard.Type.valueOf(type.name) - ) -} - fun Block.Content.Page.toEntity(): BlockEntity.Content.Page { return BlockEntity.Content.Page( style = BlockEntity.Content.Page.Style.valueOf(style.name) @@ -404,6 +377,12 @@ fun Command.Paste.toEntity() = CommandEntity.Paste( range = range ) +fun Command.Copy.toEntity() = CommandEntity.Copy( + context = context, + blocks = blocks.map { it.toEntity() }, + range = range +) + fun Command.CreateDocument.toEntity() = CommandEntity.CreateDocument( context = context, target = target, @@ -573,8 +552,14 @@ fun BlockEntity.Align.toDomain(): Block.Align = when (this) { BlockEntity.Align.AlignRight -> Block.Align.AlignRight } -fun Response.Clipboard.Paste.toDomain() = Clipboard.Paste.Response( +fun Response.Clipboard.Paste.toDomain() = Paste.Response( blocks = blocks, cursor = cursor, payload = payload.toDomain() +) + +fun Response.Clipboard.Copy.toDomain() = Copy.Response( + text = plain, + html = html, + blocks = blocks.map { it.toDomain() } ) \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/mapper/Serializer.kt b/data/src/main/java/com/agileburo/anytype/data/auth/mapper/Serializer.kt new file mode 100644 index 0000000000..936ce2fb46 --- /dev/null +++ b/data/src/main/java/com/agileburo/anytype/data/auth/mapper/Serializer.kt @@ -0,0 +1,8 @@ +package com.agileburo.anytype.data.auth.mapper + +import com.agileburo.anytype.data.auth.model.BlockEntity + +interface Serializer { + fun serialize(blocks: List) : ByteArray + fun deserialize(blob: ByteArray) : List +} \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/model/BlockEntity.kt b/data/src/main/java/com/agileburo/anytype/data/auth/model/BlockEntity.kt index bd5252204a..df3b7c6178 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/model/BlockEntity.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/model/BlockEntity.kt @@ -56,18 +56,10 @@ data class BlockEntity( enum class Type { ROW, COLUMN, DIV } } - data class Image( - val path: String - ) : Content() - data class Icon( val name: String ) : Content() - data class Dashboard(val type: Type) : Content() { - enum class Type { MAIN_SCREEN, ARCHIVE } - } - data class Page(val style: Style) : Content() { enum class Style { EMPTY, TASK, SET } } diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/model/ClipEntity.kt b/data/src/main/java/com/agileburo/anytype/data/auth/model/ClipEntity.kt new file mode 100644 index 0000000000..3fa4ff1c81 --- /dev/null +++ b/data/src/main/java/com/agileburo/anytype/data/auth/model/ClipEntity.kt @@ -0,0 +1,12 @@ +package com.agileburo.anytype.data.auth.model + +import com.agileburo.anytype.domain.clipboard.Clip + +/** + * @see Clip + */ +class ClipEntity( + override val text: String, + override val html: String?, + override val uri: String? +) : Clip \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/model/CommandEntity.kt b/data/src/main/java/com/agileburo/anytype/data/auth/model/CommandEntity.kt index fe19328c70..0d82902b85 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/model/CommandEntity.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/model/CommandEntity.kt @@ -133,4 +133,10 @@ class CommandEntity { val html: String?, val blocks: List ) + + data class Copy( + val context: String, + val range: IntRange?, + val blocks: List + ) } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/model/Response.kt b/data/src/main/java/com/agileburo/anytype/data/auth/model/Response.kt index 862c5d2c50..fe6bd93f4b 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/model/Response.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/model/Response.kt @@ -2,10 +2,16 @@ package com.agileburo.anytype.data.auth.model sealed class Response { sealed class Clipboard : Response() { - data class Paste( + class Paste( val cursor: Int, val blocks: List, val payload: PayloadEntity ) : Clipboard() + + class Copy( + val plain: String, + val html: String?, + val blocks: List + ) : Clipboard() } } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardDataUriMatcher.kt b/data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardDataUriMatcher.kt new file mode 100644 index 0000000000..4bef2eb82e --- /dev/null +++ b/data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardDataUriMatcher.kt @@ -0,0 +1,12 @@ +package com.agileburo.anytype.data.auth.other + +import com.agileburo.anytype.domain.clipboard.Clipboard + +class ClipboardDataUriMatcher( + private val matcher: ClipboardUriMatcher +) : Clipboard.UriMatcher { + + override fun isAnytypeUri( + uri: String + ): Boolean = matcher.isAnytypeUri(uri) +} \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardUriMatcher.kt b/data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardUriMatcher.kt new file mode 100644 index 0000000000..3ec9b0d3b3 --- /dev/null +++ b/data/src/main/java/com/agileburo/anytype/data/auth/other/ClipboardUriMatcher.kt @@ -0,0 +1,5 @@ +package com.agileburo.anytype.data.auth.other + +interface ClipboardUriMatcher { + fun isAnytypeUri(uri: String) : Boolean +} \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt index 283156541c..af3f6d555a 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataRepository.kt @@ -2,9 +2,10 @@ package com.agileburo.anytype.data.auth.repo.block import com.agileburo.anytype.data.auth.mapper.toDomain import com.agileburo.anytype.data.auth.mapper.toEntity -import com.agileburo.anytype.domain.block.interactor.Clipboard import com.agileburo.anytype.domain.block.model.Command import com.agileburo.anytype.domain.block.repo.BlockRepository +import com.agileburo.anytype.domain.clipboard.Copy +import com.agileburo.anytype.domain.clipboard.Paste import com.agileburo.anytype.domain.common.Id import com.agileburo.anytype.domain.event.model.Payload @@ -126,5 +127,9 @@ class BlockDataRepository( override suspend fun paste( command: Command.Paste - ): Clipboard.Paste.Response = factory.remote.paste(command.toEntity()).toDomain() + ): Paste.Response = factory.remote.paste(command.toEntity()).toDomain() + + override suspend fun copy( + command: Command.Copy + ): Copy.Response = factory.remote.copy(command.toEntity()).toDomain() } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt index 346dce84ac..16548c10d2 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockDataStore.kt @@ -40,4 +40,5 @@ interface BlockDataStore { suspend fun redo(command: CommandEntity.Redo) : PayloadEntity suspend fun archiveDocument(command: CommandEntity.ArchiveDocument) suspend fun paste(command: CommandEntity.Paste) : Response.Clipboard.Paste + suspend fun copy(command: CommandEntity.Copy) : Response.Clipboard.Copy } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt index 86ccd46c1e..7559b65d13 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemote.kt @@ -40,4 +40,5 @@ interface BlockRemote { suspend fun redo(command: CommandEntity.Redo) : PayloadEntity suspend fun archiveDocument(command: CommandEntity.ArchiveDocument) suspend fun paste(command: CommandEntity.Paste) : Response.Clipboard.Paste + suspend fun copy(command: CommandEntity.Copy) : Response.Clipboard.Copy } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt index 2eeb899f4d..cfd9b87c2b 100644 --- a/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/block/BlockRemoteDataStore.kt @@ -109,4 +109,8 @@ class BlockRemoteDataStore(private val remote: BlockRemote) : BlockDataStore { override suspend fun paste( command: CommandEntity.Paste ): Response.Clipboard.Paste = remote.paste(command) + + override suspend fun copy( + command: CommandEntity.Copy + ): Response.Clipboard.Copy = remote.copy(command) } \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataRepository.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataRepository.kt new file mode 100644 index 0000000000..fa6c9f1816 --- /dev/null +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataRepository.kt @@ -0,0 +1,28 @@ +package com.agileburo.anytype.data.auth.repo.clipboard + +import com.agileburo.anytype.data.auth.mapper.toDomain +import com.agileburo.anytype.data.auth.mapper.toEntity +import com.agileburo.anytype.domain.block.model.Block +import com.agileburo.anytype.domain.clipboard.Clip +import com.agileburo.anytype.domain.clipboard.Clipboard + +class ClipboardDataRepository( + private val factory: ClipboardDataStore.Factory +) : Clipboard { + + override suspend fun put(text: String, html: String?, blocks: List) { + factory.storage.persist( + blocks = blocks.map { it.toEntity() } + ) + factory.system.put( + text = text, + html = html + ) + } + + override suspend fun blocks(): List { + return factory.storage.fetch().map { it.toDomain() } + } + + override suspend fun clip(): Clip? = factory.system.clip() +} \ No newline at end of file diff --git a/data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataStore.kt b/data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataStore.kt new file mode 100644 index 0000000000..052cc8ab96 --- /dev/null +++ b/data/src/main/java/com/agileburo/anytype/data/auth/repo/clipboard/ClipboardDataStore.kt @@ -0,0 +1,35 @@ +package com.agileburo.anytype.data.auth.repo.clipboard + +import com.agileburo.anytype.data.auth.model.BlockEntity +import com.agileburo.anytype.data.auth.model.ClipEntity + +interface ClipboardDataStore { + /** + * Stores last copied Anytype blocks. + * @see ClipEntity + */ + interface Storage { + fun persist(blocks: List) + fun fetch() : List + } + + /** + * Provides access to system clipboard. + */ + interface System { + /** + * Stores copied [text] and optionally a [html] representation on the systen clipboard. + */ + suspend fun put(text: String, html: String?) + + /** + * @return current clip on the clipboard. + */ + suspend fun clip() : ClipEntity? + } + + class Factory( + val storage: Storage, + val system: System + ) +} \ No newline at end of file diff --git a/device/build.gradle b/device/build.gradle index d5ef4fa03b..36f4280e8d 100644 --- a/device/build.gradle +++ b/device/build.gradle @@ -53,4 +53,6 @@ dependencies { testImplementation unitTestDependencies.junit testImplementation unitTestDependencies.kotlinTest + testImplementation unitTestDependencies.androidXTestCore + testImplementation unitTestDependencies.robolectric } \ No newline at end of file diff --git a/device/src/main/java/com/agileburo/anytype/device/base/AndroidDevice.kt b/device/src/main/java/com/agileburo/anytype/device/base/AndroidDevice.kt index 0876df418d..2afdeb8f6f 100644 --- a/device/src/main/java/com/agileburo/anytype/device/base/AndroidDevice.kt +++ b/device/src/main/java/com/agileburo/anytype/device/base/AndroidDevice.kt @@ -1,10 +1,11 @@ package com.agileburo.anytype.device.base import com.agileburo.anytype.data.auth.other.Device -import com.agileburo.anytype.device.download.DeviceDownloader - -class AndroidDevice(private val downloader: DeviceDownloader) : Device { +import com.agileburo.anytype.device.download.AndroidDeviceDownloader +class AndroidDevice( + private val downloader: AndroidDeviceDownloader +) : Device { override fun download(url: String, name: String) { downloader.download(url = url, name = name) } diff --git a/device/src/main/java/com/agileburo/anytype/device/download/DeviceDownloader.kt b/device/src/main/java/com/agileburo/anytype/device/download/AndroidDeviceDownloader.kt similarity index 94% rename from device/src/main/java/com/agileburo/anytype/device/download/DeviceDownloader.kt rename to device/src/main/java/com/agileburo/anytype/device/download/AndroidDeviceDownloader.kt index 8167172348..eb2dd1bf1d 100644 --- a/device/src/main/java/com/agileburo/anytype/device/download/DeviceDownloader.kt +++ b/device/src/main/java/com/agileburo/anytype/device/download/AndroidDeviceDownloader.kt @@ -8,7 +8,7 @@ import android.net.Uri import android.os.Environment.DIRECTORY_DOWNLOADS import timber.log.Timber -class DeviceDownloader(private val context: Context) { +class AndroidDeviceDownloader(private val context: Context) { private val manager by lazy { context.getSystemService(DOWNLOAD_SERVICE) as DownloadManager diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/interactor/Clipboard.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/interactor/Clipboard.kt deleted file mode 100644 index 2fafc54997..0000000000 --- a/domain/src/main/java/com/agileburo/anytype/domain/block/interactor/Clipboard.kt +++ /dev/null @@ -1,65 +0,0 @@ -package com.agileburo.anytype.domain.block.interactor - -import com.agileburo.anytype.domain.base.BaseUseCase -import com.agileburo.anytype.domain.block.model.Block -import com.agileburo.anytype.domain.block.model.Command -import com.agileburo.anytype.domain.block.repo.BlockRepository -import com.agileburo.anytype.domain.common.Id -import com.agileburo.anytype.domain.event.model.Payload - -interface Clipboard { - - /** - * Use-case for pasting to Anytype clipboard. - */ - class Paste( - private val repo: BlockRepository - ) : BaseUseCase(), Clipboard { - - override suspend fun run(params: Params) = safe { - repo.paste( - command = Command.Paste( - context = params.context, - focus = params.focus, - selected = params.selected, - range = params.range, - text = params.text, - html = params.html, - blocks = params.blocks - ) - ) - } - - /** - * Params for pasting to Anytype clipboard - * @property context id of the context - * @property focus id of the focused/target block - * @property selected id of currently selected blocks - * @property range selected text range - * @property text plain text to paste - * @property html optional html to paste - * @property blocks blocks currently contained in clipboard - */ - data class Params( - val context: Id, - val focus: Id, - val selected: List, - val range: IntRange, - val text: String, - val html: String?, - val blocks: List - ) - - /** - * Response for [Clipboard.Paste] use-case. - * @param cursor caret position - * @param blocks ids of the new blocks - * @param payload response payload - */ - data class Response( - val cursor: Int, - val blocks: List, - val payload: Payload - ) - } -} \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/model/Block.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/model/Block.kt index 2f4f0d2090..8c6827d600 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/block/model/Block.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/block/model/Block.kt @@ -53,9 +53,6 @@ data class Block( fun asText() = this as Text fun asLink() = this as Link - fun asDashboard() = this as Dashboard - fun asDivider() = this as Divider - fun asFile() = this as File /** * Smart block. @@ -135,14 +132,6 @@ data class Block( enum class Type { ROW, COLUMN, DIV } } - data class Image( - val path: String - ) : Content() - - data class Dashboard(val type: Type) : Content() { - enum class Type { MAIN_SCREEN, ARCHIVE } - } - data class Page(val style: Style) : Content() { enum class Style { EMPTY, TASK, SET } } diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/model/Command.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/model/Command.kt index c98263cefd..8bf7d41ae2 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/block/model/Command.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/block/model/Command.kt @@ -231,7 +231,7 @@ sealed class Command { data class Redo(val context: Id) /** - * Params for clipboard pasting operation + * Command for clipboard paste operation * @property context id of the context * @property focus id of the focused/target block * @property selected id of currently selected blocks @@ -249,4 +249,16 @@ sealed class Command { val html: String?, val blocks: List ) + + /** + * Command for clipboard copy operation. + * @param context id of the context + * @param range selected text range + * @param blocks associated blocks + */ + data class Copy( + val context: Id, + val range: IntRange?, + val blocks: List + ) } \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt b/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt index fc47da51e0..2bf7209363 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/block/repo/BlockRepository.kt @@ -1,12 +1,14 @@ package com.agileburo.anytype.domain.block.repo -import com.agileburo.anytype.domain.block.interactor.Clipboard import com.agileburo.anytype.domain.block.model.Command +import com.agileburo.anytype.domain.clipboard.Copy +import com.agileburo.anytype.domain.clipboard.Paste import com.agileburo.anytype.domain.common.Id import com.agileburo.anytype.domain.config.Config import com.agileburo.anytype.domain.event.model.Payload interface BlockRepository { + suspend fun dnd(command: Command.Dnd) suspend fun unlink(command: Command.Unlink): Payload @@ -79,5 +81,6 @@ interface BlockRepository { suspend fun undo(command: Command.Undo) : Payload suspend fun redo(command: Command.Redo) : Payload - suspend fun paste(command: Command.Paste) : Clipboard.Paste.Response + suspend fun copy(command: Command.Copy) : Copy.Response + suspend fun paste(command: Command.Paste) : Paste.Response } \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clip.kt b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clip.kt new file mode 100644 index 0000000000..0bac9e5aa5 --- /dev/null +++ b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clip.kt @@ -0,0 +1,13 @@ +package com.agileburo.anytype.domain.clipboard + +/** + * A clip on the clipboard. + * @property text plain text + * @property html html representation + * @property uri uri for the copied content (Anytype URI or an external app URI) + */ +interface Clip { + val text: String + val html: String? + val uri: String? +} \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clipboard.kt b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clipboard.kt new file mode 100644 index 0000000000..525df952d8 --- /dev/null +++ b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Clipboard.kt @@ -0,0 +1,29 @@ +package com.agileburo.anytype.domain.clipboard + +import com.agileburo.anytype.domain.block.model.Block + +interface Clipboard { + /** + * @param text plain text to put on the clipboard + * @param html optional html to put on the clipboard + * @param blocks Anytype blocks to store on the clipboard) + */ + suspend fun put(text: String, html: String?, blocks: List) + + /** + * @return Anytype blocks currently stored on (or linked to) the clipboard + */ + suspend fun blocks() : List + + /** + * @return return current clip on the clipboard + */ + suspend fun clip() : Clip? + + interface UriMatcher { + /** + * Checks whether this [uri] is internal Anytype clipboard URI. + */ + fun isAnytypeUri(uri: String) : Boolean + } +} \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Copy.kt b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Copy.kt new file mode 100644 index 0000000000..48141006e1 --- /dev/null +++ b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Copy.kt @@ -0,0 +1,51 @@ +package com.agileburo.anytype.domain.clipboard + +import com.agileburo.anytype.domain.base.BaseUseCase +import com.agileburo.anytype.domain.block.model.Block +import com.agileburo.anytype.domain.block.model.Command +import com.agileburo.anytype.domain.block.repo.BlockRepository +import com.agileburo.anytype.domain.common.Id + +class Copy( + private val repo: BlockRepository, + private val clipboard: Clipboard +) : BaseUseCase() { + + override suspend fun run(params: Params) = safe { + val result = repo.copy( + command = Command.Copy( + context = params.context, + range = params.range, + blocks = params.blocks + ) + ) + clipboard.put( + text = result.text, + html = result.html, + blocks = result.blocks + ) + } + + /** + * Params for clipboard paste operation. + * @param context id of the context + * @param range selected text range + * @param blocks associated blocks + */ + data class Params( + val context: Id, + val range: IntRange?, + val blocks: List + ) + + /** + * @param text plain text + * @param html optional html + * @param blocks anytype clipboard slot + */ + class Response( + val text: String, + val html: String?, + val blocks: List + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Paste.kt b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Paste.kt new file mode 100644 index 0000000000..2755548064 --- /dev/null +++ b/domain/src/main/java/com/agileburo/anytype/domain/clipboard/Paste.kt @@ -0,0 +1,69 @@ +package com.agileburo.anytype.domain.clipboard + +import com.agileburo.anytype.domain.base.BaseUseCase +import com.agileburo.anytype.domain.block.model.Command +import com.agileburo.anytype.domain.block.repo.BlockRepository +import com.agileburo.anytype.domain.common.Id +import com.agileburo.anytype.domain.event.model.Payload + +/** + * Use-case for pasting to Anytype clipboard. + */ +class Paste( + private val repo: BlockRepository, + private val clipboard: Clipboard, + private val matcher: Clipboard.UriMatcher +) : BaseUseCase() { + + override suspend fun run(params: Params) = safe { + val clip = clipboard.clip() + + if (clip != null) { + + val uri = clip.uri + + val blocks = if (uri != null && matcher.isAnytypeUri(uri)) + clipboard.blocks() + else + emptyList() + + repo.paste( + command = Command.Paste( + context = params.context, + focus = params.focus, + selected = emptyList(), + range = params.range, + text = clip.text, + html = clip.html, + blocks = blocks + ) + ) + } else { + throw IllegalStateException("Empty clip!") + } + } + + /** + * Params for pasting to Anytype clipboard + * @property context id of the context + * @property focus id of the focused/target block + * @property range selected text range + */ + data class Params( + val context: Id, + val focus: Id, + val range: IntRange + ) + + /** + * Response for the use-case. + * @param cursor caret position + * @param blocks ids of the new blocks + * @param payload response payload + */ + data class Response( + val cursor: Int, + val blocks: List, + val payload: Payload + ) +} \ No newline at end of file diff --git a/domain/src/main/java/com/agileburo/anytype/domain/ext/BlockExt.kt b/domain/src/main/java/com/agileburo/anytype/domain/ext/BlockExt.kt index 6f4ab8a540..3369d3aa7c 100644 --- a/domain/src/main/java/com/agileburo/anytype/domain/ext/BlockExt.kt +++ b/domain/src/main/java/com/agileburo/anytype/domain/ext/BlockExt.kt @@ -25,7 +25,6 @@ fun Map>.asRender(anchor: String): List { children.forEach { child -> when (child.content) { is Content.Text, - is Content.Image, is Content.Link, is Content.Divider, is Content.Bookmark, diff --git a/domain/src/test/java/com/agileburo/anytype/domain/ext/BlockExtensionTest.kt b/domain/src/test/java/com/agileburo/anytype/domain/ext/BlockExtensionTest.kt index c78d47273a..1afd5c3810 100644 --- a/domain/src/test/java/com/agileburo/anytype/domain/ext/BlockExtensionTest.kt +++ b/domain/src/test/java/com/agileburo/anytype/domain/ext/BlockExtensionTest.kt @@ -683,12 +683,11 @@ class BlockExtensionTest { @Test(expected = ClassCastException::class) fun `should throw exception when block is not text`() { + val block = Block( id = MockDataFactory.randomUuid(), fields = Block.Fields.empty(), - content = Block.Content.Dashboard( - type = Block.Content.Dashboard.Type.MAIN_SCREEN - ), + content = Block.Content.Divider, children = emptyList() ) val range = IntRange(10, 13) diff --git a/middleware/src/androidTest/java/com/agileburo/anytype/ExampleInstrumentedTest.java b/middleware/src/androidTest/java/com/agileburo/anytype/ExampleInstrumentedTest.java deleted file mode 100644 index 56c473c36a..0000000000 --- a/middleware/src/androidTest/java/com/agileburo/anytype/ExampleInstrumentedTest.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.agileburo.anytype; - -import android.content.Context; -import androidx.test.InstrumentationRegistry; -import androidx.test.runner.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.*; - -/** - * Instrumented test, which will execute on an Android device. - * - * @see Testing documentation - */ -@RunWith(AndroidJUnit4.class) -public class ExampleInstrumentedTest { - @Test - public void useAppContext() { - // Context of the app under test. - Context appContext = InstrumentationRegistry.getTargetContext(); - - assertEquals("com.agileburo.anytype.test", appContext.getPackageName()); - } -} diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/auth/AuthMiddleware.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/auth/AuthMiddleware.kt index 4076d1ec32..92c4ced6c8 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/auth/AuthMiddleware.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/auth/AuthMiddleware.kt @@ -6,8 +6,8 @@ import com.agileburo.anytype.data.auth.model.AccountEntity import com.agileburo.anytype.data.auth.model.WalletEntity import com.agileburo.anytype.data.auth.repo.AuthRemote import com.agileburo.anytype.middleware.EventProxy +import com.agileburo.anytype.middleware.converters.toAccountEntity import com.agileburo.anytype.middleware.interactor.Middleware -import com.agileburo.anytype.middleware.toAccountEntity import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.filter diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt index 7449639783..b307c40973 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/block/BlockMiddleware.kt @@ -5,8 +5,8 @@ import com.agileburo.anytype.data.auth.model.ConfigEntity import com.agileburo.anytype.data.auth.model.PayloadEntity import com.agileburo.anytype.data.auth.model.Response import com.agileburo.anytype.data.auth.repo.block.BlockRemote +import com.agileburo.anytype.middleware.converters.mark import com.agileburo.anytype.middleware.interactor.Middleware -import com.agileburo.anytype.middleware.toMiddleware class BlockMiddleware( private val middleware: Middleware @@ -41,7 +41,7 @@ class BlockMiddleware( command.contextId, command.blockId, command.text, - command.marks.map { it.toMiddleware() } + command.marks.map { it.mark() } ) } @@ -133,4 +133,8 @@ class BlockMiddleware( override suspend fun paste( command: CommandEntity.Paste ): Response.Clipboard.Paste = middleware.paste(command) + + override suspend fun copy( + command: CommandEntity.Copy + ): Response.Clipboard.Copy = middleware.copy(command) } \ No newline at end of file diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/converters/ClipboardSerializer.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/converters/ClipboardSerializer.kt new file mode 100644 index 0000000000..a50fe80cd6 --- /dev/null +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/converters/ClipboardSerializer.kt @@ -0,0 +1,18 @@ +package com.agileburo.anytype.middleware.converters + +import anytype.clipboard.ClipboardOuterClass.Clipboard +import com.agileburo.anytype.data.auth.mapper.Serializer +import com.agileburo.anytype.data.auth.model.BlockEntity + +class ClipboardSerializer : Serializer { + + override fun serialize(blocks: List): ByteArray { + val models = blocks.map { it.block() } + val clipboard = Clipboard.newBuilder().addAllBlocks(models).build() + return clipboard.toByteArray() + } + + override fun deserialize(blob: ByteArray): List { + return Clipboard.parseFrom(blob).blocksList.blocks() + } +} \ No newline at end of file diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/converters/MapperExtension.kt similarity index 85% rename from middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt rename to middleware/src/main/java/com/agileburo/anytype/middleware/converters/MapperExtension.kt index 7e2f73220c..4d988d9f92 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/MapperExtension.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/converters/MapperExtension.kt @@ -1,7 +1,6 @@ -package com.agileburo.anytype.middleware +package com.agileburo.anytype.middleware.converters import anytype.Events -import anytype.model.Models import anytype.model.Models.Account import anytype.model.Models.Block import com.agileburo.anytype.data.auth.model.AccountEntity @@ -11,7 +10,6 @@ import com.google.protobuf.Struct import com.google.protobuf.Value import timber.log.Timber - fun Events.Event.Account.Show.toAccountEntity(): AccountEntity { return AccountEntity( id = account.id, @@ -22,69 +20,6 @@ fun Events.Event.Account.Show.toAccountEntity(): AccountEntity { ) } -fun BlockEntity.Content.Text.Mark.toMiddleware(): Block.Content.Text.Mark { - val rangeModel = Models.Range.newBuilder() - .setFrom(range.first) - .setTo(range.last) - .build() - - return when (type) { - BlockEntity.Content.Text.Mark.Type.BOLD -> { - Block.Content.Text.Mark - .newBuilder() - .setType(Block.Content.Text.Mark.Type.Bold) - .setRange(rangeModel) - .build() - } - BlockEntity.Content.Text.Mark.Type.ITALIC -> { - Block.Content.Text.Mark - .newBuilder() - .setType(Block.Content.Text.Mark.Type.Italic) - .setRange(rangeModel) - .build() - } - BlockEntity.Content.Text.Mark.Type.STRIKETHROUGH -> { - Block.Content.Text.Mark - .newBuilder() - .setType(Block.Content.Text.Mark.Type.Strikethrough) - .setRange(rangeModel) - .build() - } - BlockEntity.Content.Text.Mark.Type.TEXT_COLOR -> { - Block.Content.Text.Mark - .newBuilder() - .setType(Block.Content.Text.Mark.Type.TextColor) - .setRange(rangeModel) - .setParam(param) - .build() - } - BlockEntity.Content.Text.Mark.Type.LINK -> { - Block.Content.Text.Mark - .newBuilder() - .setType(Block.Content.Text.Mark.Type.Link) - .setRange(rangeModel) - .setParam(param) - .build() - } - BlockEntity.Content.Text.Mark.Type.BACKGROUND_COLOR -> { - Block.Content.Text.Mark - .newBuilder() - .setType(Block.Content.Text.Mark.Type.BackgroundColor) - .setRange(rangeModel) - .setParam(param) - .build() - } - BlockEntity.Content.Text.Mark.Type.KEYBOARD -> { - Block.Content.Text.Mark - .newBuilder() - .setType(Block.Content.Text.Mark.Type.Keyboard) - .setRange(rangeModel) - .build() - } - else -> throw IllegalStateException("Unsupported mark type: ${type.name}") - } -} - fun Block.fields(): BlockEntity.Fields = BlockEntity.Fields().also { result -> fields.fieldsMap.forEach { (key, value) -> result.map[key] = when (val case = value.kindCase) { diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/converters/ToMiddleware.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/converters/ToMiddleware.kt new file mode 100644 index 0000000000..3cdcba31b7 --- /dev/null +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/converters/ToMiddleware.kt @@ -0,0 +1,234 @@ +package com.agileburo.anytype.middleware.converters + +import anytype.model.Models.Block +import anytype.model.Models.Range +import com.agileburo.anytype.data.auth.model.BlockEntity +import com.google.protobuf.Struct +import com.google.protobuf.Value + +typealias Mark = Block.Content.Text.Mark +typealias File = Block.Content.File +typealias FileState = Block.Content.File.State +typealias FileType = Block.Content.File.Type +typealias Link = Block.Content.Link +typealias LinkType = Block.Content.Link.Style +typealias Bookmark = Block.Content.Bookmark +typealias Marks = Block.Content.Text.Marks +typealias Text = Block.Content.Text +typealias Layout = Block.Content.Layout +typealias LayoutStyle = Block.Content.Layout.Style +typealias Divider = Block.Content.Div +typealias DividerStyle = Block.Content.Div.Style + +//region block mapping + +fun BlockEntity.block(): Block { + + val builder = Block.newBuilder() + + builder.id = id + + when (val content = content) { + is BlockEntity.Content.Text -> { + builder.text = content.text() + } + is BlockEntity.Content.Bookmark -> { + builder.bookmark = content.bookmark() + } + is BlockEntity.Content.File -> { + builder.file = content.file() + } + is BlockEntity.Content.Link -> { + builder.link = content.link() + } + is BlockEntity.Content.Layout -> { + builder.layout = content.layout() + } + is BlockEntity.Content.Divider -> { + builder.div = content.divider() + } + } + + return builder.build() +} + +//endregion + +//region text block mapping + +fun BlockEntity.Content.Text.text(): Text { + return Text + .newBuilder() + .setText(text) + .setMarks(marks()) + .setStyle(style.toMiddleware()) + .build() +} + +fun BlockEntity.Content.Text.marks(): Marks { + return Marks + .newBuilder() + .addAllMarks(marks.map { it.mark() }) + .build() +} + +fun BlockEntity.Content.Text.Mark.mark(): Mark = when (type) { + BlockEntity.Content.Text.Mark.Type.BOLD -> { + Mark.newBuilder() + .setType(Block.Content.Text.Mark.Type.Bold) + .setRange(range.range()) + .build() + } + BlockEntity.Content.Text.Mark.Type.ITALIC -> { + Mark.newBuilder() + .setType(Block.Content.Text.Mark.Type.Italic) + .setRange(range.range()) + .build() + } + BlockEntity.Content.Text.Mark.Type.STRIKETHROUGH -> { + Mark.newBuilder() + .setType(Block.Content.Text.Mark.Type.Strikethrough) + .setRange(range.range()) + .build() + } + BlockEntity.Content.Text.Mark.Type.TEXT_COLOR -> { + Mark.newBuilder() + .setType(Block.Content.Text.Mark.Type.TextColor) + .setRange(range.range()) + .setParam(param) + .build() + } + BlockEntity.Content.Text.Mark.Type.LINK -> { + Mark.newBuilder() + .setType(Block.Content.Text.Mark.Type.Link) + .setRange(range.range()) + .setParam(param) + .build() + } + BlockEntity.Content.Text.Mark.Type.BACKGROUND_COLOR -> { + Mark.newBuilder() + .setType(Block.Content.Text.Mark.Type.BackgroundColor) + .setRange(range.range()) + .setParam(param) + .build() + } + BlockEntity.Content.Text.Mark.Type.KEYBOARD -> { + Mark.newBuilder() + .setType(Block.Content.Text.Mark.Type.Keyboard) + .setRange(range.range()) + .build() + } + else -> throw IllegalStateException("Unsupported mark type: ${type.name}") +} + +//endregion + +//region bookmark block mapping + +fun BlockEntity.Content.Bookmark.bookmark(): Bookmark { + val builder = Bookmark.newBuilder() + description?.let { builder.setDescription(it) } + favicon?.let { builder.setFaviconHash(it) } + title?.let { builder.setTitle(it) } + url?.let { builder.setUrl(it) } + image?.let { builder.setImageHash(it) } + return builder.build() +} + +//endregion + +//region file block mapping + +fun BlockEntity.Content.File.file(): File { + val builder = File.newBuilder() + hash?.let { builder.setHash(it) } + name?.let { builder.setName(it) } + mime?.let { builder.setMime(it) } + size?.let { builder.setSize(it) } + state?.let { builder.setState(it.state()) } + type?.let { builder.setType(it.type()) } + return builder.build() +} + +fun BlockEntity.Content.File.State.state(): FileState = when (this) { + BlockEntity.Content.File.State.EMPTY -> FileState.Empty + BlockEntity.Content.File.State.UPLOADING -> FileState.Uploading + BlockEntity.Content.File.State.DONE -> FileState.Done + BlockEntity.Content.File.State.ERROR -> FileState.Error +} + +fun BlockEntity.Content.File.Type.type(): FileType = when (this) { + BlockEntity.Content.File.Type.NONE -> FileType.None + BlockEntity.Content.File.Type.FILE -> FileType.File + BlockEntity.Content.File.Type.IMAGE -> FileType.Image + BlockEntity.Content.File.Type.VIDEO -> FileType.Video +} + +//endregion + +//region link mapping + +fun BlockEntity.Content.Link.link(): Link { + return Link.newBuilder() + .setTargetBlockId(target) + .setStyle(type.type()) + .setFields(fields.fields()) + .build() +} + +fun BlockEntity.Content.Link.Type.type() : LinkType = when(this) { + BlockEntity.Content.Link.Type.ARCHIVE -> LinkType.Archive + BlockEntity.Content.Link.Type.DASHBOARD -> LinkType.Dashboard + BlockEntity.Content.Link.Type.DATA_VIEW -> LinkType.Dataview + BlockEntity.Content.Link.Type.PAGE -> LinkType.Page +} + +//endregion + +//region layout mapping + +fun BlockEntity.Content.Layout.layout() : Layout { + val builder = Layout.newBuilder() + when(type) { + BlockEntity.Content.Layout.Type.ROW -> builder.style = LayoutStyle.Row + BlockEntity.Content.Layout.Type.COLUMN -> builder.style = LayoutStyle.Column + BlockEntity.Content.Layout.Type.DIV -> builder.style = LayoutStyle.Div + } + return builder.build() +} + +//endregion + +//region divider mapping + +fun BlockEntity.Content.Divider.divider() : Divider { + return Divider.newBuilder().setStyle(DividerStyle.Line).build() +} + +//endregion + +//region other mapping + +fun BlockEntity.Fields.fields() : Struct { + val builder = Struct.newBuilder() + map.forEach { (key, value) -> + if (key != null && value != null) + when(value) { + is String -> { + builder.putFields(key, Value.newBuilder().setStringValue(value).build()) + } + is Boolean -> { + builder.putFields(key, Value.newBuilder().setBoolValue(value).build()) + } + is Double-> { + builder.putFields(key, Value.newBuilder().setNumberValue(value).build()) + } + else -> throw IllegalStateException("Unexpected value type: ${value::class.java}") + } + } + return builder.build() +} + +fun IntRange.range(): Range = Range.newBuilder().setFrom(first).setTo(last).build() + +//endregion \ No newline at end of file diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java index b3fffe8ba4..daabbd5a11 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/Middleware.java @@ -782,6 +782,8 @@ public class Middleware { html = command.getHtml(); } + List blocks = mapper.toMiddleware(command.getBlocks()); + Block.Paste.Request request = Block.Paste.Request .newBuilder() .setContextId(command.getContext()) @@ -789,6 +791,7 @@ public class Middleware { .setTextSlot(command.getText()) .setHtmlSlot(html) .setSelectedTextRange(range) + .addAllAnySlot(blocks) .addAllSelectedBlockIds(command.getSelected()) .build(); @@ -808,4 +811,47 @@ public class Middleware { mapper.toPayload(response.getEvent()) ); } + + public Response.Clipboard.Copy copy(CommandEntity.Copy command) throws Exception { + + Range range; + + if (command.getRange() != null) { + range = Range.newBuilder() + .setFrom(command.getRange().getFirst()) + .setTo(command.getRange().getLast()) + .build(); + } else { + range = Range.getDefaultInstance(); + } + + List blocks = mapper.toMiddleware(command.getBlocks()); + + Block.Copy.Request.Builder builder = Block.Copy.Request.newBuilder(); + + if (range != null) { + builder.setSelectedTextRange(range); + } + + Block.Copy.Request request = builder + .setContextId(command.getContext()) + .addAllBlocks(blocks) + .build(); + + if (BuildConfig.DEBUG) { + Timber.d(request.getClass().getName() + "\n" + request.toString()); + } + + Block.Copy.Response response = service.blockCopy(request); + + if (BuildConfig.DEBUG) { + Timber.d(response.getClass().getName() + "\n" + response.toString()); + } + + return new Response.Clipboard.Copy( + response.getTextSlot(), + response.getHtmlSlot(), + mapper.toEntity(response.getAnySlotList()) + ); + } } diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventMapper.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventMapper.kt index 2312ec0499..0dca78947b 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventMapper.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareEventMapper.kt @@ -3,10 +3,10 @@ package com.agileburo.anytype.middleware.interactor import anytype.Events.Event import com.agileburo.anytype.data.auth.model.BlockEntity import com.agileburo.anytype.data.auth.model.EventEntity -import com.agileburo.anytype.middleware.blocks -import com.agileburo.anytype.middleware.entity -import com.agileburo.anytype.middleware.fields -import com.agileburo.anytype.middleware.marks +import com.agileburo.anytype.middleware.converters.blocks +import com.agileburo.anytype.middleware.converters.entity +import com.agileburo.anytype.middleware.converters.fields +import com.agileburo.anytype.middleware.converters.marks fun Event.Message.toEntity( context: String diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareFactory.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareFactory.kt index 46062d7ef9..685d0ef123 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareFactory.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareFactory.kt @@ -2,7 +2,9 @@ package com.agileburo.anytype.middleware.interactor import anytype.model.Models.Block import com.agileburo.anytype.data.auth.model.BlockEntity -import com.agileburo.anytype.middleware.toMiddleware +import com.agileburo.anytype.middleware.converters.state +import com.agileburo.anytype.middleware.converters.toMiddleware +import com.agileburo.anytype.middleware.converters.type class MiddlewareFactory { @@ -29,8 +31,8 @@ class MiddlewareFactory { } is BlockEntity.Prototype.File -> { val file = Block.Content.File.newBuilder().apply { - state = prototype.state.toMiddleware() - type = prototype.type.toMiddleware() + state = prototype.state.state() + type = prototype.type.type() } builder.setFile(file).build() } diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareMapper.kt b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareMapper.kt index 088788e1fa..e8461dd822 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareMapper.kt +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/interactor/MiddlewareMapper.kt @@ -5,10 +5,16 @@ import anytype.model.Models.Block import com.agileburo.anytype.data.auth.model.BlockEntity import com.agileburo.anytype.data.auth.model.PayloadEntity import com.agileburo.anytype.data.auth.model.PositionEntity -import com.agileburo.anytype.middleware.toMiddleware +import com.agileburo.anytype.middleware.converters.block +import com.agileburo.anytype.middleware.converters.blocks +import com.agileburo.anytype.middleware.converters.toMiddleware class MiddlewareMapper { + fun toMiddleware(blocks: List) : List { + return blocks.map { it.block() } + } + fun toMiddleware(style: BlockEntity.Content.Text.Style): Block.Content.Text.Style { return style.toMiddleware() } @@ -30,4 +36,8 @@ class MiddlewareMapper { fun toMiddleware(alignment: BlockEntity.Align): Block.Align { return alignment.toMiddleware() } + + fun toEntity(blocks: List) : List { + return blocks.blocks() + } } \ No newline at end of file diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/service/DefaultMiddlewareService.java b/middleware/src/main/java/com/agileburo/anytype/middleware/service/DefaultMiddlewareService.java index 47edb9a33f..20ded0845e 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/service/DefaultMiddlewareService.java +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/service/DefaultMiddlewareService.java @@ -327,4 +327,15 @@ public class DefaultMiddlewareService implements MiddlewareService { return response; } } + + @Override + public Block.Copy.Response blockCopy(Block.Copy.Request request) throws Exception { + byte[] encoded = Lib.blockCopy(request.toByteArray()); + Block.Copy.Response response = Block.Copy.Response.parseFrom(encoded); + if (response.getError() != null && response.getError().getCode() != Block.Copy.Response.Error.Code.NULL) { + throw new Exception(response.getError().getDescription()); + } else { + return response; + } + } } diff --git a/middleware/src/main/java/com/agileburo/anytype/middleware/service/MiddlewareService.java b/middleware/src/main/java/com/agileburo/anytype/middleware/service/MiddlewareService.java index 7532c71806..6a85b0f4d9 100644 --- a/middleware/src/main/java/com/agileburo/anytype/middleware/service/MiddlewareService.java +++ b/middleware/src/main/java/com/agileburo/anytype/middleware/service/MiddlewareService.java @@ -67,4 +67,6 @@ public interface MiddlewareService { Block.Set.Details.Response blockSetDetails(Block.Set.Details.Request request) throws Exception; Block.Paste.Response blockPaste(Block.Paste.Request request) throws Exception; + + Block.Copy.Response blockCopy(Block.Copy.Request request) throws Exception; } diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt index 18d6b172a1..85c91345ef 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/PageViewModel.kt @@ -1117,6 +1117,20 @@ class PageViewModel( } } + fun onMultiSelectCopyClicked() { + viewModelScope.launch { + orchestrator.proxies.intents.send( + Intent.Clipboard.Copy( + context = context, + blocks = blocks.filter { block -> + currentSelection().contains(block.id) + }, + range = null + ) + ) + } + } + fun onMultiSelectModeSelectAllClicked() { (stateData.value as ViewState.Success).let { state -> val update = state.blocks.map { block -> @@ -1436,8 +1450,6 @@ class PageViewModel( } fun onPaste( - plain: String, - html: String?, range: IntRange ) { viewModelScope.launch { @@ -1446,10 +1458,21 @@ class PageViewModel( context = context, focus = orchestrator.stores.focus.current(), range = range, - blocks = emptyList(), - selected = emptyList(), - html = html, - text = plain + selected = emptyList() + ) + ) + } + } + + fun onCopy( + range: IntRange + ) { + viewModelScope.launch { + orchestrator.proxies.intents.send( + Intent.Clipboard.Copy( + context = context, + range = range, + blocks = listOf(blocks.first { it.id == focus.value }) ) ) } diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Intent.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Intent.kt index 381b99b0fb..d0301d3cff 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Intent.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Intent.kt @@ -56,9 +56,11 @@ sealed class Intent { val context: Id, val focus: Id, val selected: List, - val range: IntRange, - val text: String, - val html: String?, + val range: IntRange + ) : Clipboard() + class Copy( + val context: Id, + val range: IntRange?, val blocks: List ) : Clipboard() } diff --git a/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Orchestrator.kt b/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Orchestrator.kt index 8ccb628751..024d57d41c 100644 --- a/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Orchestrator.kt +++ b/presentation/src/main/java/com/agileburo/anytype/presentation/page/editor/Orchestrator.kt @@ -1,6 +1,8 @@ package com.agileburo.anytype.presentation.page.editor import com.agileburo.anytype.domain.block.interactor.* +import com.agileburo.anytype.domain.clipboard.Copy +import com.agileburo.anytype.domain.clipboard.Paste import com.agileburo.anytype.domain.common.Id import com.agileburo.anytype.domain.download.DownloadFile import com.agileburo.anytype.domain.event.model.Payload @@ -28,7 +30,8 @@ class Orchestrator( private val updateText: UpdateText, private val updateAlignment: UpdateAlignment, private val setupBookmark: SetupBookmark, - private val paste: Clipboard.Paste, + private val copy: Copy, + private val paste: Paste, private val undo: Undo, private val redo: Redo, val memory: Editor.Memory, @@ -263,14 +266,10 @@ class Orchestrator( } is Intent.Clipboard.Paste -> { paste( - params = Clipboard.Paste.Params( + params = Paste.Params( context = intent.context, focus = intent.focus, - range = intent.range, - blocks = emptyList(), - html = intent.html, - text = intent.text, - selected = intent.selected + range = intent.range ) ).proceed( failure = defaultOnError, @@ -280,6 +279,20 @@ class Orchestrator( } ) } + is Intent.Clipboard.Copy -> { + copy( + params = Copy.Params( + context = intent.context, + blocks = intent.blocks, + range = intent.range + ) + ).proceed( + success = { + Timber.d("Copy sucessful") + }, + failure = defaultOnError + ) + } } } } diff --git a/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt b/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt index 8aa9b0fe16..b338fcd0af 100644 --- a/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt +++ b/presentation/src/test/java/com/agileburo/anytype/presentation/page/PageViewModelTest.kt @@ -11,6 +11,8 @@ import com.agileburo.anytype.domain.base.Either import com.agileburo.anytype.domain.block.interactor.* import com.agileburo.anytype.domain.block.model.Block import com.agileburo.anytype.domain.block.model.Position +import com.agileburo.anytype.domain.clipboard.Copy +import com.agileburo.anytype.domain.clipboard.Paste import com.agileburo.anytype.domain.common.Id import com.agileburo.anytype.domain.config.Config import com.agileburo.anytype.domain.download.DownloadFile @@ -113,7 +115,10 @@ class PageViewModelTest { lateinit var uploadUrl: UploadUrl @Mock - lateinit var paste: Clipboard.Paste + lateinit var paste: Paste + + @Mock + lateinit var copy: Copy @Mock lateinit var undo: Undo @@ -4266,7 +4271,8 @@ class PageViewModelTest { ), updateAlignment = updateAlignment, setupBookmark = setupBookmark, - paste = paste + paste = paste, + copy = copy ) ) } diff --git a/protobuf/src/main/proto/clipboard.proto b/protobuf/src/main/proto/clipboard.proto new file mode 100644 index 0000000000..9f0f73cda3 --- /dev/null +++ b/protobuf/src/main/proto/clipboard.proto @@ -0,0 +1,8 @@ +syntax = "proto3"; +package anytype.clipboard; + +import "models.proto"; + +message Clipboard { + repeated anytype.model.Block blocks = 1; +} \ No newline at end of file diff --git a/sample/build.gradle b/sample/build.gradle index 8979feaac8..abd3931b16 100644 --- a/sample/build.gradle +++ b/sample/build.gradle @@ -42,6 +42,10 @@ dependencies { def applicationDependencies = rootProject.ext.mainApplication + def protobufDependencies = rootProject.ext.protobuf + + implementation protobufDependencies.protobufJava + implementation 'com.github.HBiSoft:PickiT:0.1.9' implementation 'com.vdurmont:emoji-java:5.1.1' @@ -49,6 +53,8 @@ dependencies { implementation project(':core-utils') implementation project(':core-ui') implementation project(':library-page-icon-picker-widget') + implementation project(':middleware') + implementation project(':protobuf') implementation applicationDependencies.timber diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml index a0be0706e1..cddebdbdf7 100644 --- a/sample/src/main/AndroidManifest.xml +++ b/sample/src/main/AndroidManifest.xml @@ -18,13 +18,17 @@ android:supportsRtl="true" android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning"> - - + + + + \ No newline at end of file diff --git a/sample/src/main/java/com/agileburo/anytype/sample/ClipboardActivity.kt b/sample/src/main/java/com/agileburo/anytype/sample/ClipboardActivity.kt new file mode 100644 index 0000000000..4fac563072 --- /dev/null +++ b/sample/src/main/java/com/agileburo/anytype/sample/ClipboardActivity.kt @@ -0,0 +1,85 @@ +package com.agileburo.anytype.sample + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.net.Uri +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import anytype.clipboard.ClipboardOuterClass.Clipboard +import anytype.model.Models.Block +import com.agileburo.anytype.core_ui.common.ThemeColor +import kotlinx.android.synthetic.main.activity_clipboard.* + +class ClipboardActivity : AppCompatActivity() { + + private val cm: ClipboardManager + get() = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_clipboard) + setup() + } + + private fun setup() { + write.setOnClickListener { write() } + read.setOnClickListener { read() } + copy.setOnClickListener { copy() } + paste.setOnClickListener { paste() } + } + + private fun write() { + + val text = Block.Content.Text + .newBuilder() + .setText("Everything was in confusion") + .setColor(ThemeColor.ICE.title) + .setStyle(Block.Content.Text.Style.Checkbox) + .build() + + val blocks = listOf( + Block.newBuilder() + .setId("1") + .setText(text) + .build(), + Block.newBuilder() + .setId("2") + .setText(text) + .build() + ) + + val clipboard = Clipboard.newBuilder().addAllBlocks(blocks).build() + + val stream = openFileOutput(DEFAULT_FILE_NAME, Context.MODE_PRIVATE) + + clipboard.writeTo(stream) + + stream.flush() + + stream.close() + } + + private fun read() { + val stream = openFileInput(DEFAULT_FILE_NAME) + val board = Clipboard.parseFrom(stream) + output.text = board.toString() + } + + private fun copy() { + output.clearComposingText() + val uri = Uri.parse(BASE_URI) + val clip = ClipData.newUri(contentResolver, "URI", uri) + cm.setPrimaryClip(clip) + } + + private fun paste() { + output.text = cm.primaryClip.toString() + } + + companion object { + private const val DEFAULT_FILE_NAME = "test" + private const val AUTHORITY = "com.agileburo.anytype.sample" + private const val BASE_URI = "content://$AUTHORITY" + } +} diff --git a/sample/src/main/java/com/agileburo/anytype/sample/SampleApp.kt b/sample/src/main/java/com/agileburo/anytype/sample/SampleApp.kt index f144897117..1e241d67b4 100644 --- a/sample/src/main/java/com/agileburo/anytype/sample/SampleApp.kt +++ b/sample/src/main/java/com/agileburo/anytype/sample/SampleApp.kt @@ -18,4 +18,8 @@ class SampleApp : Application() { else Timber.plant(CrashlyticsTree()) } + + companion object { + const val BASE_URI = "content://com.agileburo.anytype" + } } \ No newline at end of file diff --git a/sample/src/main/java/com/agileburo/anytype/sample/helpers/MockDataFactory.kt b/sample/src/main/java/com/agileburo/anytype/sample/helpers/MockDataFactory.kt new file mode 100644 index 0000000000..b49939aa71 --- /dev/null +++ b/sample/src/main/java/com/agileburo/anytype/sample/helpers/MockDataFactory.kt @@ -0,0 +1,64 @@ +package com.agileburo.anytype.sample.helpers + +import java.util.* +import java.util.concurrent.ThreadLocalRandom + +object MockDataFactory { + + fun randomUuid(): String { + return UUID.randomUUID().toString() + } + + fun randomString(): String { + return randomUuid() + } + + + fun randomInt(): Int { + return ThreadLocalRandom.current().nextInt(0, 1000 + 1) + } + + fun randomInt(max: Int): Int { + return ThreadLocalRandom.current().nextInt(0, max) + } + + fun randomLong(): Long { + return randomInt().toLong() + } + + fun randomFloat(): Float { + return randomInt().toFloat() + } + + fun randomDouble(): Double { + return randomInt().toDouble() + } + + fun randomBoolean(): Boolean { + return Math.random() < 0.5 + } + + fun makeIntList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomInt()) + } + return items + } + + fun makeStringList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomUuid()) + } + return items + } + + fun makeDoubleList(count: Int): List { + val items = mutableListOf() + repeat(count) { + items.add(randomDouble()) + } + return items + } +} \ No newline at end of file diff --git a/sample/src/main/res/layout/activity_clipboard.xml b/sample/src/main/res/layout/activity_clipboard.xml new file mode 100644 index 0000000000..684bc1c26f --- /dev/null +++ b/sample/src/main/res/layout/activity_clipboard.xml @@ -0,0 +1,69 @@ + + +