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

DROID-2642 Sharing extension | Support Videos and Files (#1367)

This commit is contained in:
Konstantin Ivanov 2024-07-08 16:57:28 +02:00 committed by GitHub
parent 6480c22c92
commit 366d994fa5
Signed by: github
GPG key ID: B5690EEEBB952194
16 changed files with 217 additions and 30 deletions

View file

@ -84,6 +84,21 @@
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" />
</intent-filter>
</activity>

View file

@ -153,6 +153,12 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr
SHARE_DIALOG_LABEL
)
}
is Command.Sharing.Videos -> {
SharingFragment.videos(command.uris).show(
supportFragmentManager,
SHARE_DIALOG_LABEL
)
}
is Command.Sharing.Files -> {
SharingFragment.files(command.uris).show(
supportFragmentManager,
@ -342,6 +348,9 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr
intent.type?.startsWith(SHARE_IMAGE_INTENT_PATTERN) == true -> {
proceedWithImageShareIntent(intent)
}
intent.type?.startsWith(SHARE_VIDEO_INTENT_PATTERN) == true -> {
proceedWithVideoShareIntent(intent)
}
intent.type?.startsWith(SHARE_FILE_INTENT_PATTERN) == true -> {
proceedWithFileShareIntent(intent)
}
@ -378,6 +387,19 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr
}
}
private fun proceedWithVideoShareIntent(intent: Intent) {
if (intent.action == Intent.ACTION_SEND_MULTIPLE) {
vm.onIntentMultipleVideoShare(uris = intent.parseActionSendMultipleUris())
} else {
val uri = intent.parseActionSendUri()
if (uri != null) {
vm.onIntentMultipleVideoShare(listOf(uri))
} else {
toast("Could not parse URI")
}
}
}
private fun proceedWithNotificationIntent(intent: Intent) {
when(val type = intent.getIntExtra(NOTIFICATION_TYPE, -1)) {
AnytypeNotificationService.REQUEST_TO_JOIN_TYPE -> {
@ -560,6 +582,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main), AppNavigation.Pr
const val AUTO_UPDATE_URL = "https://fra1.digitaloceanspaces.com/anytype-release/latest-android.json"
const val SHARE_DIALOG_LABEL = "anytype.dialog.share.label"
const val SHARE_IMAGE_INTENT_PATTERN = "image/"
const val SHARE_VIDEO_INTENT_PATTERN = "video/"
const val SHARE_FILE_INTENT_PATTERN = "application/"
}
}

View file

