From 248e1c4fe9c6dddac747a3054910761455e381c1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 16 Apr 2026 14:56:04 +0200 Subject: [PATCH 01/44] feat(unified-share): ui Signed-off-by: alperozturk96 # Conflicts: # app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java --- .../ui/fragment/share/UnifiedShareView.kt | 459 ++++++++++++++++++ .../layout/file_details_sharing_fragment.xml | 7 + 2 files changed, 466 insertions(+) create mode 100644 app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt new file mode 100644 index 000000000000..12c2993e62f3 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -0,0 +1,459 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.owncloud.android.ui.fragment.share + +import android.content.Context +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.FilledTonalButton +import androidx.compose.material3.FilledTonalIconButton +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.unit.dp +import com.owncloud.android.utils.theme.ViewThemeUtils + +enum class UnifiedShareCategory { + Invited, Anyone +} + +enum class UnifiedShareType { + InternalUser, InternalGroup, InternalLink, ExternalLink, ExternalFederated, ExternalMail; + + fun icon(): String { + return when (this) { + InternalUser -> "👤" + InternalGroup -> "👥" + InternalLink -> "🔗" + ExternalLink -> "🌍" + ExternalFederated -> "☁️" + ExternalMail -> "📧" + } + } +} + +data class UnifiedShareDownloadLimit( + val limit: Int, + val downloadCount: Int +) + +sealed class UnifiedSharePermission { + // file drop only for folder + data object FileDrop : UnifiedSharePermission() + + data object CanView : UnifiedSharePermission() + data object CanEdit : UnifiedSharePermission() + + // create only for folder + data class Custom(val read: Boolean, val edit: Boolean, val delete: Boolean, val create: Boolean) : + UnifiedSharePermission() + + fun getText(): String { + return when(this) { + FileDrop -> "FileDrop" + CanView -> "CanView" + CanEdit -> "CanEdit" + is Custom -> "Custom permissions" + } + } +} + +data class UnifiedShares( + val id: Int, + val password: String, + val note: String, + val limit: UnifiedShareDownloadLimit, + val expirationDate: Int, + val permission: UnifiedSharePermission, + val label: String, + val sharedTo: String, + val type: UnifiedShareType, + val category: UnifiedShareCategory, +) + +// TODO: MOVE TO THE ANDROID: COMMON +// TODO: MAKE LAZY COLUMN +// TODO: EXPOSE ACTIONS, IMPLEMENT VIEWMODEL, REPOSITORY TO FETCH ACTUAL SHARE, INJECT NECESSARY PARAMETERS + +@Composable +fun UnifiedShareView() { + var showAddShare by remember { mutableStateOf(false) } + + val mockUnifiedShares = listOf( + UnifiedShares( + id = 1, + password = "", + note = "Design review – please check latest changes", + limit = UnifiedShareDownloadLimit( + limit = 100, + downloadCount = 12 + ), + expirationDate = 0, + permission = UnifiedSharePermission.CanView, + label = "Alice Johnson", + sharedTo = "alice@company.com", + type = UnifiedShareType.InternalUser, + category = UnifiedShareCategory.Invited + ), + + UnifiedShares( + id = 2, + password = "", + note = "", + limit = UnifiedShareDownloadLimit( + limit = 0, + downloadCount = 0 + ), + expirationDate = 0, + permission = UnifiedSharePermission.CanEdit, + label = "Marketing Team", + sharedTo = "marketing", + type = UnifiedShareType.InternalGroup, + category = UnifiedShareCategory.Invited + ), + + UnifiedShares( + id = 3, + password = "1234", + note = "Public link for client review", + limit = UnifiedShareDownloadLimit( + limit = 50, + downloadCount = 5 + ), + expirationDate = 1710000000, + permission = UnifiedSharePermission.Custom( + read = true, + edit = false, + delete = false, + create = false + ), + label = "Public Link", + sharedTo = "https://nextcloud.com/s/abc123", + type = UnifiedShareType.InternalLink, + category = UnifiedShareCategory.Anyone + ), + + UnifiedShares( + id = 4, + password = "", + note = "External partner access", + limit = UnifiedShareDownloadLimit( + limit = 20, + downloadCount = 2 + ), + expirationDate = 0, + permission = UnifiedSharePermission.CanView, + label = "John External", + sharedTo = "john@external.com", + type = UnifiedShareType.ExternalMail, + category = UnifiedShareCategory.Anyone + ), + + UnifiedShares( + id = 5, + password = "", + note = "Federated sharing with partner instance", + limit = UnifiedShareDownloadLimit( + limit = 0, + downloadCount = 0 + ), + expirationDate = 0, + permission = UnifiedSharePermission.FileDrop, + label = "Partner Cloud", + sharedTo = "partner@nextcloud.org", + type = UnifiedShareType.ExternalFederated, + category = UnifiedShareCategory.Anyone + ) + ) + + Column( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + mockUnifiedShares.forEachIndexed { index, share -> + val type = when (index) { + 0 -> { + UnifiedSharesListItemType.Top + } + + mockUnifiedShares.lastIndex -> { + UnifiedSharesListItemType.Bottom + } + + else -> { + UnifiedSharesListItemType.Mid + } + } + + UnifiedSharesListItem(share, type) + } + + FloatingActionButton( + onClick = { showAddShare = true }, + modifier = Modifier + .align(Alignment.End) + .padding(top = 16.dp) + ) { + Icon(Icons.Default.Add, contentDescription = "Add") + } + + if (showAddShare) { + AddShareBottomSheet("Abc.txt",onDismiss = { showAddShare = false }) + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { + val sheetState = rememberModalBottomSheetState() + + var category by remember { mutableStateOf(UnifiedShareCategory.Invited) } + var permission by remember { mutableStateOf(UnifiedSharePermission.CanView) } + var categoryDropDownExpanded by remember { mutableStateOf(false) } + var permissionDropDownExpanded by remember { mutableStateOf(false) } + val availablePermissions = remember { + listOf( + UnifiedSharePermission.CanView, + UnifiedSharePermission.CanEdit, + UnifiedSharePermission.FileDrop + ) + } + var searchQuery by remember { mutableStateOf("") } + var note by remember { mutableStateOf("") } + + ModalBottomSheet( + onDismissRequest = onDismiss, + sheetState = sheetState, + containerColor = MaterialTheme.colorScheme.surface, + ) { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .padding(bottom = 48.dp) // Extra padding for bottom navigation bars + ) { + Text( + text = "Share $filename", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface + ) + + Spacer(modifier = Modifier.height(24.dp)) + + ExposedDropdownMenuBox( + expanded = categoryDropDownExpanded, + onExpandedChange = { categoryDropDownExpanded = !categoryDropDownExpanded }, + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = category.name, + onValueChange = {}, + readOnly = true, + label = { Text("Share type") }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryDropDownExpanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + modifier = Modifier + .menuAnchor() + .fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = categoryDropDownExpanded, + onDismissRequest = { categoryDropDownExpanded = false } + ) { + UnifiedShareCategory.entries.forEach { selectionOption -> + DropdownMenuItem( + text = { Text(selectionOption.name) }, + onClick = { + category = selectionOption + categoryDropDownExpanded = false + }, + contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + if (category == UnifiedShareCategory.Invited) { + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + modifier = Modifier.fillMaxWidth(), + label = { Text("Add people") }, + placeholder = { Text("Name, team, email or federated cloud ID") }, + singleLine = true, + shape = RoundedCornerShape(8.dp) + ) + + Spacer(modifier = Modifier.height(16.dp)) + + ExposedDropdownMenuBox( + expanded = permissionDropDownExpanded, + onExpandedChange = { permissionDropDownExpanded = !permissionDropDownExpanded }, + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = permission.getText(), + onValueChange = {}, + readOnly = true, + label = { Text("Participants") }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = permissionDropDownExpanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + modifier = Modifier.menuAnchor().fillMaxWidth() + ) + + ExposedDropdownMenu( + expanded = permissionDropDownExpanded, + onDismissRequest = { permissionDropDownExpanded = false } + ) { + availablePermissions.forEach { selectionOption -> + DropdownMenuItem( + text = { Text(selectionOption.getText()) }, + onClick = { + // permission = selectionOption + permissionDropDownExpanded = false + } + ) + } + } + } + + Spacer(modifier = Modifier.height(16.dp)) + + OutlinedTextField( + value = note, + onValueChange = { note = it }, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("Note to recipients") }, + singleLine = true, + shape = RoundedCornerShape(8.dp) + ) + } else { + Text( + text = "Creating a public link will allow anyone with the link to access this file.", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(vertical = 8.dp) + ) + + Button( + onClick = { /* TODO: Create Public Link Logic */ }, + modifier = Modifier.fillMaxWidth().padding(top = 8.dp) + ) { + Text("Create public link") + } + } + + Row(modifier = Modifier.fillMaxWidth()) { + FilledTonalIconButton(onClick = { + + }) { + Text("Copy link") + } + + Spacer(modifier = Modifier.width(16.dp)) + + FilledTonalIconButton(onClick = { + + }) { + Text("Send") + } + } + } + } +} +enum class UnifiedSharesListItemType { + Top, Mid, Bottom; + + @Composable + fun getShape(): RoundedCornerShape { + return when (this) { + Top -> RoundedCornerShape(12.dp, 12.dp, 4.dp, 4.dp) + Mid -> RoundedCornerShape(4.dp, 4.dp, 4.dp, 4.dp) + Bottom -> RoundedCornerShape(4.dp, 4.dp, 12.dp, 12.dp) + } + } +} + +@Composable +private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListItemType) { + ListItem( + modifier = Modifier + .fillMaxWidth() + .clip(type.getShape()) + .clickable( + onClick = { } + ) + .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), + headlineContent = { + Text( + text = share.label, + style = MaterialTheme.typography.titleSmall + ) + }, + supportingContent = { + Text( + text = share.sharedTo, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + }, + colors = ListItemDefaults.colors( + containerColor = Color.Transparent + ) + ) +} + +fun ComposeView.setupUnifiedShare(viewThemeUtils: ViewThemeUtils, context: Context) { + setContent { + MaterialTheme( + colorScheme = viewThemeUtils.getColorScheme(context), + content = { + UnifiedShareView() + } + ) + } +} diff --git a/app/src/main/res/layout/file_details_sharing_fragment.xml b/app/src/main/res/layout/file_details_sharing_fragment.xml index 14a056da8f5c..a03e8e26ba39 100644 --- a/app/src/main/res/layout/file_details_sharing_fragment.xml +++ b/app/src/main/res/layout/file_details_sharing_fragment.xml @@ -231,10 +231,17 @@ android:text="@string/show_all" /> + + + From 73772f43d63171b1790e2e7a7ea48b8119a43c33 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 16 Apr 2026 15:15:26 +0200 Subject: [PATCH 02/44] feat(unified-share): ui Signed-off-by: alperozturk96 --- .../ui/fragment/share/UnifiedShareView.kt | 422 +++++++++++++----- 1 file changed, 301 insertions(+), 121 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 12c2993e62f3..9ac4b0fc71df 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -18,7 +18,9 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material3.Button @@ -27,15 +29,14 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.FilledTonalIconButton import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable @@ -88,9 +89,9 @@ sealed class UnifiedSharePermission { fun getText(): String { return when(this) { - FileDrop -> "FileDrop" - CanView -> "CanView" - CanEdit -> "CanEdit" + FileDrop -> "File drop" + CanView -> "Can view" + CanEdit -> "Can edit" is Custom -> "Custom permissions" } } @@ -246,21 +247,27 @@ fun UnifiedShareView() { @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { - val sheetState = rememberModalBottomSheetState() + val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + val scrollState = rememberScrollState() var category by remember { mutableStateOf(UnifiedShareCategory.Invited) } - var permission by remember { mutableStateOf(UnifiedSharePermission.CanView) } - var categoryDropDownExpanded by remember { mutableStateOf(false) } - var permissionDropDownExpanded by remember { mutableStateOf(false) } + var permission by remember { mutableStateOf(UnifiedSharePermission.CanView) } + var searchQuery by remember { mutableStateOf("") } + var note by remember { mutableStateOf("") } + + var viewFiles by remember { mutableStateOf(false) } + var editFiles by remember { mutableStateOf(false) } + var createFiles by remember { mutableStateOf(false) } + var deleteFiles by remember { mutableStateOf(false) } + val availablePermissions = remember { listOf( UnifiedSharePermission.CanView, UnifiedSharePermission.CanEdit, - UnifiedSharePermission.FileDrop + UnifiedSharePermission.FileDrop, + UnifiedSharePermission.Custom(false, false, false, false) ) } - var searchQuery by remember { mutableStateOf("") } - var note by remember { mutableStateOf("") } ModalBottomSheet( onDismissRequest = onDismiss, @@ -271,140 +278,313 @@ private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { modifier = Modifier .fillMaxWidth() .padding(horizontal = 16.dp) - .padding(bottom = 48.dp) // Extra padding for bottom navigation bars + .padding(bottom = 32.dp) + .verticalScroll(scrollState), + verticalArrangement = Arrangement.spacedBy(16.dp) ) { - Text( - text = "Share $filename", - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface + ShareBottomSheetHeader(filename) + + ShareCategoryDropdown( + selectedCategory = category, + onCategoryChange = { category = it } ) - Spacer(modifier = Modifier.height(24.dp)) + if (category == UnifiedShareCategory.Invited) { + InvitedShareContent( + searchQuery = searchQuery, + onSearchChange = { searchQuery = it }, + permission = permission, + availablePermissions = availablePermissions, + onPermissionChange = { permission = it }, + ) + + InvitedInlineSettings() - ExposedDropdownMenuBox( - expanded = categoryDropDownExpanded, - onExpandedChange = { categoryDropDownExpanded = !categoryDropDownExpanded }, - modifier = Modifier.fillMaxWidth() - ) { - OutlinedTextField( - value = category.name, - onValueChange = {}, - readOnly = true, - label = { Text("Share type") }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = categoryDropDownExpanded) }, - colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), - modifier = Modifier - .menuAnchor() - .fillMaxWidth() + NoteToRecipients(note = note, onNoteChange = { note = it }) + } else { + AnyoneShareContent( + permission = permission, + availablePermissions = availablePermissions, + onPermissionChange = { permission = it }, ) - ExposedDropdownMenu( - expanded = categoryDropDownExpanded, - onDismissRequest = { categoryDropDownExpanded = false } - ) { - UnifiedShareCategory.entries.forEach { selectionOption -> - DropdownMenuItem( - text = { Text(selectionOption.name) }, - onClick = { - category = selectionOption - categoryDropDownExpanded = false - }, - contentPadding = ExposedDropdownMenuDefaults.ItemContentPadding - ) - } + if (permission is UnifiedSharePermission.Custom) { + SettingsSwitchRow("View files", viewFiles) { viewFiles = it } + SettingsSwitchRow("Edit files", editFiles) { editFiles = it } + SettingsSwitchRow("Create files", createFiles) { createFiles = it } + SettingsSwitchRow("Delete files", deleteFiles) { deleteFiles = it } } + + AnyoneInlineSettings() + + NoteToRecipients(note = note, onNoteChange = { note = it }) } - Spacer(modifier = Modifier.height(16.dp)) + ShareActionButtons( + category = category, + isSendEnabled = searchQuery.isNotBlank(), + onCopyClick = { /* TODO */ }, + onSendClick = { /* TODO */ } + ) + } + } +} - if (category == UnifiedShareCategory.Invited) { - OutlinedTextField( - value = searchQuery, - onValueChange = { searchQuery = it }, - modifier = Modifier.fillMaxWidth(), - label = { Text("Add people") }, - placeholder = { Text("Name, team, email or federated cloud ID") }, - singleLine = true, - shape = RoundedCornerShape(8.dp) - ) +@Composable +private fun ShareBottomSheetHeader(filename: String) { + Text( + text = "Share $filename", + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(bottom = 8.dp) + ) +} - Spacer(modifier = Modifier.height(16.dp)) - - ExposedDropdownMenuBox( - expanded = permissionDropDownExpanded, - onExpandedChange = { permissionDropDownExpanded = !permissionDropDownExpanded }, - modifier = Modifier.fillMaxWidth() - ) { - OutlinedTextField( - value = permission.getText(), - onValueChange = {}, - readOnly = true, - label = { Text("Participants") }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = permissionDropDownExpanded) }, - colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), - modifier = Modifier.menuAnchor().fillMaxWidth() - ) - - ExposedDropdownMenu( - expanded = permissionDropDownExpanded, - onDismissRequest = { permissionDropDownExpanded = false } - ) { - availablePermissions.forEach { selectionOption -> - DropdownMenuItem( - text = { Text(selectionOption.getText()) }, - onClick = { - // permission = selectionOption - permissionDropDownExpanded = false - } - ) - } +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun ShareCategoryDropdown( + selectedCategory: UnifiedShareCategory, + onCategoryChange: (UnifiedShareCategory) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = selectedCategory.name, + onValueChange = {}, + readOnly = true, + label = { Text("Share type") }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + modifier = Modifier + .menuAnchor() + .fillMaxWidth() + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + UnifiedShareCategory.entries.forEach { option -> + DropdownMenuItem( + text = { Text(option.name) }, + onClick = { + onCategoryChange(option) + expanded = false } - } + ) + } + } + } +} - Spacer(modifier = Modifier.height(16.dp)) +@Composable +private fun InvitedShareContent( + searchQuery: String, + onSearchChange: (String) -> Unit, + permission: UnifiedSharePermission, + availablePermissions: List, + onPermissionChange: (UnifiedSharePermission) -> Unit, + +) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + OutlinedTextField( + value = searchQuery, + onValueChange = onSearchChange, + modifier = Modifier.fillMaxWidth(), + label = { Text("Add people") }, + placeholder = { Text("Name, team, email or federated ID") }, + singleLine = true, + shape = RoundedCornerShape(8.dp) + ) - OutlinedTextField( - value = note, - onValueChange = { note = it }, - modifier = Modifier.fillMaxWidth(), - placeholder = { Text("Note to recipients") }, - singleLine = true, - shape = RoundedCornerShape(8.dp) - ) - } else { - Text( - text = "Creating a public link will allow anyone with the link to access this file.", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(vertical = 8.dp) - ) + PermissionDropdown( + label = "Participants", + selectedPermission = permission, + availablePermissions = availablePermissions, + onPermissionChange = onPermissionChange + ) + } +} - Button( - onClick = { /* TODO: Create Public Link Logic */ }, - modifier = Modifier.fillMaxWidth().padding(top = 8.dp) - ) { - Text("Create public link") - } +@Composable +private fun NoteToRecipients( + note: String, + onNoteChange: (String) -> Unit +) { + OutlinedTextField( + value = note, + onValueChange = onNoteChange, + modifier = Modifier.fillMaxWidth(), + placeholder = { Text("Note to recipients") }, + shape = RoundedCornerShape(8.dp) + ) +} + +@Composable +private fun AnyoneShareContent( + permission: UnifiedSharePermission, + availablePermissions: List, + onPermissionChange: (UnifiedSharePermission) -> Unit, +) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + PermissionDropdown( + label = "Anyone with the link", + selectedPermission = permission, + availablePermissions = availablePermissions, + onPermissionChange = onPermissionChange + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +private fun PermissionDropdown( + label: String, + selectedPermission: UnifiedSharePermission, + availablePermissions: List, + onPermissionChange: (UnifiedSharePermission) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + expanded = expanded, + onExpandedChange = { expanded = !expanded }, + modifier = Modifier.fillMaxWidth() + ) { + OutlinedTextField( + value = selectedPermission.getText(), + onValueChange = {}, + readOnly = true, + label = { Text(label) }, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), + modifier = Modifier + .menuAnchor() + .fillMaxWidth() + ) + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + availablePermissions.forEach { option -> + DropdownMenuItem( + text = { Text(option.getText()) }, + onClick = { + onPermissionChange(option) + expanded = false + } + ) } + } + } +} - Row(modifier = Modifier.fillMaxWidth()) { - FilledTonalIconButton(onClick = { - }) { - Text("Copy link") - } +@Composable +private fun InvitedInlineSettings() { + var shareWithOthers by remember { mutableStateOf(false) } + var editFile by remember { mutableStateOf(false) } + var hasExpiration by remember { mutableStateOf(false) } + var hideDownload by remember { mutableStateOf(false) } + + Column { + SettingsSwitchRow("Share with others", shareWithOthers) { shareWithOthers = it } + SettingsSwitchRow("Edit file", editFile) { editFile = it } + SettingsSwitchRow("Expiration date", hasExpiration) { hasExpiration = it } + SettingsSwitchRow("Hide download and sync options", hideDownload) { hideDownload = it } + } +} - Spacer(modifier = Modifier.width(16.dp)) +@Composable +private fun AnyoneInlineSettings() { + var hasPassword by remember { mutableStateOf(false) } + var hasExpiration by remember { mutableStateOf(false) } + var limitDownloads by remember { mutableStateOf(false) } + + var hideDownloads by remember { mutableStateOf(false) } + var videoVerification by remember { mutableStateOf(false) } + var showFilesInGridView by remember { mutableStateOf(false) } + + Column { + OutlinedTextField( + value = "", + onValueChange = {}, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 8.dp), + label = { Text("Label") }, + placeholder = { Text("Optional name for this link") }, + singleLine = true + ) - FilledTonalIconButton(onClick = { + SettingsSwitchRow("Expiration date", hasExpiration) { hasExpiration = it } + SettingsSwitchRow("Password", hasPassword) { hasPassword = it } + SettingsSwitchRow("Limit downloads", limitDownloads) { limitDownloads = it } - }) { - Text("Send") - } + SettingsSwitchRow("Hide downloads", hideDownloads) { hideDownloads = it } + SettingsSwitchRow("Video verification", videoVerification) { videoVerification = it } + SettingsSwitchRow("Show files in grid view", showFilesInGridView) { showFilesInGridView = it } + + } +} + +@Composable +private fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .height(48.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text(text = label, style = MaterialTheme.typography.bodyLarge) + Switch(checked = checked, onCheckedChange = onCheckedChange) + } +} + +// --- ACTION BUTTONS --- + +@Composable +private fun ShareActionButtons( + category: UnifiedShareCategory, + isSendEnabled: Boolean, + onCopyClick: () -> Unit, + onSendClick: () -> Unit +) { + Row(modifier = Modifier + .fillMaxWidth() + .padding(top = 16.dp)) { + if (category == UnifiedShareCategory.Invited) { + FilledTonalButton( + onClick = onCopyClick, + modifier = Modifier.weight(1f) + ) { + Text("Copy link") + } + Spacer(modifier = Modifier.width(16.dp)) + Button( + onClick = onSendClick, + modifier = Modifier.weight(1f), + enabled = isSendEnabled // Disabled if search query is empty + ) { + Text("Send") + } + } else { + // For "Anyone" (Public link), usually just one big action to create/copy + Button( + onClick = onCopyClick, + modifier = Modifier.fillMaxWidth() + ) { + Text("Create public link") } } } } + enum class UnifiedSharesListItemType { Top, Mid, Bottom; From 8338cde323ac4abecddf2042fda25204ede651ff Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:06:24 +0200 Subject: [PATCH 03/44] add todos Signed-off-by: alperozturk96 --- .../android/ui/fragment/share/UnifiedShareView.kt | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 9ac4b0fc71df..01859705454b 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -244,6 +244,13 @@ fun UnifiedShareView() { } } +// TODO: Instead of showing all options in the bottom sheet collect extra sharing options inside the expadable/collable sub-menu + +// TODO: Use conntected button group for invited and anyone type + +// TODO: Use like inner tags whenever user add a new people to the search and it should look like User 1, Group 1 etc. + + @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { @@ -598,6 +605,13 @@ enum class UnifiedSharesListItemType { } } +// TODO: - Show avatar, email group or user in the leading content as a one rounded ICON +// TODO: - Replace supporting content with share permission but without editing option +// TODO: - Add right arrow icon end of the list item to access share detail and change share settings +// TODO: - Add context menu to access copy link, delete, and other actions u have with previous 3 dot menu + +// NOTE: To just create a public link anyone tab + just send DOES SAME THING + @Composable private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListItemType) { ListItem( From cc4f8701ce442404066d504cdb47caeba4e7094d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:27:21 +0200 Subject: [PATCH 04/44] add todos Signed-off-by: alperozturk96 --- .../owncloud/android/ui/fragment/share/UnifiedShareView.kt | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 01859705454b..870ec507fde8 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -607,8 +607,9 @@ enum class UnifiedSharesListItemType { // TODO: - Show avatar, email group or user in the leading content as a one rounded ICON // TODO: - Replace supporting content with share permission but without editing option -// TODO: - Add right arrow icon end of the list item to access share detail and change share settings -// TODO: - Add context menu to access copy link, delete, and other actions u have with previous 3 dot menu +// TODO: - Add more icon end of the list item to access options we have before, delete, send email ... +// TODO: - Add context menu just does same thing like more icon +// TODO: - When user taps the list item it should show share detail bottom sheet // NOTE: To just create a public link anyone tab + just send DOES SAME THING From ec997d3bcefba289b5b9eb72ddcf2f00fa8b8782 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:29:05 +0200 Subject: [PATCH 05/44] add todos Signed-off-by: alperozturk96 --- .../com/owncloud/android/ui/fragment/share/UnifiedShareView.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 870ec507fde8..0dd36f154728 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -250,6 +250,7 @@ fun UnifiedShareView() { // TODO: Use like inner tags whenever user add a new people to the search and it should look like User 1, Group 1 etc. +// TODO: Replace FAB with person icon @OptIn(ExperimentalMaterial3Api::class) @Composable From e6ffe47fa372f1f459e4537005f0a716a84f7de8 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:47:41 +0200 Subject: [PATCH 06/44] wip Signed-off-by: alperozturk96 # Conflicts: # gradle/libs.versions.toml --- app/build.gradle.kts | 1 + .../ui/fragment/share/UnifiedShareView.kt | 117 +++++++++++------- 2 files changed, 75 insertions(+), 43 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c28ced6a35b6..c3178b6c2979 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -375,6 +375,7 @@ dependencies { implementation(libs.compose.activity) implementation(libs.compose.ui.tooling.preview) implementation(libs.foundation) + implementation(libs.material3) debugImplementation(libs.compose.ui.tooling) // endregion diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 0dd36f154728..11d346569ae7 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -8,6 +8,7 @@ package com.owncloud.android.ui.fragment.share import android.content.Context +import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement @@ -23,6 +24,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.KeyboardArrowDown +import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api @@ -36,6 +40,9 @@ import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.SegmentedButton +import androidx.compose.material3.SegmentedButtonDefaults +import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Switch import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState @@ -235,7 +242,7 @@ fun UnifiedShareView() { .align(Alignment.End) .padding(top = 16.dp) ) { - Icon(Icons.Default.Add, contentDescription = "Add") + Icon(Icons.Default.Person, contentDescription = "Add") } if (showAddShare) { @@ -244,14 +251,8 @@ fun UnifiedShareView() { } } -// TODO: Instead of showing all options in the bottom sheet collect extra sharing options inside the expadable/collable sub-menu - -// TODO: Use conntected button group for invited and anyone type - // TODO: Use like inner tags whenever user add a new people to the search and it should look like User 1, Group 1 etc. -// TODO: Replace FAB with person icon - @OptIn(ExperimentalMaterial3Api::class) @Composable private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { @@ -263,6 +264,10 @@ private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { var searchQuery by remember { mutableStateOf("") } var note by remember { mutableStateOf("") } + // Toggle states for collapse/expand + var showInvitedSettings by remember { mutableStateOf(false) } + var showAnyoneSettings by remember { mutableStateOf(false) } + var viewFiles by remember { mutableStateOf(false) } var editFiles by remember { mutableStateOf(false) } var createFiles by remember { mutableStateOf(false) } @@ -292,7 +297,7 @@ private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { ) { ShareBottomSheetHeader(filename) - ShareCategoryDropdown( + ShareCategoryButtonGroup( selectedCategory = category, onCategoryChange = { category = it } ) @@ -306,9 +311,12 @@ private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { onPermissionChange = { permission = it }, ) - InvitedInlineSettings() - - NoteToRecipients(note = note, onNoteChange = { note = it }) + CollapsibleSettingsSection( + isExpanded = showInvitedSettings, + onToggle = { showInvitedSettings = !showInvitedSettings } + ) { + InvitedInlineSettings() + } } else { AnyoneShareContent( permission = permission, @@ -323,11 +331,17 @@ private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { SettingsSwitchRow("Delete files", deleteFiles) { deleteFiles = it } } - AnyoneInlineSettings() - - NoteToRecipients(note = note, onNoteChange = { note = it }) + CollapsibleSettingsSection( + isExpanded = showAnyoneSettings, + onToggle = { showAnyoneSettings = !showAnyoneSettings } + ) { + AnyoneInlineSettings() + } } + NoteToRecipients(note = note, onNoteChange = { note = it }) + + ShareActionButtons( category = category, isSendEnabled = searchQuery.isNotBlank(), @@ -338,6 +352,41 @@ private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { } } +@Composable +private fun CollapsibleSettingsSection( + isExpanded: Boolean, + onToggle: () -> Unit, + content: @Composable () -> Unit +) { + Column(modifier = Modifier.fillMaxWidth()) { + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { onToggle() } + .padding(vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Text( + text = "Settings", + style = MaterialTheme.typography.titleMedium, + color = MaterialTheme.colorScheme.primary + ) + Icon( + imageVector = if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary + ) + } + + AnimatedVisibility(visible = isExpanded) { + Column { + content() + } + } + } +} + @Composable private fun ShareBottomSheetHeader(filename: String) { Text( @@ -350,40 +399,23 @@ private fun ShareBottomSheetHeader(filename: String) { @OptIn(ExperimentalMaterial3Api::class) @Composable -private fun ShareCategoryDropdown( +private fun ShareCategoryButtonGroup( selectedCategory: UnifiedShareCategory, onCategoryChange: (UnifiedShareCategory) -> Unit ) { - var expanded by remember { mutableStateOf(false) } - - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded }, + SingleChoiceSegmentedButtonRow( modifier = Modifier.fillMaxWidth() ) { - OutlinedTextField( - value = selectedCategory.name, - onValueChange = {}, - readOnly = true, - label = { Text("Share type") }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), - modifier = Modifier - .menuAnchor() - .fillMaxWidth() - ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - UnifiedShareCategory.entries.forEach { option -> - DropdownMenuItem( - text = { Text(option.name) }, - onClick = { - onCategoryChange(option) - expanded = false - } + UnifiedShareCategory.entries.forEachIndexed { index, option -> + SegmentedButton( + selected = selectedCategory == option, + onClick = { onCategoryChange(option) }, + shape = SegmentedButtonDefaults.itemShape( + index = index, + count = UnifiedShareCategory.entries.size ) + ) { + Text(option.name) } } } @@ -491,7 +523,6 @@ private fun PermissionDropdown( } } - @Composable private fun InvitedInlineSettings() { var shareWithOthers by remember { mutableStateOf(false) } From a9a8af55c45f918f5597326ee3e33e44af8a83cf Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:48:52 +0200 Subject: [PATCH 07/44] wip Signed-off-by: alperozturk96 --- .../android/ui/fragment/share/UnifiedShareView.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 11d346569ae7..a394cd7eba7b 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -26,6 +26,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp +import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Button import androidx.compose.material3.DropdownMenuItem @@ -638,13 +639,11 @@ enum class UnifiedSharesListItemType { } // TODO: - Show avatar, email group or user in the leading content as a one rounded ICON -// TODO: - Replace supporting content with share permission but without editing option // TODO: - Add more icon end of the list item to access options we have before, delete, send email ... // TODO: - Add context menu just does same thing like more icon // TODO: - When user taps the list item it should show share detail bottom sheet // NOTE: To just create a public link anyone tab + just send DOES SAME THING - @Composable private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListItemType) { ListItem( @@ -663,11 +662,14 @@ private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListI }, supportingContent = { Text( - text = share.sharedTo, + text = share.permission.getText(), style = MaterialTheme.typography.bodyMedium, color = MaterialTheme.colorScheme.onSurfaceVariant ) }, + trailingContent = { + Icon(Icons.Default.MoreVert, contentDescription = "More") + }, colors = ListItemDefaults.colors( containerColor = Color.Transparent ) From 5a618877b7ce16ac3e3bf761d4f196c84cb2fcd1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:54:06 +0200 Subject: [PATCH 08/44] wip Signed-off-by: alperozturk96 --- .../ui/fragment/share/UnifiedShareView.kt | 78 ++++++++++++++++--- 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index a394cd7eba7b..9a6584ef4210 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -11,15 +11,19 @@ import android.content.Context import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons @@ -29,13 +33,16 @@ import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Person import androidx.compose.material3.Button +import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.FilledTonalButton import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme @@ -56,7 +63,9 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.unit.dp import com.owncloud.android.utils.theme.ViewThemeUtils @@ -586,8 +595,6 @@ private fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: } } -// --- ACTION BUTTONS --- - @Composable private fun ShareActionButtons( category: UnifiedShareCategory, @@ -638,22 +645,36 @@ enum class UnifiedSharesListItemType { } } -// TODO: - Show avatar, email group or user in the leading content as a one rounded ICON -// TODO: - Add more icon end of the list item to access options we have before, delete, send email ... -// TODO: - Add context menu just does same thing like more icon -// TODO: - When user taps the list item it should show share detail bottom sheet - // NOTE: To just create a public link anyone tab + just send DOES SAME THING @Composable private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListItemType) { + var showContextMenu by remember { mutableStateOf(false) } + var showDetailSheet by remember { mutableStateOf(false) } + val haptics = LocalHapticFeedback.current + ListItem( modifier = Modifier .fillMaxWidth() .clip(type.getShape()) - .clickable( - onClick = { } + .combinedClickable( + onClick = { showDetailSheet = true }, + onLongClick = { + haptics.performHapticFeedback(HapticFeedbackType.LongPress) + showContextMenu = true + }, ) .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), + leadingContent = { + Box( + modifier = Modifier + .size(40.dp) + .clip(CircleShape) + .background(MaterialTheme.colorScheme.primaryContainer), + contentAlignment = Alignment.Center + ) { + Text(text = share.type.icon()) + } + }, headlineContent = { Text( text = share.label, @@ -668,12 +689,49 @@ private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListI ) }, trailingContent = { - Icon(Icons.Default.MoreVert, contentDescription = "More") + Box { + IconButton(onClick = { showContextMenu = true }) { + Icon(Icons.Default.MoreVert, contentDescription = "More options") + } + + DropdownMenu( + expanded = showContextMenu, + onDismissRequest = { showContextMenu = false } + ) { + DropdownMenuItem( + text = { Text("Edit") }, + onClick = { + showContextMenu = false + showDetailSheet = true + } + ) + + DropdownMenuItem( + text = { Text("Send email") }, + onClick = { showContextMenu = false } + ) + + HorizontalDivider() + + DropdownMenuItem( + text = { Text("Delete", color = MaterialTheme.colorScheme.error) }, + onClick = { showContextMenu = false } + ) + } + } }, colors = ListItemDefaults.colors( containerColor = Color.Transparent ) ) + + // TODO: USE EXISTING SHARE DETAILS + if (showDetailSheet) { + AddShareBottomSheet( + filename = share.label, + onDismiss = { showDetailSheet = false } + ) + } } fun ComposeView.setupUnifiedShare(viewThemeUtils: ViewThemeUtils, context: Context) { From 166c82de6c95b0fb725751d50c55991926c7d550 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 17 Apr 2026 10:54:22 +0200 Subject: [PATCH 09/44] wip Signed-off-by: alperozturk96 --- .../com/owncloud/android/ui/fragment/share/UnifiedShareView.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt index 9a6584ef4210..0b14bea5f943 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt @@ -27,7 +27,6 @@ import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.KeyboardArrowDown import androidx.compose.material.icons.filled.KeyboardArrowUp import androidx.compose.material.icons.filled.MoreVert From 441ed3400849ade2bb8df518c46b598cfa77de9b Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 13:38:07 +0200 Subject: [PATCH 10/44] wip Signed-off-by: alperozturk96 # Conflicts: # app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java --- .../ui/activity/FileDisplayActivity.kt | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index db54643b7786..f3d16bef2bec 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -322,6 +322,28 @@ class FileDisplayActivity : setupDrawer(menuItemId) } + @Suppress("DEPRECATION") + private fun logOcsCredentials() { + + lifecycleScope.launch(Dispatchers.IO) { + val user = accountManager.user + val serverUrl = user.server.uri.toString() + val accountName = user.accountName + + try { + val client = clientFactory.create(user) + val username = client.userIdPlain + val authToken = client.credentials.authToken + Log_OC.d(TAG, "OCS credentials — serverUrl=$serverUrl") + Log_OC.d(TAG, "OCS credentials — accountName=$accountName username=$username authToken=$authToken") + + } catch (e: CreationException) { + Log_OC.e(TAG, "OCS credentials — serverUrl=$serverUrl accountName=$accountName (client creation failed)", e) + } + } + + } + /** * Determines which navigation drawer item should be selected. * From 0c3fd768faccef3323393a69995351f7f89c3dc1 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 5 May 2026 16:00:59 +0200 Subject: [PATCH 11/44] wip Signed-off-by: alperozturk96 # Conflicts: # app/src/main/java/com/nextcloud/utils/extensions/OwnCloudClientExtensions.kt --- .../ui/activity/FileDisplayActivity.kt | 2 + .../ui/fragment/share/UnifiedShareView.kt | 745 ------------------ gradle/verification-metadata.xml | 24 + 3 files changed, 26 insertions(+), 745 deletions(-) delete mode 100644 app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index f3d16bef2bec..d04ec8ba0847 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -320,6 +320,7 @@ class FileDisplayActivity : startMetadataSyncForRoot() handleBackPress() setupDrawer(menuItemId) + logOcsCredentials() } @Suppress("DEPRECATION") @@ -337,6 +338,7 @@ class FileDisplayActivity : Log_OC.d(TAG, "OCS credentials — serverUrl=$serverUrl") Log_OC.d(TAG, "OCS credentials — accountName=$accountName username=$username authToken=$authToken") + } catch (e: CreationException) { Log_OC.e(TAG, "OCS credentials — serverUrl=$serverUrl accountName=$accountName (client creation failed)", e) } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt b/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt deleted file mode 100644 index 0b14bea5f943..000000000000 --- a/app/src/main/java/com/owncloud/android/ui/fragment/share/UnifiedShareView.kt +++ /dev/null @@ -1,745 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.owncloud.android.ui.fragment.share - -import android.content.Context -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.combinedClickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.foundation.verticalScroll -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.KeyboardArrowDown -import androidx.compose.material.icons.filled.KeyboardArrowUp -import androidx.compose.material.icons.filled.MoreVert -import androidx.compose.material.icons.filled.Person -import androidx.compose.material3.Button -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExposedDropdownMenuBox -import androidx.compose.material3.ExposedDropdownMenuDefaults -import androidx.compose.material3.FilledTonalButton -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.HorizontalDivider -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.SegmentedButton -import androidx.compose.material3.SegmentedButtonDefaults -import androidx.compose.material3.SingleChoiceSegmentedButtonRow -import androidx.compose.material3.Switch -import androidx.compose.material3.Text -import androidx.compose.material3.rememberModalBottomSheetState -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.ComposeView -import androidx.compose.ui.platform.LocalHapticFeedback -import androidx.compose.ui.unit.dp -import com.owncloud.android.utils.theme.ViewThemeUtils - -enum class UnifiedShareCategory { - Invited, Anyone -} - -enum class UnifiedShareType { - InternalUser, InternalGroup, InternalLink, ExternalLink, ExternalFederated, ExternalMail; - - fun icon(): String { - return when (this) { - InternalUser -> "👤" - InternalGroup -> "👥" - InternalLink -> "🔗" - ExternalLink -> "🌍" - ExternalFederated -> "☁️" - ExternalMail -> "📧" - } - } -} - -data class UnifiedShareDownloadLimit( - val limit: Int, - val downloadCount: Int -) - -sealed class UnifiedSharePermission { - // file drop only for folder - data object FileDrop : UnifiedSharePermission() - - data object CanView : UnifiedSharePermission() - data object CanEdit : UnifiedSharePermission() - - // create only for folder - data class Custom(val read: Boolean, val edit: Boolean, val delete: Boolean, val create: Boolean) : - UnifiedSharePermission() - - fun getText(): String { - return when(this) { - FileDrop -> "File drop" - CanView -> "Can view" - CanEdit -> "Can edit" - is Custom -> "Custom permissions" - } - } -} - -data class UnifiedShares( - val id: Int, - val password: String, - val note: String, - val limit: UnifiedShareDownloadLimit, - val expirationDate: Int, - val permission: UnifiedSharePermission, - val label: String, - val sharedTo: String, - val type: UnifiedShareType, - val category: UnifiedShareCategory, -) - -// TODO: MOVE TO THE ANDROID: COMMON -// TODO: MAKE LAZY COLUMN -// TODO: EXPOSE ACTIONS, IMPLEMENT VIEWMODEL, REPOSITORY TO FETCH ACTUAL SHARE, INJECT NECESSARY PARAMETERS - -@Composable -fun UnifiedShareView() { - var showAddShare by remember { mutableStateOf(false) } - - val mockUnifiedShares = listOf( - UnifiedShares( - id = 1, - password = "", - note = "Design review – please check latest changes", - limit = UnifiedShareDownloadLimit( - limit = 100, - downloadCount = 12 - ), - expirationDate = 0, - permission = UnifiedSharePermission.CanView, - label = "Alice Johnson", - sharedTo = "alice@company.com", - type = UnifiedShareType.InternalUser, - category = UnifiedShareCategory.Invited - ), - - UnifiedShares( - id = 2, - password = "", - note = "", - limit = UnifiedShareDownloadLimit( - limit = 0, - downloadCount = 0 - ), - expirationDate = 0, - permission = UnifiedSharePermission.CanEdit, - label = "Marketing Team", - sharedTo = "marketing", - type = UnifiedShareType.InternalGroup, - category = UnifiedShareCategory.Invited - ), - - UnifiedShares( - id = 3, - password = "1234", - note = "Public link for client review", - limit = UnifiedShareDownloadLimit( - limit = 50, - downloadCount = 5 - ), - expirationDate = 1710000000, - permission = UnifiedSharePermission.Custom( - read = true, - edit = false, - delete = false, - create = false - ), - label = "Public Link", - sharedTo = "https://nextcloud.com/s/abc123", - type = UnifiedShareType.InternalLink, - category = UnifiedShareCategory.Anyone - ), - - UnifiedShares( - id = 4, - password = "", - note = "External partner access", - limit = UnifiedShareDownloadLimit( - limit = 20, - downloadCount = 2 - ), - expirationDate = 0, - permission = UnifiedSharePermission.CanView, - label = "John External", - sharedTo = "john@external.com", - type = UnifiedShareType.ExternalMail, - category = UnifiedShareCategory.Anyone - ), - - UnifiedShares( - id = 5, - password = "", - note = "Federated sharing with partner instance", - limit = UnifiedShareDownloadLimit( - limit = 0, - downloadCount = 0 - ), - expirationDate = 0, - permission = UnifiedSharePermission.FileDrop, - label = "Partner Cloud", - sharedTo = "partner@nextcloud.org", - type = UnifiedShareType.ExternalFederated, - category = UnifiedShareCategory.Anyone - ) - ) - - Column( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - mockUnifiedShares.forEachIndexed { index, share -> - val type = when (index) { - 0 -> { - UnifiedSharesListItemType.Top - } - - mockUnifiedShares.lastIndex -> { - UnifiedSharesListItemType.Bottom - } - - else -> { - UnifiedSharesListItemType.Mid - } - } - - UnifiedSharesListItem(share, type) - } - - FloatingActionButton( - onClick = { showAddShare = true }, - modifier = Modifier - .align(Alignment.End) - .padding(top = 16.dp) - ) { - Icon(Icons.Default.Person, contentDescription = "Add") - } - - if (showAddShare) { - AddShareBottomSheet("Abc.txt",onDismiss = { showAddShare = false }) - } - } -} - -// TODO: Use like inner tags whenever user add a new people to the search and it should look like User 1, Group 1 etc. - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun AddShareBottomSheet(filename: String, onDismiss: () -> Unit) { - val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - val scrollState = rememberScrollState() - - var category by remember { mutableStateOf(UnifiedShareCategory.Invited) } - var permission by remember { mutableStateOf(UnifiedSharePermission.CanView) } - var searchQuery by remember { mutableStateOf("") } - var note by remember { mutableStateOf("") } - - // Toggle states for collapse/expand - var showInvitedSettings by remember { mutableStateOf(false) } - var showAnyoneSettings by remember { mutableStateOf(false) } - - var viewFiles by remember { mutableStateOf(false) } - var editFiles by remember { mutableStateOf(false) } - var createFiles by remember { mutableStateOf(false) } - var deleteFiles by remember { mutableStateOf(false) } - - val availablePermissions = remember { - listOf( - UnifiedSharePermission.CanView, - UnifiedSharePermission.CanEdit, - UnifiedSharePermission.FileDrop, - UnifiedSharePermission.Custom(false, false, false, false) - ) - } - - ModalBottomSheet( - onDismissRequest = onDismiss, - sheetState = sheetState, - containerColor = MaterialTheme.colorScheme.surface, - ) { - Column( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = 16.dp) - .padding(bottom = 32.dp) - .verticalScroll(scrollState), - verticalArrangement = Arrangement.spacedBy(16.dp) - ) { - ShareBottomSheetHeader(filename) - - ShareCategoryButtonGroup( - selectedCategory = category, - onCategoryChange = { category = it } - ) - - if (category == UnifiedShareCategory.Invited) { - InvitedShareContent( - searchQuery = searchQuery, - onSearchChange = { searchQuery = it }, - permission = permission, - availablePermissions = availablePermissions, - onPermissionChange = { permission = it }, - ) - - CollapsibleSettingsSection( - isExpanded = showInvitedSettings, - onToggle = { showInvitedSettings = !showInvitedSettings } - ) { - InvitedInlineSettings() - } - } else { - AnyoneShareContent( - permission = permission, - availablePermissions = availablePermissions, - onPermissionChange = { permission = it }, - ) - - if (permission is UnifiedSharePermission.Custom) { - SettingsSwitchRow("View files", viewFiles) { viewFiles = it } - SettingsSwitchRow("Edit files", editFiles) { editFiles = it } - SettingsSwitchRow("Create files", createFiles) { createFiles = it } - SettingsSwitchRow("Delete files", deleteFiles) { deleteFiles = it } - } - - CollapsibleSettingsSection( - isExpanded = showAnyoneSettings, - onToggle = { showAnyoneSettings = !showAnyoneSettings } - ) { - AnyoneInlineSettings() - } - } - - NoteToRecipients(note = note, onNoteChange = { note = it }) - - - ShareActionButtons( - category = category, - isSendEnabled = searchQuery.isNotBlank(), - onCopyClick = { /* TODO */ }, - onSendClick = { /* TODO */ } - ) - } - } -} - -@Composable -private fun CollapsibleSettingsSection( - isExpanded: Boolean, - onToggle: () -> Unit, - content: @Composable () -> Unit -) { - Column(modifier = Modifier.fillMaxWidth()) { - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onToggle() } - .padding(vertical = 8.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text( - text = "Settings", - style = MaterialTheme.typography.titleMedium, - color = MaterialTheme.colorScheme.primary - ) - Icon( - imageVector = if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary - ) - } - - AnimatedVisibility(visible = isExpanded) { - Column { - content() - } - } - } -} - -@Composable -private fun ShareBottomSheetHeader(filename: String) { - Text( - text = "Share $filename", - style = MaterialTheme.typography.headlineSmall, - color = MaterialTheme.colorScheme.onSurface, - modifier = Modifier.padding(bottom = 8.dp) - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun ShareCategoryButtonGroup( - selectedCategory: UnifiedShareCategory, - onCategoryChange: (UnifiedShareCategory) -> Unit -) { - SingleChoiceSegmentedButtonRow( - modifier = Modifier.fillMaxWidth() - ) { - UnifiedShareCategory.entries.forEachIndexed { index, option -> - SegmentedButton( - selected = selectedCategory == option, - onClick = { onCategoryChange(option) }, - shape = SegmentedButtonDefaults.itemShape( - index = index, - count = UnifiedShareCategory.entries.size - ) - ) { - Text(option.name) - } - } - } -} - -@Composable -private fun InvitedShareContent( - searchQuery: String, - onSearchChange: (String) -> Unit, - permission: UnifiedSharePermission, - availablePermissions: List, - onPermissionChange: (UnifiedSharePermission) -> Unit, - -) { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - OutlinedTextField( - value = searchQuery, - onValueChange = onSearchChange, - modifier = Modifier.fillMaxWidth(), - label = { Text("Add people") }, - placeholder = { Text("Name, team, email or federated ID") }, - singleLine = true, - shape = RoundedCornerShape(8.dp) - ) - - PermissionDropdown( - label = "Participants", - selectedPermission = permission, - availablePermissions = availablePermissions, - onPermissionChange = onPermissionChange - ) - } -} - -@Composable -private fun NoteToRecipients( - note: String, - onNoteChange: (String) -> Unit -) { - OutlinedTextField( - value = note, - onValueChange = onNoteChange, - modifier = Modifier.fillMaxWidth(), - placeholder = { Text("Note to recipients") }, - shape = RoundedCornerShape(8.dp) - ) -} - -@Composable -private fun AnyoneShareContent( - permission: UnifiedSharePermission, - availablePermissions: List, - onPermissionChange: (UnifiedSharePermission) -> Unit, -) { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - PermissionDropdown( - label = "Anyone with the link", - selectedPermission = permission, - availablePermissions = availablePermissions, - onPermissionChange = onPermissionChange - ) - } -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -private fun PermissionDropdown( - label: String, - selectedPermission: UnifiedSharePermission, - availablePermissions: List, - onPermissionChange: (UnifiedSharePermission) -> Unit -) { - var expanded by remember { mutableStateOf(false) } - - ExposedDropdownMenuBox( - expanded = expanded, - onExpandedChange = { expanded = !expanded }, - modifier = Modifier.fillMaxWidth() - ) { - OutlinedTextField( - value = selectedPermission.getText(), - onValueChange = {}, - readOnly = true, - label = { Text(label) }, - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, - colors = ExposedDropdownMenuDefaults.outlinedTextFieldColors(), - modifier = Modifier - .menuAnchor() - .fillMaxWidth() - ) - ExposedDropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false } - ) { - availablePermissions.forEach { option -> - DropdownMenuItem( - text = { Text(option.getText()) }, - onClick = { - onPermissionChange(option) - expanded = false - } - ) - } - } - } -} - -@Composable -private fun InvitedInlineSettings() { - var shareWithOthers by remember { mutableStateOf(false) } - var editFile by remember { mutableStateOf(false) } - var hasExpiration by remember { mutableStateOf(false) } - var hideDownload by remember { mutableStateOf(false) } - - Column { - SettingsSwitchRow("Share with others", shareWithOthers) { shareWithOthers = it } - SettingsSwitchRow("Edit file", editFile) { editFile = it } - SettingsSwitchRow("Expiration date", hasExpiration) { hasExpiration = it } - SettingsSwitchRow("Hide download and sync options", hideDownload) { hideDownload = it } - } -} - -@Composable -private fun AnyoneInlineSettings() { - var hasPassword by remember { mutableStateOf(false) } - var hasExpiration by remember { mutableStateOf(false) } - var limitDownloads by remember { mutableStateOf(false) } - - var hideDownloads by remember { mutableStateOf(false) } - var videoVerification by remember { mutableStateOf(false) } - var showFilesInGridView by remember { mutableStateOf(false) } - - Column { - OutlinedTextField( - value = "", - onValueChange = {}, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp), - label = { Text("Label") }, - placeholder = { Text("Optional name for this link") }, - singleLine = true - ) - - SettingsSwitchRow("Expiration date", hasExpiration) { hasExpiration = it } - SettingsSwitchRow("Password", hasPassword) { hasPassword = it } - SettingsSwitchRow("Limit downloads", limitDownloads) { limitDownloads = it } - - SettingsSwitchRow("Hide downloads", hideDownloads) { hideDownloads = it } - SettingsSwitchRow("Video verification", videoVerification) { videoVerification = it } - SettingsSwitchRow("Show files in grid view", showFilesInGridView) { showFilesInGridView = it } - - } -} - -@Composable -private fun SettingsSwitchRow(label: String, checked: Boolean, onCheckedChange: (Boolean) -> Unit) { - Row( - modifier = Modifier - .fillMaxWidth() - .height(48.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween - ) { - Text(text = label, style = MaterialTheme.typography.bodyLarge) - Switch(checked = checked, onCheckedChange = onCheckedChange) - } -} - -@Composable -private fun ShareActionButtons( - category: UnifiedShareCategory, - isSendEnabled: Boolean, - onCopyClick: () -> Unit, - onSendClick: () -> Unit -) { - Row(modifier = Modifier - .fillMaxWidth() - .padding(top = 16.dp)) { - if (category == UnifiedShareCategory.Invited) { - FilledTonalButton( - onClick = onCopyClick, - modifier = Modifier.weight(1f) - ) { - Text("Copy link") - } - Spacer(modifier = Modifier.width(16.dp)) - Button( - onClick = onSendClick, - modifier = Modifier.weight(1f), - enabled = isSendEnabled // Disabled if search query is empty - ) { - Text("Send") - } - } else { - // For "Anyone" (Public link), usually just one big action to create/copy - Button( - onClick = onCopyClick, - modifier = Modifier.fillMaxWidth() - ) { - Text("Create public link") - } - } - } -} - -enum class UnifiedSharesListItemType { - Top, Mid, Bottom; - - @Composable - fun getShape(): RoundedCornerShape { - return when (this) { - Top -> RoundedCornerShape(12.dp, 12.dp, 4.dp, 4.dp) - Mid -> RoundedCornerShape(4.dp, 4.dp, 4.dp, 4.dp) - Bottom -> RoundedCornerShape(4.dp, 4.dp, 12.dp, 12.dp) - } - } -} - -// NOTE: To just create a public link anyone tab + just send DOES SAME THING -@Composable -private fun UnifiedSharesListItem(share: UnifiedShares, type: UnifiedSharesListItemType) { - var showContextMenu by remember { mutableStateOf(false) } - var showDetailSheet by remember { mutableStateOf(false) } - val haptics = LocalHapticFeedback.current - - ListItem( - modifier = Modifier - .fillMaxWidth() - .clip(type.getShape()) - .combinedClickable( - onClick = { showDetailSheet = true }, - onLongClick = { - haptics.performHapticFeedback(HapticFeedbackType.LongPress) - showContextMenu = true - }, - ) - .background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f)), - leadingContent = { - Box( - modifier = Modifier - .size(40.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer), - contentAlignment = Alignment.Center - ) { - Text(text = share.type.icon()) - } - }, - headlineContent = { - Text( - text = share.label, - style = MaterialTheme.typography.titleSmall - ) - }, - supportingContent = { - Text( - text = share.permission.getText(), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - }, - trailingContent = { - Box { - IconButton(onClick = { showContextMenu = true }) { - Icon(Icons.Default.MoreVert, contentDescription = "More options") - } - - DropdownMenu( - expanded = showContextMenu, - onDismissRequest = { showContextMenu = false } - ) { - DropdownMenuItem( - text = { Text("Edit") }, - onClick = { - showContextMenu = false - showDetailSheet = true - } - ) - - DropdownMenuItem( - text = { Text("Send email") }, - onClick = { showContextMenu = false } - ) - - HorizontalDivider() - - DropdownMenuItem( - text = { Text("Delete", color = MaterialTheme.colorScheme.error) }, - onClick = { showContextMenu = false } - ) - } - } - }, - colors = ListItemDefaults.colors( - containerColor = Color.Transparent - ) - ) - - // TODO: USE EXISTING SHARE DETAILS - if (showDetailSheet) { - AddShareBottomSheet( - filename = share.label, - onDismiss = { showDetailSheet = false } - ) - } -} - -fun ComposeView.setupUnifiedShare(viewThemeUtils: ViewThemeUtils, context: Context) { - setContent { - MaterialTheme( - colorScheme = viewThemeUtils.getColorScheme(context), - content = { - UnifiedShareView() - } - ) - } -} diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index dc246117dcf0..a5dd635fb5b0 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -22833,6 +22833,14 @@ + + + + + + + + @@ -23077,6 +23085,14 @@ + + + + + + + + @@ -23317,6 +23333,14 @@ + + + + + + + + From 936be3741122490002c046d22ce8a7dfcf0e7d12 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 6 May 2026 14:23:35 +0200 Subject: [PATCH 12/44] wip Signed-off-by: alperozturk96 --- .../layout/file_details_sharing_fragment.xml | 65 +++++++++---------- gradle/verification-metadata.xml | 24 +++++++ 2 files changed, 56 insertions(+), 33 deletions(-) diff --git a/app/src/main/res/layout/file_details_sharing_fragment.xml b/app/src/main/res/layout/file_details_sharing_fragment.xml index a03e8e26ba39..54daff819b9f 100644 --- a/app/src/main/res/layout/file_details_sharing_fragment.xml +++ b/app/src/main/res/layout/file_details_sharing_fragment.xml @@ -5,25 +5,25 @@ ~ SPDX-FileCopyrightText: 2018 Andy Scherzinger ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> - + android:layout_below="@id/appbar"> - + android:paddingTop="@dimen/standard_eight_padding"> + tools:visibility="visible"> + app:iconGravity="textStart" /> + android:textAppearance="?android:attr/textAppearanceMedium" + android:visibility="gone" + tools:visibility="visible" /> @@ -209,9 +209,9 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/standard_margin" android:layout_marginEnd="@dimen/standard_margin" + android:text="@string/create_link" app:icon="@drawable/file_link" - app:iconGravity="textStart" - android:text="@string/create_link" /> + app:iconGravity="textStart" /> - - - + - + + - + diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index a5dd635fb5b0..66df39704ee7 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -22769,6 +22769,14 @@ + + + + + + + + @@ -23021,6 +23029,14 @@ + + + + + + + + @@ -23269,6 +23285,14 @@ + + + + + + + + From 39048915d704e32e3698f1b85bacf15256b6cef3 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 9 Jun 2026 11:22:50 +0200 Subject: [PATCH 13/44] wip Signed-off-by: alperozturk96 --- app/build.gradle.kts | 1 - gradle/libs.versions.toml | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c3178b6c2979..c28ced6a35b6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -375,7 +375,6 @@ dependencies { implementation(libs.compose.activity) implementation(libs.compose.ui.tooling.preview) implementation(libs.foundation) - implementation(libs.material3) debugImplementation(libs.compose.ui.tooling) // endregion diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2858e905d32a..40551726cb3d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -114,7 +114,7 @@ photoview = { module = "com.github.Baseflow:PhotoView", version.ref = "photoview material = { module = "com.google.android.material:material", version.ref = "materialVersion" } android-gif-drawable = { module = "pl.droidsonroids.gif:android-gif-drawable", version.ref = "androidGifDrawableVersion" } android-image-cropper = { module = "com.vanniktech:android-image-cropper", version.ref = "androidImageCropperVersion" } -androidsvg = { module = "com.caverock:androidsvg", version.ref = "androidsvgVersion" } +androidsvg = { module = "com.caverock:androidsvg-aar", version.ref = "androidsvgVersion" } coil = { module = "io.coil-kt:coil", version.ref = "coilVersion" } constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayoutVersion" } emoji-google = { module = "com.vanniktech:emoji-google", version.ref = "emojiGoogleVersion" } From 7c0da4df2714f2c571335be4db1b26637ef4520d Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 12 Jun 2026 11:15:09 +0200 Subject: [PATCH 14/44] use getMaxCompatibleE2EEVersion Signed-off-by: alperozturk96 --- .../utils/EndToEndEncryptionVersionTests.kt | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 app/src/androidTest/java/com/nextcloud/utils/EndToEndEncryptionVersionTests.kt diff --git a/app/src/androidTest/java/com/nextcloud/utils/EndToEndEncryptionVersionTests.kt b/app/src/androidTest/java/com/nextcloud/utils/EndToEndEncryptionVersionTests.kt new file mode 100644 index 000000000000..7002c8d6daa1 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/utils/EndToEndEncryptionVersionTests.kt @@ -0,0 +1,63 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils + +import com.nextcloud.utils.e2ee.E2EVersionHelper +import com.owncloud.android.lib.resources.status.E2EVersion +import org.junit.Assert.assertEquals +import org.junit.Test + +class EndToEndEncryptionVersionTests { + + @Test + fun testGetMaxCompatibleE2EEVersionWhenGivenUnknownShouldReturnUnknown() { + assertEquals(E2EVersion.UNKNOWN, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.UNKNOWN)) + } + + @Test + fun testGetMaxCompatibleE2EEVersionWhenGivenV1_0ShouldReturnV1_0() { + assertEquals(E2EVersion.V1_0, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_0)) + } + + @Test + fun testGetMaxCompatibleE2EEVersionWhenGivenV1_1ShouldReturnV1_1() { + assertEquals(E2EVersion.V1_1, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_1)) + } + + @Test + fun testGetMaxCompatibleE2EEVersionWhenGivenV1_2ShouldReturnV1_2() { + assertEquals(E2EVersion.V1_2, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2)) + } + + @Test + fun testGetMaxCompatibleE2EEVersionWhenGivenV1AboveClientMaxShouldReturnClientV1Max() { + assertEquals(E2EVersion.V1_2, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2)) + } + + @Test + fun testGetMaxCompatibleE2EEVersionWhenGivenV2_0ShouldReturnV2_0() { + assertEquals(E2EVersion.V2_0, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_0)) + } + + @Test + fun testGetMaxCompatibleE2EEVersionWhenGivenV2_1ShouldReturnV2_1() { + assertEquals(E2EVersion.V2_1, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_1)) + } + + @Test + fun testGetMaxCompatibleE2EEVersionWhenGivenV1_2ShouldNotApplyV2CeilingShouldReturnV1_2() { + val result = E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2) + assertEquals(E2EVersion.V1_2, result) + } + + @Test + fun testGetMaxCompatibleE2EEVersionWhenGivenV2_0ShouldNotApplyV1CeilingShouldReturnV2_0() { + val result = E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_0) + assertEquals(E2EVersion.V2_0, result) + } +} From 858b70cfe08842a7628fde307d891db143a5e6a2 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 17 Jun 2026 12:26:14 +0200 Subject: [PATCH 15/44] wip Signed-off-by: alperozturk96 --- .../utils/EndToEndEncryptionVersionTests.kt | 55 +++++++++++++++---- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/app/src/androidTest/java/com/nextcloud/utils/EndToEndEncryptionVersionTests.kt b/app/src/androidTest/java/com/nextcloud/utils/EndToEndEncryptionVersionTests.kt index 7002c8d6daa1..7e1e000bd23e 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/EndToEndEncryptionVersionTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/EndToEndEncryptionVersionTests.kt @@ -16,48 +16,81 @@ class EndToEndEncryptionVersionTests { @Test fun testGetMaxCompatibleE2EEVersionWhenGivenUnknownShouldReturnUnknown() { - assertEquals(E2EVersion.UNKNOWN, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.UNKNOWN)) + assertEquals( + E2EVersion.UNKNOWN, + E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.UNKNOWN) + ) } @Test fun testGetMaxCompatibleE2EEVersionWhenGivenV1_0ShouldReturnV1_0() { - assertEquals(E2EVersion.V1_0, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_0)) + assertEquals( + E2EVersion.V1_0, + E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_0) + ) } @Test fun testGetMaxCompatibleE2EEVersionWhenGivenV1_1ShouldReturnV1_1() { - assertEquals(E2EVersion.V1_1, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_1)) + assertEquals( + E2EVersion.V1_1, + E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_1) + ) } @Test fun testGetMaxCompatibleE2EEVersionWhenGivenV1_2ShouldReturnV1_2() { - assertEquals(E2EVersion.V1_2, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2)) + assertEquals( + E2EVersion.V1_2, + E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2) + ) } @Test fun testGetMaxCompatibleE2EEVersionWhenGivenV1AboveClientMaxShouldReturnClientV1Max() { - assertEquals(E2EVersion.V1_2, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2)) + assertEquals( + E2EVersion.V1_2, + E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2) + ) } @Test fun testGetMaxCompatibleE2EEVersionWhenGivenV2_0ShouldReturnV2_0() { - assertEquals(E2EVersion.V2_0, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_0)) + assertEquals( + E2EVersion.V2_0, + E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_0) + ) } @Test fun testGetMaxCompatibleE2EEVersionWhenGivenV2_1ShouldReturnV2_1() { - assertEquals(E2EVersion.V2_1, E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_1)) + assertEquals( + E2EVersion.V2_1, + E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_1) + ) } @Test fun testGetMaxCompatibleE2EEVersionWhenGivenV1_2ShouldNotApplyV2CeilingShouldReturnV1_2() { - val result = E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2) - assertEquals(E2EVersion.V1_2, result) + assertEquals( + E2EVersion.V1_2, + E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2) + ) } @Test fun testGetMaxCompatibleE2EEVersionWhenGivenV2_0ShouldNotApplyV1CeilingShouldReturnV2_0() { - val result = E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_0) - assertEquals(E2EVersion.V2_0, result) + assertEquals( + E2EVersion.V2_0, + E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_0) + ) + } + + @Test + fun testGetMaxCompatibleE2EEVersionWhenGivenV3_0ShouldReturnV2_1() { + assertEquals( + E2EVersion.V2_1, + E2EVersionHelper.getMaxCompatibleE2EEVersionFromString("3.0") + ) } } From ea0da2d2d1ec25db9d80327a7cc12a832a091e42 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Jul 2026 13:51:40 +0200 Subject: [PATCH 16/44] wip Signed-off-by: alperozturk96 # Conflicts: # gradle.properties # Conflicts: # gradle/verification-metadata.xml --- .../ui/activity/FileDisplayActivity.kt | 10 ++--- .../ui/fragment/FileDetailSharingFragment.kt | 43 ++++++++++++++++++- settings.gradle.kts | 4 +- 3 files changed, 50 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index d04ec8ba0847..8cd35ea38370 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -325,7 +325,6 @@ class FileDisplayActivity : @Suppress("DEPRECATION") private fun logOcsCredentials() { - lifecycleScope.launch(Dispatchers.IO) { val user = accountManager.user val serverUrl = user.server.uri.toString() @@ -337,13 +336,14 @@ class FileDisplayActivity : val authToken = client.credentials.authToken Log_OC.d(TAG, "OCS credentials — serverUrl=$serverUrl") Log_OC.d(TAG, "OCS credentials — accountName=$accountName username=$username authToken=$authToken") - - } catch (e: CreationException) { - Log_OC.e(TAG, "OCS credentials — serverUrl=$serverUrl accountName=$accountName (client creation failed)", e) + Log_OC.e( + TAG, + "OCS credentials — serverUrl=$serverUrl accountName=$accountName (client creation failed)", + e + ) } } - } /** diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index cdacde73a97f..893b96134fa7 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -31,6 +31,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.button.MaterialButton +import com.nextcloud.android.common.ui.share.initShareScreen import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager @@ -38,8 +39,10 @@ import com.nextcloud.client.di.Injectable import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.utils.IntentUtil import com.nextcloud.utils.extensions.getParcelableArgument +import com.nextcloud.utils.extensions.getTypedActivity import com.nextcloud.utils.extensions.mergeDistinctByToken import com.nextcloud.utils.extensions.setVisibleIf +import com.nextcloud.utils.extensions.toServerCredentials import com.nextcloud.utils.mdm.MDMConfig.shareViaUser import com.owncloud.android.R import com.owncloud.android.databinding.FileDetailsSharingFragmentBinding @@ -141,10 +144,18 @@ class FileDetailSharingFragment : binding?.pickContactEmailBtn?.setOnClickListener { checkContactPermission() } fetchSharees() + + // TODO: REPLACE FAKE CONDITION + if (user?.server?.version?.isNewerOrEqual(NextcloudVersion.nextcloud_34) == true || 2 < 4) { + showUnifiedShare() + } else { + fetchSharees() + } + setupView() } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FileDetailsSharingFragmentBinding.inflate(inflater, container, false) return binding!!.getRoot() } @@ -177,6 +188,36 @@ class FileDetailSharingFragment : // endregion // region private methods + private fun showUnifiedShare() { + val binding = binding ?: return + + binding.shareContainer.visibility = View.GONE + binding.unifiedShare.visibility = View.VISIBLE + + val shimmerLayout = binding.shimmerLayout.root + shimmerLayout.clearAnimation() + shimmerLayout.visibility = View.GONE + + val fileActivity = getTypedActivity(FileActivity::class.java) + + lifecycleScope.launch(Dispatchers.IO) { + val client = fileActivity?.clientRepository?.getOwncloudClient() ?: return@launch + val baseURL = user?.server?.uri?.toString() ?: return@launch + val serverCredentials = client.toServerCredentials(baseURL) + val sharingCapabilities = fileDataStorageManager?.getCapability(user)?.sharingJson ?: return@launch + val sourceId = file?.remoteId ?: return@launch + + withContext(Dispatchers.Main) { + binding.unifiedShare.initShareScreen( + sourceId, + sharingCapabilities, + serverCredentials, + viewThemeUtils.files.getColorScheme(fileActivity) + ) + } + } + } + private fun initArguments(savedInstanceState: Bundle?) { val args = (savedInstanceState ?: arguments) ?: return file = args.getParcelableArgument(ARG_FILE, OCFile::class.java) diff --git a/settings.gradle.kts b/settings.gradle.kts index a5b397949b05..a9ccc5b9d7c9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -55,6 +55,9 @@ includeBuild("../android_library") { /* Needed for local android common library + +*/ + includeBuild("../android_common") { dependencySubstitution { substitute(module("com.github.nextcloud.android-common:core")) @@ -64,6 +67,5 @@ includeBuild("../android_common") { .using(project(":ui")) } } -*/ include(":app", ":appscan") From 2072ca2a24ebcc04c2dd8e9fbc1b8bf2bbc55554 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 2 Jul 2026 15:00:26 +0200 Subject: [PATCH 17/44] wip Signed-off-by: alperozturk96 --- .../owncloud/android/ui/fragment/FileDetailSharingFragment.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 893b96134fa7..0981d34b083a 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -143,8 +143,6 @@ class FileDetailSharingFragment : binding?.pickContactEmailBtn?.setOnClickListener { checkContactPermission() } - fetchSharees() - // TODO: REPLACE FAKE CONDITION if (user?.server?.version?.isNewerOrEqual(NextcloudVersion.nextcloud_34) == true || 2 < 4) { showUnifiedShare() From 24abf09c5805d88846114a87573be2416e135775 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 3 Jul 2026 12:44:12 +0200 Subject: [PATCH 18/44] wip Signed-off-by: alperozturk96 --- .../owncloud/android/ui/fragment/FileDetailSharingFragment.kt | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 0981d34b083a..e32d0acd1e5a 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -202,13 +202,11 @@ class FileDetailSharingFragment : val client = fileActivity?.clientRepository?.getOwncloudClient() ?: return@launch val baseURL = user?.server?.uri?.toString() ?: return@launch val serverCredentials = client.toServerCredentials(baseURL) - val sharingCapabilities = fileDataStorageManager?.getCapability(user)?.sharingJson ?: return@launch val sourceId = file?.remoteId ?: return@launch withContext(Dispatchers.Main) { binding.unifiedShare.initShareScreen( sourceId, - sharingCapabilities, serverCredentials, viewThemeUtils.files.getColorScheme(fileActivity) ) From 45673c00e35ea0fe246bfe7dd4179a71b25c0fe4 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 6 Jul 2026 15:42:26 +0200 Subject: [PATCH 19/44] wip Signed-off-by: alperozturk96 --- .../android/ui/fragment/FileDetailSharingFragment.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index e32d0acd1e5a..b4d87a449b71 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -203,11 +203,17 @@ class FileDetailSharingFragment : val baseURL = user?.server?.uri?.toString() ?: return@launch val serverCredentials = client.toServerCredentials(baseURL) val sourceId = file?.remoteId ?: return@launch + val internalLink = if (accountManager.currentOwnCloudAccount != null && file != null) { + createInternalLink(accountManager.currentOwnCloudAccount!!, file!!) + } else { + "" + } withContext(Dispatchers.Main) { binding.unifiedShare.initShareScreen( sourceId, serverCredentials, + internalLink, viewThemeUtils.files.getColorScheme(fileActivity) ) } From a40810c8dc62aa35599943321ddb6ef642ff2390 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 6 Jul 2026 16:01:33 +0200 Subject: [PATCH 20/44] wip Signed-off-by: alperozturk96 --- .../android/ui/fragment/FileDetailSharingFragment.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index b4d87a449b71..e32d0acd1e5a 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -203,17 +203,11 @@ class FileDetailSharingFragment : val baseURL = user?.server?.uri?.toString() ?: return@launch val serverCredentials = client.toServerCredentials(baseURL) val sourceId = file?.remoteId ?: return@launch - val internalLink = if (accountManager.currentOwnCloudAccount != null && file != null) { - createInternalLink(accountManager.currentOwnCloudAccount!!, file!!) - } else { - "" - } withContext(Dispatchers.Main) { binding.unifiedShare.initShareScreen( sourceId, serverCredentials, - internalLink, viewThemeUtils.files.getColorScheme(fileActivity) ) } From 8b47f4305b8167ecb88c3d5d42809f246d73cbd6 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 8 Jul 2026 15:10:49 +0200 Subject: [PATCH 21/44] wip Signed-off-by: alperozturk96 --- .../android/ui/fragment/FileDetailSharingFragment.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index e32d0acd1e5a..1f3dab038465 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -203,10 +203,15 @@ class FileDetailSharingFragment : val baseURL = user?.server?.uri?.toString() ?: return@launch val serverCredentials = client.toServerCredentials(baseURL) val sourceId = file?.remoteId ?: return@launch + var internalLink = "" + if (accountManager.currentOwnCloudAccount != null && file != null) { + internalLink = createInternalLink(accountManager.currentOwnCloudAccount!!, file!!) + } withContext(Dispatchers.Main) { binding.unifiedShare.initShareScreen( sourceId, + internalLink, serverCredentials, viewThemeUtils.files.getColorScheme(fileActivity) ) From 0df88c8257256a22e0f75a875678bb40033696bb Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 9 Jul 2026 15:36:40 +0200 Subject: [PATCH 22/44] wip Signed-off-by: alperozturk96 --- .../utils/EndToEndEncryptionVersionTests.kt | 96 ------------------- .../ui/activity/FileDisplayActivity.kt | 24 ----- .../ui/fragment/FileDetailSharingFragment.kt | 12 +-- 3 files changed, 6 insertions(+), 126 deletions(-) delete mode 100644 app/src/androidTest/java/com/nextcloud/utils/EndToEndEncryptionVersionTests.kt diff --git a/app/src/androidTest/java/com/nextcloud/utils/EndToEndEncryptionVersionTests.kt b/app/src/androidTest/java/com/nextcloud/utils/EndToEndEncryptionVersionTests.kt deleted file mode 100644 index 7e1e000bd23e..000000000000 --- a/app/src/androidTest/java/com/nextcloud/utils/EndToEndEncryptionVersionTests.kt +++ /dev/null @@ -1,96 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2026 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ - -package com.nextcloud.utils - -import com.nextcloud.utils.e2ee.E2EVersionHelper -import com.owncloud.android.lib.resources.status.E2EVersion -import org.junit.Assert.assertEquals -import org.junit.Test - -class EndToEndEncryptionVersionTests { - - @Test - fun testGetMaxCompatibleE2EEVersionWhenGivenUnknownShouldReturnUnknown() { - assertEquals( - E2EVersion.UNKNOWN, - E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.UNKNOWN) - ) - } - - @Test - fun testGetMaxCompatibleE2EEVersionWhenGivenV1_0ShouldReturnV1_0() { - assertEquals( - E2EVersion.V1_0, - E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_0) - ) - } - - @Test - fun testGetMaxCompatibleE2EEVersionWhenGivenV1_1ShouldReturnV1_1() { - assertEquals( - E2EVersion.V1_1, - E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_1) - ) - } - - @Test - fun testGetMaxCompatibleE2EEVersionWhenGivenV1_2ShouldReturnV1_2() { - assertEquals( - E2EVersion.V1_2, - E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2) - ) - } - - @Test - fun testGetMaxCompatibleE2EEVersionWhenGivenV1AboveClientMaxShouldReturnClientV1Max() { - assertEquals( - E2EVersion.V1_2, - E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2) - ) - } - - @Test - fun testGetMaxCompatibleE2EEVersionWhenGivenV2_0ShouldReturnV2_0() { - assertEquals( - E2EVersion.V2_0, - E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_0) - ) - } - - @Test - fun testGetMaxCompatibleE2EEVersionWhenGivenV2_1ShouldReturnV2_1() { - assertEquals( - E2EVersion.V2_1, - E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_1) - ) - } - - @Test - fun testGetMaxCompatibleE2EEVersionWhenGivenV1_2ShouldNotApplyV2CeilingShouldReturnV1_2() { - assertEquals( - E2EVersion.V1_2, - E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V1_2) - ) - } - - @Test - fun testGetMaxCompatibleE2EEVersionWhenGivenV2_0ShouldNotApplyV1CeilingShouldReturnV2_0() { - assertEquals( - E2EVersion.V2_0, - E2EVersionHelper.getMaxCompatibleE2EEVersion(E2EVersion.V2_0) - ) - } - - @Test - fun testGetMaxCompatibleE2EEVersionWhenGivenV3_0ShouldReturnV2_1() { - assertEquals( - E2EVersion.V2_1, - E2EVersionHelper.getMaxCompatibleE2EEVersionFromString("3.0") - ) - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt index 8cd35ea38370..db54643b7786 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileDisplayActivity.kt @@ -320,30 +320,6 @@ class FileDisplayActivity : startMetadataSyncForRoot() handleBackPress() setupDrawer(menuItemId) - logOcsCredentials() - } - - @Suppress("DEPRECATION") - private fun logOcsCredentials() { - lifecycleScope.launch(Dispatchers.IO) { - val user = accountManager.user - val serverUrl = user.server.uri.toString() - val accountName = user.accountName - - try { - val client = clientFactory.create(user) - val username = client.userIdPlain - val authToken = client.credentials.authToken - Log_OC.d(TAG, "OCS credentials — serverUrl=$serverUrl") - Log_OC.d(TAG, "OCS credentials — accountName=$accountName username=$username authToken=$authToken") - } catch (e: CreationException) { - Log_OC.e( - TAG, - "OCS credentials — serverUrl=$serverUrl accountName=$accountName (client creation failed)", - e - ) - } - } } /** diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 1f3dab038465..74bbafb8c16e 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -199,14 +199,14 @@ class FileDetailSharingFragment : val fileActivity = getTypedActivity(FileActivity::class.java) lifecycleScope.launch(Dispatchers.IO) { + val user = user ?: return@launch + val file = file ?: return@launch val client = fileActivity?.clientRepository?.getOwncloudClient() ?: return@launch - val baseURL = user?.server?.uri?.toString() ?: return@launch + val sourceId = file.remoteId ?: return@launch + + val baseURL = user.server.uri.toString() val serverCredentials = client.toServerCredentials(baseURL) - val sourceId = file?.remoteId ?: return@launch - var internalLink = "" - if (accountManager.currentOwnCloudAccount != null && file != null) { - internalLink = createInternalLink(accountManager.currentOwnCloudAccount!!, file!!) - } + val internalLink = createInternalLink(user, file, capabilities) withContext(Dispatchers.Main) { binding.unifiedShare.initShareScreen( From ae7f65ec921adf84cae01ad55e19632c8aa5b03c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 14 Jul 2026 12:36:22 +0200 Subject: [PATCH 23/44] wip Signed-off-by: alperozturk96 --- .../ui/fragment/FileDetailSharingFragment.kt | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 74bbafb8c16e..3554d8d2be52 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -32,6 +32,7 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.button.MaterialButton import com.nextcloud.android.common.ui.share.initShareScreen +import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilitiesParser import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager @@ -50,11 +51,13 @@ import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.SharesType import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile +import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.accounts.AccountUtils import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.lib.resources.status.GetCapabilitiesRemoteOperation import com.owncloud.android.lib.resources.status.NextcloudVersion import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.operations.RefreshFolderOperation @@ -207,18 +210,28 @@ class FileDetailSharingFragment : val baseURL = user.server.uri.toString() val serverCredentials = client.toServerCredentials(baseURL) val internalLink = createInternalLink(user, file, capabilities) + val sharingJson = capabilities?.sharingJson ?: fetchFreshSharingJson(client) + val permissionPresets = + SharingCapabilitiesParser.parse(sharingJson)?.permissionPresets ?: emptyList() withContext(Dispatchers.Main) { binding.unifiedShare.initShareScreen( sourceId, internalLink, serverCredentials, - viewThemeUtils.files.getColorScheme(fileActivity) + viewThemeUtils.files.getColorScheme(fileActivity), + permissionPresets ) } } } + private fun fetchFreshSharingJson(client: OwnCloudClient): String? { + val capability = GetCapabilitiesRemoteOperation().execute(client).resultData ?: return null + fileDataStorageManager?.saveCapabilities(capability) + return capability.sharingJson + } + private fun initArguments(savedInstanceState: Bundle?) { val args = (savedInstanceState ?: arguments) ?: return file = args.getParcelableArgument(ARG_FILE, OCFile::class.java) From 424e41b62a2dcd8e69c236ea39b1269b1cde5186 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 14 Jul 2026 12:45:43 +0200 Subject: [PATCH 24/44] wip Signed-off-by: alperozturk96 --- .../ui/fragment/FileDetailSharingFragment.kt | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 3554d8d2be52..74bbafb8c16e 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -32,7 +32,6 @@ import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.button.MaterialButton import com.nextcloud.android.common.ui.share.initShareScreen -import com.nextcloud.android.common.ui.share.model.api.capabilities.SharingCapabilitiesParser import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User import com.nextcloud.client.account.UserAccountManager @@ -51,13 +50,11 @@ import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.SharesType import com.owncloud.android.datamodel.e2e.v2.decrypted.DecryptedFolderMetadataFile -import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.accounts.AccountUtils import com.owncloud.android.lib.common.operations.RemoteOperationResult import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.ShareType -import com.owncloud.android.lib.resources.status.GetCapabilitiesRemoteOperation import com.owncloud.android.lib.resources.status.NextcloudVersion import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.operations.RefreshFolderOperation @@ -210,28 +207,18 @@ class FileDetailSharingFragment : val baseURL = user.server.uri.toString() val serverCredentials = client.toServerCredentials(baseURL) val internalLink = createInternalLink(user, file, capabilities) - val sharingJson = capabilities?.sharingJson ?: fetchFreshSharingJson(client) - val permissionPresets = - SharingCapabilitiesParser.parse(sharingJson)?.permissionPresets ?: emptyList() withContext(Dispatchers.Main) { binding.unifiedShare.initShareScreen( sourceId, internalLink, serverCredentials, - viewThemeUtils.files.getColorScheme(fileActivity), - permissionPresets + viewThemeUtils.files.getColorScheme(fileActivity) ) } } } - private fun fetchFreshSharingJson(client: OwnCloudClient): String? { - val capability = GetCapabilitiesRemoteOperation().execute(client).resultData ?: return null - fileDataStorageManager?.saveCapabilities(capability) - return capability.sharingJson - } - private fun initArguments(savedInstanceState: Bundle?) { val args = (savedInstanceState ?: arguments) ?: return file = args.getParcelableArgument(ARG_FILE, OCFile::class.java) From 8adf8b335c86960335ce62ce30e17f74b8205956 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 22 Jul 2026 15:23:14 +0200 Subject: [PATCH 25/44] wip Signed-off-by: alperozturk96 # Conflicts: # gradle/libs.versions.toml --- gradle.properties | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gradle.properties b/gradle.properties index 91f3dedc1c10..aa2eb9ed75c0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,8 +23,8 @@ org.gradle.configuration-cache=true org.gradle.workers.max=4 # Needed for local libs -# org.gradle.dependency.verification=off -# systemProp.org.gradle.internal.publish.checksums.insecure=true +org.gradle.dependency.verification=off +systemProp.org.gradle.internal.publish.checksums.insecure=true # Enabled parallel sync for Gradle 9.4+ org.gradle.tooling.parallel=true From d1bc1f202fa5b554db6715a819efd4484adb0913 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 10:16:02 +0200 Subject: [PATCH 26/44] wip Signed-off-by: alperozturk96 --- gradle/verification-metadata.xml | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 66df39704ee7..9d1d0bcd7e82 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -127,6 +127,7 @@ + @@ -147,11 +148,17 @@ + + + + + + @@ -168,6 +175,8 @@ + + @@ -271,6 +280,7 @@ + @@ -293,6 +303,7 @@ + @@ -526,11 +537,15 @@ - + + + + + @@ -10787,7 +10802,7 @@ - + @@ -21036,6 +21051,14 @@ + + + + + + + + @@ -37811,6 +37834,11 @@ + + + + + From fc8375a352aae2396a669770690242e11ecf6f17 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Wed, 29 Jul 2026 10:49:20 +0200 Subject: [PATCH 27/44] wip Signed-off-by: alperozturk96 --- .../android/ui/fragment/FileDetailSharingFragment.kt | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 74bbafb8c16e..1e2a89712560 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -198,7 +198,7 @@ class FileDetailSharingFragment : val fileActivity = getTypedActivity(FileActivity::class.java) - lifecycleScope.launch(Dispatchers.IO) { + viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { val user = user ?: return@launch val file = file ?: return@launch val client = fileActivity?.clientRepository?.getOwncloudClient() ?: return@launch @@ -210,10 +210,11 @@ class FileDetailSharingFragment : withContext(Dispatchers.Main) { binding.unifiedShare.initShareScreen( - sourceId, - internalLink, - serverCredentials, - viewThemeUtils.files.getColorScheme(fileActivity) + viewModelStoreOwner = this@FileDetailSharingFragment, + sourceId = sourceId, + internalLink = internalLink, + credentials = serverCredentials, + colorScheme = viewThemeUtils.files.getColorScheme(fileActivity) ) } } From 9fe07f719638b68d580e2e243f70e0b60bb78880 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 10 Aug 2026 15:15:09 +0200 Subject: [PATCH 28/44] wip Signed-off-by: alperozturk96 --- .../android/ui/adapter/OCFileListAdapter.java | 9 ++- .../adapter/helper/OCFileListAdapterHelper.kt | 71 ++++++++++++++++--- 2 files changed, 69 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 3544b988e5f9..aa4d4ff72aa8 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -608,9 +608,12 @@ private void bindSharedAvatars(ListItemViewHolder holder, OCFile file) { if (sharedAvatars.getChildCount() > 0) { sharedAvatars.removeAllViews(); } - final var avatars = helper.getAvatarSharees(file, userId); - sharedAvatars.setAvatars(user, avatars, viewThemeUtils); - sharedAvatars.setOnClickListener(view -> ocFileListFragmentInterface.onShareIconClick(file)); + + helper.getAvatarSharees(file, user, userId, avatars -> { + sharedAvatars.setAvatars(user, avatars, viewThemeUtils); + sharedAvatars.setOnClickListener(view -> ocFileListFragmentInterface.onShareIconClick(file)); + return Unit.INSTANCE; + }); } private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt index 398b2fa30d0d..d29377925e5c 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt @@ -7,14 +7,22 @@ package com.owncloud.android.ui.adapter.helper +import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.android.common.ui.share.avatar.ShareAvatarRepository +import com.nextcloud.android.common.ui.share.model.api.share.Share +import com.nextcloud.client.account.User import com.nextcloud.client.database.entity.FileEntity import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.utils.extensions.filterFilenames import com.nextcloud.utils.extensions.isTempFile +import com.nextcloud.utils.extensions.toServerCredentials import com.owncloud.android.MainApp import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.lib.resources.shares.ShareeUser +import com.owncloud.android.lib.resources.status.NextcloudVersion import com.owncloud.android.utils.FileSortOrder import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.CoroutineScope @@ -23,11 +31,14 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.util.concurrent.ConcurrentHashMap class OCFileListAdapterHelper { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var job: Job? = null + private val remoteSharees = ConcurrentHashMap>() + @Suppress("LongParameterList") fun prepareFileList( directory: OCFile, @@ -56,17 +67,56 @@ class OCFileListAdapterHelper { } } - fun getAvatarSharees(file: OCFile, userId: String?): List { - val sharees = file.sharees - val ownerId = file.ownerId + fun getAvatarSharees( + file: OCFile, + user: User?, + userId: String?, + onComplete: (List) -> Unit + ) { + scope.launch { + val result = if (supportsUnifiedShare(user) && user != null) { + val credentials = getServerCredentials(user) ?: return@launch + val sourceId = file.remoteId + val repository = ShareAvatarRepository(credentials).fetchShareAvatars(sourceId) + repository?.toAvatarSharees() ?: listOf() + } else { + val sharees = file.sharees + val ownerId = file.ownerId + + val ownerSharee = if (!ownerId.isNullOrEmpty() && ownerId != userId) { + ShareeUser(ownerId, file.ownerDisplayName, ShareType.USER).takeIf { it !in sharees } + } else { + null + } + + listOfNotNull(ownerSharee) + sharees.asReversed() + } - val ownerSharee = if (!ownerId.isNullOrEmpty() && ownerId != userId) { - ShareeUser(ownerId, file.ownerDisplayName, ShareType.USER).takeIf { it !in sharees } - } else { - null + withContext(Dispatchers.Main) { + onComplete(result) + } } + } - return listOfNotNull(ownerSharee) + sharees.asReversed() + private fun supportsUnifiedShare(user: User?): Boolean { + return user?.server?.version?.isNewerOrEqual(NextcloudVersion.nextcloud_34) == true + } + + private fun List.toAvatarSharees(): List = asSequence() + .flatMap { share -> share.invitedRecipients } + .distinctBy { recipient -> recipient.value } + .map { recipient -> ShareeUser(recipient.value, recipient.displayName, ShareType.USER) } + .toList() + + @Suppress("TooGenericExceptionCaught") + private fun getServerCredentials(user: User): ServerCredentials? = try { + OwnCloudClientManagerFactory + .getDefaultSingleton() + .getClientFor(user.toOwnCloudAccount(), MainApp.getAppContext()) + .toServerCredentials(user.server.uri.toString()) + } catch (e: Exception) { + Log_OC.e(TAG, "Failed to create client for share avatars", e) + null } suspend fun prepareFileList( @@ -199,5 +249,10 @@ class OCFileListAdapterHelper { fun cleanup() { job?.cancel() job = null + remoteSharees.clear() + } + + companion object { + private val TAG = OCFileListAdapterHelper::class.java.simpleName } } From 11651952ea9057aa15bfc20b264b257042be9842 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 10 Aug 2026 15:17:46 +0200 Subject: [PATCH 29/44] wip Signed-off-by: alperozturk96 --- .../ui/adapter/helper/OCFileListAdapterHelper.kt | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt index d29377925e5c..13dc5aeaa429 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt @@ -31,14 +31,11 @@ import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import kotlinx.coroutines.withContext -import java.util.concurrent.ConcurrentHashMap class OCFileListAdapterHelper { private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) private var job: Job? = null - private val remoteSharees = ConcurrentHashMap>() - @Suppress("LongParameterList") fun prepareFileList( directory: OCFile, @@ -67,12 +64,7 @@ class OCFileListAdapterHelper { } } - fun getAvatarSharees( - file: OCFile, - user: User?, - userId: String?, - onComplete: (List) -> Unit - ) { + fun getAvatarSharees(file: OCFile, user: User?, userId: String?, onComplete: (List) -> Unit) { scope.launch { val result = if (supportsUnifiedShare(user) && user != null) { val credentials = getServerCredentials(user) ?: return@launch @@ -98,9 +90,8 @@ class OCFileListAdapterHelper { } } - private fun supportsUnifiedShare(user: User?): Boolean { - return user?.server?.version?.isNewerOrEqual(NextcloudVersion.nextcloud_34) == true - } + private fun supportsUnifiedShare(user: User?): Boolean = + user?.server?.version?.isNewerOrEqual(NextcloudVersion.nextcloud_34) == true private fun List.toAvatarSharees(): List = asSequence() .flatMap { share -> share.invitedRecipients } @@ -249,7 +240,6 @@ class OCFileListAdapterHelper { fun cleanup() { job?.cancel() job = null - remoteSharees.clear() } companion object { From 6b3ab1339b250d910a68cdf7ac824a1a8c00a8dd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Mon, 10 Aug 2026 15:56:54 +0200 Subject: [PATCH 30/44] wip Signed-off-by: alperozturk96 --- .../android/ui/adapter/helper/OCFileListAdapterHelper.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt index 13dc5aeaa429..7a67ddf7c7a4 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt @@ -64,6 +64,7 @@ class OCFileListAdapterHelper { } } + // TODO: check if server provides backward compatibility fun getAvatarSharees(file: OCFile, user: User?, userId: String?, onComplete: (List) -> Unit) { scope.launch { val result = if (supportsUnifiedShare(user) && user != null) { From 5e4c6c5fce7db95805a182cf0e86612a3a85ff9c Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 20 Aug 2026 14:40:40 +0200 Subject: [PATCH 31/44] wip Signed-off-by: alperozturk96 --- gradle/verification-metadata.xml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 9d1d0bcd7e82..85638065392b 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -21091,6 +21091,14 @@ + + + + + + + + From 8de7b88fc7bc4d72c342ced4ae81460169d4be58 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 11:21:02 +0200 Subject: [PATCH 32/44] wip Signed-off-by: alperozturk96 --- .../owncloud/android/ui/fragment/FileDetailSharingFragment.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 1e2a89712560..a9a0d696915c 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -212,6 +212,7 @@ class FileDetailSharingFragment : binding.unifiedShare.initShareScreen( viewModelStoreOwner = this@FileDetailSharingFragment, sourceId = sourceId, + filename = file.fileName, internalLink = internalLink, credentials = serverCredentials, colorScheme = viewThemeUtils.files.getColorScheme(fileActivity) From 1ed14331d0c8d604d849d2f8b235063163aa7651 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 21 Aug 2026 11:25:51 +0200 Subject: [PATCH 33/44] wip Signed-off-by: alperozturk96 --- .../owncloud/android/ui/fragment/FileDetailSharingFragment.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index a9a0d696915c..1e2a89712560 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -212,7 +212,6 @@ class FileDetailSharingFragment : binding.unifiedShare.initShareScreen( viewModelStoreOwner = this@FileDetailSharingFragment, sourceId = sourceId, - filename = file.fileName, internalLink = internalLink, credentials = serverCredentials, colorScheme = viewThemeUtils.files.getColorScheme(fileActivity) From 474f28e3f7615483f1e486d3d7272c8f0e7a36c2 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Tue, 1 Sep 2026 09:30:18 +0200 Subject: [PATCH 34/44] wip Signed-off-by: alperozturk96 --- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 18 +++++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 40551726cb3d..0f043a49edcd 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -5,7 +5,7 @@ androidCommonLibraryVersion = "0.33.2" androidGifDrawableVersion = "1.2.32" androidImageCropperVersion = "4.7.0" -androidLibraryVersion ="94372d7cbb8efe1c779ab8c2244a61510efafb28" +androidLibraryVersion ="308e123c67" androidOpensslVersion = "3.5.6" androidPluginVersion = "9.3.2" androidsvgVersion = "1.4" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 85638065392b..16ed5ec13799 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -570,7 +570,10 @@ - + + + + @@ -20120,6 +20123,11 @@ + + + + + @@ -21291,6 +21299,14 @@ + + + + + + + + From 7612c525f6c066ed38ab31048809b3d20fd4ccd3 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 08:40:32 +0200 Subject: [PATCH 35/44] wip Signed-off-by: alperozturk96 # Conflicts: # app/schemas/com.nextcloud.client.database.NextcloudDatabase/104.json --- .../nextcloud/client/database/entity/CapabilityEntity.kt | 4 +++- .../nextcloud/utils/extensions/OwnCloudClientExtensions.kt | 7 +++++++ .../owncloud/android/datamodel/FileDataStorageManager.java | 4 ++++ .../main/java/com/owncloud/android/db/ProviderMeta.java | 1 + 4 files changed, 15 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt index 0ff14aa9e944..253ddc547a1a 100644 --- a/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt +++ b/app/src/main/java/com/nextcloud/client/database/entity/CapabilityEntity.kt @@ -157,7 +157,9 @@ data class CapabilityEntity( @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_MOD_REWRITE_WORKING) val modRewriteWorking: Int?, @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE) - val chunkedUploadMaxSize: Long? + val chunkedUploadMaxSize: Long?, + @ColumnInfo(name = ProviderTableMeta.CAPABILITIES_SHARING_JSON) + val sharingJson: String? ) @Suppress("LongMethod", "ReturnCount") diff --git a/app/src/main/java/com/nextcloud/utils/extensions/OwnCloudClientExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/OwnCloudClientExtensions.kt index 857b3e8399b7..621bf4a75f48 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/OwnCloudClientExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/OwnCloudClientExtensions.kt @@ -9,6 +9,7 @@ package com.nextcloud.utils.extensions import android.content.Context +import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.common.NextcloudClient import com.owncloud.android.lib.common.OwnCloudClient import com.owncloud.android.lib.common.OwnCloudClientFactory @@ -34,3 +35,9 @@ fun OwnCloudClient.getVideoPreviewEndpoint(localFileId: Long, size: Int): String localFileId + "&x=" + size + "&y=" + size + "&a=1&forceIcon=0" + +/** + * Used in Android Common + */ +fun OwnCloudClient.toServerCredentials(baseURL: String): ServerCredentials = + ServerCredentials(baseURL, userIdPlain, credentials.authToken) diff --git a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java index 3bbd25b3ffa9..00f88f479123 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java +++ b/app/src/main/java/com/owncloud/android/datamodel/FileDataStorageManager.java @@ -2446,6 +2446,8 @@ private ContentValues createContentValues(String accountName, OCCapability capab contentValues.put(ProviderTableMeta.CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE, capability.getChunkedUploadMaxSize()); + contentValues.put(ProviderTableMeta.CAPABILITIES_SHARING_JSON, capability.getSharingJson()); + return contentValues; } @@ -2649,6 +2651,8 @@ private OCCapability createCapabilityInstance(Cursor cursor) { capability.setChunkedUploadMaxSize( getLong(cursor, ProviderTableMeta.CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE)); + + capability.setSharingJson(getString(cursor, ProviderTableMeta.CAPABILITIES_SHARING_JSON)); } return capability; diff --git a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java index 173b81fb9a19..66c31e3c6c56 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -299,6 +299,7 @@ static public class ProviderTableMeta implements BaseColumns { public static final String CAPABILITIES_CLIENT_INTEGRATION_JSON = "client_integration_json"; public static final String CAPABILITIES_MOD_REWRITE_WORKING = "mod_rewrite_working"; public static final String CAPABILITIES_CHUNKED_UPLOAD_MAX_SIZE = "chunked_upload_max_size"; + public static final String CAPABILITIES_SHARING_JSON = "sharing_json"; //Columns of Uploads table public static final String UPLOADS_LOCAL_PATH = "local_path"; From 8d7f457b2e0bd8a896a483250e02e9afc65dec78 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 09:15:58 +0200 Subject: [PATCH 36/44] wip Signed-off-by: alperozturk96 --- .../extensions/ServerCredentialsExtensions.kt | 24 ++++++++ .../utils/extensions/UserExtensions.kt | 27 +++++++++ .../adapter/helper/OCFileListAdapterHelper.kt | 50 ++++------------ .../ui/fragment/FileDetailSharingFragment.kt | 59 +++++++++---------- 4 files changed, 93 insertions(+), 67 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/utils/extensions/ServerCredentialsExtensions.kt create mode 100644 app/src/main/java/com/nextcloud/utils/extensions/UserExtensions.kt diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ServerCredentialsExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ServerCredentialsExtensions.kt new file mode 100644 index 000000000000..99fc3ae4e5d3 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/ServerCredentialsExtensions.kt @@ -0,0 +1,24 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.android.common.ui.network.http.NextcloudHttpClient +import com.nextcloud.android.common.ui.network.model.dataOrElse +import com.nextcloud.android.common.ui.share.repository.ShareRemoteRepository +import com.owncloud.android.lib.common.utils.Log_OC + +private const val TAG = "ServerCredentialsExtensions" + +suspend fun ServerCredentials.supportsUnifiedShare(): Boolean { + val capabilities = ShareRemoteRepository(NextcloudHttpClient.create(this)) + .fetchSharingCapabilities() + .dataOrElse { Log_OC.e(TAG, "Failed to fetch sharing capabilities") } + + return capabilities?.isUnifiedShareEnabled == true +} diff --git a/app/src/main/java/com/nextcloud/utils/extensions/UserExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/UserExtensions.kt new file mode 100644 index 000000000000..45d0cbf23ee2 --- /dev/null +++ b/app/src/main/java/com/nextcloud/utils/extensions/UserExtensions.kt @@ -0,0 +1,27 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nextcloud.utils.extensions + +import com.nextcloud.android.common.ui.network.auth.ServerCredentials +import com.nextcloud.client.account.User +import com.owncloud.android.MainApp +import com.owncloud.android.lib.common.OwnCloudClientManagerFactory +import com.owncloud.android.lib.common.utils.Log_OC + +private const val TAG = "UserExtensions" + +@Suppress("TooGenericExceptionCaught") +fun User.toServerCredentials(): ServerCredentials? = try { + OwnCloudClientManagerFactory + .getDefaultSingleton() + .getClientFor(toOwnCloudAccount(), MainApp.getAppContext()) + .toServerCredentials(server.uri.toString()) +} catch (e: Exception) { + Log_OC.e(TAG, "Failed to create client for $accountName", e) + null +} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt index 7a67ddf7c7a4..e5b24d7002cd 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/helper/OCFileListAdapterHelper.kt @@ -7,7 +7,6 @@ package com.owncloud.android.ui.adapter.helper -import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.share.avatar.ShareAvatarRepository import com.nextcloud.android.common.ui.share.model.api.share.Share import com.nextcloud.client.account.User @@ -15,14 +14,12 @@ import com.nextcloud.client.database.entity.FileEntity import com.nextcloud.client.preferences.AppPreferences import com.nextcloud.utils.extensions.filterFilenames import com.nextcloud.utils.extensions.isTempFile +import com.nextcloud.utils.extensions.supportsUnifiedShare import com.nextcloud.utils.extensions.toServerCredentials import com.owncloud.android.MainApp import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.lib.common.OwnCloudClientManagerFactory -import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.lib.resources.shares.ShareeUser -import com.owncloud.android.lib.resources.status.NextcloudVersion import com.owncloud.android.utils.FileSortOrder import com.owncloud.android.utils.MimeTypeUtil import kotlinx.coroutines.CoroutineScope @@ -64,25 +61,13 @@ class OCFileListAdapterHelper { } } - // TODO: check if server provides backward compatibility fun getAvatarSharees(file: OCFile, user: User?, userId: String?, onComplete: (List) -> Unit) { scope.launch { - val result = if (supportsUnifiedShare(user) && user != null) { - val credentials = getServerCredentials(user) ?: return@launch - val sourceId = file.remoteId - val repository = ShareAvatarRepository(credentials).fetchShareAvatars(sourceId) - repository?.toAvatarSharees() ?: listOf() + val credentials = user?.toServerCredentials() + val result = if (credentials != null && credentials.supportsUnifiedShare()) { + ShareAvatarRepository(credentials).fetchShareAvatars(file.remoteId)?.toAvatarSharees().orEmpty() } else { - val sharees = file.sharees - val ownerId = file.ownerId - - val ownerSharee = if (!ownerId.isNullOrEmpty() && ownerId != userId) { - ShareeUser(ownerId, file.ownerDisplayName, ShareType.USER).takeIf { it !in sharees } - } else { - null - } - - listOfNotNull(ownerSharee) + sharees.asReversed() + file.toLocalSharees(userId) } withContext(Dispatchers.Main) { @@ -91,8 +76,14 @@ class OCFileListAdapterHelper { } } - private fun supportsUnifiedShare(user: User?): Boolean = - user?.server?.version?.isNewerOrEqual(NextcloudVersion.nextcloud_34) == true + private fun OCFile.toLocalSharees(userId: String?): List { + val ownerSharee = ownerId + ?.takeIf { it.isNotEmpty() && it != userId } + ?.let { ShareeUser(it, ownerDisplayName, ShareType.USER) } + ?.takeIf { it !in sharees } + + return listOfNotNull(ownerSharee) + sharees.asReversed() + } private fun List.toAvatarSharees(): List = asSequence() .flatMap { share -> share.invitedRecipients } @@ -100,17 +91,6 @@ class OCFileListAdapterHelper { .map { recipient -> ShareeUser(recipient.value, recipient.displayName, ShareType.USER) } .toList() - @Suppress("TooGenericExceptionCaught") - private fun getServerCredentials(user: User): ServerCredentials? = try { - OwnCloudClientManagerFactory - .getDefaultSingleton() - .getClientFor(user.toOwnCloudAccount(), MainApp.getAppContext()) - .toServerCredentials(user.server.uri.toString()) - } catch (e: Exception) { - Log_OC.e(TAG, "Failed to create client for share avatars", e) - null - } - suspend fun prepareFileList( directory: OCFile, dataProvider: OCFileListAdapterDataProvider, @@ -242,8 +222,4 @@ class OCFileListAdapterHelper { job?.cancel() job = null } - - companion object { - private val TAG = OCFileListAdapterHelper::class.java.simpleName - } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 1e2a89712560..3d0d421cdc51 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -31,6 +31,7 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.button.MaterialButton +import com.nextcloud.android.common.ui.network.auth.ServerCredentials import com.nextcloud.android.common.ui.share.initShareScreen import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.account.User @@ -39,9 +40,9 @@ import com.nextcloud.client.di.Injectable import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.utils.IntentUtil import com.nextcloud.utils.extensions.getParcelableArgument -import com.nextcloud.utils.extensions.getTypedActivity import com.nextcloud.utils.extensions.mergeDistinctByToken import com.nextcloud.utils.extensions.setVisibleIf +import com.nextcloud.utils.extensions.supportsUnifiedShare import com.nextcloud.utils.extensions.toServerCredentials import com.nextcloud.utils.mdm.MDMConfig.shareViaUser import com.owncloud.android.R @@ -143,12 +144,7 @@ class FileDetailSharingFragment : binding?.pickContactEmailBtn?.setOnClickListener { checkContactPermission() } - // TODO: REPLACE FAKE CONDITION - if (user?.server?.version?.isNewerOrEqual(NextcloudVersion.nextcloud_34) == true || 2 < 4) { - showUnifiedShare() - } else { - fetchSharees() - } + setupUI() setupView() } @@ -186,8 +182,26 @@ class FileDetailSharingFragment : // endregion // region private methods - private fun showUnifiedShare() { + private fun setupUI() { + viewLifecycleOwner.lifecycleScope.launch { + val credentials = withContext(Dispatchers.IO) { + user?.toServerCredentials()?.takeIf { it.supportsUnifiedShare() } + } + + if (credentials == null) { + fetchSharees() + return@launch + } + + showUnifiedShare(credentials) + } + } + + private fun showUnifiedShare(credentials: ServerCredentials) { val binding = binding ?: return + val user = user ?: return + val file = file ?: return + val sourceId = file.remoteId ?: return binding.shareContainer.visibility = View.GONE binding.unifiedShare.visibility = View.VISIBLE @@ -196,28 +210,13 @@ class FileDetailSharingFragment : shimmerLayout.clearAnimation() shimmerLayout.visibility = View.GONE - val fileActivity = getTypedActivity(FileActivity::class.java) - - viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) { - val user = user ?: return@launch - val file = file ?: return@launch - val client = fileActivity?.clientRepository?.getOwncloudClient() ?: return@launch - val sourceId = file.remoteId ?: return@launch - - val baseURL = user.server.uri.toString() - val serverCredentials = client.toServerCredentials(baseURL) - val internalLink = createInternalLink(user, file, capabilities) - - withContext(Dispatchers.Main) { - binding.unifiedShare.initShareScreen( - viewModelStoreOwner = this@FileDetailSharingFragment, - sourceId = sourceId, - internalLink = internalLink, - credentials = serverCredentials, - colorScheme = viewThemeUtils.files.getColorScheme(fileActivity) - ) - } - } + binding.unifiedShare.initShareScreen( + viewModelStoreOwner = this, + sourceId = sourceId, + internalLink = createInternalLink(user, file, capabilities), + credentials = credentials, + colorScheme = viewThemeUtils.files.getColorScheme(requireContext()) + ) } private fun initArguments(savedInstanceState: Bundle?) { From 5a192d7c02c87143f37f4f1eb2c38f2a649e31bd Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 09:17:05 +0200 Subject: [PATCH 37/44] wip Signed-off-by: alperozturk96 --- .../nextcloud/utils/extensions/ServerCredentialsExtensions.kt | 2 +- .../main/java/com/nextcloud/utils/extensions/UserExtensions.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/utils/extensions/ServerCredentialsExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/ServerCredentialsExtensions.kt index 99fc3ae4e5d3..e30b915d210e 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/ServerCredentialsExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/ServerCredentialsExtensions.kt @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ diff --git a/app/src/main/java/com/nextcloud/utils/extensions/UserExtensions.kt b/app/src/main/java/com/nextcloud/utils/extensions/UserExtensions.kt index 45d0cbf23ee2..327b004bfa43 100644 --- a/app/src/main/java/com/nextcloud/utils/extensions/UserExtensions.kt +++ b/app/src/main/java/com/nextcloud/utils/extensions/UserExtensions.kt @@ -1,7 +1,7 @@ /* * Nextcloud - Android Client * - * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2026 Alper Ozturk * SPDX-License-Identifier: AGPL-3.0-or-later */ From d157d857008382baef357f8fdcd1f73e89980455 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 09:20:34 +0200 Subject: [PATCH 38/44] wip Signed-off-by: alperozturk96 --- .../105.json | 1333 +++++++++++++++++ .../client/database/NextcloudDatabase.kt | 3 +- .../com/owncloud/android/db/ProviderMeta.java | 2 +- 3 files changed, 1336 insertions(+), 2 deletions(-) create mode 100644 app/schemas/com.nextcloud.client.database.NextcloudDatabase/105.json diff --git a/app/schemas/com.nextcloud.client.database.NextcloudDatabase/105.json b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/105.json new file mode 100644 index 000000000000..34a6057e0369 --- /dev/null +++ b/app/schemas/com.nextcloud.client.database.NextcloudDatabase/105.json @@ -0,0 +1,1333 @@ +{ + "formatVersion": 1, + "database": { + "version": 105, + "identityHash": "9893dcfea42be7f14cf1d6ce04f36c7a", + "entities": [ + { + "tableName": "arbitrary_data", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `cloud_id` TEXT, `key` TEXT, `value` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "cloudId", + "columnName": "cloud_id", + "affinity": "TEXT" + }, + { + "fieldPath": "key", + "columnName": "key", + "affinity": "TEXT" + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "capabilities", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `assistant` INTEGER, `account` TEXT, `version_mayor` INTEGER, `version_minor` INTEGER, `version_micro` INTEGER, `version_string` TEXT, `version_edition` TEXT, `extended_support` INTEGER, `core_pollinterval` INTEGER, `sharing_api_enabled` INTEGER, `sharing_public_enabled` INTEGER, `sharing_public_password_enforced` INTEGER, `sharing_public_expire_date_enabled` INTEGER, `sharing_public_expire_date_days` INTEGER, `sharing_public_expire_date_enforced` INTEGER, `sharing_public_send_mail` INTEGER, `sharing_public_upload` INTEGER, `sharing_user_send_mail` INTEGER, `sharing_resharing` INTEGER, `sharing_federation_outgoing` INTEGER, `sharing_federation_incoming` INTEGER, `files_bigfilechunking` INTEGER, `files_undelete` INTEGER, `files_versioning` INTEGER, `external_links` INTEGER, `server_name` TEXT, `server_color` TEXT, `server_text_color` TEXT, `server_element_color` TEXT, `server_slogan` TEXT, `server_logo` TEXT, `background_url` TEXT, `end_to_end_encryption` INTEGER, `end_to_end_encryption_keys_exist` INTEGER, `end_to_end_encryption_api_version` TEXT, `activity` INTEGER, `background_default` INTEGER, `background_plain` INTEGER, `richdocument` INTEGER, `richdocument_mimetype_list` TEXT, `richdocument_direct_editing` INTEGER, `richdocument_direct_templates` INTEGER, `richdocument_optional_mimetype_list` TEXT, `sharing_public_ask_for_optional_password` INTEGER, `richdocument_product_name` TEXT, `direct_editing_etag` TEXT, `user_status` INTEGER, `user_status_supports_emoji` INTEGER, `etag` TEXT, `files_locking_version` TEXT, `groupfolders` INTEGER, `drop_account` INTEGER, `security_guard` INTEGER, `governance` INTEGER, `forbidden_filename_characters` TEXT, `forbidden_filenames` TEXT, `forbidden_filename_extensions` TEXT, `forbidden_filename_basenames` TEXT, `files_download_limit` INTEGER, `files_download_limit_default` INTEGER, `recommendation` INTEGER, `notes_folder_path` TEXT, `default_permissions` INTEGER, `user_status_supports_busy` INTEGER, `windows_compatible_filenames` INTEGER, `has_valid_subscription` INTEGER, `client_integration_json` TEXT, `mod_rewrite_working` INTEGER, `chunked_upload_max_size` INTEGER, `sharing_json` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "assistant", + "columnName": "assistant", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountName", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "versionMajor", + "columnName": "version_mayor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMinor", + "columnName": "version_minor", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionMicro", + "columnName": "version_micro", + "affinity": "INTEGER" + }, + { + "fieldPath": "versionString", + "columnName": "version_string", + "affinity": "TEXT" + }, + { + "fieldPath": "versionEditor", + "columnName": "version_edition", + "affinity": "TEXT" + }, + { + "fieldPath": "extendedSupport", + "columnName": "extended_support", + "affinity": "INTEGER" + }, + { + "fieldPath": "corePollinterval", + "columnName": "core_pollinterval", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingApiEnabled", + "columnName": "sharing_api_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicEnabled", + "columnName": "sharing_public_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicPasswordEnforced", + "columnName": "sharing_public_password_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnabled", + "columnName": "sharing_public_expire_date_enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateDays", + "columnName": "sharing_public_expire_date_days", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicExpireDateEnforced", + "columnName": "sharing_public_expire_date_enforced", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicSendMail", + "columnName": "sharing_public_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingPublicUpload", + "columnName": "sharing_public_upload", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingUserSendMail", + "columnName": "sharing_user_send_mail", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingResharing", + "columnName": "sharing_resharing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationOutgoing", + "columnName": "sharing_federation_outgoing", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingFederationIncoming", + "columnName": "sharing_federation_incoming", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesBigfilechunking", + "columnName": "files_bigfilechunking", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesUndelete", + "columnName": "files_undelete", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesVersioning", + "columnName": "files_versioning", + "affinity": "INTEGER" + }, + { + "fieldPath": "externalLinks", + "columnName": "external_links", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverName", + "columnName": "server_name", + "affinity": "TEXT" + }, + { + "fieldPath": "serverColor", + "columnName": "server_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverTextColor", + "columnName": "server_text_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverElementColor", + "columnName": "server_element_color", + "affinity": "TEXT" + }, + { + "fieldPath": "serverSlogan", + "columnName": "server_slogan", + "affinity": "TEXT" + }, + { + "fieldPath": "serverLogo", + "columnName": "server_logo", + "affinity": "TEXT" + }, + { + "fieldPath": "serverBackgroundUrl", + "columnName": "background_url", + "affinity": "TEXT" + }, + { + "fieldPath": "endToEndEncryption", + "columnName": "end_to_end_encryption", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionKeysExist", + "columnName": "end_to_end_encryption_keys_exist", + "affinity": "INTEGER" + }, + { + "fieldPath": "endToEndEncryptionApiVersion", + "columnName": "end_to_end_encryption_api_version", + "affinity": "TEXT" + }, + { + "fieldPath": "activity", + "columnName": "activity", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundDefault", + "columnName": "background_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "serverBackgroundPlain", + "columnName": "background_plain", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocument", + "columnName": "richdocument", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentMimetypeList", + "columnName": "richdocument_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "richdocumentDirectEditing", + "columnName": "richdocument_direct_editing", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentTemplates", + "columnName": "richdocument_direct_templates", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentOptionalMimetypeList", + "columnName": "richdocument_optional_mimetype_list", + "affinity": "TEXT" + }, + { + "fieldPath": "sharingPublicAskForOptionalPassword", + "columnName": "sharing_public_ask_for_optional_password", + "affinity": "INTEGER" + }, + { + "fieldPath": "richdocumentProductName", + "columnName": "richdocument_product_name", + "affinity": "TEXT" + }, + { + "fieldPath": "directEditingEtag", + "columnName": "direct_editing_etag", + "affinity": "TEXT" + }, + { + "fieldPath": "userStatus", + "columnName": "user_status", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsEmoji", + "columnName": "user_status_supports_emoji", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "filesLockingVersion", + "columnName": "files_locking_version", + "affinity": "TEXT" + }, + { + "fieldPath": "groupfolders", + "columnName": "groupfolders", + "affinity": "INTEGER" + }, + { + "fieldPath": "dropAccount", + "columnName": "drop_account", + "affinity": "INTEGER" + }, + { + "fieldPath": "securityGuard", + "columnName": "security_guard", + "affinity": "INTEGER" + }, + { + "fieldPath": "governance", + "columnName": "governance", + "affinity": "INTEGER" + }, + { + "fieldPath": "forbiddenFileNameCharacters", + "columnName": "forbidden_filename_characters", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFileNames", + "columnName": "forbidden_filenames", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFileNameExtensions", + "columnName": "forbidden_filename_extensions", + "affinity": "TEXT" + }, + { + "fieldPath": "forbiddenFilenameBaseNames", + "columnName": "forbidden_filename_basenames", + "affinity": "TEXT" + }, + { + "fieldPath": "filesDownloadLimit", + "columnName": "files_download_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "filesDownloadLimitDefault", + "columnName": "files_download_limit_default", + "affinity": "INTEGER" + }, + { + "fieldPath": "recommendation", + "columnName": "recommendation", + "affinity": "INTEGER" + }, + { + "fieldPath": "notesFolderPath", + "columnName": "notes_folder_path", + "affinity": "TEXT" + }, + { + "fieldPath": "defaultPermissions", + "columnName": "default_permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "userStatusSupportsBusy", + "columnName": "user_status_supports_busy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWCFEnabled", + "columnName": "windows_compatible_filenames", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasValidSubscription", + "columnName": "has_valid_subscription", + "affinity": "INTEGER" + }, + { + "fieldPath": "clientIntegrationJson", + "columnName": "client_integration_json", + "affinity": "TEXT" + }, + { + "fieldPath": "modRewriteWorking", + "columnName": "mod_rewrite_working", + "affinity": "INTEGER" + }, + { + "fieldPath": "chunkedUploadMaxSize", + "columnName": "chunked_upload_max_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharingJson", + "columnName": "sharing_json", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "external_links", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `icon_url` TEXT, `language` TEXT, `type` INTEGER, `name` TEXT, `url` TEXT, `redirect` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "iconUrl", + "columnName": "icon_url", + "affinity": "TEXT" + }, + { + "fieldPath": "language", + "columnName": "language", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT" + }, + { + "fieldPath": "url", + "columnName": "url", + "affinity": "TEXT" + }, + { + "fieldPath": "redirect", + "columnName": "redirect", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filelist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `filename` TEXT, `encrypted_filename` TEXT, `path` TEXT, `path_decrypted` TEXT, `parent` INTEGER, `created` INTEGER, `modified` INTEGER, `content_type` TEXT, `content_length` INTEGER, `media_path` TEXT, `file_owner` TEXT, `last_sync_date` INTEGER, `last_sync_date_for_data` INTEGER, `modified_at_last_sync_for_data` INTEGER, `etag` TEXT, `etag_on_server` TEXT, `share_by_link` INTEGER, `permissions` TEXT, `remote_id` TEXT, `local_id` INTEGER NOT NULL DEFAULT -1, `update_thumbnail` INTEGER, `is_downloading` INTEGER, `favorite` INTEGER, `hidden` INTEGER, `is_encrypted` INTEGER, `etag_in_conflict` TEXT, `shared_via_users` INTEGER, `mount_type` INTEGER, `has_preview` INTEGER, `unread_comments_count` INTEGER, `owner_id` TEXT, `owner_display_name` TEXT, `note` TEXT, `sharees` TEXT, `rich_workspace` TEXT, `metadata_size` TEXT, `metadata_live_photo` TEXT, `locked` INTEGER, `lock_type` INTEGER, `lock_owner` TEXT, `lock_owner_display_name` TEXT, `lock_owner_editor` TEXT, `lock_timestamp` INTEGER, `lock_timeout` INTEGER, `lock_token` TEXT, `tags` TEXT, `metadata_gps` TEXT, `e2e_counter` INTEGER, `internal_two_way_sync_timestamp` INTEGER, `internal_two_way_sync_result` TEXT, `uploaded` INTEGER, `is_read_only` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "filename", + "affinity": "TEXT" + }, + { + "fieldPath": "encryptedName", + "columnName": "encrypted_filename", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "pathDecrypted", + "columnName": "path_decrypted", + "affinity": "TEXT" + }, + { + "fieldPath": "parent", + "columnName": "parent", + "affinity": "INTEGER" + }, + { + "fieldPath": "creation", + "columnName": "created", + "affinity": "INTEGER" + }, + { + "fieldPath": "modified", + "columnName": "modified", + "affinity": "INTEGER" + }, + { + "fieldPath": "contentType", + "columnName": "content_type", + "affinity": "TEXT" + }, + { + "fieldPath": "contentLength", + "columnName": "content_length", + "affinity": "INTEGER" + }, + { + "fieldPath": "storagePath", + "columnName": "media_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountOwner", + "columnName": "file_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lastSyncDate", + "columnName": "last_sync_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastSyncDateForData", + "columnName": "last_sync_date_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAtLastSyncForData", + "columnName": "modified_at_last_sync_for_data", + "affinity": "INTEGER" + }, + { + "fieldPath": "etag", + "columnName": "etag", + "affinity": "TEXT" + }, + { + "fieldPath": "etagOnServer", + "columnName": "etag_on_server", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedViaLink", + "columnName": "share_by_link", + "affinity": "INTEGER" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "TEXT" + }, + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT" + }, + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true, + "defaultValue": "-1" + }, + { + "fieldPath": "updateThumbnail", + "columnName": "update_thumbnail", + "affinity": "INTEGER" + }, + { + "fieldPath": "isDownloading", + "columnName": "is_downloading", + "affinity": "INTEGER" + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "isEncrypted", + "columnName": "is_encrypted", + "affinity": "INTEGER" + }, + { + "fieldPath": "etagInConflict", + "columnName": "etag_in_conflict", + "affinity": "TEXT" + }, + { + "fieldPath": "sharedWithSharee", + "columnName": "shared_via_users", + "affinity": "INTEGER" + }, + { + "fieldPath": "mountType", + "columnName": "mount_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER" + }, + { + "fieldPath": "unreadCommentsCount", + "columnName": "unread_comments_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "ownerId", + "columnName": "owner_id", + "affinity": "TEXT" + }, + { + "fieldPath": "ownerDisplayName", + "columnName": "owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "sharees", + "columnName": "sharees", + "affinity": "TEXT" + }, + { + "fieldPath": "richWorkspace", + "columnName": "rich_workspace", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataSize", + "columnName": "metadata_size", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataLivePhoto", + "columnName": "metadata_live_photo", + "affinity": "TEXT" + }, + { + "fieldPath": "locked", + "columnName": "locked", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockType", + "columnName": "lock_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockOwner", + "columnName": "lock_owner", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerDisplayName", + "columnName": "lock_owner_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "lockOwnerEditor", + "columnName": "lock_owner_editor", + "affinity": "TEXT" + }, + { + "fieldPath": "lockTimestamp", + "columnName": "lock_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockTimeout", + "columnName": "lock_timeout", + "affinity": "INTEGER" + }, + { + "fieldPath": "lockToken", + "columnName": "lock_token", + "affinity": "TEXT" + }, + { + "fieldPath": "tags", + "columnName": "tags", + "affinity": "TEXT" + }, + { + "fieldPath": "metadataGPS", + "columnName": "metadata_gps", + "affinity": "TEXT" + }, + { + "fieldPath": "e2eCounter", + "columnName": "e2e_counter", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySync", + "columnName": "internal_two_way_sync_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "internalTwoWaySyncResult", + "columnName": "internal_two_way_sync_result", + "affinity": "TEXT" + }, + { + "fieldPath": "uploaded", + "columnName": "uploaded", + "affinity": "INTEGER" + }, + { + "fieldPath": "isReadOnly", + "columnName": "is_read_only", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "filesystem", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `is_folder` INTEGER, `found_at` INTEGER, `upload_triggered` INTEGER, `syncedfolder_id` TEXT, `crc32` TEXT, `modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "fileIsFolder", + "columnName": "is_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileFoundRecently", + "columnName": "found_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSentForUpload", + "columnName": "upload_triggered", + "affinity": "INTEGER" + }, + { + "fieldPath": "syncedFolderId", + "columnName": "syncedfolder_id", + "affinity": "TEXT" + }, + { + "fieldPath": "crc32", + "columnName": "crc32", + "affinity": "TEXT" + }, + { + "fieldPath": "fileModified", + "columnName": "modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "ocshares", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `file_source` INTEGER, `item_source` INTEGER, `share_type` INTEGER, `shate_with` TEXT, `path` TEXT, `permissions` INTEGER, `shared_date` INTEGER, `expiration_date` INTEGER, `token` TEXT, `shared_with_display_name` TEXT, `is_directory` INTEGER, `user_id` TEXT, `id_remote_shared` INTEGER, `owner_share` TEXT, `is_password_protected` INTEGER, `note` TEXT, `hide_download` INTEGER, `share_link` TEXT, `share_label` TEXT, `download_limit_limit` INTEGER, `download_limit_count` INTEGER, `attributes` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "fileSource", + "columnName": "file_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "itemSource", + "columnName": "item_source", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareType", + "columnName": "share_type", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareWith", + "columnName": "shate_with", + "affinity": "TEXT" + }, + { + "fieldPath": "path", + "columnName": "path", + "affinity": "TEXT" + }, + { + "fieldPath": "permissions", + "columnName": "permissions", + "affinity": "INTEGER" + }, + { + "fieldPath": "sharedDate", + "columnName": "shared_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "expirationDate", + "columnName": "expiration_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "token", + "columnName": "token", + "affinity": "TEXT" + }, + { + "fieldPath": "shareWithDisplayName", + "columnName": "shared_with_display_name", + "affinity": "TEXT" + }, + { + "fieldPath": "isDirectory", + "columnName": "is_directory", + "affinity": "INTEGER" + }, + { + "fieldPath": "userId", + "columnName": "user_id", + "affinity": "TEXT" + }, + { + "fieldPath": "idRemoteShared", + "columnName": "id_remote_shared", + "affinity": "INTEGER" + }, + { + "fieldPath": "accountOwner", + "columnName": "owner_share", + "affinity": "TEXT" + }, + { + "fieldPath": "isPasswordProtected", + "columnName": "is_password_protected", + "affinity": "INTEGER" + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT" + }, + { + "fieldPath": "hideDownload", + "columnName": "hide_download", + "affinity": "INTEGER" + }, + { + "fieldPath": "shareLink", + "columnName": "share_link", + "affinity": "TEXT" + }, + { + "fieldPath": "shareLabel", + "columnName": "share_label", + "affinity": "TEXT" + }, + { + "fieldPath": "downloadLimitLimit", + "columnName": "download_limit_limit", + "affinity": "INTEGER" + }, + { + "fieldPath": "downloadLimitCount", + "columnName": "download_limit_count", + "affinity": "INTEGER" + }, + { + "fieldPath": "attributes", + "columnName": "attributes", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "synced_folders", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `wifi_only` INTEGER, `charging_only` INTEGER, `existing` INTEGER, `enabled` INTEGER, `enabled_timestamp_ms` INTEGER, `subfolder_by_date` INTEGER, `account` TEXT, `upload_option` INTEGER, `name_collision_policy` INTEGER, `type` INTEGER, `hidden` INTEGER, `sub_folder_rule` INTEGER, `exclude_hidden` INTEGER, `last_scan_timestamp_ms` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "wifiOnly", + "columnName": "wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "chargingOnly", + "columnName": "charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "existing", + "columnName": "existing", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabled", + "columnName": "enabled", + "affinity": "INTEGER" + }, + { + "fieldPath": "enabledTimestampMs", + "columnName": "enabled_timestamp_ms", + "affinity": "INTEGER" + }, + { + "fieldPath": "subfolderByDate", + "columnName": "subfolder_by_date", + "affinity": "INTEGER" + }, + { + "fieldPath": "account", + "columnName": "account", + "affinity": "TEXT" + }, + { + "fieldPath": "uploadAction", + "columnName": "upload_option", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER" + }, + { + "fieldPath": "hidden", + "columnName": "hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "subFolderRule", + "columnName": "sub_folder_rule", + "affinity": "INTEGER" + }, + { + "fieldPath": "excludeHidden", + "columnName": "exclude_hidden", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastScanTimestampMs", + "columnName": "last_scan_timestamp_ms", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "list_of_uploads", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `local_path` TEXT, `remote_path` TEXT, `account_name` TEXT, `file_size` INTEGER, `status` INTEGER, `local_behaviour` INTEGER, `upload_time` INTEGER, `name_collision_policy` INTEGER, `is_create_remote_folder` INTEGER, `upload_end_timestamp` INTEGER, `upload_end_timestamp_long` INTEGER, `last_result` INTEGER, `is_while_charging_only` INTEGER, `is_wifi_only` INTEGER, `created_by` INTEGER, `folder_unlock_token` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "localPath", + "columnName": "local_path", + "affinity": "TEXT" + }, + { + "fieldPath": "remotePath", + "columnName": "remote_path", + "affinity": "TEXT" + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + }, + { + "fieldPath": "fileSize", + "columnName": "file_size", + "affinity": "INTEGER" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "INTEGER" + }, + { + "fieldPath": "localBehaviour", + "columnName": "local_behaviour", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadTime", + "columnName": "upload_time", + "affinity": "INTEGER" + }, + { + "fieldPath": "nameCollisionPolicy", + "columnName": "name_collision_policy", + "affinity": "INTEGER" + }, + { + "fieldPath": "isCreateRemoteFolder", + "columnName": "is_create_remote_folder", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestamp", + "columnName": "upload_end_timestamp", + "affinity": "INTEGER" + }, + { + "fieldPath": "uploadEndTimestampLong", + "columnName": "upload_end_timestamp_long", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastResult", + "columnName": "last_result", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWhileChargingOnly", + "columnName": "is_while_charging_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "isWifiOnly", + "columnName": "is_wifi_only", + "affinity": "INTEGER" + }, + { + "fieldPath": "createdBy", + "columnName": "created_by", + "affinity": "INTEGER" + }, + { + "fieldPath": "folderUnlockToken", + "columnName": "folder_unlock_token", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "virtual", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `type` TEXT, `ocfile_id` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "ocFileId", + "columnName": "ocfile_id", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "offline_operations", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT, `offline_operations_parent_oc_file_id` INTEGER, `offline_operations_path` TEXT, `offline_operations_type` TEXT, `offline_operations_file_name` TEXT, `offline_operations_created_at` INTEGER, `offline_operations_modified_at` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "parentOCFileId", + "columnName": "offline_operations_parent_oc_file_id", + "affinity": "INTEGER" + }, + { + "fieldPath": "path", + "columnName": "offline_operations_path", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "offline_operations_type", + "affinity": "TEXT" + }, + { + "fieldPath": "filename", + "columnName": "offline_operations_file_name", + "affinity": "TEXT" + }, + { + "fieldPath": "createdAt", + "columnName": "offline_operations_created_at", + "affinity": "INTEGER" + }, + { + "fieldPath": "modifiedAt", + "columnName": "offline_operations_modified_at", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "recommended_files", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL, `directory` TEXT NOT NULL, `extension` TEXT NOT NULL, `mime_type` TEXT NOT NULL, `has_preview` INTEGER NOT NULL, `reason` TEXT NOT NULL, `timestamp` INTEGER NOT NULL, `account_name` TEXT)", + "fields": [ + { + "fieldPath": "id", + "columnName": "_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "directory", + "columnName": "directory", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "extension", + "columnName": "extension", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "mimeType", + "columnName": "mime_type", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "hasPreview", + "columnName": "has_preview", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "reason", + "columnName": "reason", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "timestamp", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "account_name", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "_id" + ] + } + }, + { + "tableName": "assistant", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `accountName` TEXT, `type` TEXT, `status` TEXT, `userId` TEXT, `appId` TEXT, `input` TEXT, `output` TEXT, `completionExpectedAt` INTEGER, `progress` INTEGER, `lastUpdated` INTEGER, `scheduledAt` INTEGER, `endedAt` INTEGER)", + "fields": [ + { + "fieldPath": "id", + "columnName": "id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "accountName", + "columnName": "accountName", + "affinity": "TEXT" + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "TEXT" + }, + { + "fieldPath": "status", + "columnName": "status", + "affinity": "TEXT" + }, + { + "fieldPath": "userId", + "columnName": "userId", + "affinity": "TEXT" + }, + { + "fieldPath": "appId", + "columnName": "appId", + "affinity": "TEXT" + }, + { + "fieldPath": "input", + "columnName": "input", + "affinity": "TEXT" + }, + { + "fieldPath": "output", + "columnName": "output", + "affinity": "TEXT" + }, + { + "fieldPath": "completionExpectedAt", + "columnName": "completionExpectedAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "progress", + "columnName": "progress", + "affinity": "INTEGER" + }, + { + "fieldPath": "lastUpdated", + "columnName": "lastUpdated", + "affinity": "INTEGER" + }, + { + "fieldPath": "scheduledAt", + "columnName": "scheduledAt", + "affinity": "INTEGER" + }, + { + "fieldPath": "endedAt", + "columnName": "endedAt", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "id" + ] + } + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '9893dcfea42be7f14cf1d6ce04f36c7a')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index 49b0763400da..a32d16226cf1 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -103,7 +103,8 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 100, to = 101, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 101, to = 102, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 102, to = 103, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), - AutoMigration(from = 103, to = 104) + AutoMigration(from = 103, to = 104), + AutoMigration(from = 104, to = 105, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), ], exportSchema = true ) diff --git a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java index 66c31e3c6c56..e13562b314cd 100644 --- a/app/src/main/java/com/owncloud/android/db/ProviderMeta.java +++ b/app/src/main/java/com/owncloud/android/db/ProviderMeta.java @@ -24,7 +24,7 @@ */ public class ProviderMeta { public static final String DB_NAME = "filelist"; - public static final int DB_VERSION = 104; + public static final int DB_VERSION = 105; private ProviderMeta() { // No instance From 6f45ded7db3dd7ef0950a41fa899a677d10a8224 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Thu, 3 Sep 2026 15:01:40 +0200 Subject: [PATCH 39/44] wip Signed-off-by: alperozturk96 --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0f043a49edcd..8bdb8945bf1c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ androidGifDrawableVersion = "1.2.32" androidImageCropperVersion = "4.7.0" androidLibraryVersion ="308e123c67" androidOpensslVersion = "3.5.6" -androidPluginVersion = "9.3.2" +androidPluginVersion = "9.4.0" androidsvgVersion = "1.4" androidxTestVersion = "1.7.0" annotationVersion = "1.10.0" From 5b9c74440c806f95aa922c817d77556af8ef6370 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 11:02:25 +0200 Subject: [PATCH 40/44] wip Signed-off-by: alperozturk96 --- .../ui/fragment/FileDetailSharingFragment.kt | 80 +++++++++---------- 1 file changed, 36 insertions(+), 44 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 3d0d421cdc51..1618002d69c3 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -130,23 +130,10 @@ class FileDetailSharingFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - fileActivity ?: return fileDataStorageManager = fileActivity?.storageManager fileOperationsHelper = fileActivity?.fileOperationsHelper - - startAnimation() - - val userId = getUserId() - - setupInternalShares(userId) - setupExternalShares(userId) - - binding?.pickContactEmailBtn?.setOnClickListener { checkContactPermission() } - - setupUI() - - setupView() + initializeSharingMode() } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { @@ -182,19 +169,22 @@ class FileDetailSharingFragment : // endregion // region private methods - private fun setupUI() { + private fun initializeSharingMode() { viewLifecycleOwner.lifecycleScope.launch { - val credentials = withContext(Dispatchers.IO) { + withContext(Dispatchers.IO) { user?.toServerCredentials()?.takeIf { it.supportsUnifiedShare() } - } + }?.let(::showUnifiedShare) ?: showLegacyShare() + } + } - if (credentials == null) { - fetchSharees() - return@launch - } + private fun showLegacyShare() { + val userId = getUserId() + setupInternalShares(userId) + setupExternalShares(userId) - showUnifiedShare(credentials) - } + startShimmerAnimation() + setupLegacyShareUi() + fetchSharees() } private fun showUnifiedShare(credentials: ServerCredentials) { @@ -264,7 +254,7 @@ class FileDetailSharingFragment : private fun createShareListLayoutManager(): LinearLayoutManager = LinearLayoutManager(requireContext()) - private fun startAnimation() { + private fun startShimmerAnimation() { val blinkAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.blink) binding?.shimmerLayout?.getRoot()?.startAnimation(blinkAnimation) } @@ -285,16 +275,16 @@ class FileDetailSharingFragment : if (result) { refreshCapabilitiesFromDB() refreshSharesFromDB() - stopLoadingAnimationAndShowShareContainer() + hideShimmerAndShowShareContainer() return@launch } - stopLoadingAnimationAndShowShareContainer() + hideShimmerAndShowShareContainer() DisplayUtils.showSnackMessage(this@FileDetailSharingFragment, R.string.error_fetching_sharees) } } - private fun stopLoadingAnimationAndShowShareContainer() { + private fun hideShimmerAndShowShareContainer() { binding?.run { shimmerLayout.root.run { clearAnimation() @@ -314,25 +304,27 @@ class FileDetailSharingFragment : } } - private fun setupView() { + private fun setupLegacyShareUi() { resetSearchView() setShareWithYou() binding?.run { + pickContactEmailBtn.setOnClickListener { checkContactPermission() } + FileDetailSharingFragmentHelper.setupSearchView( fileActivity?.getSystemService(Context.SEARCH_SERVICE) as SearchManager?, searchView, fileActivity?.componentName ) - themeView(this) - setupShareList(this) + applyLegacyShareTheme(this) + setupShowAllButtons(this) if (file?.canReshare() == true && !FileDetailSharingFragmentHelper.isPublicShareDisabled(capabilities)) { val parentFile = file?.parentId?.let { fileDataStorageManager?.getFileById(it) } - setupShareView(this, parentFile) + configureCreateLinkSection(this, parentFile) } else { - setupDisabledShareView(this) + disableResharingUi(this) } checkShareViaUser() @@ -352,7 +344,7 @@ class FileDetailSharingFragment : } } - private fun themeView(binding: FileDetailsSharingFragmentBinding) { + private fun applyLegacyShareTheme(binding: FileDetailsSharingFragmentBinding) { binding.run { viewThemeUtils.material.run { themeSearchCardView(searchCardWrapper) @@ -374,7 +366,7 @@ class FileDetailSharingFragment : } } - private fun setupShareList(binding: FileDetailsSharingFragmentBinding) { + private fun setupShowAllButtons(binding: FileDetailsSharingFragmentBinding) { binding.run { sharesListInternalShowAll.setOnClickListener { expandOrCollapseAdapter(internalShareeListAdapter, sharesListInternalShowAll) @@ -394,7 +386,7 @@ class FileDetailSharingFragment : button.setText(actionTextId) } - private fun setupViewForEncryptedShare(binding: FileDetailsSharingFragmentBinding) { + private fun configureEncryptedShareUi(binding: FileDetailsSharingFragmentBinding) { binding.run { internalShareHeadline.text = resources.getString(R.string.internal_share_headline_end_to_end_encrypted) internalShareDescription.visibility = View.VISIBLE @@ -416,7 +408,7 @@ class FileDetailSharingFragment : searchView.setQueryHint(resources.getString(R.string.secure_share_search)) if (file?.isSharedViaLink == true) { - setupSearchViewForSharedLink(searchView) + disableSearchViewForFileDrop(searchView) } } } @@ -428,16 +420,16 @@ class FileDetailSharingFragment : binding.createLink.visibility = View.GONE } - private fun setupSearchViewForSharedLink(searchView: SearchView) { + private fun disableSearchViewForFileDrop(searchView: SearchView) { searchView.setQueryHint(resources.getString(R.string.share_not_allowed_when_file_drop)) searchView.inputType = InputType.TYPE_NULL toggleSearchViewEnable(searchView, false) } - private fun setupShareView(binding: FileDetailsSharingFragmentBinding, parentFile: OCFile?) { + private fun configureCreateLinkSection(binding: FileDetailsSharingFragmentBinding, parentFile: OCFile?) { binding.run { if (file?.isEncrypted == true || (parentFile != null && parentFile.isEncrypted)) { - setupViewForEncryptedShare(this) + configureEncryptedShareUi(this) } else { createLink.setText(R.string.create_link) searchView.setQueryHint(getResources().getString(R.string.share_search_internal)) @@ -447,7 +439,7 @@ class FileDetailSharingFragment : } } - private fun setupDisabledShareView(binding: FileDetailsSharingFragmentBinding) { + private fun disableResharingUi(binding: FileDetailsSharingFragmentBinding) { binding.run { searchView.setQueryHint(getResources().getString(R.string.resharing_is_not_allowed)) createLink.visibility = View.GONE @@ -560,9 +552,9 @@ class FileDetailSharingFragment : FileActivity.showShareLinkDialog(fileActivity, file, publicShare.shareLink) } - private fun refreshUiFromDB() { + private fun refreshLegacyShareUiFromDb() { refreshSharesFromDB() - setupView() + setupLegacyShareUi() } private fun unShareWith(share: OCShare) { @@ -854,9 +846,9 @@ class FileDetailSharingFragment : } if (result.isSuccess) { - refreshUiFromDB() + refreshLegacyShareUiFromDb() } else { - setupView() + setupLegacyShareUi() } } From 639e7bd228220e599bffdf7599a84fa0a2b8ba34 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 11:05:39 +0200 Subject: [PATCH 41/44] wip Signed-off-by: alperozturk96 --- .../owncloud/android/ui/fragment/FileDetailSharingFragment.kt | 4 +++- app/src/main/res/layout/file_details_sharing_shimmer.xml | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 1618002d69c3..872d0327c502 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -255,8 +255,10 @@ class FileDetailSharingFragment : private fun createShareListLayoutManager(): LinearLayoutManager = LinearLayoutManager(requireContext()) private fun startShimmerAnimation() { + val shimmerLayout = binding?.shimmerLayout?.root ?: return + shimmerLayout.visibility = View.VISIBLE val blinkAnimation = AnimationUtils.loadAnimation(requireContext(), R.anim.blink) - binding?.shimmerLayout?.getRoot()?.startAnimation(blinkAnimation) + shimmerLayout.startAnimation(blinkAnimation) } private fun fetchSharees() { diff --git a/app/src/main/res/layout/file_details_sharing_shimmer.xml b/app/src/main/res/layout/file_details_sharing_shimmer.xml index f679386e4a1b..99a1c8cbdfb1 100644 --- a/app/src/main/res/layout/file_details_sharing_shimmer.xml +++ b/app/src/main/res/layout/file_details_sharing_shimmer.xml @@ -7,7 +7,7 @@ --> From 2ab0e55701cabcc95eddfcc0bc8372d03ca6ca70 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 11:17:38 +0200 Subject: [PATCH 42/44] wip Signed-off-by: alperozturk96 --- .../android/ui/fragment/FileDetailSharingFragment.kt | 3 +++ app/src/main/res/layout/file_details_sharing_fragment.xml | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt index 872d0327c502..adc057fa3611 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.kt @@ -178,6 +178,8 @@ class FileDetailSharingFragment : } private fun showLegacyShare() { + binding?.sharingModeProgress?.visibility = View.GONE + val userId = getUserId() setupInternalShares(userId) setupExternalShares(userId) @@ -193,6 +195,7 @@ class FileDetailSharingFragment : val file = file ?: return val sourceId = file.remoteId ?: return + binding.sharingModeProgress.visibility = View.GONE binding.shareContainer.visibility = View.GONE binding.unifiedShare.visibility = View.VISIBLE diff --git a/app/src/main/res/layout/file_details_sharing_fragment.xml b/app/src/main/res/layout/file_details_sharing_fragment.xml index 54daff819b9f..00be18b0f850 100644 --- a/app/src/main/res/layout/file_details_sharing_fragment.xml +++ b/app/src/main/res/layout/file_details_sharing_fragment.xml @@ -243,4 +243,12 @@ android:id="@+id/shimmer_layout" layout="@layout/file_details_sharing_shimmer" /> + + From bdbacb9d2f9c27e5b44445915e093996b699bb53 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 13:48:05 +0200 Subject: [PATCH 43/44] wip Signed-off-by: alperozturk96 --- gradle.properties | 4 ++-- gradle/libs.versions.toml | 2 +- gradle/verification-metadata.xml | 25 +++++++++++++++++++++++++ settings.gradle.kts | 5 +---- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/gradle.properties b/gradle.properties index aa2eb9ed75c0..91f3dedc1c10 100644 --- a/gradle.properties +++ b/gradle.properties @@ -23,8 +23,8 @@ org.gradle.configuration-cache=true org.gradle.workers.max=4 # Needed for local libs -org.gradle.dependency.verification=off -systemProp.org.gradle.internal.publish.checksums.insecure=true +# org.gradle.dependency.verification=off +# systemProp.org.gradle.internal.publish.checksums.insecure=true # Enabled parallel sync for Gradle 9.4+ org.gradle.tooling.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8bdb8945bf1c..43bc036a1b2b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,7 +2,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later [versions] -androidCommonLibraryVersion = "0.33.2" +androidCommonLibraryVersion = "a387470ae5" androidGifDrawableVersion = "1.2.32" androidImageCropperVersion = "4.7.0" androidLibraryVersion ="308e123c67" diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 16ed5ec13799..93e08a33b274 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -192,6 +192,7 @@ + @@ -22872,6 +22873,14 @@ + + + + + + + + @@ -23132,6 +23141,14 @@ + + + + + + + + @@ -23388,6 +23405,14 @@ + + + + + + + + diff --git a/settings.gradle.kts b/settings.gradle.kts index a9ccc5b9d7c9..60a88fda3bd4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -54,10 +54,6 @@ includeBuild("../android_library") { /* Needed for local android common library - - -*/ - includeBuild("../android_common") { dependencySubstitution { substitute(module("com.github.nextcloud.android-common:core")) @@ -67,5 +63,6 @@ includeBuild("../android_common") { .using(project(":ui")) } } +*/ include(":app", ":appscan") From 30c4ce697cc21adaa6aa4c7ab26046e25192ff15 Mon Sep 17 00:00:00 2001 From: alperozturk96 Date: Fri, 4 Sep 2026 13:57:23 +0200 Subject: [PATCH 44/44] wip Signed-off-by: alperozturk96 --- .../java/com/nextcloud/client/database/NextcloudDatabase.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt index a32d16226cf1..9cde53a5b88c 100644 --- a/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt +++ b/app/src/main/java/com/nextcloud/client/database/NextcloudDatabase.kt @@ -104,7 +104,7 @@ import com.owncloud.android.db.ProviderMeta AutoMigration(from = 101, to = 102, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 102, to = 103, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 103, to = 104), - AutoMigration(from = 104, to = 105, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), + AutoMigration(from = 104, to = 105, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class) ], exportSchema = true )