Merge remote-tracking branch 'origin/master' into dev

This commit is contained in:
Tobias Kaminsky 2025-06-11 09:59:38 +02:00
commit 0c69072e9d
42 changed files with 3694 additions and 1390 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,268 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.utils
import com.google.gson.Gson
import com.owncloud.android.datamodel.quickPermission.QuickPermissionType
import com.owncloud.android.lib.resources.shares.OCShare
import com.owncloud.android.lib.resources.shares.ShareType
import com.owncloud.android.lib.resources.shares.attributes.ShareAttributes
import com.owncloud.android.ui.fragment.util.SharePermissionManager
import junit.framework.TestCase.assertEquals
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertNotNull
import junit.framework.TestCase.assertTrue
import org.junit.Test
@Suppress("TooManyFunctions")
class SharePermissionManagerTest {
private fun createShare(sharePermission: Int, isFolder: Boolean = false, attributesJson: String? = null): OCShare {
return if (isFolder) {
OCShare("/test")
.apply {
permissions = sharePermission
attributes = attributesJson
shareType = ShareType.INTERNAL
sharedDate = 1188206955
shareWith = "User 1"
sharedWithDisplayName = "User 1"
}
} else {
OCShare("/test.png")
.apply {
permissions = sharePermission
attributes = attributesJson
shareType = ShareType.INTERNAL
sharedDate = 1188206955
shareWith = "User 1"
sharedWithDisplayName = "User 1"
}
}.apply {
this.isFolder = isFolder
}
}
// region Permission change tests
@Test
fun testTogglePermissionShouldAddPermissionFlagWhenChecked() {
val initialPermission = OCShare.READ_PERMISSION_FLAG
val updatedPermission =
SharePermissionManager.togglePermission(true, initialPermission, OCShare.UPDATE_PERMISSION_FLAG)
val updatedShare = createShare(updatedPermission)
assertTrue(SharePermissionManager.isCustomPermission(updatedShare))
}
@Test
fun testTogglePermissionShouldRemovePermissionFlagWhenUnchecked() {
val initialPermission = OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG
val updatedPermission =
SharePermissionManager.togglePermission(false, initialPermission, OCShare.UPDATE_PERMISSION_FLAG)
val updatedShare = createShare(updatedPermission)
assertTrue(SharePermissionManager.isViewOnly(updatedShare))
}
// endregion
// region HasPermissions tests
@Test
fun testHasPermissionShouldReturnTrueIfPermissionPresent() {
val permission = OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG
assertTrue(SharePermissionManager.hasPermission(permission, OCShare.UPDATE_PERMISSION_FLAG))
}
@Test
fun testHasPermissionShouldReturnFalseIfPermissionNotPresent() {
val permission = OCShare.READ_PERMISSION_FLAG
assertFalse(SharePermissionManager.hasPermission(permission, OCShare.UPDATE_PERMISSION_FLAG))
}
// endregion
// region Helper Method Tests
@Test
fun testCanEditShouldReturnTrueIfAllPermissionsPresent() {
val share = createShare(OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER, isFolder = true)
assertTrue(SharePermissionManager.canEdit(share))
}
@Test
fun testCanEditShouldReturnFalseIfPermissionsAreInsufficient() {
val share = createShare(OCShare.READ_PERMISSION_FLAG)
assertFalse(SharePermissionManager.canEdit(share))
}
@Test
fun testIsViewOnlyShouldReturnTrueIfOnlyReadPermissionSet() {
val share = createShare(OCShare.READ_PERMISSION_FLAG)
assertTrue(SharePermissionManager.isViewOnly(share))
}
@Test
fun testIsFileRequestShouldReturnTrueIfOnlyCreatePermissionSetOnFolder() {
val share = createShare(OCShare.CREATE_PERMISSION_FLAG, isFolder = true)
assertTrue(SharePermissionManager.isFileRequest(share))
}
@Test
fun testIsFileRequestShouldReturnFalseIfOnlyCreatePermissionSetOnFile() {
val share = createShare(OCShare.CREATE_PERMISSION_FLAG)
assertFalse(SharePermissionManager.isFileRequest(share))
}
@Test
fun testIsSecureFileDropShouldReturnTrueIfReadAndCreatePermissionsPresent() {
val permission = OCShare.READ_PERMISSION_FLAG + OCShare.CREATE_PERMISSION_FLAG
val share = createShare(permission)
assertTrue(SharePermissionManager.isSecureFileDrop(share))
}
@Test
fun testCanReshareShouldReturnTrueIfSharePermissionIsPresent() {
val share = createShare(OCShare.SHARE_PERMISSION_FLAG)
assertTrue(SharePermissionManager.canReshare(share))
}
@Test
fun testGetMaximumPermissionForFolder() {
assertEquals(
OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER,
SharePermissionManager.getMaximumPermission(isFolder = true)
)
}
@Test
fun testGetMaximumPermissionForFile() {
assertEquals(
OCShare.MAXIMUM_PERMISSIONS_FOR_FILE,
SharePermissionManager.getMaximumPermission(isFolder = false)
)
}
// endregion
// region GetSelectedTypeTests
@Test
fun testGetSelectedTypeShouldReturnCanEditWhenFullPermissionsGiven() {
val share = createShare(OCShare.MAXIMUM_PERMISSIONS_FOR_FILE)
assertEquals(QuickPermissionType.CAN_EDIT, SharePermissionManager.getSelectedType(share, encrypted = false))
}
@Test
fun testGetSelectedTypeShouldReturnSecureFileDropWhenEncryptedAndReadCreateGiven() {
val permission = OCShare.READ_PERMISSION_FLAG + OCShare.CREATE_PERMISSION_FLAG
val share = createShare(permission)
assertEquals(
QuickPermissionType.SECURE_FILE_DROP,
SharePermissionManager.getSelectedType(share, encrypted = true)
)
}
@Test
fun testGetSelectedTypeShouldReturnFileRequestWhenCreatePermissionGiven() {
val share = createShare(OCShare.CREATE_PERMISSION_FLAG, isFolder = true)
assertEquals(QuickPermissionType.FILE_REQUEST, SharePermissionManager.getSelectedType(share, encrypted = false))
}
@Test
fun testGetSelectedTypeShouldReturnViewOnlyWhenReadPermissionGiven() {
val share = createShare(OCShare.READ_PERMISSION_FLAG)
assertEquals(QuickPermissionType.VIEW_ONLY, SharePermissionManager.getSelectedType(share, encrypted = false))
}
@Test
fun testGetSelectedTypeShouldReturnCustomPermissionOnlyWhenCustomPermissionGiven() {
val share = createShare(OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG)
assertEquals(
QuickPermissionType.CUSTOM_PERMISSIONS,
SharePermissionManager.getSelectedType(share, encrypted = false)
)
}
@Test
fun testGetSelectedTypeShouldReturnNoneOnlyWhenNoPermissionGiven() {
val share = createShare(OCShare.NO_PERMISSION)
assertEquals(
QuickPermissionType.NONE,
SharePermissionManager.getSelectedType(share, encrypted = false)
)
}
// endregion
// region CustomPermissions Tests
@Test
fun testIsCustomPermissionShouldReturnFalseWhenNoPermissionsGiven() {
val permission = OCShare.NO_PERMISSION
val share = createShare(permission, isFolder = false)
assertFalse(SharePermissionManager.isCustomPermission(share))
}
@Test
fun testIsCustomPermissionShouldReturnFalseWhenNoReadPermissionsGiven() {
val permission = OCShare.SHARE_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG
val share = createShare(permission, isFolder = false)
assertFalse(SharePermissionManager.isCustomPermission(share))
}
@Test
fun testIsCustomPermissionShouldReturnTrueWhenUpdatePermissionsGivenOnFile() {
val permission = OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG
val share = createShare(permission, isFolder = false)
assertTrue(SharePermissionManager.isCustomPermission(share))
}
@Test
fun testIsCustomPermissionShouldReturnTrueWhenUpdateAndSharePermissionsGivenOnFile() {
val permission = OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG + OCShare.SHARE_PERMISSION_FLAG
val share = createShare(permission, isFolder = false)
assertTrue(SharePermissionManager.isCustomPermission(share))
}
@Test
fun testIsCustomPermissionShouldReturnFalseWhenCreatePermissionsGivenOnFile() {
val permission = OCShare.READ_PERMISSION_FLAG + OCShare.CREATE_PERMISSION_FLAG
val share = createShare(permission, isFolder = false)
assertFalse(SharePermissionManager.isCustomPermission(share))
}
@Test
fun testIsCustomPermissionShouldReturnFalseWhenDeletePermissionsGivenOnFile() {
val permission = OCShare.READ_PERMISSION_FLAG + OCShare.DELETE_PERMISSION_FLAG
val share = createShare(permission, isFolder = false)
assertFalse(SharePermissionManager.isCustomPermission(share))
}
@Test
fun testIsCustomPermissionShouldReturnTrueWhenCreatePermissionsGivenOnFolder() {
val permission = OCShare.READ_PERMISSION_FLAG + OCShare.CREATE_PERMISSION_FLAG
val share = createShare(permission, isFolder = true)
assertTrue(SharePermissionManager.isCustomPermission(share))
}
@Test
fun testIsCustomPermissionShouldReturnTrueWhenMixedPermissionsOnFile() {
val permission = OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG
val share = createShare(permission, isFolder = false)
assertTrue(SharePermissionManager.isCustomPermission(share))
}
// endregion
// region Attributes Tests
@Test
fun testToggleAllowDownloadAndSyncShouldCreateAttributeJsonIfNoneExists() {
val json = SharePermissionManager.toggleAllowDownloadAndSync(true, null)
assertNotNull(json)
val downloadAttribute = ShareAttributes.createDownloadAttributes(true)
val expectedJson = Gson().toJson(listOf(downloadAttribute))
assertEquals(json, expectedJson)
}
@Test
fun testIsAllowDownloadAndSyncEnabledShouldReturnFalseIfAttributeIsMissing() {
val share = createShare(OCShare.READ_PERMISSION_FLAG, attributesJson = null)
assertFalse(SharePermissionManager.isAllowDownloadAndSyncEnabled(share))
}
// endregion
}

View File

@ -72,7 +72,8 @@ import com.owncloud.android.db.ProviderMeta
AutoMigration(from = 85, to = 86, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 85, to = 86, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 86, to = 87, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 86, to = 87, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 87, to = 88, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class), AutoMigration(from = 87, to = 88, spec = DatabaseMigrationUtil.ResetCapabilitiesPostMigration::class),
AutoMigration(from = 88, to = 89) AutoMigration(from = 88, to = 89),
AutoMigration(from = 89, to = 90)
], ],
exportSchema = true exportSchema = true
) )

View File

@ -58,5 +58,7 @@ data class ShareEntity(
@ColumnInfo(name = ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT) @ColumnInfo(name = ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT)
val downloadLimitLimit: Int?, val downloadLimitLimit: Int?,
@ColumnInfo(name = ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT) @ColumnInfo(name = ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT)
val downloadLimitCount: Int? val downloadLimitCount: Int?,
@ColumnInfo(name = ProviderTableMeta.OCSHARES_ATTRIBUTES)
val attributes: String?
) )

View File

@ -0,0 +1,49 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.utils.extensions
import android.content.Context
import android.graphics.drawable.GradientDrawable
import android.util.TypedValue
import android.view.ViewOutlineProvider
import android.widget.ImageView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import androidx.core.content.ContextCompat
import com.owncloud.android.R
@JvmOverloads
fun ImageView.makeRoundedWithIcon(
context: Context,
@DrawableRes icon: Int,
paddingDp: Int = 6,
@ColorInt backgroundColor: Int = ContextCompat.getColor(context, R.color.primary),
@ColorInt foregroundColor: Int = ContextCompat.getColor(context, R.color.white)
) {
setImageResource(icon)
val drawable = GradientDrawable().apply {
shape = GradientDrawable.OVAL
setColor(backgroundColor)
}
background = drawable
clipToOutline = true
scaleType = ImageView.ScaleType.CENTER_INSIDE
outlineProvider = ViewOutlineProvider.BACKGROUND
setColorFilter(foregroundColor)
val paddingPx = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
paddingDp.toFloat(),
context.resources.displayMetrics
).toInt()
setPadding(paddingPx, paddingPx, paddingPx, paddingPx)
}

View File

@ -0,0 +1,18 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Your Name <your@email.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.utils.extensions
import com.owncloud.android.lib.resources.shares.OCShare
fun OCShare.hasFileRequestPermission(): Boolean {
return (isFolder && shareType?.isPublicOrMail() == true)
}
fun List<OCShare>.mergeDistinctByToken(other: List<OCShare>): List<OCShare> {
return (this + other).distinctBy { it.token }
}

View File

@ -0,0 +1,12 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.utils.extensions
import com.owncloud.android.lib.resources.shares.ShareType
fun ShareType.isPublicOrMail(): Boolean = (this == ShareType.PUBLIC_LINK || this == ShareType.EMAIL)

View File

@ -7,6 +7,8 @@
*/ */
package com.nextcloud.utils.extensions package com.nextcloud.utils.extensions
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context import android.content.Context
import android.graphics.Outline import android.graphics.Outline
import android.util.TypedValue import android.util.TypedValue
@ -19,6 +21,30 @@ fun View?.setVisibleIf(condition: Boolean) {
visibility = if (condition) View.VISIBLE else View.GONE visibility = if (condition) View.VISIBLE else View.GONE
} }
fun View?.setVisibilityWithAnimation(condition: Boolean, duration: Long = 200L) {
this ?: return
if (condition) {
this.apply {
alpha = 0f
visibility = View.VISIBLE
animate()
.alpha(1f)
.setDuration(duration)
.setListener(null)
}
} else {
animate()
.alpha(0f)
.setDuration(duration)
.setListener(object : AnimatorListenerAdapter() {
override fun onAnimationEnd(animation: Animator) {
visibility = View.GONE
}
})
}
}
fun View?.makeRounded(context: Context, cornerRadius: Float) { fun View?.makeRounded(context: Context, cornerRadius: Float) {
this?.let { this?.let {
it.apply { it.apply {

View File

@ -1569,6 +1569,8 @@ public class FileDataStorageManager {
contentValues.putNull(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT); contentValues.putNull(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT);
} }
contentValues.put(ProviderTableMeta.OCSHARES_ATTRIBUTES, share.getAttributes());
return contentValues; return contentValues;
} }
@ -1599,6 +1601,8 @@ public class FileDataStorageManager {
getInt(cursor, ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT)); getInt(cursor, ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT));
share.setFileDownloadLimit(downloadLimit); share.setFileDownloadLimit(downloadLimit);
share.setAttributes(getString(cursor, ProviderTableMeta.OCSHARES_ATTRIBUTES));
return share; return share;
} }

View File