@ -85,7 +85,6 @@ fun AddToAnytypeScreenUrlPreview() {
onOpenClicked = {},
content = "https://en.wikipedia.org/wiki/Walter_Benjamin",
progressState = AddToAnytypeViewModel.ProgressState.Done(""),
onCancelProcessClicked = {}
//progressState = AddToAnytypeViewModel.ProgressState.Error(" I understand that contributing to this repository will require me to agree with the CLA I understand that contributing to this repository will require me to agree with the CLA\n")
//progressState = AddToAnytypeViewModel.ProgressState.Progress(processId = "dasda", progress = 0.8f)
)
@ -113,7 +112,6 @@ fun AddToAnytypeScreenNotePreview() {
wrapperObjId = ""
),
onOpenClicked = {},
onCancelProcessClicked = {}
)
}
@ -124,7 +122,6 @@ fun AddToAnytypeScreen(
data: SharingData,
progressState: AddToAnytypeViewModel.ProgressState,
onCancelClicked: () -> Unit,
onCancelProcessClicked: (Id) -> Unit,
onAddClicked: (SaveAsOption) -> Unit,
onSelectSpaceClicked: (SpaceView) -> Unit,
onOpenClicked: (Id) -> Unit
@ -137,6 +134,7 @@ fun AddToAnytypeScreen(
is SharingData.Images -> listOf(SAVE_AS_IMAGES)
is SharingData.Files -> listOf(SAVE_AS_FILES)
is SharingData.Text -> listOf(SAVE_AS_NOTE)
is SharingData.Videos -> listOf(SAVE_AS_VIDEOS)
}
var selectedIndex by remember {
mutableStateOf(
@ -147,6 +145,7 @@ fun AddToAnytypeScreen(
is SharingData.Images -> SAVE_AS_IMAGES
is SharingData.Files -> SAVE_AS_FILES
is SharingData.Text -> SAVE_AS_NOTE
is SharingData.Videos -> SAVE_AS_VIDEOS
}
)
}
@ -181,6 +180,7 @@ fun AddToAnytypeScreen(
SAVE_AS_FILE -> stringResource(id = R.string.sharing_menu_save_as_file_option)
SAVE_AS_IMAGES -> stringResource(id = R.string.sharing_menu_save_as_images_option)
SAVE_AS_FILES -> stringResource(id = R.string.sharing_menu_save_as_files_option)
SAVE_AS_VIDEOS -> stringResource(id = R.string.sharing_menu_save_as_videos_option)
else -> stringResource(id = R.string.sharing_menu_save_as_note_option)
},
modifier = Modifier
@ -281,10 +281,7 @@ fun AddToAnytypeScreen(
)
}
is AddToAnytypeViewModel.ProgressState.Progress -> {
ButtonsProgress(
onCancelProcessClicked = onCancelProcessClicked,
progressState = progressState,
)
ButtonsProgress(onCancelClicked = onCancelClicked)
}
}
}
@ -438,8 +435,7 @@ private fun ButtonsDone(
@Composable
private fun ButtonsProgress(
onCancelProcessClicked: (Id) -> Unit,
progressState: AddToAnytypeViewModel.ProgressState.Progress
onCancelClicked: () -> Unit,
) {
Row(
modifier = Modifier
@ -449,7 +445,7 @@ private fun ButtonsProgress(
verticalAlignment = Alignment.CenterVertically
) {
ButtonSecondary(
onClick = { onCancelProcessClicked(progressState.processId) },
onClick = onCancelClicked,
size = ButtonSize.Large,
text = stringResource(id = R.string.cancel),
modifier = Modifier.weight(1.0f)
@ -646,6 +642,7 @@ const val SAVE_AS_IMAGE = 2
const val SAVE_AS_FILE = 3
const val SAVE_AS_IMAGES = 4
const val SAVE_AS_FILES = 5
const val SAVE_AS_VIDEOS = 6
typealias SaveAsOption = Int
sealed class SharingData {
@ -680,6 +677,11 @@ sealed class SharingData {
override val data: String
get() = uri
}
data class Videos(val uris: List<String>) : SharingData() {
override val data: String
get() = uris.toString()
}
}
const val DROPDOWN_MENU_VISIBILITY_WINDOW_INTERVAL = 150L

View file

@ -53,7 +53,11 @@ class SharingFragment : BaseBottomSheetComposeFragment() {
} else if (args.containsKey(SHARING_MULTIPLE_FILES_KEY)) {
val result = argStringList(SHARING_MULTIPLE_FILES_KEY)
SharingData.Files(uris = result)
} else {
} else if (args.containsKey(SHARING_MULTIPLE_VIDEOS_KEY)) {
val result = argStringList(SHARING_MULTIPLE_VIDEOS_KEY)
SharingData.Videos(uris = result)
}
else {
throw IllegalStateException("Unexpcted shared data")
}
}
@ -85,17 +89,17 @@ class SharingFragment : BaseBottomSheetComposeFragment() {
when(option) {
SAVE_AS_BOOKMARK -> vm.onCreateBookmark(url = sharedData.data)
SAVE_AS_NOTE -> vm.onCreateNote(sharedData.data)
SAVE_AS_FILE -> vm.onShareMedia(listOf(sharedData.data))
SAVE_AS_IMAGES, SAVE_AS_IMAGE -> {
SAVE_AS_FILE -> {
val formattedDateTime = getFormattedDateTime(Locale.getDefault())
val objTitle =
getString(R.string.sharing_media_wrapper_object_title, formattedDateTime)
val data = sharedData
if (data is SharingData.Images) {
vm.onShareMedia(uris = data.uris, wrapperObjTitle = objTitle)
} else {
toast("Unexpected data format")
}
getString(
R.string.sharing_files_wrapper_object_title,
formattedDateTime
)
vm.onShareFiles(
uris = listOf(sharedData.data),
wrapperObjTitle = objTitle
)
}
SAVE_AS_FILES -> {
val formattedDateTime = getFormattedDateTime(Locale.getDefault())
@ -103,11 +107,45 @@ class SharingFragment : BaseBottomSheetComposeFragment() {
getString(R.string.sharing_files_wrapper_object_title, formattedDateTime)
val data = sharedData
if (data is SharingData.Files) {
vm.onShareMedia(uris = data.uris, wrapperObjTitle = objTitle)
vm.onShareFiles(uris = data.uris, wrapperObjTitle = objTitle)
} else {
toast("Unexpected data format")
}
}
SAVE_AS_IMAGES, SAVE_AS_IMAGE, SAVE_AS_VIDEOS -> {
val formattedDateTime = getFormattedDateTime(Locale.getDefault())
val objTitle =
getString(
R.string.sharing_media_wrapper_object_title,
formattedDateTime
)
when (val data = sharedData) {
is SharingData.Image -> {
vm.onShareFiles(
uris = listOf(data.uri),
wrapperObjTitle = objTitle
)
}
is SharingData.Images -> {
vm.onShareFiles(
uris = data.uris,
wrapperObjTitle = objTitle
)
}
is SharingData.Videos -> {
vm.onShareFiles(
uris = data.uris,
wrapperObjTitle = objTitle
)
}
else -> {
toast("Unexpected data format")
}
}
}
}
},
onCancelClicked = {
@ -119,7 +157,6 @@ class SharingFragment : BaseBottomSheetComposeFragment() {
onSelectSpaceClicked = { vm.onSelectSpaceClicked(it) },
progressState = vm.progressState.collectAsStateWithLifecycle().value,
onOpenClicked = vm::proceedWithNavigation,
onCancelProcessClicked = { processId -> }
)
LaunchedEffect(Unit) {
vm.navigation.collect { nav ->
@ -175,6 +212,9 @@ class SharingFragment : BaseBottomSheetComposeFragment() {
is SharingData.Url -> {
vm.onSharedTextData(data.url)
}
is SharingData.Videos -> {
vm.onSharedMediaData(data.uris)
}
}
}
@ -204,6 +244,7 @@ class SharingFragment : BaseBottomSheetComposeFragment() {
private const val SHARING_IMAGE_KEY = "arg.sharing.image-key"
private const val SHARING_FILE_KEY = "arg.sharing.file-key"
private const val SHARING_MULTIPLE_IMAGES_KEY = "arg.sharing.multiple-images-key"
private const val SHARING_MULTIPLE_VIDEOS_KEY = "arg.sharing.multiple-videos-key"
private const val SHARING_MULTIPLE_FILES_KEY = "arg.sharing.multiple-files-key"
fun text(data: String) : SharingFragment = SharingFragment().apply {
@ -218,6 +259,10 @@ class SharingFragment : BaseBottomSheetComposeFragment() {
arguments = bundleOf(SHARING_MULTIPLE_IMAGES_KEY to ArrayList(uris))
}
fun videos(uris: List<String>) : SharingFragment = SharingFragment().apply {
arguments = bundleOf(SHARING_MULTIPLE_VIDEOS_KEY to ArrayList(uris))
}
fun files(uris: List<String>) : SharingFragment = SharingFragment().apply {
arguments = bundleOf(SHARING_MULTIPLE_FILES_KEY to ArrayList(uris))
}

View file

@ -541,4 +541,8 @@ sealed class Command {
}
}
}
data class ProcessCancel(
val processId: Id
) : Command()
}

View file

@ -1027,4 +1027,8 @@ class BlockDataRepository(
override suspend fun membershipGetTiers(command: Command.Membership.GetTiers): List<MembershipTierData> {
return remote.membershipGetTiers(command)
}
override suspend fun processCancel(command: Command.ProcessCancel) {
remote.processCancel(command)
}
}

View file

@ -434,4 +434,6 @@ interface BlockRemote {
suspend fun membershipGetVerificationEmail(command: Command.Membership.GetVerificationEmail)
suspend fun membershipVerifyEmailCode(command: Command.Membership.VerifyEmailCode)
suspend fun membershipGetTiers(command: Command.Membership.GetTiers): List<MembershipTierData>
suspend fun processCancel(command: Command.ProcessCancel)
}

View file

@ -475,4 +475,6 @@ interface BlockRepository {
suspend fun membershipGetVerificationEmail(command: Command.Membership.GetVerificationEmail)
suspend fun membershipVerifyEmailCode(command: Command.Membership.VerifyEmailCode)
suspend fun membershipGetTiers(command: Command.Membership.GetTiers): List<MembershipTierData>
suspend fun processCancel(command: Command.ProcessCancel)
}

View file

@ -0,0 +1,24 @@
package com.anytypeio.anytype.domain.download
import com.anytypeio.anytype.core_models.Command
import com.anytypeio.anytype.domain.base.AppCoroutineDispatchers
import com.anytypeio.anytype.domain.base.ResultInteractor
import com.anytypeio.anytype.domain.block.repo.BlockRepository
import javax.inject.Inject
class ProcessCancel @Inject constructor(
private val repository: BlockRepository,
dispatchers: AppCoroutineDispatchers
) : ResultInteractor<ProcessCancel.Params, Unit>(dispatchers.io) {
override suspend fun doWork(params: Params) {
val command = Command.ProcessCancel(
processId = params.processId
)
return repository.processCancel(command)
}
data class Params(
val processId: String
)
}

View file

@ -1210,6 +1210,7 @@
<string name="sharing_menu_save_as_file_option">File</string>
<string name="sharing_menu_save_as_images_option">Images</string>
<string name="sharing_menu_save_as_files_option">Files</string>
<string name="sharing_menu_save_as_videos_option">Videos</string>
<string name="sharing_menu_data">Data</string>
<string name="sharing_menu_add_to_anytype_header_title">Add to Anytype</string>
<string name="sharing_menu_add_to_anytype_error">Anytype file upload error: %1$s</string>

View file

@ -990,4 +990,8 @@ class BlockMiddleware(
override suspend fun membershipGetTiers(command: Command.Membership.GetTiers): List<MembershipTierData> {
return middleware.membershipGetTiers(command)
}
override suspend fun processCancel(command: Command.ProcessCancel) {
middleware.processCancel(command)
}
}

View file

@ -2691,6 +2691,16 @@ class Middleware @Inject constructor(
return response.tiers.map { it.toCoreModel() }
}
@Throws
fun processCancel(command: Command.ProcessCancel) {
val request = Rpc.Process.Cancel.Request(
id = command.processId
)
if (BuildConfig.DEBUG) logRequest(request)
val response = service.processCancel(request)
if (BuildConfig.DEBUG) logResponse(response)
}
private fun logRequest(any: Any) {
logger.logRequest(any).also {
if (BuildConfig.DEBUG && threadInfo.isOnMainThread()) {

View file

@ -203,6 +203,9 @@ interface MiddlewareService {
@Throws(Exception::class)
fun fileDrop(request: Rpc.File.Drop.Request): Rpc.File.Drop.Response
@Throws(Exception::class)
fun processCancel(request: Rpc.Process.Cancel.Request): Rpc.Process.Cancel.Response
//endregion
//region UNSPLASH commands

View file

@ -1706,6 +1706,19 @@ class MiddlewareServiceImplementation @Inject constructor(
}
}
override fun processCancel(request: Rpc.Process.Cancel.Request): Rpc.Process.Cancel.Response {
val encoded = Service.processCancel(
Rpc.Process.Cancel.Request.ADAPTER.encode(request)
)
val response = Rpc.Process.Cancel.Response.ADAPTER.decode(encoded)
val error = response.error
if (error != null && error.code != Rpc.Process.Cancel.Response.Error.Code.NULL) {
throw Exception(error.description)
} else {
return response
}
}
override fun setInternalFlags(request: Rpc.Object.SetInternalFlags.Request): Rpc.Object.SetInternalFlags.Response {
val encoded = Service.objectSetInternalFlags(
Rpc.Object.SetInternalFlags.Request.ADAPTER.encode(request)

View file

@ -292,6 +292,20 @@ class MainViewModel(
}
}
fun onIntentMultipleVideoShare(uris: List<String>) {
Timber.d("onIntentVideoShare: $uris")
viewModelScope.launch {
checkAuthorizationStatus(Unit).process(
failure = { e -> Timber.e(e, "Error while checking auth status") },
success = { status ->
if (status == AuthStatus.AUTHORIZED) {
commands.emit(Command.Sharing.Videos(uris))
}
}
)
}
}
fun onInterceptNotificationAction(action: NotificationAction) {
viewModelScope.launch {
proceedWithNotificationAction(action)
@ -362,6 +376,7 @@ class MainViewModel(
data class Text(val data: String) : Sharing()
data class Image(val uri: String): Sharing()
data class Images(val uris: List<String>): Sharing()
data class Videos(val uris: List<String>): Sharing()
data class File(val uri: String): Sharing()
data class Files(val uris: List<String>): Sharing()
}

View file

@ -16,17 +16,17 @@ import com.anytypeio.anytype.core_models.MarketplaceObjectTypeIds
import com.anytypeio.anytype.core_models.NO_VALUE
import com.anytypeio.anytype.core_models.ObjectOrigin
import com.anytypeio.anytype.core_models.ObjectWrapper
import com.anytypeio.anytype.core_models.Process.Event
import com.anytypeio.anytype.core_models.Process.State
import com.anytypeio.anytype.core_models.Relations
import com.anytypeio.anytype.core_models.ext.EMPTY_STRING_VALUE
import com.anytypeio.anytype.core_models.primitives.SpaceId
import com.anytypeio.anytype.core_models.Process.Event
import com.anytypeio.anytype.core_models.Process.State
import com.anytypeio.anytype.core_utils.ext.msg
import com.anytypeio.anytype.domain.account.AwaitAccountStartManager
import com.anytypeio.anytype.domain.base.fold
import com.anytypeio.anytype.domain.device.FileSharer
import com.anytypeio.anytype.domain.download.ProcessCancel
import com.anytypeio.anytype.domain.media.FileDrop
import com.anytypeio.anytype.domain.media.UploadFile
import com.anytypeio.anytype.domain.misc.UrlBuilder
import com.anytypeio.anytype.domain.multiplayer.Permissions
import com.anytypeio.anytype.domain.objects.CreateBookmarkObject
@ -48,10 +48,13 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.onSubscription
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import timber.log.Timber
@ -145,12 +148,29 @@ class AddToAnytypeViewModel(
}
}
private fun subscribeToEventProcessChannel(wrapperObjId: Id) {
private fun subscribeToEventProcessChannel(
wrapperObjId: Id,
filePaths: List<String>,
targetSpaceId: String
) {
if (progressJob?.isActive == true) {
Timber.d("Progress job is already active")
progressJob?.cancel()
}
progressJob = viewModelScope.launch {
eventProcessChannel.observe()
.shareIn(
viewModelScope,
replay = 0,
started = SharingStarted.WhileSubscribed()
)
.onSubscription {
proceedWithFilesDrop(
wrapperObjId = wrapperObjId,
filePaths = filePaths,
targetSpaceId = targetSpaceId
)
}
.collect { events ->
events.forEach { event ->
when (event) {
@ -208,7 +228,7 @@ class AddToAnytypeViewModel(
}
}
fun onShareMedia(uris: List<String>, wrapperObjTitle: String? = null) {
fun onShareFiles(uris: List<String>, wrapperObjTitle: String? = null) {
viewModelScope.launch(Dispatchers.IO) {
val targetSpaceView = spaceViews.value.firstOrNull { view ->
view.isSelected
@ -255,7 +275,7 @@ class AddToAnytypeViewModel(
startTime = startTime,
spaceParams = provideParams(spaceManager.get())
)
proceedWithFilesDrop(
subscribeToEventProcessChannel(
wrapperObjId = wrapperObjId,
filePaths = filePaths,
targetSpaceId = targetSpaceId
@ -273,7 +293,6 @@ class AddToAnytypeViewModel(
filePaths: List<String>,
targetSpaceId: String,
) {
subscribeToEventProcessChannel(wrapperObjId = wrapperObjId)
val params = FileDrop.Params(
ctx = wrapperObjId,
space = SpaceId(targetSpaceId),
@ -466,7 +485,8 @@ class AddToAnytypeViewModel(
private val permissions: Permissions,
private val analyticSpaceHelperDelegate: AnalyticSpaceHelperDelegate,
private val fileDrop: FileDrop,
private val eventProcessChannel: EventProcessDropFilesChannel
private val eventProcessChannel: EventProcessDropFilesChannel,
private val processCancel: ProcessCancel
) : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {