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 = 86, to = 87, 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
)

View File

@ -58,5 +58,7 @@ data class ShareEntity(
@ColumnInfo(name = ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_LIMIT)
val downloadLimitLimit: Int?,
@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
import android.animation.Animator
import android.animation.AnimatorListenerAdapter
import android.content.Context
import android.graphics.Outline
import android.util.TypedValue
@ -19,6 +21,30 @@ fun View?.setVisibleIf(condition: Boolean) {
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) {
this?.let {
it.apply {

View File

@ -1569,6 +1569,8 @@ public class FileDataStorageManager {
contentValues.putNull(ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT);
}
contentValues.put(ProviderTableMeta.OCSHARES_ATTRIBUTES, share.getAttributes());
return contentValues;
}
@ -1599,6 +1601,8 @@ public class FileDataStorageManager {
getInt(cursor, ProviderTableMeta.OCSHARES_DOWNLOADLIMIT_COUNT));
share.setFileDownloadLimit(downloadLimit);
share.setAttributes(getString(cursor, ProviderTableMeta.OCSHARES_ATTRIBUTES));
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 static final String DB_NAME = "filelist";
public static final int DB_VERSION = 89;
public static final int DB_VERSION = 90;
private ProviderMeta() {
// No instance
@ -203,6 +203,7 @@ public class ProviderMeta {
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_COUNT = "download_limit_count";
public static final String OCSHARES_ATTRIBUTES = "attributes";
public static final String OCSHARES_DEFAULT_SORT_ORDER = OCSHARES_FILE_SOURCE
+ " collate nocase asc";

View File

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

View File

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

View File

@ -35,6 +35,7 @@ public class UpdateShareInfoOperation extends SyncOperation {
private int permissions = -1;
private String password;
private String label;
private String attributes;
/**
* Constructor
@ -78,7 +79,7 @@ public class UpdateShareInfoOperation extends SyncOperation {
if (share == null) {
// 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
@ -93,11 +94,12 @@ public class UpdateShareInfoOperation extends SyncOperation {
}
updateOp.setPassword(password);
updateOp.setLabel(label);
updateOp.setAttributes(attributes);
RemoteOperationResult result = updateOp.execute(client);
var result = updateOp.execute(client);
if (result.isSuccess()) {
RemoteOperation getShareOp = new GetShareRemoteOperation(share.getRemoteId());
final var getShareOp = new GetShareRemoteOperation(share.getRemoteId());
result = getShareOp.execute(client);
//only update the share in storage if shareId is available
@ -125,6 +127,10 @@ public class UpdateShareInfoOperation extends SyncOperation {
this.hideFileDownload = hideFileDownload;
}
public void setAttributes(String attributes) {
this.attributes = attributes;
}
public void setPermissions(int 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_IN_BACKGROUND = "IN_BACKGROUND";
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_SECURE_FILE_DROP = "CREATE_SECURE_FILE_DROP";
@ -437,6 +438,11 @@ public class OperationsService extends Service {
// perform the operation
try {
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) {
// 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);
boolean hideFileDownload = operationIntent.getBooleanExtra(EXTRA_SHARE_HIDE_FILE_DOWNLOAD,
false);
String attributes = operationIntent.getStringExtra(EXTRA_SHARE_ATTRIBUTES);
if (!TextUtils.isEmpty(remotePath)) {
CreateShareWithShareeOperation createShareWithShareeOperation =
new CreateShareWithShareeOperation(remotePath,
@ -603,6 +611,7 @@ public class OperationsService extends Service {
sharePassword,
expirationDateInMillis,
hideFileDownload,
attributes,
fileDataStorageManager,
getApplicationContext(),
user,
@ -641,6 +650,9 @@ public class OperationsService extends Service {
updateShare.setLabel(operationIntent.getStringExtra(EXTRA_SHARE_PUBLIC_LABEL));
}
String shareAttributes = operationIntent.getStringExtra(EXTRA_SHARE_ATTRIBUTES);
updateShare.setAttributes(shareAttributes);
operation = updateShare;
}
break;

View File

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

View File

@ -15,7 +15,6 @@
package com.owncloud.android.ui.adapter;
import android.content.Context;
import android.graphics.PorterDuff;
import android.text.TextUtils;
import android.view.View;
@ -23,9 +22,10 @@ import com.nextcloud.android.lib.resources.files.FileDownloadLimit;
import com.nextcloud.utils.mdm.MDMConfig;
import com.owncloud.android.R;
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.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 androidx.annotation.NonNull;
@ -36,6 +36,7 @@ class LinkShareViewHolder extends RecyclerView.ViewHolder {
private FileDetailsShareLinkShareItemBinding binding;
private Context context;
private ViewThemeUtils viewThemeUtils;
private boolean encrypted;
public LinkShareViewHolder(@NonNull View itemView) {
super(itemView);
@ -43,40 +44,42 @@ class LinkShareViewHolder extends RecyclerView.ViewHolder {
public LinkShareViewHolder(FileDetailsShareLinkShareItemBinding binding,
Context context,
final ViewThemeUtils viewThemeUtils) {
final ViewThemeUtils viewThemeUtils,
boolean encrypted) {
this(binding.getRoot());
this.binding = binding;
this.context = context;
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()) {
final var res = context.getResources();
binding.name.setText(publicShare.getSharedWithDisplayName());
binding.icon.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(),
R.drawable.ic_email,
null));
final var emailDrawable = ResourcesCompat.getDrawable(res, R.drawable.ic_email, null);
binding.icon.setImageDrawable(emailDrawable);
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 {
if (!TextUtils.isEmpty(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);
}
}
String label = publicShare.getLabel();
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();
if (downloadLimit != null && downloadLimit.getLimit() > 0) {
int remaining = downloadLimit.getLimit() - downloadLimit.getCount();
@ -88,11 +91,11 @@ class LinkShareViewHolder extends RecyclerView.ViewHolder {
binding.subline.setVisibility(View.GONE);
}
String permissionName = SharingMenuHelper.getPermissionName(context, publicShare);
setPermissionName(publicShare, permissionName);
QuickPermissionType quickPermissionType = SharePermissionManager.INSTANCE.getSelectedType(publicShare, encrypted);
setPermissionName(publicShare, quickPermissionType.getText(context));
binding.overflowMenu.setOnClickListener(v -> listener.showSharingMenuActionSheet(publicShare));
if (!SharingMenuHelper.isSecureFileDrop(publicShare)) {
if (!SharePermissionManager.INSTANCE.isSecureFileDrop(publicShare) && !encrypted) {
binding.shareByLinkContainer.setOnClickListener(v -> listener.showPermissionsDialog(publicShare));
}
@ -104,12 +107,13 @@ class LinkShareViewHolder extends RecyclerView.ViewHolder {
}
private void setPermissionName(OCShare publicShare, String permissionName) {
if (!TextUtils.isEmpty(permissionName) && !SharingMenuHelper.isSecureFileDrop(publicShare)) {
binding.permissionName.setText(permissionName);
binding.permissionName.setVisibility(View.VISIBLE);
viewThemeUtils.androidx.colorPrimaryTextViewElement(binding.permissionName);
} else {
if (TextUtils.isEmpty(permissionName) || (SharePermissionManager.INSTANCE.isSecureFileDrop(publicShare) && encrypted)) {
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.ViewGroup
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.datamodel.QuickPermissionModel
import com.owncloud.android.datamodel.quickPermission.QuickPermission
import com.owncloud.android.utils.theme.ViewThemeUtils
class QuickSharingPermissionsAdapter(
private val quickPermissionList: MutableList<QuickPermissionModel>,
private val quickPermissionList: MutableList<QuickPermission>,
private val onPermissionChangeListener: QuickSharingPermissionViewHolder.OnPermissionChangeListener,
private val viewThemeUtils: ViewThemeUtils
) :
@ -40,27 +42,35 @@ class QuickSharingPermissionsAdapter(
}
class QuickSharingPermissionViewHolder(
val binding: ItemQuickSharePermissionsBinding,
private val binding: ItemQuickSharePermissionsBinding,
itemView: View,
val onPermissionChangeListener: OnPermissionChangeListener,
private val onPermissionChangeListener: OnPermissionChangeListener,
private val viewThemeUtils: ViewThemeUtils
) :
RecyclerView
.ViewHolder(itemView) {
) : RecyclerView.ViewHolder(itemView) {
fun bindData(quickPermissionModel: QuickPermissionModel) {
binding.tvQuickShareName.text = quickPermissionModel.permissionName
if (quickPermissionModel.isSelected) {
viewThemeUtils.platform.colorImageView(binding.tvQuickShareCheckIcon)
binding.tvQuickShareCheckIcon.visibility = View.VISIBLE
} else {
binding.tvQuickShareCheckIcon.visibility = View.INVISIBLE
fun bindData(quickPermission: QuickPermission) {
val context = itemView.context
val permissionName = quickPermission.type.getText(context)
binding.run {
quickPermissionButton.text = permissionName
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 {
// if user select different options then only update the permission
if (!quickPermissionModel.isSelected) {
onPermissionChangeListener.onPermissionChanged(adapterPosition)
if (isCustomPermission) {
onPermissionChangeListener.onCustomPermissionSelected()
} else if (!quickPermission.isSelected) {
// if user select different options then only update the permission
onPermissionChangeListener.onPermissionChanged(absoluteAdapterPosition)
} else {
// dismiss sheet on selection of same permission
onPermissionChangeListener.onDismissSheet()
@ -70,6 +80,7 @@ class QuickSharingPermissionsAdapter(
interface OnPermissionChangeListener {
fun onPermissionChanged(position: Int)
fun onCustomPermissionSelected()
fun onDismissSheet()
}
}

View File

@ -19,15 +19,16 @@ import android.view.View;
import android.widget.ImageView;
import com.nextcloud.client.account.User;
import com.nextcloud.utils.extensions.ImageViewExtensionsKt;
import com.owncloud.android.R;
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.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.theme.ViewThemeUtils;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
@ -37,6 +38,7 @@ class ShareViewHolder extends RecyclerView.ViewHolder {
private User user;
private Context context;
private ViewThemeUtils viewThemeUtils;
private boolean encrypted;
public ShareViewHolder(@NonNull View itemView) {
super(itemView);
@ -45,12 +47,14 @@ class ShareViewHolder extends RecyclerView.ViewHolder {
public ShareViewHolder(FileDetailsShareShareItemBinding binding,
User user,
Context context,
final ViewThemeUtils viewThemeUtils) {
final ViewThemeUtils viewThemeUtils,
boolean encrypted) {
this(binding.getRoot());
this.binding = binding;
this.user = user;
this.context = context;
this.viewThemeUtils = viewThemeUtils;
this.encrypted = encrypted;
}
public void bind(OCShare share,
@ -67,47 +71,53 @@ class ShareViewHolder extends RecyclerView.ViewHolder {
binding.icon.setTag(null);
switch (share.getShareType()) {
case GROUP:
name = context.getString(R.string.share_group_clarification, name);
viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context);
break;
case ROOM:
name = context.getString(R.string.share_room_clarification, name);
viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context);
break;
case CIRCLE:
viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context);
break;
case FEDERATED:
name = context.getString(R.string.share_remote_clarification, name);
setImage(binding.icon, share.getSharedWithDisplayName(), R.drawable.ic_user);
break;
case USER:
binding.icon.setTag(share.getShareWith());
float avatarRadius = context.getResources().getDimension(R.dimen.list_item_avatar_icon_radius);
DisplayUtils.setAvatar(user,
share.getShareWith(),
share.getSharedWithDisplayName(),
avatarListener,
avatarRadius,
context.getResources(),
binding.icon,
context);
if (share.getShareType() != null) {
switch (share.getShareType()) {
case GROUP:
name = context.getString(R.string.share_group_clarification, name);
viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context);
break;
case ROOM:
name = context.getString(R.string.share_room_clarification, name);
viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context);
break;
case CIRCLE:
viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context);
break;
case FEDERATED:
name = context.getString(R.string.share_remote_clarification, name);
setImage(binding.icon, share.getSharedWithDisplayName());
break;
case USER:
binding.icon.setTag(share.getShareWith());
float avatarRadius = context.getResources().getDimension(R.dimen.list_item_avatar_icon_radius);
binding.icon.setOnClickListener(v -> listener.showProfileBottomSheet(user, share.getShareWith()));
default:
setImage(binding.icon, name, R.drawable.ic_user);
break;
if (share.getShareWith() != null) {
DisplayUtils.setAvatar(user,
share.getShareWith(),
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);
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);
String permissionName = SharingMenuHelper.getPermissionName(context, share);
setPermissionName(permissionName);
QuickPermissionType quickPermissionType = SharePermissionManager.INSTANCE.getSelectedType(share, encrypted);
setPermissionName(quickPermissionType.getText(context));
// bind listener to edit privileges
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 {
avatar.setImageDrawable(TextDrawable.createNamedAvatar(name, avatarRadiusDimension));
} 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
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
@Override
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
boolean shareViaLink = MDMConfig.INSTANCE.shareViaLink(fileActivity);
final var parentViewGroup = LayoutInflater.from(fileActivity);
if (shareViaLink) {
switch (ShareType.fromValue(viewType)) {
case PUBLIC_LINK, EMAIL -> {
return new LinkShareViewHolder(
FileDetailsShareLinkShareItemBinding.inflate(LayoutInflater.from(fileActivity),
parent,
false),
fileActivity,
viewThemeUtils);
}
case NEW_PUBLIC_LINK -> {
if (encrypted) {
return new NewSecureFileDropViewHolder(
FileDetailsShareSecureFileDropAddNewItemBinding.inflate(LayoutInflater.from(fileActivity),
parent,
false)
);
} 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);
if (!shareViaLink) {
final var binding = FileDetailsShareInternalShareLinkBinding.inflate(parentViewGroup, parent, false);
return new InternalShareViewHolder(binding, fileActivity);
}
switch (ShareType.fromValue(viewType)) {
case PUBLIC_LINK, EMAIL -> {
final var binding = FileDetailsShareLinkShareItemBinding.inflate(parentViewGroup, parent, false);
return new LinkShareViewHolder(binding, fileActivity, viewThemeUtils, encrypted);
}
case NEW_PUBLIC_LINK -> {
if (encrypted) {
final var binding = FileDetailsShareSecureFileDropAddNewItemBinding.inflate(parentViewGroup, parent, false);
return new NewSecureFileDropViewHolder(binding);
} else {
final var binding = FileDetailsSharePublicLinkAddNewItemBinding.inflate(parentViewGroup, parent, false);
return new NewLinkShareViewHolder(binding);
}
}
} else {
return new InternalShareViewHolder(
FileDetailsShareInternalShareLinkBinding.inflate(LayoutInflater.from(fileActivity), parent, false),
fileActivity);
case INTERNAL -> {
final var binding = FileDetailsShareInternalShareLinkBinding.inflate(parentViewGroup, parent, false);
return new InternalShareViewHolder(binding, 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) {
publicShareViewHolder.bind(share, listener);
publicShareViewHolder.bind(share, listener, position);
} else if (holder instanceof InternalShareViewHolder internalShareViewHolder) {
internalShareViewHolder.bind(share, listener);
} else if (holder instanceof NewLinkShareViewHolder newLinkShareViewHolder) {
@ -186,7 +189,7 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
@SuppressLint("NotifyDataSetChanged")
public void toggleShowAll() {
this.showAll = !this.showAll;
showAll = !showAll;
notifyDataSetChanged();
}
@ -201,6 +204,12 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
notifyDataSetChanged();
}
@SuppressLint("NotifyDataSetChanged")
public void removeAll() {
shares.clear();
notifyDataSetChanged();
}
@Override
public void avatarGenerated(Drawable avatarDrawable, Object callContext) {
if (callContext instanceof ImageView iv) {
@ -217,10 +226,12 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
return false;
}
@SuppressLint("NotifyDataSetChanged")
public void remove(OCShare share) {
shares.remove(share);
notifyDataSetChanged();
int position = shares.indexOf(share);
if (position != -1) {
shares.remove(position);
notifyItemRemoved(position);
}
}
/**
@ -255,13 +266,4 @@ public class ShareeListAdapter extends RecyclerView.Adapter<RecyclerView.ViewHol
public List<OCShare> getShares() {
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.Arrays;
import java.util.List;
import java.util.Objects;
import javax.inject.Inject;
@ -509,11 +510,11 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
setFileModificationTimestamp(getFile(), showDetailedTimestamp);
} else if (id == R.id.folder_sync_button) {
if (binding.folderSyncButton.isChecked()) {
getFile().setInternalFolderSyncTimestamp(0L);
getFile().setInternalFolderSyncTimestamp(0L);
} else {
getFile().setInternalFolderSyncTimestamp(-1L);
}
storageManager.saveFile(getFile());
} else {
Log_OC.e(TAG, "Incorrect view clicked!");
@ -598,11 +599,11 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
if (fabMain != null) {
fabMain.hide();
}
binding.syncBlock.setVisibility(file.isFolder() ? View.VISIBLE : View.GONE);
if (file.isInternalFolderSync()) {
binding.folderSyncButton.setChecked(file.isInternalFolderSync());
binding.folderSyncButton.setChecked(file.isInternalFolderSync());
} else {
if (storageManager.isPartOfInternalTwoWaySync(file)) {
binding.folderSyncButton.setChecked(true);
@ -814,18 +815,27 @@ public class FileDetailFragment extends FileFragment implements OnClickListener,
/**
* open the sharing process fragment for creating new share
*
* @param shareeName
* @param shareType
*/
public void initiateSharingProcess(String shareeName,
ShareType shareType,
boolean secureShare) {
requireActivity().getSupportFragmentManager().beginTransaction().add(R.id.sharing_frame_container,
FileDetailsSharingProcessFragment.newInstance(getFile(),
shareeName,
shareType,
secureShare),
FileDetailsSharingProcessFragment.TAG)
if (getFile() == null) {
DisplayUtils.showSnackMessage(requireView(), R.string.file_not_found_cannot_share);
return;
}
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();
showHideFragmentView(true);

View File

@ -37,6 +37,7 @@ import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.network.ClientFactory;
import com.nextcloud.utils.extensions.BundleExtensionsKt;
import com.nextcloud.utils.extensions.FileExtensionsKt;
import com.nextcloud.utils.extensions.OCShareExtensionsKt;
import com.nextcloud.utils.extensions.ViewExtensionsKt;
import com.nextcloud.utils.mdm.MDMConfig;
import com.owncloud.android.R;
@ -178,6 +179,8 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
file.isEncrypted(),
SharesType.INTERNAL);
internalShareeListAdapter.setHasStableIds(true);
binding.sharesListInternal.setAdapter(internalShareeListAdapter);
binding.sharesListInternal.setLayoutManager(new LinearLayoutManager(requireContext()));
@ -190,6 +193,8 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
viewThemeUtils,
file.isEncrypted(),
SharesType.EXTERNAL);
externalShareeListAdapter.setHasStableIds(true);
binding.sharesListExternal.setAdapter(externalShareeListAdapter);
@ -214,10 +219,11 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
if (!(getActivity() instanceof FileActivity)) {
throw new IllegalArgumentException("Calling activity must be of type FileActivity");
}
try {
onEditShareListener = (OnEditShareListener) context;
} catch (Exception ignored) {
throw new IllegalArgumentException("Calling activity must implement the interface", ignored);
} catch (Exception e) {
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);
binding.sharesListInternalShowAll.setOnClickListener(view -> {
internalShareeListAdapter.toggleShowAll();
if (internalShareeListAdapter.isShowAll()) {
binding.sharesListInternalShowAll.setText(R.string.show_less);
} else {
binding.sharesListInternalShowAll.setText(R.string.show_all);
}
int textRes = internalShareeListAdapter.isShowAll() ? R.string.show_less : R.string.show_all;
binding.sharesListInternalShowAll.setText(textRes);
});
viewThemeUtils.material.colorMaterialTextButton(binding.sharesListExternalShowAll);
binding.sharesListExternalShowAll.setOnClickListener(view -> {
externalShareeListAdapter.toggleShowAll();
if (internalShareeListAdapter.isShowAll()) {
binding.sharesListExternalShowAll.setText(R.string.show_less);
} else {
binding.sharesListExternalShowAll.setText(R.string.show_all);
}
int textRes = externalShareeListAdapter.isShowAll() ? R.string.show_less : R.string.show_all;
binding.sharesListExternalShowAll.setText(textRes);
});
if (file.canReshare() && !FileDetailSharingFragmentHelper.isPublicShareDisabled(capabilities)) {
@ -409,7 +407,7 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
@VisibleForTesting
public void showSharingMenuActionSheet(OCShare share) {
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
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();
}
private void unshareWith(OCShare share) {
fileOperationsHelper.unshareShare(file, share);
private void unShareWith(OCShare 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));
return;
}
internalShareeListAdapter.getShares().clear();
internalShareeListAdapter.removeAll();
// to show share with users/groups info
List<OCShare> shares = fileDataStorageManager.getSharesWithForAFile(file.getRemotePath(),
@ -544,25 +543,17 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
}
internalShareeListAdapter.addShares(internalShares);
ViewExtensionsKt.setVisibleIf(binding.sharesListInternalShowAll, internalShareeListAdapter.getShares().size() > 3);
ViewExtensionsKt.setVisibleIf(binding.sharesListInternalShowAll,
internalShareeListAdapter.getShares().size() > 3
);
addExternalAndPublicShares(externalShares);
ViewExtensionsKt.setVisibleIf(binding.sharesListExternalShowAll, externalShareeListAdapter.getShares().size() > 3);
}
externalShareeListAdapter.getShares().clear();
// Get public share
List<OCShare> publicShares = fileDataStorageManager.getSharesByPathAndType(file.getRemotePath(),
ShareType.PUBLIC_LINK,
"");
externalShareeListAdapter.addShares(externalShares);
externalShareeListAdapter.addShares(publicShares);
ViewExtensionsKt.setVisibleIf(binding.sharesListExternalShowAll,
externalShareeListAdapter.getShares().size() > 3
);
private void addExternalAndPublicShares(List<OCShare> externalShares) {
final var publicShares = fileDataStorageManager.getSharesByPathAndType(file.getRemotePath(), ShareType.PUBLIC_LINK, "");
externalShareeListAdapter.removeAll();
final var shares = OCShareExtensionsKt.mergeDistinctByToken(externalShares, publicShares);
externalShareeListAdapter.addShares(shares);
}
private void checkContactPermission() {
@ -650,7 +641,6 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_PERMISSION);
}
@Override
public void sendNewEmail(OCShare share) {
modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_NOTE);
@ -658,13 +648,15 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
@Override
public void unShare(OCShare share) {
unshareWith(share);
ShareeListAdapter adapter = (ShareeListAdapter) binding.sharesListInternal.getAdapter();
if (adapter == null) {
unShareWith(share);
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));
return;
}
adapter.remove(share);
}
@Override
@ -691,6 +683,11 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda
fileOperationsHelper.setPermissionsToShare(share, permission);
}
@Override
public void openShareDetailWithCustomPermissions(OCShare share) {
modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_PERMISSION_WITH_CUSTOM_PERMISSION);
}
//launcher for contact permission
private final ActivityResultLauncher<String> requestContactPermissionLauncher =
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.BottomSheetDialog;
import com.nextcloud.android.common.ui.theme.utils.ColorRole;
import com.nextcloud.utils.mdm.MDMConfig;
import com.owncloud.android.databinding.FileDetailsSharingMenuBottomSheetFragmentBinding;
import com.owncloud.android.lib.resources.shares.OCShare;
import com.owncloud.android.lib.resources.shares.ShareType;
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;
/**
@ -32,14 +33,18 @@ public class FileDetailSharingMenuBottomSheetDialog extends BottomSheetDialog {
private final FileDetailsSharingMenuBottomSheetActions actions;
private final OCShare ocShare;
private final ViewThemeUtils viewThemeUtils;
private final boolean encrypted;
public FileDetailSharingMenuBottomSheetDialog(FileActivity fileActivity,
FileDetailsSharingMenuBottomSheetActions actions,
OCShare ocShare,
ViewThemeUtils viewThemeUtils) {
ViewThemeUtils viewThemeUtils,
boolean encrypted) {
super(fileActivity);
this.actions = actions;
this.ocShare = ocShare;
this.viewThemeUtils = viewThemeUtils;
this.encrypted = encrypted;
}
@Override
@ -54,10 +59,10 @@ public class FileDetailSharingMenuBottomSheetDialog extends BottomSheetDialog {
viewThemeUtils.platform.themeDialog(binding.getRoot());
viewThemeUtils.platform.colorImageView(binding.menuIconAdvancedPermissions);
viewThemeUtils.platform.colorImageView(binding.menuIconSendLink);
viewThemeUtils.platform.colorImageView(binding.menuIconUnshare);
viewThemeUtils.platform.colorImageView(binding.menuIconSendNewEmail);
viewThemeUtils.platform.colorImageView(binding.menuIconAdvancedPermissions, ColorRole.PRIMARY);
viewThemeUtils.platform.colorImageView(binding.menuIconSendLink, ColorRole.PRIMARY);
viewThemeUtils.platform.colorImageView(binding.menuIconUnshare, ColorRole.PRIMARY);
viewThemeUtils.platform.colorImageView(binding.menuIconSendNewEmail, ColorRole.PRIMARY);
updateUI();
@ -78,7 +83,7 @@ public class FileDetailSharingMenuBottomSheetDialog extends BottomSheetDialog {
binding.menuShareSendLink.setVisibility(View.GONE);
}
if (SharingMenuHelper.isSecureFileDrop(ocShare)) {
if (SharePermissionManager.INSTANCE.isSecureFileDrop(ocShare) && encrypted) {
binding.menuShareAdvancedPermissions.setVisibility(View.GONE);
}
}

View File

@ -1,11 +1,8 @@
/*
* Nextcloud Android client application
* Nextcloud - Android Client
*
* @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
* SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.owncloud.android.ui.fragment
@ -21,17 +18,20 @@ import androidx.fragment.app.Fragment
import com.nextcloud.client.di.Injectable
import com.nextcloud.utils.extensions.getParcelableArgument
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.owncloud.android.R
import com.owncloud.android.databinding.FileDetailsSharingProcessFragmentBinding
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.SharePermissionsBuilder
import com.owncloud.android.lib.resources.shares.ShareType
import com.owncloud.android.lib.resources.status.OCCapability
import com.owncloud.android.ui.activity.FileActivity
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.utils.ClipboardUtil
import com.owncloud.android.utils.DisplayUtils
@ -69,6 +69,7 @@ class FileDetailsSharingProcessFragment :
// types of screens to be displayed
const val SCREEN_TYPE_PERMISSION = 1 // permissions 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
@ -80,14 +81,16 @@ class FileDetailsSharingProcessFragment :
shareType: ShareType,
secureShare: Boolean
): FileDetailsSharingProcessFragment {
val args = Bundle()
args.putParcelable(ARG_OCFILE, file)
args.putSerializable(ARG_SHARE_TYPE, shareType)
args.putString(ARG_SHAREE_NAME, shareeName)
args.putBoolean(ARG_SECURE_SHARE, secureShare)
val fragment = FileDetailsSharingProcessFragment()
fragment.arguments = args
return fragment
val bundle = Bundle().apply {
putParcelable(ARG_OCFILE, file)
putSerializable(ARG_SHARE_TYPE, shareType)
putString(ARG_SHAREE_NAME, shareeName)
putBoolean(ARG_SECURE_SHARE, secureShare)
}
return FileDetailsSharingProcessFragment().apply {
arguments = bundle
}
}
/**
@ -100,14 +103,16 @@ class FileDetailsSharingProcessFragment :
isReshareShown: Boolean,
isExpirationDateShown: Boolean
): FileDetailsSharingProcessFragment {
val args = Bundle()
args.putParcelable(ARG_OCSHARE, share)
args.putInt(ARG_SCREEN_TYPE, screenType)
args.putBoolean(ARG_RESHARE_SHOWN, isReshareShown)
args.putBoolean(ARG_EXP_DATE_SHOWN, isExpirationDateShown)
val fragment = FileDetailsSharingProcessFragment()
fragment.arguments = args
return fragment
val bundle = Bundle().apply {
putParcelable(ARG_OCSHARE, share)
putInt(ARG_SCREEN_TYPE, screenType)
putBoolean(ARG_RESHARE_SHOWN, isReshareShown)
putBoolean(ARG_EXP_DATE_SHOWN, isExpirationDateShown)
}
return FileDetailsSharingProcessFragment().apply {
arguments = bundle
}
}
}
@ -135,12 +140,13 @@ class FileDetailsSharingProcessFragment :
private lateinit var capabilities: OCCapability
private var expirationDatePickerFragment: ExpirationDatePickerDialogFragment? = null
private var downloadAttribute: String? = null
override fun onAttach(context: Context) {
super.onAttach(context)
try {
onEditShareListener = context as FileDetailSharingFragment.OnEditShareListener
} catch (e: ClassCastException) {
} catch (_: ClassCastException) {
throw IllegalStateException("Calling activity must implement the interface")
}
}
@ -148,6 +154,18 @@ class FileDetailsSharingProcessFragment :
override fun onCreate(savedInstanceState: Bundle?) {
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 {
file = it.getParcelableArgument(ARG_OCFILE, OCFile::class.java)
shareeName = it.getString(ARG_SHAREE_NAME)
@ -164,13 +182,6 @@ class FileDetailsSharingProcessFragment :
isExpDateShown = it.getBoolean(ARG_EXP_DATE_SHOWN, true)
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 {
@ -181,39 +192,84 @@ class FileDetailsSharingProcessFragment :
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (shareProcessStep == SCREEN_TYPE_PERMISSION) {
showShareProcessFirst()
if (isShareProcessStepIsPermission()) {
setupUI()
} else {
showShareProcessSecond()
updateViewForNoteScreenType()
}
implementClickEvents()
implementClickEvents()
setCheckboxStates()
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() {
viewThemeUtils.platform.colorTextView(binding.shareProcessEditShareLink)
viewThemeUtils.platform.colorTextView(binding.shareProcessAdvancePermissionTitle)
viewThemeUtils.platform.run {
binding.run {
colorTextView(shareProcessEditShareLink)
colorTextView(shareCustomPermissionsText)
viewThemeUtils.platform.themeRadioButton(binding.shareProcessPermissionReadOnly)
viewThemeUtils.platform.themeRadioButton(binding.shareProcessPermissionUploadEditing)
viewThemeUtils.platform.themeRadioButton(binding.shareProcessPermissionFileDrop)
themeRadioButton(viewOnlyRadioButton)
themeRadioButton(canEditRadioButton)
themeRadioButton(customPermissionRadioButton)
viewThemeUtils.platform.themeCheckbox(binding.shareProcessAllowResharingCheckbox)
if (!isPublicShare()) {
themeCheckbox(shareAllowDownloadAndSyncCheckbox)
}
viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessSetPasswordSwitch)
viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessSetExpDateSwitch)
viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessSetDownloadLimitSwitch)
viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessHideDownloadCheckbox)
viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessChangeNameSwitch)
if (canSetFileRequest()) {
themeRadioButton(fileRequestRadioButton)
}
viewThemeUtils.material.colorTextInputLayout(binding.shareProcessEnterPasswordContainer)
viewThemeUtils.material.colorTextInputLayout(binding.shareProcessSetDownloadLimitInputContainer)
viewThemeUtils.material.colorTextInputLayout(binding.shareProcessChangeNameContainer)
viewThemeUtils.material.colorTextInputLayout(binding.noteContainer)
themeCheckbox(shareReadCheckbox)
themeCheckbox(shareCreateCheckbox)
themeCheckbox(shareEditCheckbox)
themeCheckbox(shareCheckbox)
themeCheckbox(shareDeleteCheckbox)
}
}
viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.shareProcessBtnNext)
viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.shareProcessBtnCancel)
viewThemeUtils.androidx.run {
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) {
@ -229,40 +285,63 @@ class FileDetailsSharingProcessFragment :
}
}
private fun showShareProcessFirst() {
binding.shareProcessGroupOne.visibility = View.VISIBLE
binding.shareProcessEditShareLink.visibility = View.VISIBLE
binding.shareProcessGroupTwo.visibility = View.GONE
if (share != null) {
setupModificationUI()
} else {
setupUpdateUI()
private fun setupUI() {
binding.run {
shareProcessGroupOne.visibility = View.VISIBLE
shareProcessEditShareLink.visibility = View.VISIBLE
shareProcessGroupTwo.visibility = View.GONE
}
if (isSecureShare) {
binding.shareProcessAdvancePermissionTitle.visibility = View.GONE
}
updateView()
// show or hide expiry date
if (isExpDateShown && !isSecureShare) {
binding.shareProcessSetExpDateSwitch.visibility = View.VISIBLE
} else {
binding.shareProcessSetExpDateSwitch.visibility = View.GONE
}
binding.shareProcessSetExpDateSwitch.setVisibleIf(isExpDateShown && !isSecureShare)
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()
// read only / allow upload and editing / file drop
if (SharingMenuHelper.isUploadAndEditingAllowed(share)) {
binding.shareProcessPermissionUploadEditing.isChecked = true
} else if (SharingMenuHelper.isFileDrop(share) && share?.isFolder == true) {
binding.shareProcessPermissionFileDrop.isChecked = true
} else if (SharingMenuHelper.isReadOnly(share)) {
binding.shareProcessPermissionReadOnly.isChecked = true
selectRadioButtonAccordingToPermission()
if (isShareProcessStepIsCustomPermission()) {
selectCustomPermissionLayout()
}
shareType = share?.shareType ?: ShareType.NO_SHARED
@ -270,7 +349,7 @@ class FileDetailsSharingProcessFragment :
// show different text for link share and other shares
// because we have link to share in Public Link
binding.shareProcessBtnNext.text = getString(
if (shareType == ShareType.PUBLIC_LINK) {
if (isPublicShare()) {
R.string.share_copy_link
} else {
R.string.common_confirm
@ -286,19 +365,34 @@ class FileDetailsSharingProcessFragment :
showFileDownloadLimitInput(binding.shareProcessSetDownloadLimitSwitch.isChecked)
}
private fun setupUpdateUI() {
binding.shareProcessBtnNext.text = getString(R.string.common_next)
file.let {
if (file?.isFolder == true) {
updateViewForFolder()
} else {
updateViewForFile()
private fun selectRadioButtonAccordingToPermission() {
val selectedType = SharePermissionManager.getSelectedType(share, encrypted = file?.isEncrypted == true)
binding.run {
when (selectedType) {
QuickPermissionType.VIEW_ONLY -> {
viewOnlyRadioButton.isChecked = true
}
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() {
@ -318,61 +412,60 @@ class FileDetailsSharingProcessFragment :
}
private fun updateViewForExternalShare() {
binding.shareProcessChangeNameSwitch.visibility = View.GONE
binding.shareProcessChangeNameContainer.visibility = View.GONE
updateViewForExternalAndLinkShare()
binding.run {
shareProcessChangeNameSwitch.visibility = View.GONE
shareProcessChangeNameContainer.visibility = View.GONE
updateViewForExternalAndLinkShare()
}
}
private fun updateViewForLinkShare() {
updateViewForExternalAndLinkShare()
binding.shareProcessChangeNameSwitch.visibility = View.VISIBLE
if (share != null) {
binding.shareProcessChangeName.setText(share?.label)
binding.shareProcessChangeNameSwitch.isChecked = !TextUtils.isEmpty(share?.label)
binding.run {
shareProcessChangeNameSwitch.visibility = View.VISIBLE
if (share != null) {
shareProcessChangeName.setText(share?.label)
shareProcessChangeNameSwitch.isChecked = !TextUtils.isEmpty(share?.label)
}
shareReadCheckbox.isEnabled = isFolder()
showChangeNameInput(shareProcessChangeNameSwitch.isChecked)
}
showChangeNameInput(binding.shareProcessChangeNameSwitch.isChecked)
}
private fun updateViewForInternalShare() {
binding.shareProcessChangeNameSwitch.visibility = View.GONE
binding.shareProcessChangeNameContainer.visibility = View.GONE
binding.shareProcessHideDownloadCheckbox.visibility = View.GONE
if (isSecureShare) {
binding.shareProcessAllowResharingCheckbox.visibility = View.GONE
} else {
binding.shareProcessAllowResharingCheckbox.visibility = View.VISIBLE
}
binding.shareProcessSetPasswordSwitch.visibility = View.GONE
binding.run {
shareProcessChangeNameSwitch.visibility = View.GONE
shareProcessChangeNameContainer.visibility = View.GONE
shareProcessHideDownloadCheckbox.visibility = View.GONE
shareCheckbox.setVisibleIf(!isSecureShare)
shareProcessSetPasswordSwitch.visibility = View.GONE
if (share != null) {
if (!isReShareShown) {
binding.shareProcessAllowResharingCheckbox.visibility = View.GONE
if (share != null) {
if (!isReShareShown) {
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() {
binding.shareProcessHideDownloadCheckbox.visibility = View.VISIBLE
binding.shareProcessAllowResharingCheckbox.visibility = View.GONE
binding.shareProcessSetPasswordSwitch.visibility = View.VISIBLE
binding.run {
shareProcessHideDownloadCheckbox.visibility = View.VISIBLE
shareCheckbox.visibility = View.GONE
shareProcessSetPasswordSwitch.visibility = View.VISIBLE
if (share != null) {
if (SharingMenuHelper.isFileDrop(share)) {
binding.shareProcessHideDownloadCheckbox.visibility = View.GONE
} else {
binding.shareProcessHideDownloadCheckbox.visibility = View.VISIBLE
binding.shareProcessHideDownloadCheckbox.isChecked = share?.isHideFileDownload == true
if (share != null) {
if (SharePermissionManager.isFileRequest(share)) {
shareProcessHideDownloadCheckbox.visibility = View.GONE
} else {
shareProcessHideDownloadCheckbox.visibility = View.VISIBLE
shareProcessHideDownloadCheckbox.isChecked = share?.isHideFileDownload == true
}
}
}
}
/**
* update expiration date view while modifying the share
*/
private fun updateExpirationDateView() {
share?.let { share ->
if (share.expirationDate > 0) {
@ -387,7 +480,7 @@ class FileDetailsSharingProcessFragment :
}
private fun updateFileDownloadLimitView() {
if (capabilities.filesDownloadLimit.isTrue && share?.isFolder == false) {
if (canSetDownloadLimit()) {
binding.shareProcessSetDownloadLimitSwitch.visibility = View.VISIBLE
val currentDownloadLimit = share?.fileDownloadLimit?.limit ?: capabilities.filesDownloadLimitDefault
@ -400,63 +493,175 @@ class FileDetailsSharingProcessFragment :
}
private fun updateViewForFile() {
binding.shareProcessPermissionUploadEditing.text = getString(R.string.link_share_editing)
binding.shareProcessPermissionFileDrop.visibility = View.GONE
binding.run {
canEditRadioButton.text = getString(R.string.link_share_editing)
}
}
private fun updateViewForFolder() {
binding.shareProcessPermissionUploadEditing.text = getString(R.string.link_share_allow_upload_and_editing)
binding.shareProcessPermissionFileDrop.visibility = View.VISIBLE
if (isSecureShare) {
binding.shareProcessPermissionFileDrop.visibility = View.GONE
binding.shareProcessAllowResharingCheckbox.visibility = View.GONE
binding.shareProcessSetExpDateSwitch.visibility = View.GONE
}
}
binding.run {
canEditRadioButton.text = getString(R.string.share_permission_can_edit)
/**
* update views for screen type Note
*/
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()
if (isSecureShare) {
shareCheckbox.visibility = View.GONE
shareProcessSetExpDateSwitch.visibility = View.GONE
}
}
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) {
@ -472,7 +677,8 @@ class FileDetailsSharingProcessFragment :
}
private fun showChangeNameInput(isChecked: Boolean) {
binding.shareProcessChangeNameContainer.visibility = if (isChecked) View.VISIBLE else View.GONE
binding.shareProcessChangeNameContainer.setVisibleIf(isChecked)
if (!isChecked) {
binding.shareProcessChangeName.setText(R.string.empty)
}
@ -483,11 +689,12 @@ class FileDetailsSharingProcessFragment :
if (share != null) {
removeCurrentFragment()
}
// 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
else {
if (shareProcessStep == SCREEN_TYPE_NOTE) {
showShareProcessFirst()
if (isShareProcessStepIsNote()) {
setupUI()
} else {
removeCurrentFragment()
}
@ -495,8 +702,8 @@ class FileDetailsSharingProcessFragment :
}
private fun showExpirationDateInput(isChecked: Boolean) {
binding.shareProcessSelectExpDate.visibility = if (isChecked) View.VISIBLE else View.GONE
binding.shareProcessExpDateDivider.visibility = if (isChecked) View.VISIBLE else View.GONE
binding.shareProcessSelectExpDate.setVisibleIf(isChecked)
binding.shareProcessExpDateDivider.setVisibleIf(isChecked)
// reset the expiration date if switch is unchecked
if (!isChecked) {
@ -515,7 +722,7 @@ class FileDetailsSharingProcessFragment :
}
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
if (!isChecked) {
@ -528,18 +735,11 @@ class FileDetailsSharingProcessFragment :
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
*/
@Suppress("ReturnCount")
private fun validateShareProcessFirst() {
permission = getSelectedPermission()
if (permission == OCShare.NO_PERMISSION) {
DisplayUtils.showSnackMessage(binding.root, R.string.no_share_permission_selected)
return
@ -572,26 +772,48 @@ class FileDetailsSharingProcessFragment :
removeCurrentFragment()
} else {
// else show step 2 (note screen)
showShareProcessSecond()
updateViewForNoteScreenType()
}
}
/**
* get the permissions on the basis of selection
*/
private fun getSelectedPermission() = when {
binding.shareProcessAllowResharingCheckbox.isChecked -> getReSharePermission()
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
@Suppress("ReturnCount")
private fun createShareOrUpdateNoteShare() {
if (!isAnyShareOptionChecked()) {
DisplayUtils.showSnackMessage(requireActivity(), R.string.share_option_required)
return
}
binding.shareProcessPermissionFileDrop.isChecked -> OCShare.CREATE_PERMISSION_FLAG
else -> permission
val noteText = binding.noteText.text.toString().trim()
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() {
// empty string causing fails
if (share?.attributes?.isEmpty() == true) {
share?.attributes = null
}
fileOperationsHelper?.updateShareInformation(
share,
permission,
@ -601,16 +823,8 @@ class FileDetailsSharingProcessFragment :
binding.shareProcessChangeName.text.toString().trim()
)
if (capabilities.filesDownloadLimit.isTrue) {
val downloadLimitInput = binding.shareProcessSetDownloadLimitInput.text.toString().trim()
val downloadLimit =
if (binding.shareProcessSetDownloadLimitSwitch.isChecked && downloadLimitInput.isNotEmpty()) {
downloadLimitInput.toInt()
} else {
0
}
fileOperationsHelper?.updateFilesDownloadLimit(share, downloadLimit)
if (canSetDownloadLimit()) {
setDownloadLimit()
}
// copy the share link if available
@ -619,31 +833,32 @@ class FileDetailsSharingProcessFragment :
}
}
/**
* method to validate step 2 (note screen) information
*/
private fun validateShareProcessSecond() {
val noteText = binding.noteText.text.toString().trim()
// if modifying existing share then directly update the note and send email
if (share != null && share?.note != noteText) {
fileOperationsHelper?.updateNoteToShare(share, noteText)
} else {
// else create new share
fileOperationsHelper?.shareFileWithSharee(
file,
shareeName,
shareType,
permission,
binding
.shareProcessHideDownloadCheckbox.isChecked,
binding.shareProcessEnterPassword.text.toString().trim(),
chosenExpDateInMills,
noteText,
binding.shareProcessChangeName.text.toString().trim(),
true
)
}
removeCurrentFragment()
private fun setDownloadLimit() {
val downloadLimitInput = binding.shareProcessSetDownloadLimitInput.text.toString().trim()
val downloadLimit =
if (binding.shareProcessSetDownloadLimitSwitch.isChecked && downloadLimitInput.isNotEmpty()) {
downloadLimitInput.toInt()
} else {
0
}
fileOperationsHelper?.updateFilesDownloadLimit(share, downloadLimit)
}
private fun createShare(noteText: String) {
fileOperationsHelper?.shareFileWithSharee(
file,
shareeName,
shareType,
permission,
binding.shareProcessHideDownloadCheckbox.isChecked,
binding.shareProcessEnterPassword.text.toString().trim(),
chosenExpDateInMills,
noteText,
downloadAttribute,
binding.shareProcessChangeName.text.toString().trim(),
true
)
}
/**
@ -664,4 +879,25 @@ class FileDetailsSharingProcessFragment :
override fun onDateUnSet() {
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;
import android.app.Dialog;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import com.google.android.material.bottomsheet.BottomSheetBehavior;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import com.nextcloud.utils.extensions.OCShareExtensionsKt;
import com.owncloud.android.R;
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.ui.activity.FileActivity;
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 java.util.ArrayList;
import java.util.List;
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;
/**
* 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 {
private QuickSharingPermissionsBottomSheetFragmentBinding binding;
@ -44,16 +46,19 @@ public class QuickSharingPermissionsBottomSheetDialog extends BottomSheetDialog
private final FileActivity fileActivity;
private final OCShare ocShare;
private final ViewThemeUtils viewThemeUtils;
private final boolean encrypted;
public QuickSharingPermissionsBottomSheetDialog(FileActivity fileActivity,
QuickPermissionSharingBottomSheetActions actions,
OCShare ocShare,
ViewThemeUtils viewThemeUtils) {
ViewThemeUtils viewThemeUtils,
boolean encrypted) {
super(fileActivity);
this.actions = actions;
this.ocShare = ocShare;
this.fileActivity = fileActivity;
this.viewThemeUtils = viewThemeUtils;
this.encrypted = encrypted;
}
@Override
@ -76,13 +81,19 @@ public class QuickSharingPermissionsBottomSheetDialog extends BottomSheetDialog
}
private void setUpRecyclerView() {
List<QuickPermissionModel> quickPermissionModelList = getQuickPermissionList();
List<QuickPermission> quickPermissionList = getQuickPermissionList();
QuickSharingPermissionsAdapter adapter = new QuickSharingPermissionsAdapter(
quickPermissionModelList,
quickPermissionList,
new QuickSharingPermissionsAdapter.QuickSharingPermissionViewHolder.OnPermissionChangeListener() {
@Override
public void onCustomPermissionSelected() {
dismiss();
actions.openShareDetailWithCustomPermissions(ocShare);
}
@Override
public void onPermissionChanged(int position) {
handlePermissionChanged(quickPermissionModelList, position);
handlePermissionChanged(quickPermissionList, position);
}
@Override
@ -98,60 +109,35 @@ public class QuickSharingPermissionsBottomSheetDialog extends BottomSheetDialog
}
/**
* handle permission changed on click of selected permission
* @param quickPermissionModelList
* @param position
* Handle permission changed on click of selected permission
*/
private void handlePermissionChanged(List<QuickPermissionModel> quickPermissionModelList, int position) {
if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(fileActivity.getResources().getString(R.string.link_share_allow_upload_and_editing))
|| quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(fileActivity.getResources().getString(R.string.link_share_editing))) {
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);
private void handlePermissionChanged(List<QuickPermission> quickPermissionList, int position) {
final var permissionName = quickPermissionList.get(position).getType().getText(getContext());
final var res = fileActivity.getResources();
} else if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(fileActivity.getResources().getString(R.string
.link_share_file_drop))) {
actions.onQuickPermissionChanged(ocShare,
CREATE_PERMISSION_FLAG);
int permissionFlag = 0;
if (permissionName.equalsIgnoreCase(res.getString(R.string.share_permission_can_edit)) || permissionName.equalsIgnoreCase(res.getString(R.string.link_share_editing))) {
permissionFlag = ocShare.isFolder() ? MAXIMUM_PERMISSIONS_FOR_FOLDER : MAXIMUM_PERMISSIONS_FOR_FILE;
} 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();
}
/**
* prepare the list of permissions needs to be displayed on recyclerview
* @return
* Prepare the list of permissions needs to be displayed on recyclerview
*/
private List<QuickPermissionModel> getQuickPermissionList() {
String[] permissionArray;
if (ocShare.isFolder()) {
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;
private List<QuickPermission> getQuickPermissionList() {
final var selectedType = SharePermissionManager.INSTANCE.getSelectedType(ocShare, encrypted);
final var hasFileRequestPermission = OCShareExtensionsKt.hasFileRequestPermission(ocShare);
return QuickPermissionType.Companion.getAvailablePermissions(hasFileRequestPermission, selectedType);
}
@Override
protected void onStop() {
super.onStop();
@ -160,5 +146,7 @@ public class QuickSharingPermissionsBottomSheetDialog extends BottomSheetDialog
public interface QuickPermissionSharingBottomSheetActions {
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,
long expirationTimeInMillis,
String note,
String attributes,
String label,
boolean showLoadingDialog) {
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_NOTE, (note == null) ? "" : note);
service.putExtra(OperationsService.EXTRA_SHARE_PUBLIC_LABEL, (label == null) ? "" : label);
service.putExtra(OperationsService.EXTRA_SHARE_ATTRIBUTES, attributes);
mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(service);
@ -588,16 +590,13 @@ public class FileOperationsHelper {
*
* @param file The file to unshare.
*/
public void unshareShare(OCFile file, OCShare share) {
// Unshare the file: Create the intent
Intent unshareService = new Intent(fileActivity, OperationsService.class);
unshareService.setAction(OperationsService.ACTION_UNSHARE);
unshareService.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount());
unshareService.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
unshareService.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId());
queueShareIntent(unshareService);
public void unShareShare(OCFile file, OCShare share) {
Intent intent = new Intent(fileActivity, OperationsService.class);
intent.setAction(OperationsService.ACTION_UNSHARE);
intent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount());
intent.putExtra(OperationsService.EXTRA_REMOTE_PATH, file.getRemotePath());
intent.putExtra(OperationsService.EXTRA_SHARE_ID, share.getId());
queueShareIntent(intent);
}
private void queueShareIntent(Intent shareIntent) {
@ -751,18 +750,33 @@ public class FileOperationsHelper {
* leaving the link unrestricted. Zero makes no change.
* @param label new label
*/
public void updateShareInformation(OCShare share, int permissions,
boolean hideFileDownload, String password, long expirationTimeInMillis,
public void updateShareInformation(OCShare share,
int permissions,
boolean hideFileDownload,
String password,
long expirationTimeInMillis,
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);
updateShareIntent.setAction(OperationsService.ACTION_UPDATE_SHARE_INFO);
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_HIDE_FILE_DOWNLOAD, hideFileDownload);
updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PASSWORD, (password == null) ? "" : password);
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_ATTRIBUTES, attributes);
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" />
<RadioGroup
android:id="@+id/share_process_permission_radio_group"
android:id="@+id/share_radio_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<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_height="wrap_content"
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
android:id="@+id/share_process_permission_upload_editing"
android:id="@+id/can_edit_radio_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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
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_height="wrap_content"
android:minHeight="@dimen/minimum_size_for_touchable_area"
android:text="@string/link_share_file_drop" />
android:text="@string/share_custom_permission" />
</RadioGroup>
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/share_process_advance_permission_title"
<com.google.android.material.checkbox.MaterialCheckBox
android:id="@+id/share_allow_download_and_sync_checkbox"
android:layout_width="match_parent"
android:layout_height="wrap_content"
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:text="@string/allow_resharing"
android:visibility="gone"
android:text="@string/share_allow_download_and_sync_permission"
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
android:id="@+id/share_process_set_password_switch"
android:layout_width="match_parent"

View File

@ -2,41 +2,23 @@
<!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2021 TSI-mc
~ SPDX-FileCopyrightText: 2021 Nextcloud GmbH
~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
~ SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
~ SPDX-License-Identifier: AGPL-3.0-or-later
-->
<com.google.android.material.button.MaterialButton
xmlns:android="http://schemas.android.com/apk/res/android"
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_height="@dimen/bottom_sheet_item_height"
android:background="?android:attr/selectableItemBackground"
android:paddingStart="@dimen/standard_padding"
android:paddingEnd="@dimen/standard_padding">
<ImageView
android:id="@+id/tv_quick_share_check_icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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>
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:insetTop="0dp"
android:insetBottom="0dp"
android:minHeight="56dp"
android:paddingStart="16dp"
android:paddingEnd="16dp"
app:iconGravity="textStart"
app:iconPadding="12dp" />

View File

@ -787,7 +787,7 @@
<string name="screenshot_05_autoUpload_heading">Automatisk upload</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_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="secure_share_not_set_up">Sikker deling er ikke sat op for denne bruger</string>
<string name="secure_share_search">Sikker deling...</string>
@ -1119,12 +1119,12 @@
<item quantity="other">%d sekunder siden</item>
</plurals>
<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>
</plurals>
<plurals name="time_hours_ago">
<item quantity="one">%d time sidenago</item>
<item quantity="other">%d timer sidenago</item>
<item quantity="one">%d time siden</item>
<item quantity="other">%d timer siden</item>
</plurals>
<plurals name="sync_fail_in_favourites_content">
<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_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_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="filedetails_details">Detaļas</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>
</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">
<item>@string/sub_folder_rule_month</item>
<item>@string/sub_folder_rule_year</item>

View File

@ -616,12 +616,10 @@
<string name="share_dialog_title">Sharing</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_no_expiration_date_label">Set expiration date</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_password_title">Password-protected</string>
<string name="share_no_password_title">Set password</string>
<string name="share_with_title">Share with…</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="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_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_server_public_key_unavailable">Unable to retrieve public key</string>
@ -1073,6 +1072,11 @@
<string name="create_new">New</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_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>
@ -1092,11 +1096,17 @@
<string name="public_share_name">New name</string>
<string name="share_link_with_label">Share link (%1$s)</string>
<string name="share_link">Share link</string>
<string name="allow_resharing">Allow resharing</string>
<string name="link_share_view_only">View only</string>
<string name="share_custom_permission">Custom permissions</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_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="failed_update_ui">Failed to update UI</string>
<string name="email_pick_failed">Failed to pick email address.</string>
@ -1162,12 +1172,7 @@
<string name="create">Create</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="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="advanced_settings">Advanced settings</string>
<string name="common_next">Next</string>
<string name="send_share">Send 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="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="set_download_limit_failed">Unable to set download limit. Please check capabilities.</string>
<string name="download_limit">Download limit</string>
<plurals name="share_download_limit_description">
<item quantity="one">%1$d download remaining</item>

View File

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

View File

@ -11279,6 +11279,14 @@
<sha256 value="b14de6c6ed598b5b31cb8842b41f1cbb03313bf4bc29eaaa9dfd17f07b6dd8f0" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</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">
<artifact name="android-library-406b50853375215998190d661110ae8c881a054e.aar">
<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"/>
</artifact>
</component>
<component group="com.github.nextcloud" name="android-library" version="a2ed4ca78486906086bacf873080d0046272c294">
<artifact name="android-library-a2ed4ca78486906086bacf873080d0046272c294.aar">
<sha256 value="e22b38a2e7e9ec9461af2005c573f7c3bf097651f0bda27367fb355f82ef217d" origin="Generated by Gradle" reason="Artifact is not signed"/>
<component group="com.github.nextcloud" name="android-library" version="a07602d104">
<artifact name="android-library-a07602d104.aar">
<sha256 value="b4f29732f325849d974c2547fb182322dff615ae29f8da29f56f1dfa8e64b09d" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
<artifact name="android-library-a2ed4ca78486906086bacf873080d0046272c294.module">
<sha256 value="ff8218520b3451a48d646b217d7c5bcfda75193cb11b92bffeb0bd698d494a45" origin="Generated by Gradle" reason="Artifact is not signed"/>
<artifact name="android-library-a07602d104.module">
<sha256 value="c310576fd498cdc27dbcbf15532d78cd3c9cca1e30708b7e23001b7d53633d15" origin="Generated by Gradle" reason="Artifact is not signed"/>
</artifact>
</component>
<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"/>
</artifact>
</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">
<artifact name="android-library-c219388da22028f36219acff61ae2d722a63f408.aar">
<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"/>
</artifact>
</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">
<artifact name="android-library-d8c46b28ff7f268af5dfad7075f390d8eac434cd.aar">
<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"/>
</artifact>
</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">
<artifact name="android-library-f174c1ee78.aar">
<sha256 value="8f62a8bb0b9699dfbfcdf58493d0386e6b91113e3ba2157421249ab3cbc55d8f" origin="Generated by Gradle" reason="Artifact is not signed"/>