@ -1,13 +0,0 @@
/*
* Nextcloud Android client application
*
* @author TSI-mc
* Copyright (C) 2021 TSI-mc
* Copyright (C) 2021 Nextcloud GmbH
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.owncloud.android.datamodel
data class QuickPermissionModel(val permissionName: String, val isSelected: Boolean)

View File

@ -0,0 +1,10 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.owncloud.android.datamodel.quickPermission
data class QuickPermission(val type: QuickPermissionType, var isSelected: Boolean)

View File

@ -0,0 +1,46 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.owncloud.android.datamodel.quickPermission
import android.content.Context
import android.graphics.drawable.Drawable
import androidx.core.content.ContextCompat
import com.owncloud.android.R
enum class QuickPermissionType(
val iconId: Int,
val textId: Int
) {
NONE(R.drawable.ic_unknown, R.string.unknown),
VIEW_ONLY(R.drawable.ic_eye, R.string.share_permission_view_only),
CAN_EDIT(R.drawable.ic_edit, R.string.share_permission_can_edit),
FILE_REQUEST(R.drawable.ic_file_request, R.string.share_permission_file_request),
SECURE_FILE_DROP(R.drawable.ic_file_request, R.string.share_permission_secure_file_drop),
CUSTOM_PERMISSIONS(R.drawable.ic_custom_permissions, R.string.share_custom_permission);
fun getText(context: Context): String = context.getString(textId)
fun getIcon(context: Context): Drawable? = ContextCompat.getDrawable(context, iconId)
companion object {
fun getAvailablePermissions(
hasFileRequestPermission: Boolean,
selectedType: QuickPermissionType
): List<QuickPermission> {
val permissions = listOf(VIEW_ONLY, CAN_EDIT, FILE_REQUEST, CUSTOM_PERMISSIONS)
val result = if (hasFileRequestPermission) permissions else permissions.filter { it != FILE_REQUEST }
return result.map { type ->
QuickPermission(
type = type,
isSelected = (type == selectedType)
)
}
}
}
}

View File

@ -25,7 +25,7 @@ import java.util.List;
*/ */
public class ProviderMeta { public class ProviderMeta {
public static final String DB_NAME = "filelist"; public static final String DB_NAME = "filelist";
public static final int DB_VERSION = 89; public static final int DB_VERSION = 90;
private ProviderMeta() { private ProviderMeta() {
// No instance // No instance
@ -203,6 +203,7 @@ public class ProviderMeta {
public static final String OCSHARES_SHARE_LABEL = "share_label"; public static final String OCSHARES_SHARE_LABEL = "share_label";
public static final String OCSHARES_DOWNLOADLIMIT_LIMIT = "download_limit_limit"; public static final String OCSHARES_DOWNLOADLIMIT_LIMIT = "download_limit_limit";
public static final String OCSHARES_DOWNLOADLIMIT_COUNT = "download_limit_count"; public static final String OCSHARES_DOWNLOADLIMIT_COUNT = "download_limit_count";
public static final String OCSHARES_ATTRIBUTES = "attributes";
public static final String OCSHARES_DEFAULT_SORT_ORDER = OCSHARES_FILE_SOURCE public static final String OCSHARES_DEFAULT_SORT_ORDER = OCSHARES_FILE_SOURCE
+ " collate nocase asc"; + " collate nocase asc";

View File

@ -56,6 +56,7 @@ public class CreateShareWithShareeOperation extends SyncOperation {
private String label; private String label;
private final Context context; private final Context context;
private final User user; private final User user;
private String attributes;
private ArbitraryDataProvider arbitraryDataProvider; private ArbitraryDataProvider arbitraryDataProvider;
@ -85,6 +86,7 @@ public class CreateShareWithShareeOperation extends SyncOperation {
String sharePassword, String sharePassword,
long expirationDateInMillis, long expirationDateInMillis,
boolean hideFileDownload, boolean hideFileDownload,
String attributes,
FileDataStorageManager storageManager, FileDataStorageManager storageManager,
Context context, Context context,
User user, User user,
@ -105,6 +107,7 @@ public class CreateShareWithShareeOperation extends SyncOperation {
this.context = context; this.context = context;
this.user = user; this.user = user;
this.arbitraryDataProvider = arbitraryDataProvider; this.arbitraryDataProvider = arbitraryDataProvider;
this.attributes = attributes;
} }
@Override @Override
@ -156,7 +159,8 @@ public class CreateShareWithShareeOperation extends SyncOperation {
false, false,
sharePassword, sharePassword,
permissions, permissions,
noteMessage noteMessage,
attributes
); );
operation.setGetShareDetails(true); operation.setGetShareDetails(true);
RemoteOperationResult shareResult = operation.execute(client); RemoteOperationResult shareResult = operation.execute(client);

View File

@ -113,6 +113,8 @@ public class UnshareOperation extends SyncOperation {
RemoveShareRemoteOperation operation = new RemoveShareRemoteOperation(share.getRemoteId()); RemoveShareRemoteOperation operation = new RemoveShareRemoteOperation(share.getRemoteId());
result = operation.execute(client); result = operation.execute(client);
boolean isFileExists = existsFile(client, file.getRemotePath());
boolean isShareExists = getStorageManager().getShareById(shareId) != null;
if (result.isSuccess()) { if (result.isSuccess()) {
// E2E: unlock folder // E2E: unlock folder
@ -140,10 +142,12 @@ public class UnshareOperation extends SyncOperation {
getStorageManager().saveFile(file); getStorageManager().saveFile(file);
getStorageManager().removeShare(share); getStorageManager().removeShare(share);
} else if (result.getCode() != ResultCode.MAINTENANCE_MODE && !isFileExists) {
} else if (result.getCode() != ResultCode.MAINTENANCE_MODE && !existsFile(client, file.getRemotePath())) { // UnShare failed because file was deleted before
// unshare failed because file was deleted before
getStorageManager().removeFile(file, true, true); getStorageManager().removeFile(file, true, true);
} else if (isShareExists && result.getCode() == ResultCode.FILE_NOT_FOUND) {
// UnShare failed because share was deleted before
getStorageManager().removeShare(share);
} }
} else { } else {

View File

@ -35,6 +35,7 @@ public class UpdateShareInfoOperation extends SyncOperation {
private int permissions = -1; private int permissions = -1;
private String password; private String password;
private String label; private String label;
private String attributes;
/** /**
* Constructor * Constructor
@ -78,7 +79,7 @@ public class UpdateShareInfoOperation extends SyncOperation {
if (share == null) { if (share == null) {
// TODO try to get remote share before failing? // TODO try to get remote share before failing?
return new RemoteOperationResult(RemoteOperationResult.ResultCode.SHARE_NOT_FOUND); return new RemoteOperationResult<>(RemoteOperationResult.ResultCode.SHARE_NOT_FOUND);
} }
// Update remote share // Update remote share
@ -93,11 +94,12 @@ public class UpdateShareInfoOperation extends SyncOperation {
} }
updateOp.setPassword(password); updateOp.setPassword(password);
updateOp.setLabel(label); updateOp.setLabel(label);
updateOp.setAttributes(attributes);
RemoteOperationResult result = updateOp.execute(client); var result = updateOp.execute(client);
if (result.isSuccess()) { if (result.isSuccess()) {
RemoteOperation getShareOp = new GetShareRemoteOperation(share.getRemoteId()); final var getShareOp = new GetShareRemoteOperation(share.getRemoteId());
result = getShareOp.execute(client); result = getShareOp.execute(client);
//only update the share in storage if shareId is available //only update the share in storage if shareId is available
@ -125,6 +127,10 @@ public class UpdateShareInfoOperation extends SyncOperation {
this.hideFileDownload = hideFileDownload; this.hideFileDownload = hideFileDownload;
} }
public void setAttributes(String attributes) {
this.attributes = attributes;
}
public void setPermissions(int permissions) { public void setPermissions(int permissions) {
this.permissions = permissions; this.permissions = permissions;
} }

View File

@ -101,6 +101,7 @@ public class OperationsService extends Service {
public static final String EXTRA_SHARE_NOTE = "SHARE_NOTE"; public static final String EXTRA_SHARE_NOTE = "SHARE_NOTE";
public static final String EXTRA_IN_BACKGROUND = "IN_BACKGROUND"; public static final String EXTRA_IN_BACKGROUND = "IN_BACKGROUND";
public static final String EXTRA_FILES_DOWNLOAD_LIMIT = "FILES_DOWNLOAD_LIMIT"; public static final String EXTRA_FILES_DOWNLOAD_LIMIT = "FILES_DOWNLOAD_LIMIT";
public static final String EXTRA_SHARE_ATTRIBUTES = "SHARE_ATTRIBUTES";
public static final String ACTION_CREATE_SHARE_VIA_LINK = "CREATE_SHARE_VIA_LINK"; public static final String ACTION_CREATE_SHARE_VIA_LINK = "CREATE_SHARE_VIA_LINK";
public static final String ACTION_CREATE_SECURE_FILE_DROP = "CREATE_SECURE_FILE_DROP"; public static final String ACTION_CREATE_SECURE_FILE_DROP = "CREATE_SECURE_FILE_DROP";
@ -437,6 +438,11 @@ public class OperationsService extends Service {
// perform the operation // perform the operation
try { try {
result = mCurrentOperation.execute(mOwnCloudClient); result = mCurrentOperation.execute(mOwnCloudClient);
if (!result.isSuccess()) {
final var code = "code: " + result.getCode();
final var httpCode = "HTTP_CODE: " + result.getHttpCode();
Log_OC.e(TAG,"Operation failed " + code + httpCode);
}
} catch (UnsupportedOperationException e) { } catch (UnsupportedOperationException e) {
// TODO remove - added to aid in transition to NextcloudClient // TODO remove - added to aid in transition to NextcloudClient
@ -593,6 +599,8 @@ public class OperationsService extends Service {
.getLongExtra(EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, 0L); .getLongExtra(EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, 0L);
boolean hideFileDownload = operationIntent.getBooleanExtra(EXTRA_SHARE_HIDE_FILE_DOWNLOAD, boolean hideFileDownload = operationIntent.getBooleanExtra(EXTRA_SHARE_HIDE_FILE_DOWNLOAD,
false); false);
String attributes = operationIntent.getStringExtra(EXTRA_SHARE_ATTRIBUTES);
if (!TextUtils.isEmpty(remotePath)) { if (!TextUtils.isEmpty(remotePath)) {
CreateShareWithShareeOperation createShareWithShareeOperation = CreateShareWithShareeOperation createShareWithShareeOperation =
new CreateShareWithShareeOperation(remotePath, new CreateShareWithShareeOperation(remotePath,
@ -603,6 +611,7 @@ public class OperationsService extends Service {
sharePassword, sharePassword,
expirationDateInMillis, expirationDateInMillis,
hideFileDownload, hideFileDownload,
attributes,
fileDataStorageManager, fileDataStorageManager,
getApplicationContext(), getApplicationContext(),
user, user,
@ -641,6 +650,9 @@ public class OperationsService extends Service {
updateShare.setLabel(operationIntent.getStringExtra(EXTRA_SHARE_PUBLIC_LABEL)); updateShare.setLabel(operationIntent.getStringExtra(EXTRA_SHARE_PUBLIC_LABEL));
} }
String shareAttributes = operationIntent.getStringExtra(EXTRA_SHARE_ATTRIBUTES);
updateShare.setAttributes(shareAttributes);
operation = updateShare; operation = updateShare;
} }
break; break;

View File

@ -426,8 +426,10 @@ public abstract class FileActivity extends DrawerActivity
onCreateShareViaLinkOperationFinish((CreateShareViaLinkOperation) operation, result); onCreateShareViaLinkOperationFinish((CreateShareViaLinkOperation) operation, result);
} else if (operation instanceof CreateShareWithShareeOperation) { } else if (operation instanceof CreateShareWithShareeOperation) {
onUpdateShareInformation(result, R.string.sharee_add_failed); onUpdateShareInformation(result, R.string.sharee_add_failed);
} else if (operation instanceof UpdateShareViaLinkOperation || operation instanceof UpdateShareInfoOperation || operation instanceof SetFilesDownloadLimitOperation) { } else if (operation instanceof UpdateShareViaLinkOperation || operation instanceof UpdateShareInfoOperation) {
onUpdateShareInformation(result, R.string.updating_share_failed); onUpdateShareInformation(result, R.string.updating_share_failed);
} else if (operation instanceof SetFilesDownloadLimitOperation) {
onUpdateShareInformation(result, R.string.set_download_limit_failed);
} else if (operation instanceof UpdateSharePermissionsOperation) { } else if (operation instanceof UpdateSharePermissionsOperation) {
onUpdateShareInformation(result, R.string.updating_share_failed); onUpdateShareInformation(result, R.string.updating_share_failed);
} else if (operation instanceof UnshareOperation) { } else if (operation instanceof UnshareOperation) {

View File

@ -15,7 +15,6 @@
package com.owncloud.android.ui.adapter; package com.owncloud.android.ui.adapter;
import android.content.Context; import android.content.Context;
import android.graphics.PorterDuff;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.View; import android.view.View;
@ -23,9 +22,10 @@ import com.nextcloud.android.lib.resources.files.FileDownloadLimit;
import com.nextcloud.utils.mdm.MDMConfig; import com.nextcloud.utils.mdm.MDMConfig;
import com.owncloud.android.R; import com.owncloud.android.R;
import com.owncloud.android.databinding.FileDetailsShareLinkShareItemBinding; import com.owncloud.android.databinding.FileDetailsShareLinkShareItemBinding;
import com.owncloud.android.datamodel.quickPermission.QuickPermissionType;
import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.lib.resources.shares.ShareType;
import com.owncloud.android.ui.fragment.util.SharingMenuHelper; import com.owncloud.android.ui.fragment.util.SharePermissionManager;
import com.owncloud.android.utils.theme.ViewThemeUtils; import com.owncloud.android.utils.theme.ViewThemeUtils;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
@ -36,6 +36,7 @@ class LinkShareViewHolder extends RecyclerView.ViewHolder {
private FileDetailsShareLinkShareItemBinding binding; private FileDetailsShareLinkShareItemBinding binding;
private Context context; private Context context;
private ViewThemeUtils viewThemeUtils; private ViewThemeUtils viewThemeUtils;
private boolean encrypted;
public LinkShareViewHolder(@NonNull View itemView) { public LinkShareViewHolder(@NonNull View itemView) {
super(itemView); super(itemView);
@ -43,40 +44,42 @@ class LinkShareViewHolder extends RecyclerView.ViewHolder {
public LinkShareViewHolder(FileDetailsShareLinkShareItemBinding binding, public LinkShareViewHolder(FileDetailsShareLinkShareItemBinding binding,
Context context, Context context,
final ViewThemeUtils viewThemeUtils) { final ViewThemeUtils viewThemeUtils,
boolean encrypted) {
this(binding.getRoot()); this(binding.getRoot());
this.binding = binding; this.binding = binding;
this.context = context; this.context = context;
this.viewThemeUtils = viewThemeUtils; this.viewThemeUtils = viewThemeUtils;
this.encrypted = encrypted;
} }
public void bind(OCShare publicShare, ShareeListAdapterListener listener) { public void bind(OCShare publicShare, ShareeListAdapterListener listener, int position) {
if (ShareType.EMAIL == publicShare.getShareType()) { if (ShareType.EMAIL == publicShare.getShareType()) {
final var res = context.getResources();
binding.name.setText(publicShare.getSharedWithDisplayName()); binding.name.setText(publicShare.getSharedWithDisplayName());
binding.icon.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(),
R.drawable.ic_email, final var emailDrawable = ResourcesCompat.getDrawable(res, R.drawable.ic_email, null);
null)); binding.icon.setImageDrawable(emailDrawable);
binding.copyLink.setVisibility(View.GONE); binding.copyLink.setVisibility(View.GONE);
binding.icon.getBackground().setColorFilter(context.getResources().getColor(R.color.nc_grey),
PorterDuff.Mode.SRC_IN);
binding.icon.getDrawable().mutate().setColorFilter(context.getResources().getColor(R.color.icon_on_nc_grey),
PorterDuff.Mode.SRC_IN);
} else { } else {
if (!TextUtils.isEmpty(publicShare.getLabel())) { String label = publicShare.getLabel();
String text = String.format(context.getString(R.string.share_link_with_label), publicShare.getLabel());
binding.name.setText(text);
} else {
if (SharingMenuHelper.isSecureFileDrop(publicShare)) {
binding.name.setText(context.getResources().getString(R.string.share_permission_secure_file_drop));
} else {
binding.name.setText(R.string.share_link);
}
}
viewThemeUtils.platform.colorImageViewBackgroundAndIcon(binding.icon); if (!TextUtils.isEmpty(label)) {
binding.name.setText(context.getString(R.string.share_link_with_label, label));
} else if (SharePermissionManager.INSTANCE.isFileRequest(publicShare)) {
binding.name.setText(R.string.share_permission_file_request);
} else if (SharePermissionManager.INSTANCE.isSecureFileDrop(publicShare) && encrypted) {
binding.name.setText(R.string.share_permission_secure_file_drop);
} else {
int textRes = (position == 0) ? R.string.share_link : R.string.share_link_with_label;
Object arg = (position == 0) ? null : String.valueOf(position);
binding.name.setText((position == 0) ? context.getString(textRes)
: context.getString(textRes, arg));
}
} }
viewThemeUtils.platform.colorImageViewBackgroundAndIcon(binding.icon);
FileDownloadLimit downloadLimit = publicShare.getFileDownloadLimit(); FileDownloadLimit downloadLimit = publicShare.getFileDownloadLimit();
if (downloadLimit != null && downloadLimit.getLimit() > 0) { if (downloadLimit != null && downloadLimit.getLimit() > 0) {
int remaining = downloadLimit.getLimit() - downloadLimit.getCount(); int remaining = downloadLimit.getLimit() - downloadLimit.getCount();
@ -88,11 +91,11 @@ class LinkShareViewHolder extends RecyclerView.ViewHolder {
binding.subline.setVisibility(View.GONE); binding.subline.setVisibility(View.GONE);
} }
String permissionName = SharingMenuHelper.getPermissionName(context, publicShare); QuickPermissionType quickPermissionType = SharePermissionManager.INSTANCE.getSelectedType(publicShare, encrypted);
setPermissionName(publicShare, permissionName); setPermissionName(publicShare, quickPermissionType.getText(context));
binding.overflowMenu.setOnClickListener(v -> listener.showSharingMenuActionSheet(publicShare)); binding.overflowMenu.setOnClickListener(v -> listener.showSharingMenuActionSheet(publicShare));
if (!SharingMenuHelper.isSecureFileDrop(publicShare)) { if (!SharePermissionManager.INSTANCE.isSecureFileDrop(publicShare) && !encrypted) {
binding.shareByLinkContainer.setOnClickListener(v -> listener.showPermissionsDialog(publicShare)); binding.shareByLinkContainer.setOnClickListener(v -> listener.showPermissionsDialog(publicShare));
} }
@ -104,12 +107,13 @@ class LinkShareViewHolder extends RecyclerView.ViewHolder {
} }
private void setPermissionName(OCShare publicShare, String permissionName) { private void setPermissionName(OCShare publicShare, String permissionName) {
if (!TextUtils.isEmpty(permissionName) && !SharingMenuHelper.isSecureFileDrop(publicShare)) { if (TextUtils.isEmpty(permissionName) || (SharePermissionManager.INSTANCE.isSecureFileDrop(publicShare) && encrypted)) {
binding.permissionName.setText(permissionName);
binding.permissionName.setVisibility(View.VISIBLE);
viewThemeUtils.androidx.colorPrimaryTextViewElement(binding.permissionName);
} else {
binding.permissionName.setVisibility(View.GONE); binding.permissionName.setVisibility(View.GONE);
return;
} }
binding.permissionName.setText(permissionName);
binding.permissionName.setVisibility(View.VISIBLE);
viewThemeUtils.androidx.colorPrimaryTextViewElement(binding.permissionName);
} }
} }

View File

@ -14,12 +14,14 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.button.MaterialButton
import com.owncloud.android.R
import com.owncloud.android.databinding.ItemQuickSharePermissionsBinding import com.owncloud.android.databinding.ItemQuickSharePermissionsBinding
import com.owncloud.android.datamodel.QuickPermissionModel import com.owncloud.android.datamodel.quickPermission.QuickPermission
import com.owncloud.android.utils.theme.ViewThemeUtils import com.owncloud.android.utils.theme.ViewThemeUtils
class QuickSharingPermissionsAdapter( class QuickSharingPermissionsAdapter(
private val quickPermissionList: MutableList<QuickPermissionModel>, private val quickPermissionList: MutableList<QuickPermission>,
private val onPermissionChangeListener: QuickSharingPermissionViewHolder.OnPermissionChangeListener, private val onPermissionChangeListener: QuickSharingPermissionViewHolder.OnPermissionChangeListener,
private val viewThemeUtils: ViewThemeUtils private val viewThemeUtils: ViewThemeUtils
) : ) :
@ -40,27 +42,35 @@ class QuickSharingPermissionsAdapter(
} }
class QuickSharingPermissionViewHolder( class QuickSharingPermissionViewHolder(
val binding: ItemQuickSharePermissionsBinding, private val binding: ItemQuickSharePermissionsBinding,
itemView: View, itemView: View,
val onPermissionChangeListener: OnPermissionChangeListener, private val onPermissionChangeListener: OnPermissionChangeListener,
private val viewThemeUtils: ViewThemeUtils private val viewThemeUtils: ViewThemeUtils
) : ) : RecyclerView.ViewHolder(itemView) {
RecyclerView
.ViewHolder(itemView) {
fun bindData(quickPermissionModel: QuickPermissionModel) { fun bindData(quickPermission: QuickPermission) {
binding.tvQuickShareName.text = quickPermissionModel.permissionName val context = itemView.context
if (quickPermissionModel.isSelected) { val permissionName = quickPermission.type.getText(context)
viewThemeUtils.platform.colorImageView(binding.tvQuickShareCheckIcon)
binding.tvQuickShareCheckIcon.visibility = View.VISIBLE binding.run {
} else { quickPermissionButton.text = permissionName
binding.tvQuickShareCheckIcon.visibility = View.INVISIBLE quickPermissionButton.iconGravity = MaterialButton.ICON_GRAVITY_START
quickPermissionButton.icon = quickPermission.type.getIcon(context)
if (quickPermission.isSelected) {
viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(quickPermissionButton)
}
} }
val customPermissionName = context.getString(R.string.share_custom_permission)
val isCustomPermission = permissionName.equals(customPermissionName, ignoreCase = true)
itemView.setOnClickListener { itemView.setOnClickListener {
// if user select different options then only update the permission if (isCustomPermission) {
if (!quickPermissionModel.isSelected) { onPermissionChangeListener.onCustomPermissionSelected()
onPermissionChangeListener.onPermissionChanged(adapterPosition) } else if (!quickPermission.isSelected) {
// if user select different options then only update the permission
onPermissionChangeListener.onPermissionChanged(absoluteAdapterPosition)
} else { } else {
// dismiss sheet on selection of same permission // dismiss sheet on selection of same permission
onPermissionChangeListener.onDismissSheet() onPermissionChangeListener.onDismissSheet()
@ -70,6 +80,7 @@ class QuickSharingPermissionsAdapter(
interface OnPermissionChangeListener { interface OnPermissionChangeListener {
fun onPermissionChanged(position: Int) fun onPermissionChanged(position: Int)
fun onCustomPermissionSelected()
fun onDismissSheet() fun onDismissSheet()
} }
} }

View File

@ -19,15 +19,16 @@ import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import com.nextcloud.client.account.User; import com.nextcloud.client.account.User;
import com.nextcloud.utils.extensions.ImageViewExtensionsKt;
import com.owncloud.android.R; import com.owncloud.android.R;
import com.owncloud.android.databinding.FileDetailsShareShareItemBinding; import com.owncloud.android.databinding.FileDetailsShareShareItemBinding;
import com.owncloud.android.datamodel.quickPermission.QuickPermissionType;
import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.ui.TextDrawable; import com.owncloud.android.ui.TextDrawable;
import com.owncloud.android.ui.fragment.util.SharingMenuHelper; import com.owncloud.android.ui.fragment.util.SharePermissionManager;
import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.DisplayUtils;
import com.owncloud.android.utils.theme.ViewThemeUtils; import com.owncloud.android.utils.theme.ViewThemeUtils;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@ -37,6 +38,7 @@ class ShareViewHolder extends RecyclerView.ViewHolder {
private User user; private User user;
private Context context; private Context context;
private ViewThemeUtils viewThemeUtils; private ViewThemeUtils viewThemeUtils;
private boolean encrypted;
public ShareViewHolder(@NonNull View itemView) { public ShareViewHolder(@NonNull View itemView) {
super(itemView); super(itemView);
@ -45,12 +47,14 @@ class ShareViewHolder extends RecyclerView.ViewHolder {
public ShareViewHolder(FileDetailsShareShareItemBinding binding, public ShareViewHolder(FileDetailsShareShareItemBinding binding,
User user, User user,
Context context, Context context,
final ViewThemeUtils viewThemeUtils) { final ViewThemeUtils viewThemeUtils,
boolean encrypted) {
this(binding.getRoot()); this(binding.getRoot());
this.binding = binding; this.binding = binding;
this.user = user; this.user = user;
this.context = context; this.context = context;
this.viewThemeUtils = viewThemeUtils; this.viewThemeUtils = viewThemeUtils;
this.encrypted = encrypted;
} }
public void bind(OCShare share, public void bind(OCShare share,
@ -67,47 +71,53 @@ class ShareViewHolder extends RecyclerView.ViewHolder {
binding.icon.setTag(null); binding.icon.setTag(null);
switch (share.getShareType()) { if (share.getShareType() != null) {
case GROUP: switch (share.getShareType()) {
name = context.getString(R.string.share_group_clarification, name); case GROUP:
viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context); name = context.getString(R.string.share_group_clarification, name);
break; viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context);
case ROOM: break;
name = context.getString(R.string.share_room_clarification, name); case ROOM:
viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context); name = context.getString(R.string.share_room_clarification, name);
break; viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context);
case CIRCLE: break;
viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context); case CIRCLE:
break; viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context);
case FEDERATED: break;
name = context.getString(R.string.share_remote_clarification, name); case FEDERATED:
setImage(binding.icon, share.getSharedWithDisplayName(), R.drawable.ic_user); name = context.getString(R.string.share_remote_clarification, name);
break; setImage(binding.icon, share.getSharedWithDisplayName());
case USER: break;
binding.icon.setTag(share.getShareWith()); case USER:
float avatarRadius = context.getResources().getDimension(R.dimen.list_item_avatar_icon_radius); binding.icon.setTag(share.getShareWith());
DisplayUtils.setAvatar(user, float avatarRadius = context.getResources().getDimension(R.dimen.list_item_avatar_icon_radius);
share.getShareWith(),
share.getSharedWithDisplayName(),
avatarListener,
avatarRadius,
context.getResources(),
binding.icon,
context);
binding.icon.setOnClickListener(v -> listener.showProfileBottomSheet(user, share.getShareWith())); if (share.getShareWith() != null) {
default: DisplayUtils.setAvatar(user,
setImage(binding.icon, name, R.drawable.ic_user); share.getShareWith(),
break; share.getSharedWithDisplayName(),
avatarListener,
avatarRadius,
context.getResources(),
binding.icon,
context);
}
binding.icon.setOnClickListener(v -> listener.showProfileBottomSheet(user, share.getShareWith()));
default:
setImage(binding.icon, name);
break;
}
} }
binding.name.setText(name); binding.name.setText(name);
if (share.getShareWith().equalsIgnoreCase(userId) || share.getUserId().equalsIgnoreCase(userId)) { if (share.getShareWith() != null && share.getShareWith().equalsIgnoreCase(userId) ||
share.getUserId() != null && share.getUserId().equalsIgnoreCase(userId)) {
binding.overflowMenu.setVisibility(View.VISIBLE); binding.overflowMenu.setVisibility(View.VISIBLE);
String permissionName = SharingMenuHelper.getPermissionName(context, share); QuickPermissionType quickPermissionType = SharePermissionManager.INSTANCE.getSelectedType(share, encrypted);
setPermissionName(permissionName); setPermissionName(quickPermissionType.getText(context));
// bind listener to edit privileges // bind listener to edit privileges
binding.overflowMenu.setOnClickListener(v -> listener.showSharingMenuActionSheet(share)); binding.overflowMenu.setOnClickListener(v -> listener.showSharingMenuActionSheet(share));
@ -126,11 +136,21 @@ class ShareViewHolder extends RecyclerView.ViewHolder {
} }
} }
private void setImage(ImageView avatar, String name, @DrawableRes int fallback) { private void setImage(ImageView avatar, String name) {
if (TextUtils.isEmpty(name)) {
setUserImage(avatar);
return;
}
try { try {
avatar.setImageDrawable(TextDrawable.createNamedAvatar(name, avatarRadiusDimension)); avatar.setImageDrawable(TextDrawable.createNamedAvatar(name, avatarRadiusDimension));
} catch (StringIndexOutOfBoundsException e) { } catch (StringIndexOutOfBoundsException e) {
avatar.setImageResource(fallback); setUserImage(avatar);
} }
} }
private void setUserImage(ImageView avatar) {
ImageViewExtensionsKt.makeRoundedWithIcon(avatar, context, R.drawable.ic_user);
viewThemeUtils.platform.colorImageViewBackgroundAndIcon(avatar);
}
} }

View File

@ -79,57 +79,60 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
return shares.get(position).getShareType().getValue(); if (shares == null) {
return 0;
}
if (position < 0 || position >= shares.size()) {
return 0;
}
final var share = shares.get(position);
if (share == null) {
return 0;
}
final var shareType = share.getShareType();
if (shareType == null) {
return 0;
}
return shareType.getValue();
} }
@NonNull @NonNull
@Override @Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
boolean shareViaLink = MDMConfig.INSTANCE.shareViaLink(fileActivity); boolean shareViaLink = MDMConfig.INSTANCE.shareViaLink(fileActivity);
final var parentViewGroup = LayoutInflater.from(fileActivity);
if (shareViaLink) { if (!shareViaLink) {
switch (ShareType.fromValue(viewType)) { final var binding = FileDetailsShareInternalShareLinkBinding.inflate(parentViewGroup, parent, false);
case PUBLIC_LINK, EMAIL -> { return new InternalShareViewHolder(binding, fileActivity);
return new LinkShareViewHolder( }
FileDetailsShareLinkShareItemBinding.inflate(LayoutInflater.from(fileActivity),
parent, switch (ShareType.fromValue(viewType)) {
false), case PUBLIC_LINK, EMAIL -> {
fileActivity, final var binding = FileDetailsShareLinkShareItemBinding.inflate(parentViewGroup, parent, false);
viewThemeUtils); return new LinkShareViewHolder(binding, fileActivity, viewThemeUtils, encrypted);
} }
case NEW_PUBLIC_LINK -> { case NEW_PUBLIC_LINK -> {
if (encrypted) { if (encrypted) {
return new NewSecureFileDropViewHolder( final var binding = FileDetailsShareSecureFileDropAddNewItemBinding.inflate(parentViewGroup, parent, false);
FileDetailsShareSecureFileDropAddNewItemBinding.inflate(LayoutInflater.from(fileActivity), return new NewSecureFileDropViewHolder(binding);
parent, } else {
false) final var binding = FileDetailsSharePublicLinkAddNewItemBinding.inflate(parentViewGroup, parent, false);
); return new NewLinkShareViewHolder(binding);
} else {
return new NewLinkShareViewHolder(
FileDetailsSharePublicLinkAddNewItemBinding.inflate(LayoutInflater.from(fileActivity),
parent,
false)
);
}
}
case INTERNAL -> {
return new InternalShareViewHolder(
FileDetailsShareInternalShareLinkBinding.inflate(LayoutInflater.from(fileActivity), parent, false),
fileActivity);
}
default -> {
return new ShareViewHolder(FileDetailsShareShareItemBinding.inflate(LayoutInflater.from(fileActivity),
parent,
false),
user,
fileActivity,
viewThemeUtils);
} }
} }
} else { case INTERNAL -> {
return new InternalShareViewHolder( final var binding = FileDetailsShareInternalShareLinkBinding.inflate(parentViewGroup, parent, false);
FileDetailsShareInternalShareLinkBinding.inflate(LayoutInflater.from(fileActivity), parent, false), return new InternalShareViewHolder(binding, fileActivity);
fileActivity); }
default -> {
final var binding = FileDetailsShareShareItemBinding.inflate(parentViewGroup, parent, false);
return new ShareViewHolder(binding, user, fileActivity, viewThemeUtils, encrypted);
}
} }
} }
@ -152,7 +155,7 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
} }
if (holder instanceof LinkShareViewHolder publicShareViewHolder) { if (holder instanceof LinkShareViewHolder publicShareViewHolder) {
publicShareViewHolder.bind(share, listener); publicShareViewHolder.bind(share, listener, position);
} else if (holder instanceof InternalShareViewHolder internalShareViewHolder) { } else if (holder instanceof InternalShareViewHolder internalShareViewHolder) {
internalShareViewHolder.bind(share, listener); internalShareViewHolder.bind(share, listener);
} else if (holder instanceof NewLinkShareViewHolder newLinkShareViewHolder) { } else if (holder instanceof NewLinkShareViewHolder newLinkShareViewHolder) {
@ -186,7 +189,7 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
@SuppressLint("NotifyDataSetChanged") @SuppressLint("NotifyDataSetChanged")
public void toggleShowAll() { public void toggleShowAll() {
this.showAll = !this.showAll; showAll = !showAll;
notifyDataSetChanged(); notifyDataSetChanged();
} }
@ -201,6 +204,12 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
notifyDataSetChanged(); notifyDataSetChanged();
} }
@SuppressLint("NotifyDataSetChanged")
public void removeAll() {
shares.clear();
notifyDataSetChanged();
}
@Override @Override
public void avatarGenerated(Drawable avatarDrawable, Object callContext) { public void avatarGenerated(Drawable avatarDrawable, Object callContext) {
if (callContext instanceof ImageView iv) { if (callContext instanceof ImageView iv) {
@ -217,10 +226,12 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
return false; return false;
} }
@SuppressLint("NotifyDataSetChanged")
public void remove(OCShare share) { public void remove(OCShare share) {
shares.remove(share); int position = shares.indexOf(share);
notifyDataSetChanged(); if (position != -1) {
shares.remove(position);
notifyItemRemoved(position);
}
} }
/** /**
@ -255,13 +266,4 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
public List<OCShare> getShares() { public List<OCShare> getShares() {
return shares; return shares;
} }
public void removeNewPublicShare() {
for (OCShare share : shares) {
if (share.getShareType() == ShareType.NEW_PUBLIC_LINK) {
shares.remove(share);
break;
}
}
}
} }

View File

@ -72,6 +72,7 @@ import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Objects;
import javax.inject.Inject; import javax.inject.Inject;
@ -509,11 +510,11 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
setFileModificationTimestamp(getFile(), showDetailedTimestamp); setFileModificationTimestamp(getFile(), showDetailedTimestamp);
} else if (id == R.id.folder_sync_button) { } else if (id == R.id.folder_sync_button) {
if (binding.folderSyncButton.isChecked()) { if (binding.folderSyncButton.isChecked()) {
getFile().setInternalFolderSyncTimestamp(0L); getFile().setInternalFolderSyncTimestamp(0L);
} else { } else {
getFile().setInternalFolderSyncTimestamp(-1L); getFile().setInternalFolderSyncTimestamp(-1L);
} }
storageManager.saveFile(getFile()); storageManager.saveFile(getFile());
} else { } else {
Log_OC.e(TAG, "Incorrect view clicked!"); Log_OC.e(TAG, "Incorrect view clicked!");
@ -598,11 +599,11 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
if (fabMain != null) { if (fabMain != null) {
fabMain.hide(); fabMain.hide();
} }
binding.syncBlock.setVisibility(file.isFolder() ? View.VISIBLE : View.GONE); binding.syncBlock.setVisibility(file.isFolder() ? View.VISIBLE : View.GONE);
if (file.isInternalFolderSync()) { if (file.isInternalFolderSync()) {
binding.folderSyncButton.setChecked(file.isInternalFolderSync()); binding.folderSyncButton.setChecked(file.isInternalFolderSync());
} else { } else {
if (storageManager.isPartOfInternalTwoWaySync(file)) { if (storageManager.isPartOfInternalTwoWaySync(file)) {
binding.folderSyncButton.setChecked(true); binding.folderSyncButton.setChecked(true);
@ -814,18 +815,27 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
/** /**
* open the sharing process fragment for creating new share * open the sharing process fragment for creating new share
* *
* @param shareeName
* @param shareType
*/ */
public void initiateSharingProcess(String shareeName, public void initiateSharingProcess(String shareeName,
ShareType shareType, ShareType shareType,
boolean secureShare) { boolean secureShare) {
requireActivity().getSupportFragmentManager().beginTransaction().add(R.id.sharing_frame_container, if (getFile() == null) {
FileDetailsSharingProcessFragment.newInstance(getFile(), DisplayUtils.showSnackMessage(requireView(), R.string.file_not_found_cannot_share);
shareeName, return;
shareType, }
secureShare),
FileDetailsSharingProcessFragment.TAG) final var file = getFile();
if (Objects.equals(file.getOwnerId(), shareeName)) {
DisplayUtils.showSnackMessage(requireView(), R.string.file_detail_share_already_active);
return;
}
final var fileShareDetailFragment = FileDetailsSharingProcessFragment.newInstance(file, shareeName, shareType, secureShare);
requireActivity()
.getSupportFragmentManager()
.beginTransaction()
.add(R.id.sharing_frame_container, fileShareDetailFragment, FileDetailsSharingProcessFragment.TAG)
.commit(); .commit();
showHideFragmentView(true); showHideFragmentView(true);

View File

@ -37,6 +37,7 @@ import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.network.ClientFactory; import com.nextcloud.client.network.ClientFactory;
import com.nextcloud.utils.extensions.BundleExtensionsKt; import com.nextcloud.utils.extensions.BundleExtensionsKt;
import com.nextcloud.utils.extensions.FileExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt;
import com.nextcloud.utils.extensions.OCShareExtensionsKt;
import com.nextcloud.utils.extensions.ViewExtensionsKt; import com.nextcloud.utils.extensions.ViewExtensionsKt;
import com.nextcloud.utils.mdm.MDMConfig; import com.nextcloud.utils.mdm.MDMConfig;
import com.owncloud.android.R; import com.owncloud.android.R;
@ -178,6 +179,8 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
file.isEncrypted(), file.isEncrypted(),
SharesType.INTERNAL); SharesType.INTERNAL);
internalShareeListAdapter.setHasStableIds(true);
binding.sharesListInternal.setAdapter(internalShareeListAdapter); binding.sharesListInternal.setAdapter(internalShareeListAdapter);
binding.sharesListInternal.setLayoutManager(new LinearLayoutManager(requireContext())); binding.sharesListInternal.setLayoutManager(new LinearLayoutManager(requireContext()));
@ -190,6 +193,8 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
viewThemeUtils, viewThemeUtils,
file.isEncrypted(), file.isEncrypted(),
SharesType.EXTERNAL); SharesType.EXTERNAL);
externalShareeListAdapter.setHasStableIds(true);
binding.sharesListExternal.setAdapter(externalShareeListAdapter); binding.sharesListExternal.setAdapter(externalShareeListAdapter);
@ -214,10 +219,11 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
if (!(getActivity() instanceof FileActivity)) { if (!(getActivity() instanceof FileActivity)) {
throw new IllegalArgumentException("Calling activity must be of type FileActivity"); throw new IllegalArgumentException("Calling activity must be of type FileActivity");
} }
try { try {
onEditShareListener = (OnEditShareListener) context; onEditShareListener = (OnEditShareListener) context;
} catch (Exception ignored) { } catch (Exception e) {
throw new IllegalArgumentException("Calling activity must implement the interface", ignored); throw new IllegalArgumentException("Calling activity must implement the interface" + e);
} }
} }
@ -247,23 +253,15 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
viewThemeUtils.material.colorMaterialTextButton(binding.sharesListInternalShowAll); viewThemeUtils.material.colorMaterialTextButton(binding.sharesListInternalShowAll);
binding.sharesListInternalShowAll.setOnClickListener(view -> { binding.sharesListInternalShowAll.setOnClickListener(view -> {
internalShareeListAdapter.toggleShowAll(); internalShareeListAdapter.toggleShowAll();
int textRes = internalShareeListAdapter.isShowAll() ? R.string.show_less : R.string.show_all;
if (internalShareeListAdapter.isShowAll()) { binding.sharesListInternalShowAll.setText(textRes);
binding.sharesListInternalShowAll.setText(R.string.show_less);
} else {
binding.sharesListInternalShowAll.setText(R.string.show_all);
}
}); });
viewThemeUtils.material.colorMaterialTextButton(binding.sharesListExternalShowAll); viewThemeUtils.material.colorMaterialTextButton(binding.sharesListExternalShowAll);
binding.sharesListExternalShowAll.setOnClickListener(view -> { binding.sharesListExternalShowAll.setOnClickListener(view -> {
externalShareeListAdapter.toggleShowAll(); externalShareeListAdapter.toggleShowAll();
int textRes = externalShareeListAdapter.isShowAll() ? R.string.show_less : R.string.show_all;
if (internalShareeListAdapter.isShowAll()) { binding.sharesListExternalShowAll.setText(textRes);
binding.sharesListExternalShowAll.setText(R.string.show_less);
} else {
binding.sharesListExternalShowAll.setText(R.string.show_all);
}
}); });
if (file.canReshare() && !FileDetailSharingFragmentHelper.isPublicShareDisabled(capabilities)) { if (file.canReshare() && !FileDetailSharingFragmentHelper.isPublicShareDisabled(capabilities)) {
@ -409,7 +407,7 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
@VisibleForTesting @VisibleForTesting
public void showSharingMenuActionSheet(OCShare share) { public void showSharingMenuActionSheet(OCShare share) {
if (fileActivity != null && !fileActivity.isFinishing()) { if (fileActivity != null && !fileActivity.isFinishing()) {
new FileDetailSharingMenuBottomSheetDialog(fileActivity, this, share, viewThemeUtils).show(); new FileDetailSharingMenuBottomSheetDialog(fileActivity, this, share, viewThemeUtils, file.isEncrypted()).show();
} }
} }
@ -420,7 +418,7 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
*/ */
@Override @Override
public void showPermissionsDialog(OCShare share) { public void showPermissionsDialog(OCShare share) {
new QuickSharingPermissionsBottomSheetDialog(fileActivity, this, share, viewThemeUtils).show(); new QuickSharingPermissionsBottomSheetDialog(fileActivity, this, share, viewThemeUtils, file.isEncrypted()).show();
} }
/** /**
@ -460,8 +458,8 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
setupView(); setupView();
} }
private void unshareWith(OCShare share) { private void unShareWith(OCShare share) {
fileOperationsHelper.unshareShare(file, share); fileOperationsHelper.unShareShare(file, share);
} }
/** /**
@ -517,7 +515,8 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
DisplayUtils.showSnackMessage(getView(), getString(R.string.could_not_retrieve_shares)); DisplayUtils.showSnackMessage(getView(), getString(R.string.could_not_retrieve_shares));
return; return;
} }
internalShareeListAdapter.getShares().clear();
internalShareeListAdapter.removeAll();
// to show share with users/groups info // to show share with users/groups info
List<OCShare> shares = fileDataStorageManager.getSharesWithForAFile(file.getRemotePath(), List<OCShare> shares = fileDataStorageManager.getSharesWithForAFile(file.getRemotePath(),
@ -544,25 +543,17 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
} }
internalShareeListAdapter.addShares(internalShares); internalShareeListAdapter.addShares(internalShares);
ViewExtensionsKt.setVisibleIf(binding.sharesListInternalShowAll, internalShareeListAdapter.getShares().size() > 3);
ViewExtensionsKt.setVisibleIf(binding.sharesListInternalShowAll, addExternalAndPublicShares(externalShares);
internalShareeListAdapter.getShares().size() > 3 ViewExtensionsKt.setVisibleIf(binding.sharesListExternalShowAll, externalShareeListAdapter.getShares().size() > 3);
); }
externalShareeListAdapter.getShares().clear(); private void addExternalAndPublicShares(List<OCShare> externalShares) {
final var publicShares = fileDataStorageManager.getSharesByPathAndType(file.getRemotePath(), ShareType.PUBLIC_LINK, "");
// Get public share externalShareeListAdapter.removeAll();
List<OCShare> publicShares = fileDataStorageManager.getSharesByPathAndType(file.getRemotePath(), final var shares = OCShareExtensionsKt.mergeDistinctByToken(externalShares, publicShares);
ShareType.PUBLIC_LINK, externalShareeListAdapter.addShares(shares);
"");
externalShareeListAdapter.addShares(externalShares);
externalShareeListAdapter.addShares(publicShares);
ViewExtensionsKt.setVisibleIf(binding.sharesListExternalShowAll,
externalShareeListAdapter.getShares().size() > 3
);
} }
private void checkContactPermission() { private void checkContactPermission() {
@ -650,7 +641,6 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_PERMISSION); modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_PERMISSION);
} }
@Override @Override
public void sendNewEmail(OCShare share) { public void sendNewEmail(OCShare share) {
modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_NOTE); modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_NOTE);
@ -658,13 +648,15 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
@Override @Override
public void unShare(OCShare share) { public void unShare(OCShare share) {
unshareWith(share); unShareWith(share);
ShareeListAdapter adapter = (ShareeListAdapter) binding.sharesListInternal.getAdapter();
if (adapter == null) { if (binding.sharesListInternal.getAdapter() instanceof ShareeListAdapter adapter) {
adapter.remove(share);
} else if (binding.sharesListExternal.getAdapter() instanceof ShareeListAdapter adapter) {
adapter.remove(share);
} else {
DisplayUtils.showSnackMessage(getView(), getString(R.string.failed_update_ui)); DisplayUtils.showSnackMessage(getView(), getString(R.string.failed_update_ui));
return;
} }
adapter.remove(share);
} }
@Override @Override
@ -691,6 +683,11 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
fileOperationsHelper.setPermissionsToShare(share, permission); fileOperationsHelper.setPermissionsToShare(share, permission);
} }
@Override
public void openShareDetailWithCustomPermissions(OCShare share) {
modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_PERMISSION_WITH_CUSTOM_PERMISSION);
}
//launcher for contact permission //launcher for contact permission
private final ActivityResultLauncher<String> requestContactPermissionLauncher = private final ActivityResultLauncher<String> requestContactPermissionLauncher =
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {

View File

@ -16,12 +16,13 @@ import android.view.ViewGroup;
import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.nextcloud.android.common.ui.theme.utils.ColorRole;
import com.nextcloud.utils.mdm.MDMConfig; import com.nextcloud.utils.mdm.MDMConfig;
import com.owncloud.android.databinding.FileDetailsSharingMenuBottomSheetFragmentBinding; import com.owncloud.android.databinding.FileDetailsSharingMenuBottomSheetFragmentBinding;
import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.lib.resources.shares.ShareType;
import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.FileActivity;
import com.owncloud.android.ui.fragment.util.SharingMenuHelper; import com.owncloud.android.ui.fragment.util.SharePermissionManager;
import com.owncloud.android.utils.theme.ViewThemeUtils; import com.owncloud.android.utils.theme.ViewThemeUtils;
/** /**
@ -32,14 +33,18 @@ public class FileDetailSharingMenuBottomSheetDialog extends BottomSheetDialog {
private final FileDetailsSharingMenuBottomSheetActions actions; private final FileDetailsSharingMenuBottomSheetActions actions;
private final OCShare ocShare; private final OCShare ocShare;
private final ViewThemeUtils viewThemeUtils; private final ViewThemeUtils viewThemeUtils;
private final boolean encrypted;
public FileDetailSharingMenuBottomSheetDialog(FileActivity fileActivity, public FileDetailSharingMenuBottomSheetDialog(FileActivity fileActivity,
FileDetailsSharingMenuBottomSheetActions actions, FileDetailsSharingMenuBottomSheetActions actions,
OCShare ocShare, OCShare ocShare,
ViewThemeUtils viewThemeUtils) { ViewThemeUtils viewThemeUtils,
boolean encrypted) {
super(fileActivity); super(fileActivity);
this.actions = actions; this.actions = actions;
this.ocShare = ocShare; this.ocShare = ocShare;
this.viewThemeUtils = viewThemeUtils; this.viewThemeUtils = viewThemeUtils;
this.encrypted = encrypted;
} }
@Override @Override
@ -54,10 +59,10 @@ public class FileDetailSharingMenuBottomSheetDialog extends BottomSheetDialog {
viewThemeUtils.platform.themeDialog(binding.getRoot()); viewThemeUtils.platform.themeDialog(binding.getRoot());
viewThemeUtils.platform.colorImageView(binding.menuIconAdvancedPermissions); viewThemeUtils.platform.colorImageView(binding.menuIconAdvancedPermissions, ColorRole.PRIMARY);
viewThemeUtils.platform.colorImageView(binding.menuIconSendLink); viewThemeUtils.platform.colorImageView(binding.menuIconSendLink, ColorRole.PRIMARY);
viewThemeUtils.platform.colorImageView(binding.menuIconUnshare); viewThemeUtils.platform.colorImageView(binding.menuIconUnshare, ColorRole.PRIMARY);
viewThemeUtils.platform.colorImageView(binding.menuIconSendNewEmail); viewThemeUtils.platform.colorImageView(binding.menuIconSendNewEmail, ColorRole.PRIMARY);
updateUI(); updateUI();
@ -78,7 +83,7 @@ public class FileDetailSharingMenuBottomSheetDialog extends BottomSheetDialog {
binding.menuShareSendLink.setVisibility(View.GONE); binding.menuShareSendLink.setVisibility(View.GONE);
} }
if (SharingMenuHelper.isSecureFileDrop(ocShare)) { if (SharePermissionManager.INSTANCE.isSecureFileDrop(ocShare) && encrypted) {
binding.menuShareAdvancedPermissions.setVisibility(View.GONE); binding.menuShareAdvancedPermissions.setVisibility(View.GONE);
} }
} }

View File

@ -1,11 +1,8 @@
/* /*
* Nextcloud Android client application * Nextcloud - Android Client
* *
* @author TSI-mc * SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* Copyright (C) 2021 TSI-mc * SPDX-License-Identifier: AGPL-3.0-or-later
* Copyright (C) 2021 Nextcloud GmbH
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/ */
package com.owncloud.android.ui.fragment package com.owncloud.android.ui.fragment
@ -21,17 +18,20 @@ import androidx.fragment.app.Fragment
import com.nextcloud.client.di.Injectable import com.nextcloud.client.di.Injectable
import com.nextcloud.utils.extensions.getParcelableArgument import com.nextcloud.utils.extensions.getParcelableArgument
import com.nextcloud.utils.extensions.getSerializableArgument import com.nextcloud.utils.extensions.getSerializableArgument
import com.nextcloud.utils.extensions.isPublicOrMail
import com.nextcloud.utils.extensions.setVisibilityWithAnimation
import com.nextcloud.utils.extensions.setVisibleIf import com.nextcloud.utils.extensions.setVisibleIf
import com.owncloud.android.R import com.owncloud.android.R
import com.owncloud.android.databinding.FileDetailsSharingProcessFragmentBinding import com.owncloud.android.databinding.FileDetailsSharingProcessFragmentBinding
import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.datamodel.quickPermission.QuickPermissionType
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.OCShare
import com.owncloud.android.lib.resources.shares.SharePermissionsBuilder
import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.lib.resources.shares.ShareType
import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.lib.resources.status.OCCapability
import com.owncloud.android.ui.activity.FileActivity import com.owncloud.android.ui.activity.FileActivity
import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment
import com.owncloud.android.ui.fragment.util.SharingMenuHelper import com.owncloud.android.ui.fragment.util.SharePermissionManager
import com.owncloud.android.ui.helpers.FileOperationsHelper import com.owncloud.android.ui.helpers.FileOperationsHelper
import com.owncloud.android.utils.ClipboardUtil import com.owncloud.android.utils.ClipboardUtil
import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.DisplayUtils
@ -69,6 +69,7 @@ class FileDetailsSharingProcessFragment :
// types of screens to be displayed // types of screens to be displayed
const val SCREEN_TYPE_PERMISSION = 1 // permissions screen const val SCREEN_TYPE_PERMISSION = 1 // permissions screen
const val SCREEN_TYPE_NOTE = 2 // note screen const val SCREEN_TYPE_NOTE = 2 // note screen
const val SCREEN_TYPE_PERMISSION_WITH_CUSTOM_PERMISSION = 3 // permissions screen with custom permission
/** /**
* fragment instance to be called while creating new share for internal and external share * fragment instance to be called while creating new share for internal and external share
@ -80,14 +81,16 @@ class FileDetailsSharingProcessFragment :
shareType: ShareType, shareType: ShareType,
secureShare: Boolean secureShare: Boolean
): FileDetailsSharingProcessFragment { ): FileDetailsSharingProcessFragment {
val args = Bundle() val bundle = Bundle().apply {
args.putParcelable(ARG_OCFILE, file) putParcelable(ARG_OCFILE, file)
args.putSerializable(ARG_SHARE_TYPE, shareType) putSerializable(ARG_SHARE_TYPE, shareType)
args.putString(ARG_SHAREE_NAME, shareeName) putString(ARG_SHAREE_NAME, shareeName)
args.putBoolean(ARG_SECURE_SHARE, secureShare) putBoolean(ARG_SECURE_SHARE, secureShare)
val fragment = FileDetailsSharingProcessFragment() }
fragment.arguments = args
return fragment return FileDetailsSharingProcessFragment().apply {
arguments = bundle
}
} }
/** /**
@ -100,14 +103,16 @@ class FileDetailsSharingProcessFragment :
isReshareShown: Boolean, isReshareShown: Boolean,
isExpirationDateShown: Boolean isExpirationDateShown: Boolean
): FileDetailsSharingProcessFragment { ): FileDetailsSharingProcessFragment {
val args = Bundle() val bundle = Bundle().apply {
args.putParcelable(ARG_OCSHARE, share) putParcelable(ARG_OCSHARE, share)
args.putInt(ARG_SCREEN_TYPE, screenType) putInt(ARG_SCREEN_TYPE, screenType)
args.putBoolean(ARG_RESHARE_SHOWN, isReshareShown) putBoolean(ARG_RESHARE_SHOWN, isReshareShown)
args.putBoolean(ARG_EXP_DATE_SHOWN, isExpirationDateShown) putBoolean(ARG_EXP_DATE_SHOWN, isExpirationDateShown)
val fragment = FileDetailsSharingProcessFragment() }
fragment.arguments = args
return fragment return FileDetailsSharingProcessFragment().apply {
arguments = bundle
}
} }
} }
@ -135,12 +140,13 @@ class FileDetailsSharingProcessFragment :
private lateinit var capabilities: OCCapability private lateinit var capabilities: OCCapability
private var expirationDatePickerFragment: ExpirationDatePickerDialogFragment? = null private var expirationDatePickerFragment: ExpirationDatePickerDialogFragment? = null
private var downloadAttribute: String? = null
override fun onAttach(context: Context) { override fun onAttach(context: Context) {
super.onAttach(context) super.onAttach(context)
try { try {
onEditShareListener = context as FileDetailSharingFragment.OnEditShareListener onEditShareListener = context as FileDetailSharingFragment.OnEditShareListener
} catch (e: ClassCastException) { } catch (_: ClassCastException) {
throw IllegalStateException("Calling activity must implement the interface") throw IllegalStateException("Calling activity must implement the interface")
} }
} }
@ -148,6 +154,18 @@ class FileDetailsSharingProcessFragment :
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
initArguments()
fileActivity = activity as FileActivity?
capabilities = CapabilityUtils.getCapability(context)
requireNotNull(fileActivity) { "FileActivity may not be null" }
permission = share?.permissions
?: capabilities.defaultPermissions
?: SharePermissionManager.getMaximumPermission(isFolder())
}
private fun initArguments() {
arguments?.let { arguments?.let {
file = it.getParcelableArgument(ARG_OCFILE, OCFile::class.java) file = it.getParcelableArgument(ARG_OCFILE, OCFile::class.java)
shareeName = it.getString(ARG_SHAREE_NAME) shareeName = it.getString(ARG_SHAREE_NAME)
@ -164,13 +182,6 @@ class FileDetailsSharingProcessFragment :
isExpDateShown = it.getBoolean(ARG_EXP_DATE_SHOWN, true) isExpDateShown = it.getBoolean(ARG_EXP_DATE_SHOWN, true)
isSecureShare = it.getBoolean(ARG_SECURE_SHARE, false) isSecureShare = it.getBoolean(ARG_SECURE_SHARE, false)
} }
fileActivity = activity as FileActivity?
capabilities = CapabilityUtils.getCapability(context)
requireNotNull(fileActivity) { "FileActivity may not be null" }
permission = capabilities.defaultPermissions ?: OCShare.NO_PERMISSION
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -181,39 +192,84 @@ class FileDetailsSharingProcessFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState) super.onViewCreated(view, savedInstanceState)
if (shareProcessStep == SCREEN_TYPE_PERMISSION) { if (isShareProcessStepIsPermission()) {
showShareProcessFirst() setupUI()
} else { } else {
showShareProcessSecond() updateViewForNoteScreenType()
} }
implementClickEvents()
implementClickEvents()
setCheckboxStates()
themeView() themeView()
setVisibilitiesOfShareOption()
toggleNextButtonAvailability(isAnyShareOptionChecked())
logShareInfo()
}
private fun logShareInfo() {
share?.run {
Log_OC.i(TAG, "-----BEFORE UPDATE SHARE-----")
Log_OC.i(TAG, "ID: $id")
Log_OC.i(TAG, "Permission: $permissions")
Log_OC.i(TAG, "Hide File Download: $isHideFileDownload")
Log_OC.i(TAG, "Label: $label")
Log_OC.i(TAG, "Attributes: $attributes")
}
}
private fun setVisibilitiesOfShareOption() {
binding.run {
shareAllowDownloadAndSyncCheckbox.setVisibleIf(!isPublicShare())
fileRequestRadioButton.setVisibleIf(canSetFileRequest())
}
} }
private fun themeView() { private fun themeView() {
viewThemeUtils.platform.colorTextView(binding.shareProcessEditShareLink) viewThemeUtils.platform.run {
viewThemeUtils.platform.colorTextView(binding.shareProcessAdvancePermissionTitle) binding.run {
colorTextView(shareProcessEditShareLink)
colorTextView(shareCustomPermissionsText)
viewThemeUtils.platform.themeRadioButton(binding.shareProcessPermissionReadOnly) themeRadioButton(viewOnlyRadioButton)
viewThemeUtils.platform.themeRadioButton(binding.shareProcessPermissionUploadEditing) themeRadioButton(canEditRadioButton)
viewThemeUtils.platform.themeRadioButton(binding.shareProcessPermissionFileDrop) themeRadioButton(customPermissionRadioButton)
viewThemeUtils.platform.themeCheckbox(binding.shareProcessAllowResharingCheckbox) if (!isPublicShare()) {
themeCheckbox(shareAllowDownloadAndSyncCheckbox)
}
viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessSetPasswordSwitch) if (canSetFileRequest()) {
viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessSetExpDateSwitch) themeRadioButton(fileRequestRadioButton)
viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessSetDownloadLimitSwitch) }
viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessHideDownloadCheckbox)
viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessChangeNameSwitch)
viewThemeUtils.material.colorTextInputLayout(binding.shareProcessEnterPasswordContainer) themeCheckbox(shareReadCheckbox)
viewThemeUtils.material.colorTextInputLayout(binding.shareProcessSetDownloadLimitInputContainer) themeCheckbox(shareCreateCheckbox)
viewThemeUtils.material.colorTextInputLayout(binding.shareProcessChangeNameContainer) themeCheckbox(shareEditCheckbox)
viewThemeUtils.material.colorTextInputLayout(binding.noteContainer) themeCheckbox(shareCheckbox)
themeCheckbox(shareDeleteCheckbox)
}
}
viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.shareProcessBtnNext) viewThemeUtils.androidx.run {
viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.shareProcessBtnCancel) binding.run {
colorSwitchCompat(shareProcessSetPasswordSwitch)
colorSwitchCompat(shareProcessSetExpDateSwitch)
colorSwitchCompat(shareProcessSetDownloadLimitSwitch)
colorSwitchCompat(shareProcessHideDownloadCheckbox)
colorSwitchCompat(shareProcessChangeNameSwitch)
}
}
viewThemeUtils.material.run {
binding.run {
colorTextInputLayout(shareProcessEnterPasswordContainer)
colorTextInputLayout(shareProcessSetDownloadLimitInputContainer)
colorTextInputLayout(shareProcessChangeNameContainer)
colorTextInputLayout(noteContainer)
colorMaterialButtonPrimaryFilled(shareProcessBtnNext)
colorMaterialButtonPrimaryOutlined(shareProcessBtnCancel)
}
}
} }
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
@ -229,40 +285,63 @@ class FileDetailsSharingProcessFragment :
} }
} }
private fun showShareProcessFirst() { private fun setupUI() {
binding.shareProcessGroupOne.visibility = View.VISIBLE binding.run {
binding.shareProcessEditShareLink.visibility = View.VISIBLE shareProcessGroupOne.visibility = View.VISIBLE
binding.shareProcessGroupTwo.visibility = View.GONE shareProcessEditShareLink.visibility = View.VISIBLE
shareProcessGroupTwo.visibility = View.GONE
if (share != null) {
setupModificationUI()
} else {
setupUpdateUI()
} }
if (isSecureShare) { updateView()
binding.shareProcessAdvancePermissionTitle.visibility = View.GONE
}
// show or hide expiry date // show or hide expiry date
if (isExpDateShown && !isSecureShare) { binding.shareProcessSetExpDateSwitch.setVisibleIf(isExpDateShown && !isSecureShare)
binding.shareProcessSetExpDateSwitch.visibility = View.VISIBLE
} else {
binding.shareProcessSetExpDateSwitch.visibility = View.GONE
}
shareProcessStep = SCREEN_TYPE_PERMISSION shareProcessStep = SCREEN_TYPE_PERMISSION
} }
private fun setupModificationUI() { private fun updateView() {
if (share != null) {
updateViewForUpdate()
} else {
updateViewForCreate()
}
}
private fun setMaxPermissionsIfDefaultPermissionExists() {
if (capabilities.defaultPermissions != null) {
binding.canEditRadioButton.isChecked = true
permission = SharePermissionManager.getMaximumPermission(isFolder())
}
}
// region ViewUpdates
private fun updateViewForCreate() {
binding.shareProcessBtnNext.text = getString(R.string.common_next)
updateViewAccordingToFile()
showPasswordInput(binding.shareProcessSetPasswordSwitch.isChecked)
showExpirationDateInput(binding.shareProcessSetExpDateSwitch.isChecked)
showFileDownloadLimitInput(binding.shareProcessSetDownloadLimitSwitch.isChecked)
setMaxPermissionsIfDefaultPermissionExists()
}
private fun updateViewAccordingToFile() {
file?.run {
if (isFolder == true) {
updateViewForFolder()
} else {
updateViewForFile()
}
updateViewForShareType()
}
}
private fun updateViewForUpdate() {
if (share?.isFolder == true) updateViewForFolder() else updateViewForFile() if (share?.isFolder == true) updateViewForFolder() else updateViewForFile()
// read only / allow upload and editing / file drop selectRadioButtonAccordingToPermission()
if (SharingMenuHelper.isUploadAndEditingAllowed(share)) {
binding.shareProcessPermissionUploadEditing.isChecked = true if (isShareProcessStepIsCustomPermission()) {
} else if (SharingMenuHelper.isFileDrop(share) && share?.isFolder == true) { selectCustomPermissionLayout()
binding.shareProcessPermissionFileDrop.isChecked = true
} else if (SharingMenuHelper.isReadOnly(share)) {
binding.shareProcessPermissionReadOnly.isChecked = true
} }
shareType = share?.shareType ?: ShareType.NO_SHARED shareType = share?.shareType ?: ShareType.NO_SHARED
@ -270,7 +349,7 @@ class FileDetailsSharingProcessFragment :
// show different text for link share and other shares // show different text for link share and other shares
// because we have link to share in Public Link // because we have link to share in Public Link
binding.shareProcessBtnNext.text = getString( binding.shareProcessBtnNext.text = getString(
if (shareType == ShareType.PUBLIC_LINK) { if (isPublicShare()) {
R.string.share_copy_link R.string.share_copy_link
} else { } else {
R.string.common_confirm R.string.common_confirm
@ -286,19 +365,34 @@ class FileDetailsSharingProcessFragment :
showFileDownloadLimitInput(binding.shareProcessSetDownloadLimitSwitch.isChecked) showFileDownloadLimitInput(binding.shareProcessSetDownloadLimitSwitch.isChecked)
} }
private fun setupUpdateUI() { private fun selectRadioButtonAccordingToPermission() {
binding.shareProcessBtnNext.text = getString(R.string.common_next) val selectedType = SharePermissionManager.getSelectedType(share, encrypted = file?.isEncrypted == true)
file.let { binding.run {
if (file?.isFolder == true) { when (selectedType) {
updateViewForFolder() QuickPermissionType.VIEW_ONLY -> {
} else { viewOnlyRadioButton.isChecked = true
updateViewForFile() }
QuickPermissionType.CAN_EDIT -> {
canEditRadioButton.isChecked = true
}
QuickPermissionType.FILE_REQUEST -> {
fileRequestRadioButton.isChecked = true
}
QuickPermissionType.CUSTOM_PERMISSIONS -> {
selectCustomPermissionLayout()
}
else -> Unit
} }
updateViewForShareType()
} }
showPasswordInput(binding.shareProcessSetPasswordSwitch.isChecked) }
showExpirationDateInput(binding.shareProcessSetExpDateSwitch.isChecked)
showFileDownloadLimitInput(binding.shareProcessSetDownloadLimitSwitch.isChecked) private fun selectCustomPermissionLayout() {
binding.customPermissionRadioButton.isChecked = true
binding.customPermissionLayout.setVisibilityWithAnimation(true)
} }
private fun updateViewForShareType() { private fun updateViewForShareType() {
@ -318,61 +412,60 @@ class FileDetailsSharingProcessFragment :
} }
private fun updateViewForExternalShare() { private fun updateViewForExternalShare() {
binding.shareProcessChangeNameSwitch.visibility = View.GONE binding.run {
binding.shareProcessChangeNameContainer.visibility = View.GONE shareProcessChangeNameSwitch.visibility = View.GONE
updateViewForExternalAndLinkShare() shareProcessChangeNameContainer.visibility = View.GONE
updateViewForExternalAndLinkShare()
}
} }
private fun updateViewForLinkShare() { private fun updateViewForLinkShare() {
updateViewForExternalAndLinkShare() updateViewForExternalAndLinkShare()
binding.shareProcessChangeNameSwitch.visibility = View.VISIBLE binding.run {
if (share != null) { shareProcessChangeNameSwitch.visibility = View.VISIBLE
binding.shareProcessChangeName.setText(share?.label) if (share != null) {
binding.shareProcessChangeNameSwitch.isChecked = !TextUtils.isEmpty(share?.label) shareProcessChangeName.setText(share?.label)
shareProcessChangeNameSwitch.isChecked = !TextUtils.isEmpty(share?.label)
}
shareReadCheckbox.isEnabled = isFolder()
showChangeNameInput(shareProcessChangeNameSwitch.isChecked)
} }
showChangeNameInput(binding.shareProcessChangeNameSwitch.isChecked)
} }
private fun updateViewForInternalShare() { private fun updateViewForInternalShare() {
binding.shareProcessChangeNameSwitch.visibility = View.GONE binding.run {
binding.shareProcessChangeNameContainer.visibility = View.GONE shareProcessChangeNameSwitch.visibility = View.GONE
binding.shareProcessHideDownloadCheckbox.visibility = View.GONE shareProcessChangeNameContainer.visibility = View.GONE
if (isSecureShare) { shareProcessHideDownloadCheckbox.visibility = View.GONE
binding.shareProcessAllowResharingCheckbox.visibility = View.GONE shareCheckbox.setVisibleIf(!isSecureShare)
} else { shareProcessSetPasswordSwitch.visibility = View.GONE
binding.shareProcessAllowResharingCheckbox.visibility = View.VISIBLE
}
binding.shareProcessSetPasswordSwitch.visibility = View.GONE
if (share != null) { if (share != null) {
if (!isReShareShown) { if (!isReShareShown) {
binding.shareProcessAllowResharingCheckbox.visibility = View.GONE shareCheckbox.visibility = View.GONE
}
shareCheckbox.isChecked = SharePermissionManager.canReshare(share)
} }
binding.shareProcessAllowResharingCheckbox.isChecked = SharingMenuHelper.canReshare(share)
} }
} }
/**
* update views where share type external or link share
*/
private fun updateViewForExternalAndLinkShare() { private fun updateViewForExternalAndLinkShare() {
binding.shareProcessHideDownloadCheckbox.visibility = View.VISIBLE binding.run {
binding.shareProcessAllowResharingCheckbox.visibility = View.GONE shareProcessHideDownloadCheckbox.visibility = View.VISIBLE
binding.shareProcessSetPasswordSwitch.visibility = View.VISIBLE shareCheckbox.visibility = View.GONE
shareProcessSetPasswordSwitch.visibility = View.VISIBLE
if (share != null) { if (share != null) {
if (SharingMenuHelper.isFileDrop(share)) { if (SharePermissionManager.isFileRequest(share)) {
binding.shareProcessHideDownloadCheckbox.visibility = View.GONE shareProcessHideDownloadCheckbox.visibility = View.GONE
} else { } else {
binding.shareProcessHideDownloadCheckbox.visibility = View.VISIBLE shareProcessHideDownloadCheckbox.visibility = View.VISIBLE
binding.shareProcessHideDownloadCheckbox.isChecked = share?.isHideFileDownload == true shareProcessHideDownloadCheckbox.isChecked = share?.isHideFileDownload == true
}
} }
} }
} }
/**
* update expiration date view while modifying the share
*/
private fun updateExpirationDateView() { private fun updateExpirationDateView() {
share?.let { share -> share?.let { share ->
if (share.expirationDate > 0) { if (share.expirationDate > 0) {
@ -387,7 +480,7 @@ class FileDetailsSharingProcessFragment :
} }
private fun updateFileDownloadLimitView() { private fun updateFileDownloadLimitView() {
if (capabilities.filesDownloadLimit.isTrue && share?.isFolder == false) { if (canSetDownloadLimit()) {
binding.shareProcessSetDownloadLimitSwitch.visibility = View.VISIBLE binding.shareProcessSetDownloadLimitSwitch.visibility = View.VISIBLE
val currentDownloadLimit = share?.fileDownloadLimit?.limit ?: capabilities.filesDownloadLimitDefault val currentDownloadLimit = share?.fileDownloadLimit?.limit ?: capabilities.filesDownloadLimitDefault
@ -400,63 +493,175 @@ class FileDetailsSharingProcessFragment :
} }
private fun updateViewForFile() { private fun updateViewForFile() {
binding.shareProcessPermissionUploadEditing.text = getString(R.string.link_share_editing) binding.run {
binding.shareProcessPermissionFileDrop.visibility = View.GONE canEditRadioButton.text = getString(R.string.link_share_editing)
}
} }
private fun updateViewForFolder() { private fun updateViewForFolder() {
binding.shareProcessPermissionUploadEditing.text = getString(R.string.link_share_allow_upload_and_editing) binding.run {
binding.shareProcessPermissionFileDrop.visibility = View.VISIBLE canEditRadioButton.text = getString(R.string.share_permission_can_edit)
if (isSecureShare) {
binding.shareProcessPermissionFileDrop.visibility = View.GONE
binding.shareProcessAllowResharingCheckbox.visibility = View.GONE
binding.shareProcessSetExpDateSwitch.visibility = View.GONE
}
}
/** if (isSecureShare) {
* update views for screen type Note shareCheckbox.visibility = View.GONE
*/ shareProcessSetExpDateSwitch.visibility = View.GONE
private fun showShareProcessSecond() {
binding.shareProcessGroupOne.visibility = View.GONE
binding.shareProcessEditShareLink.visibility = View.GONE
binding.shareProcessGroupTwo.visibility = View.VISIBLE
if (share != null) {
binding.shareProcessBtnNext.text = getString(R.string.set_note)
binding.noteText.setText(share?.note)
} else {
binding.shareProcessBtnNext.text = getString(R.string.send_share)
binding.noteText.setText(R.string.empty)
}
shareProcessStep = SCREEN_TYPE_NOTE
}
private fun implementClickEvents() {
binding.shareProcessBtnCancel.setOnClickListener {
onCancelClick()
}
binding.shareProcessBtnNext.setOnClickListener {
if (shareProcessStep == SCREEN_TYPE_PERMISSION) {
validateShareProcessFirst()
} else {
validateShareProcessSecond()
} }
} }
binding.shareProcessSetPasswordSwitch.setOnCheckedChangeListener { _, isChecked -> }
showPasswordInput(isChecked)
private fun updateViewForNoteScreenType() {
binding.run {
shareProcessGroupOne.visibility = View.GONE
shareProcessEditShareLink.visibility = View.GONE
shareProcessGroupTwo.visibility = View.VISIBLE
if (share != null) {
shareProcessBtnNext.text = getString(R.string.set_note)
noteText.setText(share?.note)
} else {
shareProcessBtnNext.text = getString(R.string.send_share)
noteText.setText(R.string.empty)
}
shareProcessStep = SCREEN_TYPE_NOTE
shareProcessBtnNext.performClick()
} }
binding.shareProcessSetExpDateSwitch.setOnCheckedChangeListener { _, isChecked -> }
showExpirationDateInput(isChecked) // endregion
@Suppress("LongMethod")
private fun implementClickEvents() {
binding.run {
shareProcessBtnCancel.setOnClickListener {
onCancelClick()
}
shareProcessBtnNext.setOnClickListener {
if (isShareProcessStepIsPermission()) {
validateShareProcessFirst()
} else {
createShareOrUpdateNoteShare()
}
}
shareProcessSetPasswordSwitch.setOnCheckedChangeListener { _, isChecked ->
showPasswordInput(isChecked)
}
shareProcessSetExpDateSwitch.setOnCheckedChangeListener { _, isChecked ->
showExpirationDateInput(isChecked)
}
shareProcessSetDownloadLimitSwitch.setOnCheckedChangeListener { _, isChecked ->
showFileDownloadLimitInput(isChecked)
}
shareProcessChangeNameSwitch.setOnCheckedChangeListener { _, isChecked ->
showChangeNameInput(isChecked)
}
shareProcessSelectExpDate.setOnClickListener {
showExpirationDateDialog()
}
// region RadioButtons
shareRadioGroup.setOnCheckedChangeListener { _, optionId ->
when (optionId) {
R.id.view_only_radio_button -> {
permission = OCShare.READ_PERMISSION_FLAG
}
R.id.can_edit_radio_button -> {
permission = SharePermissionManager.getMaximumPermission(isFolder())
}
R.id.file_request_radio_button -> {
permission = OCShare.CREATE_PERMISSION_FLAG
}
}
val isCustomPermissionSelected = (optionId == R.id.custom_permission_radio_button)
customPermissionLayout.setVisibilityWithAnimation(isCustomPermissionSelected)
toggleNextButtonAvailability(true)
}
// endregion
} }
binding.shareProcessSetDownloadLimitSwitch.setOnCheckedChangeListener { _, isChecked -> }
showFileDownloadLimitInput(isChecked)
private fun isAnyShareOptionChecked(): Boolean {
return binding.run {
val isCustomPermissionChecked = customPermissionRadioButton.isChecked &&
(
shareReadCheckbox.isChecked ||
shareCreateCheckbox.isChecked ||
shareEditCheckbox.isChecked ||
shareCheckbox.isChecked ||
shareDeleteCheckbox.isChecked
)
viewOnlyRadioButton.isChecked ||
canEditRadioButton.isChecked ||
fileRequestRadioButton.isChecked ||
isCustomPermissionChecked
} }
binding.shareProcessChangeNameSwitch.setOnCheckedChangeListener { _, isChecked -> }
showChangeNameInput(isChecked)
private fun toggleNextButtonAvailability(value: Boolean) {
binding.run {
shareProcessBtnNext.isEnabled = value
shareProcessBtnNext.isClickable = value
} }
binding.shareProcessSelectExpDate.setOnClickListener { }
showExpirationDateDialog()
@Suppress("NestedBlockDepth")
private fun setCheckboxStates() {
val currentPermissions = share?.permissions ?: permission
binding.run {
SharePermissionManager.run {
shareReadCheckbox.isChecked = hasPermission(currentPermissions, OCShare.READ_PERMISSION_FLAG)
shareEditCheckbox.isChecked = hasPermission(currentPermissions, OCShare.UPDATE_PERMISSION_FLAG)
shareCheckbox.isChecked = hasPermission(currentPermissions, OCShare.SHARE_PERMISSION_FLAG)
if (isFolder()) {
// Only for the folder makes sense to have create permission
// so that user can create files in the shared folder
shareCreateCheckbox.isChecked = hasPermission(currentPermissions, OCShare.CREATE_PERMISSION_FLAG)
shareDeleteCheckbox.isChecked = hasPermission(currentPermissions, OCShare.DELETE_PERMISSION_FLAG)
} else {
shareCreateCheckbox.visibility = View.GONE
shareDeleteCheckbox.apply {
isChecked = false
isEnabled = false
}
}
if (!isPublicShare()) {
shareAllowDownloadAndSyncCheckbox.isChecked = isAllowDownloadAndSyncEnabled(share)
}
}
} }
setCheckboxesListeners()
}
private fun setCheckboxesListeners() {
val checkboxes = mapOf(
binding.shareReadCheckbox to OCShare.READ_PERMISSION_FLAG,
binding.shareCreateCheckbox to OCShare.CREATE_PERMISSION_FLAG,
binding.shareEditCheckbox to OCShare.UPDATE_PERMISSION_FLAG,
binding.shareCheckbox to OCShare.SHARE_PERMISSION_FLAG,
binding.shareDeleteCheckbox to OCShare.DELETE_PERMISSION_FLAG
)
checkboxes.forEach { (checkbox, flag) ->
checkbox.setOnCheckedChangeListener { _, isChecked -> togglePermission(isChecked, flag) }
}
if (!isPublicShare()) {
binding.shareAllowDownloadAndSyncCheckbox.setOnCheckedChangeListener { _, isChecked ->
val result = SharePermissionManager.toggleAllowDownloadAndSync(isChecked, share)
share?.attributes = result
downloadAttribute = result
}
}
}
private fun togglePermission(isChecked: Boolean, permissionFlag: Int) {
permission = SharePermissionManager.togglePermission(isChecked, permission, permissionFlag)
toggleNextButtonAvailability(true)
} }
private fun showExpirationDateDialog(chosenDateInMillis: Long = chosenExpDateInMills) { private fun showExpirationDateDialog(chosenDateInMillis: Long = chosenExpDateInMills) {
@ -472,7 +677,8 @@ class FileDetailsSharingProcessFragment :
} }
private fun showChangeNameInput(isChecked: Boolean) { private fun showChangeNameInput(isChecked: Boolean) {
binding.shareProcessChangeNameContainer.visibility = if (isChecked) View.VISIBLE else View.GONE binding.shareProcessChangeNameContainer.setVisibleIf(isChecked)
if (!isChecked) { if (!isChecked) {
binding.shareProcessChangeName.setText(R.string.empty) binding.shareProcessChangeName.setText(R.string.empty)
} }
@ -483,11 +689,12 @@ class FileDetailsSharingProcessFragment :
if (share != null) { if (share != null) {
removeCurrentFragment() removeCurrentFragment()
} }
// else we have to check if user is in step 2(note screen) then show step 1 (permission screen) // else we have to check if user is in step 2(note screen) then show step 1 (permission screen)
// and if user is in step 1 (permission screen) then remove the fragment // and if user is in step 1 (permission screen) then remove the fragment
else { else {
if (shareProcessStep == SCREEN_TYPE_NOTE) { if (isShareProcessStepIsNote()) {
showShareProcessFirst() setupUI()
} else { } else {
removeCurrentFragment() removeCurrentFragment()
} }
@ -495,8 +702,8 @@ class FileDetailsSharingProcessFragment :
} }
private fun showExpirationDateInput(isChecked: Boolean) { private fun showExpirationDateInput(isChecked: Boolean) {
binding.shareProcessSelectExpDate.visibility = if (isChecked) View.VISIBLE else View.GONE binding.shareProcessSelectExpDate.setVisibleIf(isChecked)
binding.shareProcessExpDateDivider.visibility = if (isChecked) View.VISIBLE else View.GONE binding.shareProcessExpDateDivider.setVisibleIf(isChecked)
// reset the expiration date if switch is unchecked // reset the expiration date if switch is unchecked
if (!isChecked) { if (!isChecked) {
@ -515,7 +722,7 @@ class FileDetailsSharingProcessFragment :
} }
private fun showPasswordInput(isChecked: Boolean) { private fun showPasswordInput(isChecked: Boolean) {
binding.shareProcessEnterPasswordContainer.visibility = if (isChecked) View.VISIBLE else View.GONE binding.shareProcessEnterPasswordContainer.setVisibleIf(isChecked)
// reset the password if switch is unchecked // reset the password if switch is unchecked
if (!isChecked) { if (!isChecked) {
@ -528,18 +735,11 @@ class FileDetailsSharingProcessFragment :
fileActivity?.supportFragmentManager?.beginTransaction()?.remove(this)?.commit() fileActivity?.supportFragmentManager?.beginTransaction()?.remove(this)?.commit()
} }
private fun getReSharePermission(): Int {
val spb = SharePermissionsBuilder()
spb.setSharePermission(true)
return spb.build()
}
/** /**
* method to validate the step 1 screen information * method to validate the step 1 screen information
*/ */
@Suppress("ReturnCount") @Suppress("ReturnCount")
private fun validateShareProcessFirst() { private fun validateShareProcessFirst() {
permission = getSelectedPermission()
if (permission == OCShare.NO_PERMISSION) { if (permission == OCShare.NO_PERMISSION) {
DisplayUtils.showSnackMessage(binding.root, R.string.no_share_permission_selected) DisplayUtils.showSnackMessage(binding.root, R.string.no_share_permission_selected)
return return
@ -572,26 +772,48 @@ class FileDetailsSharingProcessFragment :
removeCurrentFragment() removeCurrentFragment()
} else { } else {
// else show step 2 (note screen) // else show step 2 (note screen)
showShareProcessSecond() updateViewForNoteScreenType()
} }
} }
/** @Suppress("ReturnCount")
* get the permissions on the basis of selection private fun createShareOrUpdateNoteShare() {
*/ if (!isAnyShareOptionChecked()) {
private fun getSelectedPermission() = when { DisplayUtils.showSnackMessage(requireActivity(), R.string.share_option_required)
binding.shareProcessAllowResharingCheckbox.isChecked -> getReSharePermission() return
binding.shareProcessPermissionReadOnly.isChecked -> OCShare.READ_PERMISSION_FLAG
binding.shareProcessPermissionUploadEditing.isChecked -> when {
file?.isFolder == true || share?.isFolder == true -> OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER
else -> OCShare.MAXIMUM_PERMISSIONS_FOR_FILE
} }
binding.shareProcessPermissionFileDrop.isChecked -> OCShare.CREATE_PERMISSION_FLAG val noteText = binding.noteText.text.toString().trim()
else -> permission if (file == null && (share != null && share?.note == noteText)) {
DisplayUtils.showSnackMessage(requireActivity(), R.string.share_cannot_update_empty_note)
return
}
when {
// if modifying existing share then directly update the note and send email
share != null && share?.note != noteText -> {
fileOperationsHelper?.updateNoteToShare(share, noteText)
}
file == null -> {
DisplayUtils.showSnackMessage(requireActivity(), R.string.file_not_found_cannot_share)
return
}
else -> {
createShare(noteText)
}
}
removeCurrentFragment()
} }
private fun updateShare() { private fun updateShare() {
// empty string causing fails
if (share?.attributes?.isEmpty() == true) {
share?.attributes = null
}
fileOperationsHelper?.updateShareInformation( fileOperationsHelper?.updateShareInformation(
share, share,
permission, permission,
@ -601,16 +823,8 @@ class FileDetailsSharingProcessFragment :
binding.shareProcessChangeName.text.toString().trim() binding.shareProcessChangeName.text.toString().trim()
) )
if (capabilities.filesDownloadLimit.isTrue) { if (canSetDownloadLimit()) {
val downloadLimitInput = binding.shareProcessSetDownloadLimitInput.text.toString().trim() setDownloadLimit()
val downloadLimit =
if (binding.shareProcessSetDownloadLimitSwitch.isChecked && downloadLimitInput.isNotEmpty()) {
downloadLimitInput.toInt()
} else {
0
}
fileOperationsHelper?.updateFilesDownloadLimit(share, downloadLimit)
} }
// copy the share link if available // copy the share link if available
@ -619,31 +833,32 @@ class FileDetailsSharingProcessFragment :
} }
} }
/** private fun setDownloadLimit() {
* method to validate step 2 (note screen) information val downloadLimitInput = binding.shareProcessSetDownloadLimitInput.text.toString().trim()
*/ val downloadLimit =
private fun validateShareProcessSecond() { if (binding.shareProcessSetDownloadLimitSwitch.isChecked && downloadLimitInput.isNotEmpty()) {
val noteText = binding.noteText.text.toString().trim() downloadLimitInput.toInt()
// if modifying existing share then directly update the note and send email } else {
if (share != null && share?.note != noteText) { 0
fileOperationsHelper?.updateNoteToShare(share, noteText) }
} else {
// else create new share fileOperationsHelper?.updateFilesDownloadLimit(share, downloadLimit)
fileOperationsHelper?.shareFileWithSharee( }
file,
shareeName, private fun createShare(noteText: String) {
shareType, fileOperationsHelper?.shareFileWithSharee(
permission, file,
binding shareeName,
.shareProcessHideDownloadCheckbox.isChecked, shareType,
binding.shareProcessEnterPassword.text.toString().trim(), permission,
chosenExpDateInMills, binding.shareProcessHideDownloadCheckbox.isChecked,
noteText, binding.shareProcessEnterPassword.text.toString().trim(),
binding.shareProcessChangeName.text.toString().trim(), chosenExpDateInMills,
true noteText,
) downloadAttribute,
} binding.shareProcessChangeName.text.toString().trim(),
removeCurrentFragment() true
)
} }
/** /**
@ -664,4 +879,25 @@ class FileDetailsSharingProcessFragment :
override fun onDateUnSet() { override fun onDateUnSet() {
binding.shareProcessSetExpDateSwitch.isChecked = false binding.shareProcessSetExpDateSwitch.isChecked = false
} }
// region Helpers
private fun isShareProcessStepIsPermission(): Boolean = (
shareProcessStep == SCREEN_TYPE_PERMISSION ||
isShareProcessStepIsCustomPermission()
)
private fun isShareProcessStepIsCustomPermission(): Boolean =
(shareProcessStep == SCREEN_TYPE_PERMISSION_WITH_CUSTOM_PERMISSION)
private fun isShareProcessStepIsNote(): Boolean = (shareProcessStep == SCREEN_TYPE_NOTE)
private fun isFolder(): Boolean = (file?.isFolder == true || share?.isFolder == true)
private fun canSetFileRequest(): Boolean = isFolder() && shareType.isPublicOrMail()
private fun canSetDownloadLimit(): Boolean =
(isPublicShare() && capabilities.filesDownloadLimit.isTrue && share?.isFolder == false)
private fun isPublicShare(): Boolean = (shareType == ShareType.PUBLIC_LINK)
// endregion
} }

View File

@ -10,22 +10,24 @@
package com.owncloud.android.ui.fragment; package com.owncloud.android.ui.fragment;
import android.app.Dialog;
import android.os.Bundle; import android.os.Bundle;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog; import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.nextcloud.utils.extensions.OCShareExtensionsKt;
import com.owncloud.android.R; import com.owncloud.android.R;
import com.owncloud.android.databinding.QuickSharingPermissionsBottomSheetFragmentBinding; import com.owncloud.android.databinding.QuickSharingPermissionsBottomSheetFragmentBinding;
import com.owncloud.android.datamodel.QuickPermissionModel; import com.owncloud.android.datamodel.quickPermission.QuickPermission;
import com.owncloud.android.datamodel.quickPermission.QuickPermissionType;
import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.activity.FileActivity;
import com.owncloud.android.ui.adapter.QuickSharingPermissionsAdapter; import com.owncloud.android.ui.adapter.QuickSharingPermissionsAdapter;
import com.owncloud.android.ui.fragment.util.SharingMenuHelper; import com.owncloud.android.ui.fragment.util.SharePermissionManager;
import com.owncloud.android.utils.theme.ViewThemeUtils; import com.owncloud.android.utils.theme.ViewThemeUtils;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.LinearLayoutManager;
@ -36,7 +38,7 @@ import static com.owncloud.android.lib.resources.shares.OCShare.MAXIMUM_PERMISSI
import static com.owncloud.android.lib.resources.shares.OCShare.READ_PERMISSION_FLAG; import static com.owncloud.android.lib.resources.shares.OCShare.READ_PERMISSION_FLAG;
/** /**
* File Details Quick Sharing permissions options {@link android.app.Dialog} styled as a bottom sheet for main actions. * File Details Quick Sharing permissions options {@link Dialog} styled as a bottom sheet for main actions.
*/ */
public class QuickSharingPermissionsBottomSheetDialog extends BottomSheetDialog { public class QuickSharingPermissionsBottomSheetDialog extends BottomSheetDialog {
private QuickSharingPermissionsBottomSheetFragmentBinding binding; private QuickSharingPermissionsBottomSheetFragmentBinding binding;
@ -44,16 +46,19 @@ public class QuickSharingPermissionsBottomSheetDialog extends BottomSheetDialog
private final FileActivity fileActivity; private final FileActivity fileActivity;
private final OCShare ocShare; private final OCShare ocShare;
private final ViewThemeUtils viewThemeUtils; private final ViewThemeUtils viewThemeUtils;
private final boolean encrypted;
public QuickSharingPermissionsBottomSheetDialog(FileActivity fileActivity, public QuickSharingPermissionsBottomSheetDialog(FileActivity fileActivity,
QuickPermissionSharingBottomSheetActions actions, QuickPermissionSharingBottomSheetActions actions,
OCShare ocShare, OCShare ocShare,
ViewThemeUtils viewThemeUtils) { ViewThemeUtils viewThemeUtils,
boolean encrypted) {
super(fileActivity); super(fileActivity);
this.actions = actions; this.actions = actions;
this.ocShare = ocShare; this.ocShare = ocShare;
this.fileActivity = fileActivity; this.fileActivity = fileActivity;
this.viewThemeUtils = viewThemeUtils; this.viewThemeUtils = viewThemeUtils;
this.encrypted = encrypted;
} }
@Override @Override
@ -76,13 +81,19 @@ public class QuickSharingPermissionsBottomSheetDialog extends BottomSheetDialog
} }
private void setUpRecyclerView() { private void setUpRecyclerView() {
List<QuickPermissionModel> quickPermissionModelList = getQuickPermissionList(); List<QuickPermission> quickPermissionList = getQuickPermissionList();
QuickSharingPermissionsAdapter adapter = new QuickSharingPermissionsAdapter( QuickSharingPermissionsAdapter adapter = new QuickSharingPermissionsAdapter(
quickPermissionModelList, quickPermissionList,
new QuickSharingPermissionsAdapter.QuickSharingPermissionViewHolder.OnPermissionChangeListener() { new QuickSharingPermissionsAdapter.QuickSharingPermissionViewHolder.OnPermissionChangeListener() {
@Override
public void onCustomPermissionSelected() {
dismiss();
actions.openShareDetailWithCustomPermissions(ocShare);
}
@Override @Override
public void onPermissionChanged(int position) { public void onPermissionChanged(int position) {
handlePermissionChanged(quickPermissionModelList, position); handlePermissionChanged(quickPermissionList, position);
} }
@Override @Override
@ -98,60 +109,35 @@ public class QuickSharingPermissionsBottomSheetDialog extends BottomSheetDialog
} }
/** /**
* handle permission changed on click of selected permission * Handle permission changed on click of selected permission
* @param quickPermissionModelList
* @param position
*/ */
private void handlePermissionChanged(List<QuickPermissionModel> quickPermissionModelList, int position) { private void handlePermissionChanged(List<QuickPermission> quickPermissionList, int position) {
if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(fileActivity.getResources().getString(R.string.link_share_allow_upload_and_editing)) final var permissionName = quickPermissionList.get(position).getType().getText(getContext());
|| quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(fileActivity.getResources().getString(R.string.link_share_editing))) { final var res = fileActivity.getResources();
if (ocShare.isFolder()) {
actions.onQuickPermissionChanged(ocShare,
MAXIMUM_PERMISSIONS_FOR_FOLDER);
} else {
actions.onQuickPermissionChanged(ocShare,
MAXIMUM_PERMISSIONS_FOR_FILE);
}
} else if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(fileActivity.getResources().getString(R.string
.link_share_view_only))) {
actions.onQuickPermissionChanged(ocShare,
READ_PERMISSION_FLAG);
} else if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(fileActivity.getResources().getString(R.string int permissionFlag = 0;
.link_share_file_drop))) { if (permissionName.equalsIgnoreCase(res.getString(R.string.share_permission_can_edit)) || permissionName.equalsIgnoreCase(res.getString(R.string.link_share_editing))) {
actions.onQuickPermissionChanged(ocShare, permissionFlag = ocShare.isFolder() ? MAXIMUM_PERMISSIONS_FOR_FOLDER : MAXIMUM_PERMISSIONS_FOR_FILE;
CREATE_PERMISSION_FLAG); } else if (permissionName.equalsIgnoreCase(res.getString(R.string.share_permission_view_only))) {
permissionFlag = READ_PERMISSION_FLAG;
} else if (permissionName.equalsIgnoreCase(res.getString(R.string.share_permission_file_request))) {
permissionFlag = CREATE_PERMISSION_FLAG + READ_PERMISSION_FLAG;
} }
actions.onQuickPermissionChanged(ocShare, permissionFlag);
dismiss(); dismiss();
} }
/** /**
* prepare the list of permissions needs to be displayed on recyclerview * Prepare the list of permissions needs to be displayed on recyclerview
* @return
*/ */
private List<QuickPermissionModel> getQuickPermissionList() { private List<QuickPermission> getQuickPermissionList() {
final var selectedType = SharePermissionManager.INSTANCE.getSelectedType(ocShare, encrypted);
String[] permissionArray; final var hasFileRequestPermission = OCShareExtensionsKt.hasFileRequestPermission(ocShare);
if (ocShare.isFolder()) { return QuickPermissionType.Companion.getAvailablePermissions(hasFileRequestPermission, selectedType);
permissionArray =
fileActivity.getResources().getStringArray(R.array.folder_share_permission_dialog_values);
} else {
permissionArray =
fileActivity.getResources().getStringArray(R.array.file_share_permission_dialog_values);
}
//get the checked item position
int checkedItem = SharingMenuHelper.getPermissionCheckedItem(fileActivity, ocShare, permissionArray);
final List<QuickPermissionModel> quickPermissionModelList = new ArrayList<>(permissionArray.length);
for (int i = 0; i < permissionArray.length; i++) {
QuickPermissionModel quickPermissionModel = new QuickPermissionModel(permissionArray[i], checkedItem == i);
quickPermissionModelList.add(quickPermissionModel);
}
return quickPermissionModelList;
} }
@Override @Override
protected void onStop() { protected void onStop() {
super.onStop(); super.onStop();
@ -160,5 +146,7 @@ public class QuickSharingPermissionsBottomSheetDialog extends BottomSheetDialog
public interface QuickPermissionSharingBottomSheetActions { public interface QuickPermissionSharingBottomSheetActions {
void onQuickPermissionChanged(OCShare share, int permission); void onQuickPermissionChanged(OCShare share, int permission);
void openShareDetailWithCustomPermissions(OCShare share);
} }
} }

View File

@ -0,0 +1,177 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.owncloud.android.ui.fragment.util
import com.owncloud.android.datamodel.quickPermission.QuickPermissionType
import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.resources.shares.OCShare
import com.owncloud.android.lib.resources.shares.attributes.ShareAttributes
import com.owncloud.android.lib.resources.shares.attributes.ShareAttributesJsonHandler
import com.owncloud.android.lib.resources.shares.attributes.getDownloadAttribute
import com.owncloud.android.ui.fragment.FileDetailsSharingProcessFragment.Companion.TAG
object SharePermissionManager {
// region Permission change
fun togglePermission(isChecked: Boolean, permission: Int, permissionFlag: Int): Int {
Log_OC.d(TAG, "togglePermission before: $permission")
if (!isPermissionValid(permission)) {
Log_OC.d(TAG, "permission is not valid, togglePermission cancelled")
return permission
}
val result = if (isChecked) {
permission or permissionFlag
} else {
permission and permissionFlag.inv()
}
Log_OC.d(TAG, "togglePermission after: $result")
return result
}
// endregion
// region Permission check
fun hasPermission(permission: Int, permissionFlag: Int): Boolean {
return permission != OCShare.NO_PERMISSION && (permission and permissionFlag) == permissionFlag
}
@Suppress("ReturnCount")
private fun isPermissionValid(permission: Int): Boolean {
// Must have at least READ or CREATE permission.
if (!hasPermission(permission, OCShare.READ_PERMISSION_FLAG) &&
!hasPermission(permission, OCShare.CREATE_PERMISSION_FLAG)
) {
return false
}
// Must have READ permission if have UPDATE or DELETE.
if (!hasPermission(permission, OCShare.READ_PERMISSION_FLAG) &&
(
hasPermission(permission, OCShare.UPDATE_PERMISSION_FLAG) ||
hasPermission(permission, OCShare.DELETE_PERMISSION_FLAG)
)
) {
return false
}
return true
}
// endregion
// region DownloadAttribute
fun toggleAllowDownloadAndSync(isChecked: Boolean, share: OCShare?): String? {
val shareAttributes = getShareAttributes(share)?.toMutableList()
if (shareAttributes == null) {
val downloadAttribute = ShareAttributes.createDownloadAttributes(isChecked)
val updatedShareAttributes = listOf(downloadAttribute)
return ShareAttributesJsonHandler.toJson(updatedShareAttributes)
}
val downloadAttributeIndex = shareAttributes.indexOf(shareAttributes.getDownloadAttribute())
if (downloadAttributeIndex >= 0) {
val updatedAttribute = shareAttributes[downloadAttributeIndex].copy(value = isChecked)
shareAttributes[downloadAttributeIndex] = updatedAttribute
}
return ShareAttributesJsonHandler.toJson(shareAttributes)
}
fun isAllowDownloadAndSyncEnabled(share: OCShare?): Boolean {
return getShareAttributes(share).getDownloadAttribute()?.value == true
}
private fun getShareAttributes(share: OCShare?): List<ShareAttributes>? {
return share?.attributes?.let { ShareAttributesJsonHandler.toList(it) }
}
// endregion
// region Helper Methods
fun canEdit(share: OCShare?): Boolean {
if (share == null) {
return false
}
return hasPermission(share.permissions, getMaximumPermission(share.isFolder))
}
fun isViewOnly(share: OCShare?): Boolean {
return share?.permissions != OCShare.NO_PERMISSION && share?.permissions == OCShare.READ_PERMISSION_FLAG
}
fun isFileRequest(share: OCShare?): Boolean {
if (share?.isFolder == false) {
return false
}
return share?.permissions != OCShare.NO_PERMISSION && share?.permissions == OCShare.CREATE_PERMISSION_FLAG
}
fun isSecureFileDrop(share: OCShare?): Boolean {
if (share == null) {
return false
}
return hasPermission(share.permissions, OCShare.CREATE_PERMISSION_FLAG + OCShare.READ_PERMISSION_FLAG)
}
fun canReshare(share: OCShare?): Boolean {
if (share == null) {
return false
}
return (share.permissions and OCShare.Companion.SHARE_PERMISSION_FLAG) > 0
}
fun getSelectedType(share: OCShare?, encrypted: Boolean): QuickPermissionType {
return if (canEdit(share)) {
QuickPermissionType.CAN_EDIT
} else if (encrypted && isSecureFileDrop(share)) {
QuickPermissionType.SECURE_FILE_DROP
} else if (isFileRequest(share)) {
QuickPermissionType.FILE_REQUEST
} else if (isViewOnly(share)) {
QuickPermissionType.VIEW_ONLY
} else if (isCustomPermission(share)) {
QuickPermissionType.CUSTOM_PERMISSIONS
} else {
QuickPermissionType.NONE
}
}
@Suppress("ReturnCount")
fun isCustomPermission(share: OCShare?): Boolean {
if (share == null) return false
val permissions = share.permissions
if (permissions == OCShare.NO_PERMISSION) return false
val hasRead = hasPermission(permissions, OCShare.READ_PERMISSION_FLAG)
if (!hasRead) return false
val hasCreate = hasPermission(permissions, OCShare.CREATE_PERMISSION_FLAG)
val hasUpdate = hasPermission(permissions, OCShare.UPDATE_PERMISSION_FLAG)
val hasDelete = hasPermission(permissions, OCShare.DELETE_PERMISSION_FLAG)
val hasShare = hasPermission(permissions, OCShare.SHARE_PERMISSION_FLAG)
return when {
share.isFolder -> hasCreate || hasUpdate || hasDelete || hasShare
else -> hasUpdate || hasShare
}
}
fun getMaximumPermission(isFolder: Boolean): Int {
return if (isFolder) {
OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER
} else {
OCShare.MAXIMUM_PERMISSIONS_FOR_FILE
}
}
// endregion
}

View File

@ -1,166 +0,0 @@
/*
* Nextcloud Android client application
*
* @author Andy Scherzinger
* @author TSI-mc
* Copyright (C) 2018 Andy Scherzinger
* Copyright (C) 2021 TSI-mc
*
* SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
*/
package com.owncloud.android.ui.fragment.util;
import android.content.Context;
import android.content.res.Resources;
import android.view.MenuItem;
import com.owncloud.android.R;
import com.owncloud.android.lib.resources.shares.OCShare;
import java.text.SimpleDateFormat;
import java.util.Date;
import static com.owncloud.android.lib.resources.shares.OCShare.CREATE_PERMISSION_FLAG;
import static com.owncloud.android.lib.resources.shares.OCShare.MAXIMUM_PERMISSIONS_FOR_FILE;
import static com.owncloud.android.lib.resources.shares.OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER;
import static com.owncloud.android.lib.resources.shares.OCShare.NO_PERMISSION;
import static com.owncloud.android.lib.resources.shares.OCShare.READ_PERMISSION_FLAG;
import static com.owncloud.android.lib.resources.shares.OCShare.SHARE_PERMISSION_FLAG;
/**
* Helper calls for visibility logic of the sharing menu.
*/
public final class SharingMenuHelper {
private SharingMenuHelper() {
// utility class -> private constructor
}
/**
* Sets checked/visibility state on the given {@link MenuItem} based on the given criteria.
*
* @param menuItem the {@link MenuItem} to be setup
*/
public static void setupHideFileDownload(MenuItem menuItem,
boolean hideFileDownload,
boolean isFileDrop) {
if (isFileDrop) {
menuItem.setVisible(false);
} else {
menuItem.setVisible(true);
menuItem.setChecked(hideFileDownload);
}
}
/**
* sets up the password {@link MenuItem}'s title based on the fact if a password is present.
*
* @param password the password {@link MenuItem}
* @param isPasswordProtected flag is a password is present
*/
public static void setupPasswordMenuItem(MenuItem password, boolean isPasswordProtected) {
if (isPasswordProtected) {
password.setTitle(R.string.share_password_title);
} else {
password.setTitle(R.string.share_no_password_title);
}
}
/**
* sets up the expiration date {@link MenuItem}'s title based on the fact if an expiration date is present.
*
* @param expirationDate the expiration date {@link MenuItem}
* @param expirationDateValue the expiration date
* @param res Resources to load the corresponding strings.
*/
public static void setupExpirationDateMenuItem(MenuItem expirationDate, long expirationDateValue, Resources res) {
if (expirationDateValue > 0) {
expirationDate.setTitle(res.getString(
R.string.share_expiration_date_label,
SimpleDateFormat.getDateInstance().format(new Date(expirationDateValue))
));
} else {
expirationDate.setTitle(R.string.share_no_expiration_date_label);
}
}
public static boolean isUploadAndEditingAllowed(OCShare share) {
if (share.getPermissions() == NO_PERMISSION) {
return false;
}
return (share.getPermissions() & (share.isFolder() ? MAXIMUM_PERMISSIONS_FOR_FOLDER :
MAXIMUM_PERMISSIONS_FOR_FILE)) == (share.isFolder() ? MAXIMUM_PERMISSIONS_FOR_FOLDER :
MAXIMUM_PERMISSIONS_FOR_FILE);
}
public static boolean isReadOnly(OCShare share) {
if (share.getPermissions() == NO_PERMISSION) {
return false;
}
return (share.getPermissions() & ~SHARE_PERMISSION_FLAG) == READ_PERMISSION_FLAG;
}
public static boolean isFileDrop(OCShare share) {
if (share.getPermissions() == NO_PERMISSION) {
return false;
}
return (share.getPermissions() & ~SHARE_PERMISSION_FLAG) == CREATE_PERMISSION_FLAG;
}
public static boolean isSecureFileDrop(OCShare share) {
if (share.getPermissions() == NO_PERMISSION) {
return false;
}
return (share.getPermissions() & ~SHARE_PERMISSION_FLAG) == CREATE_PERMISSION_FLAG + READ_PERMISSION_FLAG;
}
public static String getPermissionName(Context context, OCShare share) {
if (SharingMenuHelper.isUploadAndEditingAllowed(share)) {
return context.getResources().getString(R.string.share_permission_can_edit);
} else if (SharingMenuHelper.isReadOnly(share)) {
return context.getResources().getString(R.string.share_permission_view_only);
} else if (SharingMenuHelper.isSecureFileDrop(share)) {
return context.getResources().getString(R.string.share_permission_secure_file_drop);
} else if (SharingMenuHelper.isFileDrop(share)) {
return context.getResources().getString(R.string.share_permission_file_drop);
}
return null;
}
/**
* method to get the current checked index from the list of permissions
*
*/
public static int getPermissionCheckedItem(Context context, OCShare share, String[] permissionArray) {
if (SharingMenuHelper.isUploadAndEditingAllowed(share)) {
if (share.isFolder()) {
return getPermissionIndexFromArray(context, permissionArray, R.string.link_share_allow_upload_and_editing);
} else {
return getPermissionIndexFromArray(context, permissionArray, R.string.link_share_editing);
}
} else if (SharingMenuHelper.isReadOnly(share)) {
return getPermissionIndexFromArray(context, permissionArray, R.string.link_share_view_only);
} else if (SharingMenuHelper.isFileDrop(share)) {
return getPermissionIndexFromArray(context, permissionArray, R.string.link_share_file_drop);
}
return 0;//default first item selected
}
private static int getPermissionIndexFromArray(Context context, String[] permissionArray, int permissionName) {
for (int i = 0; i < permissionArray.length; i++) {
if (permissionArray[i].equalsIgnoreCase(context.getResources().getString(permissionName))) {
return i;
}
}
return 0;
}
public static boolean canReshare(OCShare share) {
return (share.getPermissions() & SHARE_PERMISSION_FLAG) > 0;
}
}

View File

@ -534,6 +534,7 @@ public class FileOperationsHelper {
String password, String password,
long expirationTimeInMillis, long expirationTimeInMillis,
String note, String note,
String attributes,
String label, String label,
boolean showLoadingDialog) { boolean showLoadingDialog) {
if (file != null) { if (file != null) {
@ -555,6 +556,7 @@ public class FileOperationsHelper {
service.putExtra(OperationsService.EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, expirationTimeInMillis); service.putExtra(OperationsService.EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, expirationTimeInMillis);
service.putExtra(OperationsService.EXTRA_SHARE_NOTE, (note == null) ? "" : note); service.putExtra(OperationsService.EXTRA_SHARE_NOTE, (note == null) ? "" : note);
service.putExtra(OperationsService.EXTRA_SHARE_PUBLIC_LABEL, (label == null) ? "" : label); service.putExtra(OperationsService.EXTRA_SHARE_PUBLIC_LABEL, (label == null) ? "" : label);
service.putExtra(OperationsService.EXTRA_SHARE_ATTRIBUTES, attributes);
mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service); mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service);
@ -588,16 +590,13 @@ public class FileOperationsHelper {
* *
* @param file The file to unshare. * @param file The file to unshare.
*/ */
public void unshareShare(OCFile file, OCShare share) { public void unShareShare(OCFile file, OCShare share) {
Intent intent = new Intent(fileActivity, OperationsService.class);
// Unshare the file: Create the intent intent.setAction(OperationsService.ACTION_UNSHARE);
Intent unshareService = new Intent(fileActivity, OperationsService.class); intent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount());
unshareService.setAction(OperationsService.ACTION_UNSHARE); intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
unshareService.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); intent.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId());
unshareService.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath()); queueShareIntent(intent);
unshareService.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId());
queueShareIntent(unshareService);
} }
private void queueShareIntent(Intent shareIntent) { private void queueShareIntent(Intent shareIntent) {
@ -751,18 +750,33 @@ public class FileOperationsHelper {
* leaving the link unrestricted. Zero makes no change. * leaving the link unrestricted. Zero makes no change.
* @param label new label * @param label new label
*/ */
public void updateShareInformation(OCShare share, int permissions, public void updateShareInformation(OCShare share,
boolean hideFileDownload, String password, long expirationTimeInMillis, int permissions,
boolean hideFileDownload,
String password,
long expirationTimeInMillis,
String label) { String label) {
final var id = share.getId();
final var attributes = share.getAttributes();
Log_OC.i(TAG, "-----AFTER UPDATE SHARE-----");
Log_OC.i(TAG, "ID: " + id);
Log_OC.i(TAG, "Permission: " + permissions);
Log_OC.i(TAG, "Hide File Download: " + hideFileDownload);
Log_OC.i(TAG, "Label: " + label);
Log_OC.i(TAG, "Attributes: " + attributes);
Intent updateShareIntent = new Intent(fileActivity, OperationsService.class); Intent updateShareIntent = new Intent(fileActivity, OperationsService.class);
updateShareIntent.setAction(OperationsService.ACTION_UPDATE_SHARE_INFO); updateShareIntent.setAction(OperationsService.ACTION_UPDATE_SHARE_INFO);
updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount());
updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId()); updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ID, id);
updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PERMISSIONS, permissions); updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PERMISSIONS, permissions);
updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_HIDE_FILE_DOWNLOAD, hideFileDownload); updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_HIDE_FILE_DOWNLOAD, hideFileDownload);
updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PASSWORD, (password == null) ? "" : password); updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PASSWORD, (password == null) ? "" : password);
updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, expirationTimeInMillis); updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, expirationTimeInMillis);
updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PUBLIC_LABEL, (label == null) ? "" : label); updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PUBLIC_LABEL, (label == null) ? "" : label);
updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_ATTRIBUTES, attributes);
queueShareIntent(updateShareIntent); queueShareIntent(updateShareIntent);
} }

View File

@ -0,0 +1,16 @@
<!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2018-2025 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M440,840L440,600L520,600L520,680L840,680L840,760L520,760L520,840L440,840ZM120,760L120,680L360,680L360,760L120,760ZM280,600L280,520L120,520L120,440L280,440L280,360L360,360L360,600L280,600ZM440,520L440,440L840,440L840,520L440,520ZM600,360L600,120L680,120L680,200L840,200L840,280L680,280L680,360L600,360ZM120,280L120,200L520,200L520,280L120,280Z" />
</vector>

View File

@ -0,0 +1,18 @@
<!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2018-2025 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FFFFFF"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z" />
</vector>

View File

@ -0,0 +1,16 @@
<!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2018-2025 Google LLC
~ SPDX-License-Identifier: Apache-2.0
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="?attr/colorControlNormal"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M202,895L146,838L264,720L174,720L174,640L400,640L400,866L320,866L320,777L202,895ZM480,880L480,560L160,560L160,160Q160,127 183.5,103.5Q207,80 240,80L560,80L800,320L800,800Q800,833 776.5,856.5Q753,880 720,880L480,880ZM520,360L720,360L520,160L720,360L520,160L520,360Z" />
</vector>

View File

@ -39,52 +39,112 @@
android:textStyle="bold" /> android:textStyle="bold" />
<RadioGroup <RadioGroup
android:id="@+id/share_process_permission_radio_group" android:id="@+id/share_radio_group"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:orientation="vertical"> android:orientation="vertical">
<com.google.android.material.radiobutton.MaterialRadioButton <com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/share_process_permission_read_only" android:id="@+id/view_only_radio_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="@dimen/minimum_size_for_touchable_area" android:minHeight="@dimen/minimum_size_for_touchable_area"
android:text="@string/link_share_view_only" /> android:text="@string/share_permission_view_only" />
<com.google.android.material.radiobutton.MaterialRadioButton <com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/share_process_permission_upload_editing" android:id="@+id/can_edit_radio_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="@dimen/minimum_size_for_touchable_area" android:minHeight="@dimen/minimum_size_for_touchable_area"
android:text="@string/link_share_allow_upload_and_editing" /> android:text="@string/share_permission_can_edit" />
<com.google.android.material.radiobutton.MaterialRadioButton <com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/share_process_permission_file_drop" android:id="@+id/file_request_radio_button"
android:layout_width="match_parent"
android:visibility="gone"
android:layout_height="wrap_content"
android:minHeight="@dimen/minimum_size_for_touchable_area"
android:text="@string/share_permission_file_request" />
<com.google.android.material.radiobutton.MaterialRadioButton
android:id="@+id/custom_permission_radio_button"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:minHeight="@dimen/minimum_size_for_touchable_area" android:minHeight="@dimen/minimum_size_for_touchable_area"
android:text="@string/link_share_file_drop" /> android:text="@string/share_custom_permission" />
</RadioGroup> </RadioGroup>
<androidx.appcompat.widget.AppCompatTextView <com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/share_process_advance_permission_title" android:id="@+id/share_allow_download_and_sync_checkbox"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_margin" android:layout_marginTop="@dimen/standard_margin"
android:text="@string/advanced_settings"
android:textColor="@color/primary"
android:textStyle="bold"/>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/share_process_allow_resharing_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/minimum_size_for_touchable_area" android:minHeight="@dimen/minimum_size_for_touchable_area"
android:text="@string/allow_resharing"
android:visibility="gone" android:visibility="gone"
android:text="@string/share_allow_download_and_sync_permission"
tools:visibility="visible" /> tools:visibility="visible" />
<LinearLayout
android:id="@+id/custom_permission_layout"
tools:visibility="visible"
android:visibility="gone"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/share_custom_permissions_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/standard_margin"
android:text="@string/share_custom_permission"
android:textColor="@color/primary"
android:textStyle="bold"/>
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/share_read_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/minimum_size_for_touchable_area"
android:enabled="false"
android:text="@string/share_read_permission"
tools:visibility="visible" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/share_create_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/minimum_size_for_touchable_area"
android:text="@string/share_create_permission"
tools:visibility="visible" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/share_edit_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/minimum_size_for_touchable_area"
android:text="@string/share_edit_permission"
tools:visibility="visible" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/share_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/minimum_size_for_touchable_area"
android:text="@string/share_re_share_permission"
tools:visibility="visible" />
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/share_delete_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/minimum_size_for_touchable_area"
android:text="@string/share_delete_permission"
tools:visibility="visible" />
</LinearLayout>
<com.google.android.material.materialswitch.MaterialSwitch <com.google.android.material.materialswitch.MaterialSwitch
android:id="@+id/share_process_set_password_switch" android:id="@+id/share_process_set_password_switch"
android:layout_width="match_parent" android:layout_width="match_parent"

View File

@ -2,41 +2,23 @@
<!-- <!--
~ Nextcloud - Android Client ~ Nextcloud - Android Client
~ ~
~ SPDX-FileCopyrightText: 2021 TSI-mc ~ SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
~ SPDX-FileCopyrightText: 2021 Nextcloud GmbH ~ SPDX-License-Identifier: AGPL-3.0-or-later
~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only -->
--> <com.google.android.material.button.MaterialButton
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
style="@style/Widget.Material3.Button.TextButton"
android:id="@+id/quickPermissionButton"
android:textColor="@color/text_color"
app:iconTint="@color/text_color"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/bottom_sheet_item_height" android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground" android:gravity="center_vertical"
android:paddingStart="@dimen/standard_padding" android:insetTop="0dp"
android:paddingEnd="@dimen/standard_padding"> android:insetBottom="0dp"
android:minHeight="56dp"
<ImageView android:paddingStart="16dp"
android:id="@+id/tv_quick_share_check_icon" android:paddingEnd="16dp"
android:layout_width="wrap_content" app:iconGravity="textStart"
android:layout_height="wrap_content" app:iconPadding="12dp" />
android:contentDescription="@null"
android:src="@drawable/ic_baseline_check_24"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:tint="@color/primary" />
<TextView
android:id="@+id/tv_quick_share_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="@dimen/bottom_sheet_text_start_margin"
android:text="@string/link_share_view_only"
android:textColor="@color/text_color"
android:textSize="@dimen/bottom_sheet_text_size"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/tv_quick_share_check_icon"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -787,7 +787,7 @@
<string name="screenshot_05_autoUpload_heading">Automatisk upload</string> <string name="screenshot_05_autoUpload_heading">Automatisk upload</string>
<string name="screenshot_05_autoUpload_subline">for dine billeder og videoer</string> <string name="screenshot_05_autoUpload_subline">for dine billeder og videoer</string>
<string name="screenshot_06_davdroid_heading">Kalender og kontakter</string> <string name="screenshot_06_davdroid_heading">Kalender og kontakter</string>
<string name="screenshot_06_davdroid_subline">Synk med DAVx⁵</string> <string name="screenshot_06_davdroid_subline">Synkronisér med DAVx⁵ </string>
<string name="search_error">Fejl ved indhenting af søgeresultater</string> <string name="search_error">Fejl ved indhenting af søgeresultater</string>
<string name="secure_share_not_set_up">Sikker deling er ikke sat op for denne bruger</string> <string name="secure_share_not_set_up">Sikker deling er ikke sat op for denne bruger</string>
<string name="secure_share_search">Sikker deling...</string> <string name="secure_share_search">Sikker deling...</string>
@ -1119,12 +1119,12 @@
<item quantity="other">%d sekunder siden</item> <item quantity="other">%d sekunder siden</item>
</plurals> </plurals>
<plurals name="time_minutes_ago"> <plurals name="time_minutes_ago">
<item quantity="one">%d minut sidenago</item> <item quantity="one">%d minut siden</item>
<item quantity="other">%d minutter siden</item> <item quantity="other">%d minutter siden</item>
</plurals> </plurals>
<plurals name="time_hours_ago"> <plurals name="time_hours_ago">
<item quantity="one">%d time sidenago</item> <item quantity="one">%d time siden</item>
<item quantity="other">%d timer sidenago</item> <item quantity="other">%d timer siden</item>
</plurals> </plurals>
<plurals name="sync_fail_in_favourites_content"> <plurals name="sync_fail_in_favourites_content">
<item quantity="one">Kunne ikke synkronisere %1$d fil (konflikter: %2$d)</item> <item quantity="one">Kunne ikke synkronisere %1$d fil (konflikter: %2$d)</item>

View File

@ -269,6 +269,7 @@
<string name="file_name_validator_current_path_is_invalid">Pašreizējais mapes nosaukums ir nederīgs, lūgums pārdēvēt mapi. Pārvirza uz sākumu...</string> <string name="file_name_validator_current_path_is_invalid">Pašreizējais mapes nosaukums ir nederīgs, lūgums pārdēvēt mapi. Pārvirza uz sākumu...</string>
<string name="file_name_validator_rename_before_move_or_copy">%s. Lūgums pirms pārvietošanas vai kopēšanas pārdēvēt datni</string> <string name="file_name_validator_rename_before_move_or_copy">%s. Lūgums pirms pārvietošanas vai kopēšanas pārdēvēt datni</string>
<string name="file_not_found">Datne nav atrasta</string> <string name="file_not_found">Datne nav atrasta</string>
<string name="file_not_synced">Datni nevarēja sinhronizēt. Rāda pēdējo pieejamo versiju.</string>
<string name="file_rename">Pārdēvēt</string> <string name="file_rename">Pārdēvēt</string>
<string name="filedetails_details">Detaļas</string> <string name="filedetails_details">Detaļas</string>
<string name="filedetails_download">Lejupielādēt</string> <string name="filedetails_download">Lejupielādēt</string>

View File

@ -26,16 +26,6 @@
<item>@string/pref_instant_name_collision_policy_entries_cancel</item> <item>@string/pref_instant_name_collision_policy_entries_cancel</item>
</string-array> </string-array>
<string-array name="folder_share_permission_dialog_values" translatable="false">
<item>@string/link_share_view_only</item>
<item>@string/link_share_allow_upload_and_editing</item>
<item>@string/link_share_file_drop</item>
</string-array>
<string-array name="file_share_permission_dialog_values" translatable="false">
<item>@string/link_share_view_only</item>
<item>@string/link_share_editing</item>
</string-array>
<string-array name="sub_folder_rule_values" translatable="false"> <string-array name="sub_folder_rule_values" translatable="false">
<item>@string/sub_folder_rule_month</item> <item>@string/sub_folder_rule_month</item>
<item>@string/sub_folder_rule_year</item> <item>@string/sub_folder_rule_year</item>

View File

@ -616,12 +616,10 @@
<string name="share_dialog_title">Sharing</string> <string name="share_dialog_title">Sharing</string>
<string name="share_file">Share %1$s</string> <string name="share_file">Share %1$s</string>
<string name="share_expiration_date_label">Expires %1$s</string>
<string name="share_expiration_date_format">%1$s</string> <string name="share_expiration_date_format">%1$s</string>
<string name="share_no_expiration_date_label">Set expiration date</string> <string name="share_no_expiration_date_label">Set expiration date</string>
<string name="share_via_link_section_title">Share link</string> <string name="share_via_link_section_title">Share link</string>
<string name="share_via_link_send_link_label">Send link</string> <string name="share_via_link_send_link_label">Send link</string>
<string name="share_password_title">Password-protected</string>
<string name="share_no_password_title">Set password</string> <string name="share_no_password_title">Set password</string>
<string name="share_with_title">Share with…</string> <string name="share_with_title">Share with…</string>
<string name="share_via_link_unset_password">Unset</string> <string name="share_via_link_unset_password">Unset</string>
@ -838,6 +836,7 @@
<string name="drawer_header_background">Background image of drawer header</string> <string name="drawer_header_background">Background image of drawer header</string>
<string name="account_icon">Account icon</string> <string name="account_icon">Account icon</string>
<string name="file_detail_sharing_fragment_no_contact_app_message">No app available to select contacts</string> <string name="file_detail_sharing_fragment_no_contact_app_message">No app available to select contacts</string>
<string name="file_detail_share_already_active">You cannot create a share, sharing is already active from this user.</string>
<string name="end_to_end_encryption_certificate_verification_failed">Failed to verify public key</string> <string name="end_to_end_encryption_certificate_verification_failed">Failed to verify public key</string>
<string name="end_to_end_encryption_server_public_key_unavailable">Unable to retrieve public key</string> <string name="end_to_end_encryption_server_public_key_unavailable">Unable to retrieve public key</string>
@ -1073,6 +1072,11 @@
<string name="create_new">New</string> <string name="create_new">New</string>
<string name="editor_placeholder" translatable="false">%1$s %2$s</string> <string name="editor_placeholder" translatable="false">%1$s %2$s</string>
<string name="share_permission_file_request">File request</string>
<string name="share_permission_view_only">View only</string>
<string name="share_permission_can_edit">Can edit</string>
<string name="share_permission_secure_file_drop">Secure file drop</string>
<string name="sync_not_enough_space_dialog_action_choose">Choose what to sync</string> <string name="sync_not_enough_space_dialog_action_choose">Choose what to sync</string>
<string name="sync_not_enough_space_dialog_action_free_space">Free up space</string> <string name="sync_not_enough_space_dialog_action_free_space">Free up space</string>
<string name="sync_not_enough_space_dialog_placeholder">%1$s is %2$s, but there is only %3$s available on device.</string> <string name="sync_not_enough_space_dialog_placeholder">%1$s is %2$s, but there is only %3$s available on device.</string>
@ -1092,11 +1096,17 @@
<string name="public_share_name">New name</string> <string name="public_share_name">New name</string>
<string name="share_link_with_label">Share link (%1$s)</string> <string name="share_link_with_label">Share link (%1$s)</string>
<string name="share_link">Share link</string> <string name="share_link">Share link</string>
<string name="allow_resharing">Allow resharing</string> <string name="share_custom_permission">Custom permissions</string>
<string name="link_share_view_only">View only</string> <string name="share_read_permission">Read</string>
<string name="file_not_found_cannot_share">File not found. Unable to create a share.</string>
<string name="share_cannot_update_empty_note">We couldnt update the share. Please add a note and try again.</string>
<string name="share_option_required">Please select at least one sharing option before continuing.</string>
<string name="share_allow_download_and_sync_permission">Allow download and sync</string>
<string name="share_create_permission">Create</string>
<string name="share_edit_permission">Edit</string>
<string name="share_re_share_permission">Share</string>
<string name="share_delete_permission">Delete</string>
<string name="link_share_editing">Editing</string> <string name="link_share_editing">Editing</string>
<string name="link_share_allow_upload_and_editing">Allow upload and editing</string>
<string name="link_share_file_drop">File drop (upload only)</string>
<string name="could_not_retrieve_shares">Could not retrieve shares</string> <string name="could_not_retrieve_shares">Could not retrieve shares</string>
<string name="failed_update_ui">Failed to update UI</string> <string name="failed_update_ui">Failed to update UI</string>
<string name="email_pick_failed">Failed to pick email address.</string> <string name="email_pick_failed">Failed to pick email address.</string>
@ -1162,12 +1172,7 @@
<string name="create">Create</string> <string name="create">Create</string>
<string name="select_one_template">Please select one template</string> <string name="select_one_template">Please select one template</string>
<string name="choose_template_helper_text">Please choose a template and enter a file name.</string> <string name="choose_template_helper_text">Please choose a template and enter a file name.</string>
<string name="share_permission_view_only">View only</string>
<string name="share_permission_can_edit">Can edit</string>
<string name="share_permission_file_drop">File drop</string>
<string name="share_permission_secure_file_drop">Secure file drop</string>
<string name="share_permissions">Share permissions</string> <string name="share_permissions">Share permissions</string>
<string name="advanced_settings">Advanced settings</string>
<string name="common_next">Next</string> <string name="common_next">Next</string>
<string name="send_share">Send share</string> <string name="send_share">Send share</string>
<string name="no_share_permission_selected">Please select at least one permission to share.</string> <string name="no_share_permission_selected">Please select at least one permission to share.</string>
@ -1330,6 +1335,7 @@
<string name="re_enable_auto_upload_desc">Due to new restrictions imposed by Google, we have been forced to remove an important permission. We are currently working with Google to resolve this issue and restore full functionality.\n\nTo re-enable auto upload for new photos and videos:\nSelect \"Allow all\" in the following dialogue or the system settings.\nAllow media location when prompted, as this allows Nextcloud to store location data when uploading images.\n\nThe permissions dialogue is only displayed when necessary. If in doubt, check the system settings.\n\nAuto upload will only be able to upload image and video files when using the Google Play version of the Nextcloud app.\n\nPlease check for any files that may not have been uploaded since December 2024.</string> <string name="re_enable_auto_upload_desc">Due to new restrictions imposed by Google, we have been forced to remove an important permission. We are currently working with Google to resolve this issue and restore full functionality.\n\nTo re-enable auto upload for new photos and videos:\nSelect \"Allow all\" in the following dialogue or the system settings.\nAllow media location when prompted, as this allows Nextcloud to store location data when uploading images.\n\nThe permissions dialogue is only displayed when necessary. If in doubt, check the system settings.\n\nAuto upload will only be able to upload image and video files when using the Google Play version of the Nextcloud app.\n\nPlease check for any files that may not have been uploaded since December 2024.</string>
<string name="click_to_learn_how_to_re_enable_auto_uploads">Manual intervention required to re-enable auto-upload</string> <string name="click_to_learn_how_to_re_enable_auto_uploads">Manual intervention required to re-enable auto-upload</string>
<string name="share_download_limit">Set download limit</string> <string name="share_download_limit">Set download limit</string>
<string name="set_download_limit_failed">Unable to set download limit. Please check capabilities.</string>
<string name="download_limit">Download limit</string> <string name="download_limit">Download limit</string>
<plurals name="share_download_limit_description"> <plurals name="share_download_limit_description">
<item quantity="one">%1$d download remaining</item> <item quantity="one">%1$d download remaining</item>

View File

@ -10,7 +10,7 @@
*/ */
buildscript { buildscript {
ext { ext {
androidLibraryVersion ="13fb6e486d82696073cc1a56a8ff8371bba690de" androidLibraryVersion ="d862794d794a7e8d8b53da98aa801753e684bf52"
androidCommonLibraryVersion = "0.25.0" androidCommonLibraryVersion = "0.25.0"
androidPluginVersion = "8.9.2" androidPluginVersion = "8.9.2"
androidxMediaVersion = "1.5.1" androidxMediaVersion = "1.5.1"

View File

@ -11279,6 +11279,14 @@
<sha256 value="b14de6c6ed598b5b31cb8842b41f1cbb03313bf4bc29eaaa9dfd17f07b6dd8f0" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="b14de6c6ed598b5b31cb8842b41f1cbb03313bf4bc29eaaa9dfd17f07b6dd8f0" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact> </artifact>
</component> </component>
<component group="com.github.nextcloud" name="android-library" version="3ff8fea794d165afc1b3be698ad04bdee59e37c1">
<artifact name="android-library-3ff8fea794d165afc1b3be698ad04bdee59e37c1.aar">
<sha256 value="43888bc29328b621703ca813bee857853e41af6979aeaa44578beef2477ce314" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="android-library-3ff8fea794d165afc1b3be698ad04bdee59e37c1.module">
<sha256 value="cba548d2fbfd1c6e1b2261017c71f9a09884d3ac20558378524051f532b989de" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.nextcloud" name="android-library" version="406b50853375215998190d661110ae8c881a054e"> <component group="com.github.nextcloud" name="android-library" version="406b50853375215998190d661110ae8c881a054e">
<artifact name="android-library-406b50853375215998190d661110ae8c881a054e.aar"> <artifact name="android-library-406b50853375215998190d661110ae8c881a054e.aar">
<sha256 value="b29e961d64d5f9e6e2e04cac07a23d50a5df0769f76550702cd8a1a29684f23d" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="b29e961d64d5f9e6e2e04cac07a23d50a5df0769f76550702cd8a1a29684f23d" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -11583,12 +11591,12 @@
<sha256 value="6907f3626be02ec7508a98ea912529780445a0336dc4fc665c96af007c5d6c49" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="6907f3626be02ec7508a98ea912529780445a0336dc4fc665c96af007c5d6c49" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact> </artifact>
</component> </component>
<component group="com.github.nextcloud" name="android-library" version="a2ed4ca78486906086bacf873080d0046272c294"> <component group="com.github.nextcloud" name="android-library" version="a07602d104">
<artifact name="android-library-a2ed4ca78486906086bacf873080d0046272c294.aar"> <artifact name="android-library-a07602d104.aar">
<sha256 value="e22b38a2e7e9ec9461af2005c573f7c3bf097651f0bda27367fb355f82ef217d" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="b4f29732f325849d974c2547fb182322dff615ae29f8da29f56f1dfa8e64b09d" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact> </artifact>
<artifact name="android-library-a2ed4ca78486906086bacf873080d0046272c294.module"> <artifact name="android-library-a07602d104.module">
<sha256 value="ff8218520b3451a48d646b217d7c5bcfda75193cb11b92bffeb0bd698d494a45" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="c310576fd498cdc27dbcbf15532d78cd3c9cca1e30708b7e23001b7d53633d15" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact> </artifact>
</component> </component>
<component group="com.github.nextcloud" name="android-library" version="a4d86ef9d1"> <component group="com.github.nextcloud" name="android-library" version="a4d86ef9d1">
@ -11655,6 +11663,14 @@
<sha256 value="29e4ea50d4197b79bdfefe7e0ce41a58cda967cd8c4307981d34f37c4c901cd8" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="29e4ea50d4197b79bdfefe7e0ce41a58cda967cd8c4307981d34f37c4c901cd8" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact> </artifact>
</component> </component>
<component group="com.github.nextcloud" name="android-library" version="bfca7ddb5e">
<artifact name="android-library-bfca7ddb5e.aar">
<sha256 value="7686a3588271fe4f90bf9708b13aab800abb89889d6e04d4a85bc24621968364" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="android-library-bfca7ddb5e.module">
<sha256 value="bb68aa824d8769dbe93875da75891dc3794519fdd13c60a7c2bd45c1353634d8" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.nextcloud" name="android-library" version="c219388da22028f36219acff61ae2d722a63f408"> <component group="com.github.nextcloud" name="android-library" version="c219388da22028f36219acff61ae2d722a63f408">
<artifact name="android-library-c219388da22028f36219acff61ae2d722a63f408.aar"> <artifact name="android-library-c219388da22028f36219acff61ae2d722a63f408.aar">
<sha256 value="c253a126ca3c32dd5b373ab1c64668e4603305b3113b052fc0fc5e3c833a913c" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="c253a126ca3c32dd5b373ab1c64668e4603305b3113b052fc0fc5e3c833a913c" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -11743,6 +11759,14 @@
<sha256 value="d7c4c3c4f9458f2de9a63ebac3c80e99c04e358c4aef347c499b52690d761c86" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="d7c4c3c4f9458f2de9a63ebac3c80e99c04e358c4aef347c499b52690d761c86" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact> </artifact>
</component> </component>
<component group="com.github.nextcloud" name="android-library" version="d862794d794a7e8d8b53da98aa801753e684bf52">
<artifact name="android-library-d862794d794a7e8d8b53da98aa801753e684bf52.aar">
<sha256 value="d82be01cadecc9c51d68ed1ef50b98cf19266bdf1c1f781a696b958d94fc4184" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="android-library-d862794d794a7e8d8b53da98aa801753e684bf52.module">
<sha256 value="01b31ce0b551f0537aa939e6d316e66d7b7ba11e81e170a58e2ceb2ff0e1939f" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.nextcloud" name="android-library" version="d8c46b28ff7f268af5dfad7075f390d8eac434cd"> <component group="com.github.nextcloud" name="android-library" version="d8c46b28ff7f268af5dfad7075f390d8eac434cd">
<artifact name="android-library-d8c46b28ff7f268af5dfad7075f390d8eac434cd.aar"> <artifact name="android-library-d8c46b28ff7f268af5dfad7075f390d8eac434cd.aar">
<sha256 value="c253a126ca3c32dd5b373ab1c64668e4603305b3113b052fc0fc5e3c833a913c" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="c253a126ca3c32dd5b373ab1c64668e4603305b3113b052fc0fc5e3c833a913c" origin="Generated by Gradle" reason="Artifact is not signed"/>
@ -11791,6 +11815,14 @@
<sha256 value="8e3efc498a87a7538eb6e4281c1c8a8ea22e0a0c38a584a10c4ca7b9fd0009e8" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="8e3efc498a87a7538eb6e4281c1c8a8ea22e0a0c38a584a10c4ca7b9fd0009e8" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact> </artifact>
</component> </component>
<component group="com.github.nextcloud" name="android-library" version="efceb970c7">
<artifact name="android-library-efceb970c7.aar">
<sha256 value="3c708625831efefa7a6dba0572b93f81dd537ee16a4b5c5e0436c744e857427b" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="android-library-efceb970c7.module">
<sha256 value="06b7cbe7980a6d07b30ac093c623a6153463ce6ff28515d004efd117acd2f5cd" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<component group="com.github.nextcloud" name="android-library" version="f174c1ee78"> <component group="com.github.nextcloud" name="android-library" version="f174c1ee78">
<artifact name="android-library-f174c1ee78.aar"> <artifact name="android-library-f174c1ee78.aar">
<sha256 value="8f62a8bb0b9699dfbfcdf58493d0386e6b91113e3ba2157421249ab3cbc55d8f" origin="Generated by Gradle" reason="Artifact is not signed"/> <sha256 value="8f62a8bb0b9699dfbfcdf58493d0386e6b91113e3ba2157421249ab3cbc55d8f" origin="Generated by Gradle" reason="Artifact is not signed"/>