diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java index f711af4ca..3bbfe0e20 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesDatabase.java @@ -50,20 +50,21 @@ import it.niedermann.owncloud.notes.shared.model.Capabilities; @Database( - entities = { - Account.class, - Note.class, - CategoryOptions.class, - SingleNoteWidgetData.class, - NotesListWidgetData.class, - ShareEntity.class, - Capabilities.class - }, version = 28, - autoMigrations = { - @AutoMigration(from = 25, to = 26), - @AutoMigration(from = 26, to = 27), - @AutoMigration(from = 27, to = 28), - } + entities = { + Account.class, + Note.class, + CategoryOptions.class, + SingleNoteWidgetData.class, + NotesListWidgetData.class, + ShareEntity.class, + Capabilities.class + }, version = 29, + autoMigrations = { + @AutoMigration(from = 25, to = 26), + @AutoMigration(from = 26, to = 27), + @AutoMigration(from = 27, to = 28), + @AutoMigration(from = 28, to = 29), + } ) @TypeConverters({Converters.class}) public abstract class NotesDatabase extends RoomDatabase { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java index 47f15caa7..f25c91e00 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/NotesRepository.java @@ -49,6 +49,7 @@ import com.nextcloud.android.sso.helper.SingleAccountHelper; import com.nextcloud.android.sso.model.SingleSignOnAccount; import com.owncloud.android.lib.common.utils.Log_OC; +import com.owncloud.android.lib.resources.shares.OCShare; import java.util.ArrayList; import java.util.Calendar; @@ -1006,8 +1007,16 @@ public void addShareEntities(List entities) { db.getShareDao().addShareEntities(entities); } - public List getShareEntities(String path) { - return db.getShareDao().getShareEntities(path); + public ShareEntity getShareByPathAndDisplayName(@NonNull OCShare share) { + if (share.getPath() == null || share.getSharedWithDisplayName() == null) { + return null; + } + + return db.getShareDao().getShareByPathAndDisplayName(share.getPath(), share.getSharedWithDisplayName()); + } + + public void deleteShareById(int id) { + db.getShareDao().deleteById(id); } public void updateNote(Note note) { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/ShareDao.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/ShareDao.kt index 01f540e19..6ccc37f7e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/ShareDao.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/dao/ShareDao.kt @@ -7,6 +7,7 @@ package it.niedermann.owncloud.notes.persistence.dao import androidx.room.Dao +import androidx.room.Delete import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query @@ -17,6 +18,17 @@ interface ShareDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun addShareEntities(entities: List) - @Query("SELECT * FROM share_table WHERE path = :path") - fun getShareEntities(path: String): List + @Query(""" + SELECT * FROM share_table + WHERE path = :path + AND share_with_displayname = :displayName + LIMIT 1 + """) + fun getShareByPathAndDisplayName( + path: String, + displayName: String + ): ShareEntity? + + @Query("DELETE FROM share_table WHERE id = :id") + fun deleteById(id: Int) } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/ShareEntity.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/ShareEntity.kt index c8bfd4fcf..759d1f7fa 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/ShareEntity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/entity/ShareEntity.kt @@ -25,5 +25,7 @@ data class ShareEntity( val uid_owner: String? = null, val displayname_owner: String? = null, val url: String? = null, - val expiration_date: Long? = null + val expiration_date: Long? = null, + val permissions: Double? = null, + val attributes: String? = null ) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt index 69c7429ba..c6a15e80e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/persistence/sync/ShareAPI.kt @@ -24,7 +24,7 @@ import retrofit2.http.Query interface ShareAPI { @GET("sharees") - fun getSharees( + fun fetchSharees( @Query("format") format: String = "json", @Query("itemType") itemType: String = "file", @Query("search") search: String, @@ -33,14 +33,8 @@ interface ShareAPI { @Query("lookup") lookup: String = "false", ): LinkedTreeMap? - @GET("shares/{remoteId}?format=json") - fun getShares( - @Path("remoteId") remoteId: Long, - @Query("include_tags") includeTags: Boolean = true, - ): Call>> - @GET("shares/?format=json") - fun getSharesForSpecificNote( + fun fetchSharesForSpecificNote( @Query("path") path: String, @Query("reshares") reshares: Boolean = true, @Query("subfiles") subfiles: Boolean = true @@ -52,9 +46,6 @@ interface ShareAPI { @POST("shares?format=json") fun addShare(@Body request: CreateShareRequest): Call> - @POST("shares/{shareId}/send-email?format=json") - fun sendEmail(@Path("shareId") shareId: Long, @Body password: SharePasswordRequest?): Call - @PUT("shares/{shareId}?format=json") fun updateShare(@Path("shareId") shareId: Long, @Body request: UpdateShareRequest): Call> @@ -65,8 +56,8 @@ interface ShareAPI { ): Call> @GET("shares/?format=json") - fun getShareFromNote( + fun fetchSharesFromNote( @Query("path") path: String, - @Query("shared_with_me") sharedWithMe: Boolean = true + @Query("shared_with_me") sharedWithMe: Boolean = false ): Call>> } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java index b5fb2d543..f7336dd38 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareActivity.java @@ -76,6 +76,7 @@ import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener; import it.niedermann.owncloud.notes.share.model.CreateShareResponse; import it.niedermann.owncloud.notes.share.model.CreateShareResponseExtensionsKt; +import it.niedermann.owncloud.notes.share.model.UpdateShareRequest; import it.niedermann.owncloud.notes.share.model.UsersAndGroupsSearchConfig; import it.niedermann.owncloud.notes.share.repository.ShareRepository; import it.niedermann.owncloud.notes.shared.model.Capabilities; @@ -84,6 +85,7 @@ import it.niedermann.owncloud.notes.shared.util.ShareUtil; import it.niedermann.owncloud.notes.shared.util.clipboard.ClipboardUtil; import it.niedermann.owncloud.notes.shared.util.extensions.BundleExtensionsKt; +import it.niedermann.owncloud.notes.util.DateUtil; public class NoteShareActivity extends BrandedActivity implements ShareeListAdapterListener, NoteShareItemAction, QuickSharingPermissionsBottomSheetDialog.QuickPermissionSharingBottomSheetActions, SharePasswordDialogFragment.SharePasswordDialogListener { @@ -161,7 +163,7 @@ private void initializeArguments() { final var ssoAcc = SingleAccountHelper.getCurrentSingleSignOnAccount(NoteShareActivity.this); repository = new ShareRepository(NoteShareActivity.this, ssoAcc); capabilities = repository.getCapabilities(); - repository.getSharesForNotesAndSaveShareEntities(); + repository.fetchSharesForNotesAndSaveShareEntities(); runOnUiThread(() -> { binding.fileName.setText(note.getTitle()); @@ -396,7 +398,7 @@ public void createPublicShareLink() { requestPasswordForShareViaLink(true, capabilities.getAskForOptionalPassword()); } else { executorService.submit(() -> { - final var result = repository.addShare(note, ShareType.PUBLIC_LINK, "", "false", "", 0, ""); + final var result = repository.addShare(note, ShareType.PUBLIC_LINK, ""); runOnUiThread(() -> { if (result instanceof ApiResult.Success> successResponse && binding.sharesList.getAdapter() instanceof ShareeListAdapter adapter) { DisplayUtils.showSnackMessage(NoteShareActivity.this, successResponse.getMessage()); @@ -519,17 +521,16 @@ public void updateShareeListAdapter() { return; } - List tempShares = new ArrayList<>(); - - // to show share with users/groups info + List remoteNotes; if (note != null) { - // get shares from local DB - populateSharesList(tempShares); + remoteNotes = repository.fetchSharesFromNote(note); + } else { + remoteNotes = new ArrayList<>(); } runOnUiThread(() -> { shares.clear(); - shares.addAll(tempShares); + shares.addAll(remoteNotes); adapter.removeAll(); adapter.addShares(shares); @@ -544,35 +545,6 @@ public void updateShareeListAdapter() { }); } - private void populateSharesList(List targetList) { - // Get shares from local DB - final var shareEntities = repository.getShareEntitiesForSpecificNote(note); - for (var entity : shareEntities) { - if (entity.getId() != null) { - addSharesToList(entity.getId(), targetList); - } - } - - // Get shares from remote - final var remoteShares = repository.getShareFromNote(note); - if (remoteShares != null) { - for (var entity : remoteShares) { - addSharesToList(entity.getId(), targetList); - } - } - } - - private void addSharesToList(long id, List targetList) { - final var result = repository.getShares(id); - if (result != null) { - for (OCShare ocShare : result) { - if (!targetList.contains(ocShare)) { - targetList.add(ocShare); - } - } - } - } - private void addPublicShares(ShareeListAdapter adapter) { List publicShares = new ArrayList<>(); @@ -672,13 +644,13 @@ public void search(String query) { @Override public void advancedPermissions(OCShare share) { - modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_PERMISSION); + modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_PERMISSION, false); } @Override public void sendNewEmail(OCShare share) { - modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_NOTE); + modifyExistingShare(share, NoteShareDetailActivity.SCREEN_TYPE_NOTE, true); } @Override @@ -711,13 +683,14 @@ public void addAnotherLink(OCShare share) { createPublicShareLink(); } - private void modifyExistingShare(OCShare share, int screenTypePermission) { + private void modifyExistingShare(OCShare share, int screenTypePermission, boolean sendEmail) { Bundle bundle = new Bundle(); bundle.putSerializable(NoteShareDetailActivity.ARG_OCSHARE, share); bundle.putInt(NoteShareDetailActivity.ARG_SCREEN_TYPE, screenTypePermission); bundle.putBoolean(NoteShareDetailActivity.ARG_RESHARE_SHOWN, !isReshareForbidden(share)); bundle.putBoolean(NoteShareDetailActivity.ARG_EXP_DATE_SHOWN, getExpDateShown()); + bundle.putBoolean(NoteShareDetailActivity.ARG_SEND_EMAIL, sendEmail); Intent intent = new Intent(this, NoteShareDetailActivity.class); intent.putExtras(bundle); @@ -759,12 +732,17 @@ public void onQuickPermissionChanged(OCShare share, int permission) { } private void updateShare(OCShare share) { + if (note == null) { + Log_OC.e(TAG, "note is null, cannot update share"); + return; + } + executorService.submit(() -> { try { - final var updatedShares = repository.getShares(share.getId()); + final var updatedShares = repository.fetchSharesFromNote(note); runOnUiThread(() -> { - if (updatedShares != null && binding.sharesList.getAdapter() instanceof ShareeListAdapter adapter) { + if (binding.sharesList.getAdapter() instanceof ShareeListAdapter adapter) { OCShare updatedShare = null; for (int i=0;i { @@ -881,14 +858,22 @@ public void setPasswordToShare(@NotNull OCShare share, @Nullable String password executorService.submit(() -> { { - final var requestBody = repository.getUpdateShareRequest( - false, - share, - "", - password, - false, - -1, - share.getPermissions() + boolean isDownloadAndAllowsSyncEnabled = repository.isAllowDownloadAndSync(share); + + String attributes = UpdateShareRequest.Companion.createAttributes( + repository.getCapabilities(), + isDownloadAndAllowsSyncEnabled, + share.getShareType() + ); + + final var requestBody = new UpdateShareRequest( + share.getPermissions(), + password, + share.getNote(), + share.getLabel(), + DateUtil.INSTANCE.getExpirationDate(share.getExpirationDate()), + Boolean.toString(share.isHideFileDownload()), + attributes ); final var result = repository.updateShare(share.getId(), requestBody); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareDetailActivity.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareDetailActivity.kt index 532d2edd0..3e5a6ceb2 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareDetailActivity.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/NoteShareDetailActivity.kt @@ -9,29 +9,33 @@ package it.niedermann.owncloud.notes.share import android.content.Intent import android.content.res.Configuration import android.os.Bundle +import android.text.Editable import android.text.TextUtils +import android.text.TextWatcher import android.view.View import androidx.lifecycle.lifecycleScope import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.android.sso.helper.SingleAccountHelper 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 it.niedermann.owncloud.notes.R import it.niedermann.owncloud.notes.branding.BrandedActivity import it.niedermann.owncloud.notes.branding.BrandingUtil import it.niedermann.owncloud.notes.databinding.ActivityNoteShareDetailBinding +import it.niedermann.owncloud.notes.persistence.ApiResult import it.niedermann.owncloud.notes.persistence.entity.Note import it.niedermann.owncloud.notes.persistence.isSuccess import it.niedermann.owncloud.notes.share.dialog.ExpirationDatePickerDialogFragment -import it.niedermann.owncloud.notes.share.helper.SharingMenuHelper -import it.niedermann.owncloud.notes.share.model.SharePasswordRequest +import it.niedermann.owncloud.notes.share.helper.SharePermissionManager +import it.niedermann.owncloud.notes.share.model.QuickPermissionType +import it.niedermann.owncloud.notes.share.model.UpdateShareRequest import it.niedermann.owncloud.notes.share.repository.ShareRepository import it.niedermann.owncloud.notes.shared.util.DisplayUtils import it.niedermann.owncloud.notes.shared.util.clipboard.ClipboardUtil import it.niedermann.owncloud.notes.shared.util.extensions.getParcelableArgument import it.niedermann.owncloud.notes.shared.util.extensions.getSerializableArgument +import it.niedermann.owncloud.notes.util.DateUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -60,6 +64,7 @@ class NoteShareDetailActivity : const val ARG_SCREEN_TYPE = "arg_screen_type" const val ARG_RESHARE_SHOWN = "arg_reshare_shown" const val ARG_EXP_DATE_SHOWN = "arg_exp_date_shown" + const val ARG_SEND_EMAIL = "ard_send_email" private const val ARG_SECURE_SHARE = "secure_share" // types of screens to be displayed @@ -79,9 +84,12 @@ class NoteShareDetailActivity : private var isReShareShown: Boolean = true // show or hide reShare option private var isExpDateShown: Boolean = true // show or hide expiry date option private var isSecureShare: Boolean = false + private var sendEmail: Boolean = false private var expirationDatePickerFragment: ExpirationDatePickerDialogFragment? = null private lateinit var repository: ShareRepository + private val passwordMask = "••••••" + private var enteredNoteText = "" override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -89,7 +97,7 @@ class NoteShareDetailActivity : binding = ActivityNoteShareDetailBinding.inflate(layoutInflater) setContentView(binding.root) setSupportActionBar(binding.toolbar) - binding.toolbar.setNavigationOnClickListener({ v -> backPressed() }) + binding.toolbar.setNavigationOnClickListener { backPressed() } val arguments = intent.extras arguments?.let { @@ -107,19 +115,20 @@ class NoteShareDetailActivity : isReShareShown = it.getBoolean(ARG_RESHARE_SHOWN, true) isExpDateShown = it.getBoolean(ARG_EXP_DATE_SHOWN, true) isSecureShare = it.getBoolean(ARG_SECURE_SHARE, false) + sendEmail = it.getBoolean(ARG_SEND_EMAIL, shareType == ShareType.EMAIL) } lifecycleScope.launch(Dispatchers.IO) { val ssoAcc = SingleAccountHelper.getCurrentSingleSignOnAccount(this@NoteShareDetailActivity) repository = ShareRepository(this@NoteShareDetailActivity, ssoAcc) - permission = repository.getCapabilities().defaultPermission + permission = share?.permissions ?: repository.getCapabilities().defaultPermission withContext(Dispatchers.Main) { if (shareProcessStep == SCREEN_TYPE_PERMISSION) { - showShareProcessFirst() + setupUI() } else { - showShareProcessSecond() + updateViewForNoteScreenType() } implementClickEvents() } @@ -135,19 +144,15 @@ class NoteShareDetailActivity : binding.run { util.platform.run { - themeRadioButton(shareProcessPermissionReadOnly) - themeRadioButton(shareProcessPermissionUploadEditing) - themeRadioButton(shareProcessPermissionFileDrop) + themeRadioButton(canViewRadioButton) + themeRadioButton(canEditRadioButton) colorTextView(shareProcessEditShareLink) - colorTextView(shareProcessAdvancePermissionTitle) - - themeCheckbox(shareProcessAllowResharingCheckbox) - colorTextView(title, ColorRole.ON_SURFACE) } util.androidx.run { + colorSwitchCompat(allowDownloadAndSync) colorSwitchCompat(shareProcessSetPasswordSwitch) colorSwitchCompat(shareProcessSetExpDateSwitch) colorSwitchCompat(shareProcessHideDownloadCheckbox) @@ -182,20 +187,14 @@ class NoteShareDetailActivity : } } - 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) { @@ -205,28 +204,30 @@ class NoteShareDetailActivity : } binding.noteText.setText(share?.note) + enteredNoteText = share?.note ?: "" shareProcessStep = SCREEN_TYPE_PERMISSION } - private fun setupModificationUI() { - 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 + private fun updateView() { + if (share != null) { + updateViewForUpdate() + } else { + updateViewForCreate() } + } + + private fun updateViewForUpdate() { + updateViewForFile() + + selectRadioButtonAccordingToPermission() shareType = share?.shareType ?: ShareType.NO_SHARED // 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.note_share_detail_activity_share_copy_link } else { R.string.note_share_detail_activity_common_confirm @@ -238,9 +239,49 @@ class NoteShareDetailActivity : showPasswordInput(binding.shareProcessSetPasswordSwitch.isChecked) updateExpirationDateView() showExpirationDateInput(binding.shareProcessSetExpDateSwitch.isChecked) + maskPasswordInput() + } + + private fun maskPasswordInput() { + if (share?.isPasswordProtected == false) { + return + } + + binding.shareProcessEnterPassword.run { + setText(passwordMask) + setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + text?.clear() + } + } + } + } + + private fun selectRadioButtonAccordingToPermission() { + val selectedType = SharePermissionManager.getSelectedType(share) + binding.run { + when (selectedType) { + QuickPermissionType.VIEW_ONLY -> { + canViewRadioButton.isChecked = true + } + + QuickPermissionType.CAN_EDIT -> { + canEditRadioButton.isChecked = true + } + + else -> Unit + } + } + } + + private fun setMaxPermissionsIfDefaultPermissionExists() { + if (repository.getCapabilities().defaultPermission != OCShare.NO_PERMISSION) { + binding.canEditRadioButton.isChecked = true + permission = SharePermissionManager.getMaximumPermission() + } } - private fun setupUpdateUI() { + private fun updateViewForCreate() { binding.shareProcessBtnNext.text = getString(R.string.note_share_detail_activity_common_next) note.let { @@ -249,6 +290,7 @@ class NoteShareDetailActivity : } showPasswordInput(binding.shareProcessSetPasswordSwitch.isChecked) showExpirationDateInput(binding.shareProcessSetExpDateSwitch.isChecked) + setMaxPermissionsIfDefaultPermissionExists() } private fun updateViewForShareType() { @@ -288,19 +330,12 @@ class NoteShareDetailActivity : shareProcessChangeNameSwitch.visibility = View.GONE shareProcessChangeNameContainer.visibility = View.GONE shareProcessHideDownloadCheckbox.visibility = View.GONE - if (isSecureShare) { - shareProcessAllowResharingCheckbox.visibility = View.GONE - } else { - shareProcessAllowResharingCheckbox.visibility = View.VISIBLE - } shareProcessSetPasswordSwitch.visibility = View.GONE + allowDownloadAndSync.visibility = View.VISIBLE - if (share != null) { - if (!isReShareShown) { - shareProcessAllowResharingCheckbox.visibility = View.GONE - } - shareProcessAllowResharingCheckbox.isChecked = - SharingMenuHelper.canReshare(share) + share?.let { + val isDownloadAndAllowsSyncEnabled = repository.isAllowDownloadAndSync(it) + binding.allowDownloadAndSync.isChecked = isDownloadAndAllowsSyncEnabled } } } @@ -311,17 +346,11 @@ class NoteShareDetailActivity : private fun updateViewForExternalAndLinkShare() { binding.run { shareProcessHideDownloadCheckbox.visibility = View.VISIBLE - shareProcessAllowResharingCheckbox.visibility = View.GONE shareProcessSetPasswordSwitch.visibility = View.VISIBLE if (share != null) { - if (SharingMenuHelper.isFileDrop(share)) { - shareProcessHideDownloadCheckbox.visibility = View.GONE - } else { - shareProcessHideDownloadCheckbox.visibility = View.VISIBLE - shareProcessHideDownloadCheckbox.isChecked = - share?.isHideFileDownload == true - } + shareProcessHideDownloadCheckbox.visibility = View.VISIBLE + shareProcessHideDownloadCheckbox.isChecked = share?.isHideFileDownload == true } } } @@ -343,27 +372,13 @@ class NoteShareDetailActivity : } private fun updateViewForFile() { - binding.shareProcessPermissionUploadEditing.text = getString(R.string.link_share_editing) - binding.shareProcessPermissionFileDrop.visibility = View.GONE - } - - private fun updateViewForFolder() { - binding.run { - shareProcessPermissionUploadEditing.text = - getString(R.string.link_share_allow_upload_and_editing) - shareProcessPermissionFileDrop.visibility = View.VISIBLE - if (isSecureShare) { - shareProcessPermissionFileDrop.visibility = View.GONE - shareProcessAllowResharingCheckbox.visibility = View.GONE - shareProcessSetExpDateSwitch.visibility = View.GONE - } - } + binding.canEditRadioButton.text = getString(R.string.link_share_editing) } /** * update views for screen type Note */ - private fun showShareProcessSecond() { + private fun updateViewForNoteScreenType() { binding.run { shareProcessGroupOne.visibility = View.GONE shareProcessEditShareLink.visibility = View.GONE @@ -372,18 +387,29 @@ class NoteShareDetailActivity : shareProcessBtnNext.text = getString(R.string.note_share_detail_activity_set_note) noteText.setText(share?.note) + enteredNoteText = share?.note ?: "" } else { shareProcessBtnNext.text = getString(R.string.note_share_detail_activity_send_share) - noteText.setText(R.string.empty) } shareProcessStep = SCREEN_TYPE_NOTE shareProcessBtnNext.performClick() } } + @Suppress("LongMethod") private fun implementClickEvents() { binding.run { + noteText.addTextChangedListener(object : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + enteredNoteText = s.toString() + } + + override fun afterTextChanged(s: Editable) = Unit + }) + shareProcessBtnCancel.setOnClickListener { onCancelClick() } @@ -406,6 +432,20 @@ class NoteShareDetailActivity : shareProcessSelectExpDate.setOnClickListener { showExpirationDateDialog() } + + // region RadioButtons + shareProcessPermissionRadioGroup.setOnCheckedChangeListener { _, optionId -> + when (optionId) { + R.id.can_view_radio_button -> { + permission = OCShare.READ_PERMISSION_FLAG + } + + R.id.can_edit_radio_button -> { + permission = SharePermissionManager.getMaximumPermission() + } + } + } + // endregion } } @@ -434,7 +474,7 @@ class NoteShareDetailActivity : // and if user is in step 1 (permission screen) then remove the activity else { if (shareProcessStep == SCREEN_TYPE_NOTE) { - showShareProcessFirst() + setupUI() } else { finish() } @@ -462,10 +502,6 @@ class NoteShareDetailActivity : } } - private fun getReSharePermission(): Int = SharePermissionsBuilder().apply { - setSharePermission(true) - }.build() - /** * method to validate the step 1 screen information */ @@ -510,14 +546,13 @@ class NoteShareDetailActivity : // if modifying existing share information then execute the process if (share != null) { lifecycleScope.launch(Dispatchers.IO) { - val noteText = binding.noteText.text.toString().trim() val password = binding.shareProcessEnterPassword.text.toString().trim() - - updateShare(noteText, password, false) + val label = binding.shareProcessChangeName.text.toString() + updateShare(label, password) } } else { // else show step 2 (note screen) - showShareProcessSecond() + updateViewForNoteScreenType() } } @@ -525,10 +560,8 @@ class NoteShareDetailActivity : * 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 -> OCShare.MAXIMUM_PERMISSIONS_FOR_FILE - binding.shareProcessPermissionFileDrop.isChecked -> OCShare.CREATE_PERMISSION_FLAG + binding.canViewRadioButton.isChecked -> OCShare.READ_PERMISSION_FLAG + binding.canEditRadioButton.isChecked -> SharePermissionManager.getMaximumPermission() else -> permission } @@ -536,49 +569,64 @@ class NoteShareDetailActivity : * method to validate step 2 (note screen) information */ private fun createOrUpdateShare() { - val noteText = binding.noteText.text.toString().trim() val password = binding.shareProcessEnterPassword.text.toString().trim() lifecycleScope.launch(Dispatchers.IO) { - if (share != null && share?.note != noteText) { - updateShare(noteText, password, true) + if (share != null && share?.note != enteredNoteText) { + val label = binding.shareProcessChangeName.text.toString() + updateShare(label, password) } else { - createShare(noteText, password) + createShare(password) } } } - private suspend fun updateShare(noteText: String, password: String, sendEmail: Boolean) { - val downloadPermission = !binding.shareProcessHideDownloadCheckbox.isChecked - val requestBody = repository.getUpdateShareRequest( - downloadPermission, - share, - noteText, - password, - sendEmail, - chosenExpDateInMills, - permission + private suspend fun updateShare( + label: String, + password: String + ) { + val request = UpdateShareRequest( + permissions = permission.takeIf { it != -1 }, + password = if (password != passwordMask) { + password + } else { + null + }, + expireDate = DateUtil.getExpirationDate(chosenExpDateInMills), + label = label, + note = enteredNoteText.trim(), + attributes = UpdateShareRequest.createAttributes( + repository.getCapabilities(), + binding.allowDownloadAndSync.isChecked, + shareType + ), + hideDownload = binding.shareProcessHideDownloadCheckbox.isChecked.toString() ) - val updateShareResult = repository.updateShare(share!!.id, requestBody) + val result = repository.updateShare(share?.id ?: return, request) - if (updateShareResult.isSuccess() && sendEmail) { - val sendEmailResult = repository.sendEmail(share!!.id, SharePasswordRequest(password)) - handleResult(sendEmailResult) - } else { - handleResult(updateShareResult.isSuccess()) + if (!result.isSuccess()) { + val errorMessage = (result as? ApiResult.Error) + ?.message + ?.takeIf { it.contains("password", ignoreCase = true) } + ?.let { getString(R.string.note_share_detail_activity_password_error_message) } + + handleResult(false, errorMessage) + return } - if (!sendEmail) { - withContext(Dispatchers.Main) { - if (!TextUtils.isEmpty(share?.shareLink)) { - ClipboardUtil.copyToClipboard(this@NoteShareDetailActivity, share?.shareLink) + handleResult(true) + + share?.shareLink + ?.takeIf { it.isNotEmpty() } + ?.let { + withContext(Dispatchers.Main) { + ClipboardUtil.copyToClipboard(this@NoteShareDetailActivity, it) } } - } } - private suspend fun createShare(noteText: String, password: String) { + private suspend fun createShare(password: String) { if (note == null || shareeName == null) { Log_OC.d(TAG, "validateShareProcessSecond cancelled") return @@ -591,26 +639,32 @@ class NoteShareDetailActivity : "false", // TODO: Check how to determine it password, permission, - noteText + enteredNoteText.trim(), + UpdateShareRequest.createAttributes( + repository.getCapabilities(), + binding.allowDownloadAndSync.isChecked, + shareType + ) ) if (result.isSuccess()) { - repository.getSharesForNotesAndSaveShareEntities() + repository.fetchSharesForNotesAndSaveShareEntities() } handleResult(result.isSuccess()) } - private suspend fun handleResult(success: Boolean) { + private suspend fun handleResult(success: Boolean, errorMessage: String? = null) { withContext(Dispatchers.Main) { if (success) { val resultIntent = Intent() setResult(RESULT_OK, resultIntent) finish() } else { + val message = errorMessage ?: getString(R.string.note_share_detail_activity_create_share_error) DisplayUtils.showSnackMessage( this@NoteShareDetailActivity, - getString(R.string.note_share_detail_activity_create_share_error) + message ) } } @@ -627,4 +681,6 @@ class NoteShareDetailActivity : override fun onDateUnSet() { binding.shareProcessSetExpDateSwitch.isChecked = false } + + private fun isPublicShare(): Boolean = (shareType == ShareType.PUBLIC_LINK) } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/QuickSharingPermissionsAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/QuickSharingPermissionsAdapter.kt index 062ff37f4..eae3259c5 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/QuickSharingPermissionsAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/QuickSharingPermissionsAdapter.kt @@ -6,19 +6,19 @@ */ package it.niedermann.owncloud.notes.share.adapter +import android.annotation.SuppressLint import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.annotation.ColorInt import androidx.recyclerview.widget.RecyclerView import com.nextcloud.android.common.ui.theme.utils.ColorRole import it.niedermann.owncloud.notes.branding.Branded import it.niedermann.owncloud.notes.branding.BrandingUtil import it.niedermann.owncloud.notes.databinding.ItemQuickSharePermissionsBinding -import it.niedermann.owncloud.notes.share.model.QuickPermissionModel +import it.niedermann.owncloud.notes.share.model.QuickPermission class QuickSharingPermissionsAdapter( - private val quickPermissionList: MutableList, + private val quickPermissionList: MutableList, private val onPermissionChangeListener: QuickSharingPermissionViewHolder.OnPermissionChangeListener, private var color: Int = 0 ) : @@ -45,6 +45,7 @@ class QuickSharingPermissionsAdapter( return quickPermissionList.size } + @SuppressLint("NotifyDataSetChanged") override fun applyBrand(color: Int) { this.color = color notifyDataSetChanged() @@ -59,9 +60,10 @@ class QuickSharingPermissionsAdapter( RecyclerView .ViewHolder(itemView) { - fun bindData(quickPermissionModel: QuickPermissionModel) { - binding.tvQuickShareName.text = quickPermissionModel.permissionName - if (quickPermissionModel.isSelected) { + fun bindData(quickPermission: QuickPermission) { + val permissionName = quickPermission.type.getText(itemView.context) + binding.tvQuickShareName.text = permissionName + if (quickPermission.isSelected) { binding.tvQuickShareCheckIcon.visibility = View.VISIBLE } else { binding.tvQuickShareCheckIcon.visibility = View.INVISIBLE @@ -69,7 +71,7 @@ class QuickSharingPermissionsAdapter( itemView.setOnClickListener { // if user select different options then only update the permission - if (!quickPermissionModel.isSelected) { + if (!quickPermission.isSelected) { onPermissionChangeListener.onPermissionChanged(adapterPosition) } else { // dismiss sheet on selection of same permission diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/ShareeListAdapter.java b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/ShareeListAdapter.java index 3c45dbd63..53a8a2818 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/ShareeListAdapter.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/ShareeListAdapter.java @@ -99,7 +99,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int layoutInflater, parent, false); brandingUtil.platform.colorTextView(binding.name, ColorRole.ON_SURFACE); brandingUtil.platform.colorTextView(binding.subline, ColorRole.ON_SURFACE_VARIANT); - return new LinkShareViewHolder(binding, activity); + return new LinkShareViewHolder(binding); } case NEW_PUBLIC_LINK -> { ItemAddPublicShareBinding binding = ItemAddPublicShareBinding.inflate( @@ -135,9 +135,9 @@ public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int positi if (holder instanceof LinkShareViewHolder publicShareViewHolder) { - publicShareViewHolder.bind(share, listener); + publicShareViewHolder.bind(share, listener, position); } else if (holder instanceof InternalShareViewHolder internalShareViewHolder) { - internalShareViewHolder.bind(share, listener); + internalShareViewHolder.bind(listener); } else if (holder instanceof NewLinkShareViewHolder newLinkShareViewHolder) { newLinkShareViewHolder.bind(listener); } else { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/SuggestionAdapter.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/SuggestionAdapter.kt index 6348ae489..08d10bd5b 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/SuggestionAdapter.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/SuggestionAdapter.kt @@ -46,7 +46,19 @@ class SuggestionAdapter(context: Context, cursor: Cursor?, private val account: override fun bindView(view: View, context: Context, cursor: Cursor) { val suggestion = cursor.getString(cursor.getColumnIndexOrThrow(SearchManager.SUGGEST_COLUMN_TEXT_1)) - view.findViewById(R.id.suggestion_text).text = suggestion + val suggestionTextView = view.findViewById(R.id.suggestion_text) + suggestionTextView.text = suggestion + + val sublineColumn = cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2) + if (sublineColumn != -1 && suggestionTextView.text.isEmpty()) { + val subline = cursor.getStringOrNull(sublineColumn) + if (!subline.isNullOrEmpty()) { + suggestionTextView.visibility = View.VISIBLE + suggestionTextView.text = subline + } else { + suggestionTextView.visibility = View.GONE + } + } val icon = view.findViewById(R.id.suggestion_icon) val iconColumn = cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/InternalShareViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/InternalShareViewHolder.java index f81984d4c..0b2e7919c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/InternalShareViewHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/InternalShareViewHolder.java @@ -7,15 +7,11 @@ package it.niedermann.owncloud.notes.share.adapter.holder; import android.content.Context; -import android.graphics.PorterDuff; import android.view.View; import androidx.annotation.NonNull; -import androidx.core.content.res.ResourcesCompat; import androidx.recyclerview.widget.RecyclerView; -import com.owncloud.android.lib.resources.shares.OCShare; - import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.databinding.ItemInternalShareLinkBinding; import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener; @@ -34,13 +30,8 @@ public InternalShareViewHolder(ItemInternalShareLinkBinding binding, Context con this.context = context; } - public void bind(OCShare share, ShareeListAdapterListener listener) { - if (share.isFolder()) { - binding.shareInternalLinkText.setText(context.getString(R.string.share_internal_link_to_folder_text)); - } else { - binding.shareInternalLinkText.setText(context.getString(R.string.share_internal_link_to_file_text)); - } - + public void bind(ShareeListAdapterListener listener) { + binding.shareInternalLinkText.setText(context.getString(R.string.share_internal_link_to_file_text)); binding.copyInternalContainer.setOnClickListener(l -> listener.copyInternalLink()); } } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/LinkShareViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/LinkShareViewHolder.java deleted file mode 100644 index 58ed535f2..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/LinkShareViewHolder.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Nextcloud Notes - Android Client - * - * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.niedermann.owncloud.notes.share.adapter.holder; - -import android.content.Context; -import android.text.TextUtils; -import android.view.View; - -import androidx.annotation.NonNull; -import androidx.core.content.res.ResourcesCompat; - -import com.nextcloud.android.common.ui.theme.utils.ColorRole; -import com.owncloud.android.lib.resources.shares.OCShare; -import com.owncloud.android.lib.resources.shares.ShareType; - -import java.text.SimpleDateFormat; -import java.util.Date; - -import it.niedermann.owncloud.notes.R; -import it.niedermann.owncloud.notes.branding.BrandedViewHolder; -import it.niedermann.owncloud.notes.branding.BrandingUtil; -import it.niedermann.owncloud.notes.databinding.ItemShareLinkShareBinding; -import it.niedermann.owncloud.notes.share.helper.SharingMenuHelper; -import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener; - -public class LinkShareViewHolder extends BrandedViewHolder { - private ItemShareLinkShareBinding binding; - private Context context; - - private BrandingUtil brandingUtil; - - public LinkShareViewHolder(@NonNull View itemView) { - super(itemView); - bindBranding(); - } - - public LinkShareViewHolder(ItemShareLinkShareBinding binding, Context context) { - this(binding.getRoot()); - this.binding = binding; - this.context = context; - bindBranding(); - } - - public void bind(OCShare publicShare, ShareeListAdapterListener listener) { - if (publicShare.getShareType() != null && ShareType.EMAIL == publicShare.getShareType()) { - binding.name.setText(publicShare.getSharedWithDisplayName()); - binding.icon.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), - R.drawable.ic_email, - null)); - if (publicShare.getLabel() != null && !publicShare.getLabel().isEmpty()) { - brandingUtil.platform.colorTextView(binding.name, ColorRole.ON_SURFACE_VARIANT); - binding.label.setText(publicShare.getLabel()); - binding.label.setVisibility(View.VISIBLE); - } else { - brandingUtil.platform.colorTextView(binding.name, ColorRole.ON_SURFACE); - binding.label.setVisibility(View.GONE); - } - binding.copyLink.setVisibility(View.GONE); - } else { - brandingUtil.platform.colorTextView(binding.name, ColorRole.ON_SURFACE); - 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); - } - } - } - - binding.subline.setVisibility(View.GONE); - - String permissionName = SharingMenuHelper.getPermissionName(context, publicShare); - setPermissionName(publicShare, permissionName); - - binding.overflowMenu.setOnClickListener(v -> listener.showSharingMenuActionSheet(publicShare)); - if (!SharingMenuHelper.isSecureFileDrop(publicShare)) { - binding.shareByLinkContainer.setOnClickListener(v -> listener.showPermissionsDialog(publicShare)); - } - - if (publicShare.getExpirationDate() > 0) { - String expirationDescription = context.getString( - R.string.share_expires, - SimpleDateFormat.getDateInstance().format(new Date(publicShare.getExpirationDate())) - ); - binding.expirationStatus.setContentDescription(expirationDescription); - binding.expirationStatus.setVisibility(View.VISIBLE); - binding.shareIconContainer.setOnClickListener( - v -> listener.showShareExpirationSnackbar(publicShare) - ); - } else { - binding.expirationStatus.setContentDescription(null); - binding.expirationStatus.setVisibility(View.GONE); - } - - binding.copyLink.setOnClickListener(v -> listener.copyLink(publicShare)); - } - - private void setPermissionName(OCShare publicShare, String permissionName) { - if (!TextUtils.isEmpty(permissionName) && !SharingMenuHelper.isSecureFileDrop(publicShare)) { - binding.permissionName.setText(permissionName); - binding.permissionName.setVisibility(View.VISIBLE); - } else { - binding.permissionName.setVisibility(View.GONE); - } - } - - @Override - public void applyBrand(int color) { - brandingUtil = BrandingUtil.of(color, context); - if (binding != null) { - brandingUtil.androidx.colorPrimaryTextViewElement(binding.permissionName); - brandingUtil.platform.colorTextView(binding.label, ColorRole.ON_SURFACE); - brandingUtil.platform.colorImageViewBackgroundAndIcon(binding.icon); - brandingUtil.platform.colorImageView(binding.expirationStatus, ColorRole.ON_PRIMARY_CONTAINER); - } - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/LinkShareViewHolder.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/LinkShareViewHolder.kt new file mode 100644 index 000000000..f8f8f7123 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/LinkShareViewHolder.kt @@ -0,0 +1,170 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2015-2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.share.adapter.holder + +import android.text.TextUtils +import android.view.View +import androidx.core.content.res.ResourcesCompat +import com.nextcloud.android.common.ui.theme.utils.ColorRole +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.lib.resources.shares.ShareType +import it.niedermann.owncloud.notes.R +import it.niedermann.owncloud.notes.branding.BrandedViewHolder +import it.niedermann.owncloud.notes.branding.BrandingUtil +import it.niedermann.owncloud.notes.databinding.ItemShareLinkShareBinding +import it.niedermann.owncloud.notes.share.helper.SharePermissionManager +import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener +import it.niedermann.owncloud.notes.share.model.QuickPermissionType +import it.niedermann.owncloud.notes.util.remainingDownloadLimit + +/** + * ViewHolder responsible for displaying public and email link shares + * inside the share list. + */ +class LinkShareViewHolder( + private val binding: ItemShareLinkShareBinding +) : BrandedViewHolder(binding.root) { + + init { + bindBranding() + } + + fun bind(publicShare: OCShare, listener: ShareeListAdapterListener, position: Int) { + val quickPermissionType = SharePermissionManager.getSelectedType(publicShare) + + setName(binding, publicShare, position) + setSubline(binding, publicShare) + setPermissionName(binding, quickPermissionType) + setOnClickListeners(binding, listener, publicShare) + configureCopyLink(binding, listener, publicShare) + } + + @Suppress("ReturnCount") + private fun setName( + binding: ItemShareLinkShareBinding?, + publicShare: OCShare, + position: Int + ) { + val context = binding?.root?.context + + if (binding == null || context == null) { + return + } + + if (ShareType.PUBLIC_LINK == publicShare.shareType) { + val label = publicShare.label + binding.name.text = when { + label.isNullOrBlank() && position == 0 -> + context.getString(R.string.share_link) + + label.isNullOrBlank() -> + context.getString(R.string.share_link_with_label, position.toString()) + + else -> + context.getString(R.string.share_link_with_label, label) + } + return + } + + if (ShareType.EMAIL == publicShare.shareType) { + binding.name.text = publicShare.sharedWithDisplayName + + val emailDrawable = ResourcesCompat.getDrawable(context.resources, R.drawable.ic_email, null) + binding.icon.setImageDrawable(emailDrawable) + binding.copyLink.visibility = View.GONE + return + } + + val label = publicShare.label + if (!label.isNullOrEmpty()) { + binding.name.text = context.getString(R.string.share_link_with_label, label) + } + } + + private fun setSubline(binding: ItemShareLinkShareBinding?, publicShare: OCShare) { + val context = binding?.root?.context + if (binding == null || context == null) { + return + } + + val downloadLimit = publicShare.fileDownloadLimit + if (downloadLimit != null) { + val remaining = publicShare.remainingDownloadLimit() ?: return + val text = context.resources.getQuantityString( + R.plurals.share_download_limit_description, + remaining, + remaining + ) + + binding.subline.text = text + binding.subline.visibility = View.VISIBLE + return + } + + binding.subline.visibility = View.GONE + } + + private fun setPermissionName( + binding: ItemShareLinkShareBinding?, + quickPermissionType: QuickPermissionType + ) { + val context = binding?.root?.context + + if (binding == null || context == null) { + return + } + + val permissionName = quickPermissionType.getText(context) + + if (TextUtils.isEmpty(permissionName)) { + binding.permissionName.visibility = View.GONE + return + } + + binding.permissionName.text = permissionName + binding.permissionName.visibility = View.VISIBLE + } + + private fun setOnClickListeners( + binding: ItemShareLinkShareBinding?, + listener: ShareeListAdapterListener, + publicShare: OCShare + ) { + if (binding == null) { + return + } + + binding.overflowMenu.setOnClickListener { + listener.showSharingMenuActionSheet(publicShare) + } + binding.shareByLinkContainer.setOnClickListener { + listener.showPermissionsDialog(publicShare) + } + } + + private fun configureCopyLink( + binding: ItemShareLinkShareBinding?, + listener: ShareeListAdapterListener, + publicShare: OCShare + ) { + val context = binding?.root?.context + + if (binding == null || context == null) { + return + } + + binding.copyLink.setOnClickListener { listener.copyLink(publicShare) } + } + + override fun applyBrand(color: Int) { + val brandingUtil = BrandingUtil.of(color, binding.root.context) + brandingUtil.androidx.colorPrimaryTextViewElement(binding.permissionName) + brandingUtil.platform.colorTextView(binding.label, ColorRole.ON_SURFACE) + brandingUtil.platform.colorImageViewBackgroundAndIcon(binding.icon) + brandingUtil.platform.colorImageView(binding.expirationStatus, ColorRole.ON_PRIMARY_CONTAINER) + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/ShareViewHolder.java b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/ShareViewHolder.java index 660461bcb..654462baf 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/ShareViewHolder.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/adapter/holder/ShareViewHolder.java @@ -23,8 +23,9 @@ import it.niedermann.owncloud.notes.databinding.ItemShareShareBinding; import it.niedermann.owncloud.notes.persistence.entity.Account; import it.niedermann.owncloud.notes.share.helper.AvatarLoader; -import it.niedermann.owncloud.notes.share.helper.SharingMenuHelper; +import it.niedermann.owncloud.notes.share.helper.SharePermissionManager; import it.niedermann.owncloud.notes.share.listener.ShareeListAdapterListener; +import it.niedermann.owncloud.notes.share.model.QuickPermissionType; import it.niedermann.owncloud.notes.shared.util.FilesSpecificViewThemeUtils; public class ShareViewHolder extends BrandedViewHolder { @@ -101,8 +102,8 @@ public void bind(OCShare share, ShareeListAdapterListener listener) { accountUserName.equalsIgnoreCase(share.getUserId())) { binding.overflowMenu.setVisibility(View.VISIBLE); - String permissionName = SharingMenuHelper.getPermissionName(context, share); - setPermissionName(permissionName); + QuickPermissionType quickPermissionType = SharePermissionManager.INSTANCE.getSelectedType(share); + setPermissionName(quickPermissionType.getText(context)); // bind listener to edit privileges binding.overflowMenu.setOnClickListener(v -> listener.showSharingMenuActionSheet(share)); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/NoteShareActivityShareItemActionBottomSheetDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/NoteShareActivityShareItemActionBottomSheetDialog.java index 6700e6367..ad20a756c 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/NoteShareActivityShareItemActionBottomSheetDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/NoteShareActivityShareItemActionBottomSheetDialog.java @@ -16,10 +16,10 @@ import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.lib.resources.shares.ShareType; +import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialog; import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.databinding.ItemNoteShareActionBinding; -import it.niedermann.owncloud.notes.share.helper.SharingMenuHelper; import it.niedermann.owncloud.notes.share.listener.NoteShareItemAction; public class NoteShareActivityShareItemActionBottomSheetDialog extends BrandedBottomSheetDialog { @@ -55,18 +55,23 @@ protected void onCreate(Bundle savedInstanceState) { } private void updateUI() { - if (ocShare.getShareType() != null && ocShare.getShareType() == ShareType.PUBLIC_LINK) { + final var shareType = ocShare.getShareType(); + + if (shareType == ShareType.PUBLIC_LINK) { binding.menuShareAddAnotherLink.setVisibility(View.VISIBLE); - binding.menuShareSendLink.setVisibility(View.VISIBLE); + + // Not implemented yet + binding.menuShareSendLink.setVisibility(View.GONE); } else { binding.menuShareAddAnotherLink.setVisibility(View.GONE); binding.menuShareSendLink.setVisibility(View.GONE); } - if (SharingMenuHelper.isSecureFileDrop(ocShare)) { - binding.menuShareAdvancedPermissions.setVisibility(View.GONE); - binding.menuShareAddAnotherLink.setVisibility(View.GONE); + int menuUnshareTextId = R.string.share_delete_link; + if (shareType == ShareType.USER) { + menuUnshareTextId = R.string.delete_share; } + binding.menuUnshareText.setText(getContext().getString(menuUnshareTextId)); } private void setupClickListener() { diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/QuickSharingPermissionsBottomSheetDialog.java b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/QuickSharingPermissionsBottomSheetDialog.java index b39c95401..665e35e5d 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/QuickSharingPermissionsBottomSheetDialog.java +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/dialog/QuickSharingPermissionsBottomSheetDialog.java @@ -6,10 +6,6 @@ */ package it.niedermann.owncloud.notes.share.dialog; -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.READ_PERMISSION_FLAG; import android.app.Activity; import android.os.Bundle; @@ -21,16 +17,14 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.owncloud.android.lib.resources.shares.OCShare; -import java.util.ArrayList; import java.util.List; -import it.niedermann.owncloud.notes.R; import it.niedermann.owncloud.notes.branding.BrandedBottomSheetDialog; import it.niedermann.owncloud.notes.branding.BrandingUtil; import it.niedermann.owncloud.notes.databinding.QuickSharingPermissionsBottomSheetFragmentBinding; import it.niedermann.owncloud.notes.share.adapter.QuickSharingPermissionsAdapter; -import it.niedermann.owncloud.notes.share.helper.SharingMenuHelper; -import it.niedermann.owncloud.notes.share.model.QuickPermissionModel; +import it.niedermann.owncloud.notes.share.helper.SharePermissionManager; +import it.niedermann.owncloud.notes.share.model.QuickPermission; /** * File Details Quick Sharing permissions options {@link android.app.Dialog} styled as a bottom sheet for main actions. @@ -72,78 +66,46 @@ protected void onCreate(Bundle savedInstanceState) { } private void setUpRecyclerView() { - List quickPermissionModelList = getQuickPermissionList(); - adapter = new QuickSharingPermissionsAdapter( - quickPermissionModelList, - new QuickSharingPermissionsAdapter.QuickSharingPermissionViewHolder.OnPermissionChangeListener() { - @Override - public void onPermissionChanged(int position) { - handlePermissionChanged(quickPermissionModelList, position); - } - - @Override - public void onDismissSheet() { - dismiss(); - } - }, - color + List quickPermissionList = getQuickPermissionList(); + QuickSharingPermissionsAdapter adapter = new QuickSharingPermissionsAdapter( + quickPermissionList, + new QuickSharingPermissionsAdapter.QuickSharingPermissionViewHolder.OnPermissionChangeListener() { + @Override + public void onPermissionChanged(int position) { + handlePermissionChanged(quickPermissionList, position); + } + + @Override + public void onDismissSheet() { + dismiss(); + } + }, + color ); LinearLayoutManager linearLayoutManager = new LinearLayoutManager(activity); - adapter.applyBrand(color); binding.rvQuickSharePermissions.setLayoutManager(linearLayoutManager); binding.rvQuickSharePermissions.setAdapter(adapter); + adapter.applyBrand(color); } - private void handlePermissionChanged(List quickPermissionModelList, int position) { - if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(activity.getResources().getString(R.string.link_share_allow_upload_and_editing)) - || quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(activity.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(activity.getResources().getString(R.string - .link_share_view_only))) { - actions.onQuickPermissionChanged(ocShare, - READ_PERMISSION_FLAG); - - } else if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(activity.getResources().getString(R.string - .link_share_file_drop))) { - actions.onQuickPermissionChanged(ocShare, - CREATE_PERMISSION_FLAG); - } + /** + * Handle permission changed on click of selected permission + */ + private void handlePermissionChanged(List quickPermissionList, int position) { + final var type = quickPermissionList.get(position).getType(); + int permissionFlag = type.getPermissionFlag(); + 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 getQuickPermissionList() { - - String[] permissionArray; - if (ocShare.isFolder()) { - permissionArray = - activity.getResources().getStringArray(R.array.quick_sharing_permission_bottom_sheet_dialog_folder_share_values); - } else { - permissionArray = - activity.getResources().getStringArray(R.array.quick_sharing_permission_bottom_sheet_dialog_note_share_values); - } - //get the checked item position - int checkedItem = SharingMenuHelper.getPermissionCheckedItem(activity, ocShare, permissionArray); - - - final List 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 getQuickPermissionList() { + final var selectedType = SharePermissionManager.INSTANCE.getSelectedType(ocShare); + return selectedType.getAvailablePermissions(); } - @Override protected void onStop() { super.onStop(); diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/helper/SharePermissionManager.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/helper/SharePermissionManager.kt new file mode 100644 index 000000000..56ef2bae5 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/helper/SharePermissionManager.kt @@ -0,0 +1,54 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2015-2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.share.helper + +import com.owncloud.android.lib.resources.shares.OCShare +import it.niedermann.owncloud.notes.share.model.QuickPermissionType + +/** + * Utility object responsible for evaluating and mapping share permissions. + * + * Provides helper methods to: + * - Check specific permission flags + * - Determine edit/view-only capability + * - Map raw permission flags to [QuickPermissionType] + */ +object SharePermissionManager { + + fun hasPermission(permission: Int, permissionFlag: Int): Boolean = + permission != OCShare.NO_PERMISSION && (permission and permissionFlag) == permissionFlag + + fun canEdit(share: OCShare?): Boolean { + if (share == null) { + return false + } + + return hasPermission(share.permissions, getMaximumPermission()) + } + + fun isViewOnly(share: OCShare?): Boolean { + if (share == null) { + return false + } + + return share.permissions != OCShare.NO_PERMISSION && + ( + share.permissions == OCShare.READ_PERMISSION_FLAG || + share.permissions == OCShare.READ_PERMISSION_FLAG + OCShare.SHARE_PERMISSION_FLAG + ) + } + + fun getSelectedType(share: OCShare?): QuickPermissionType = if (canEdit(share)) { + QuickPermissionType.CAN_EDIT + } else if (isViewOnly(share)) { + QuickPermissionType.VIEW_ONLY + } else { + QuickPermissionType.NONE + } + + fun getMaximumPermission(): Int = OCShare.MAXIMUM_PERMISSIONS_FOR_FILE +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/helper/SharingMenuHelper.java b/app/src/main/java/it/niedermann/owncloud/notes/share/helper/SharingMenuHelper.java deleted file mode 100644 index 5eb0bae70..000000000 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/helper/SharingMenuHelper.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Nextcloud Notes - Android Client - * - * SPDX-FileCopyrightText: 2015-2025 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package it.niedermann.owncloud.notes.share.helper; - -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; - -import android.content.Context; - -import com.owncloud.android.lib.resources.shares.OCShare; - -import it.niedermann.owncloud.notes.R; - -/** - * Helper calls for visibility logic of the sharing menu. - */ -public final class SharingMenuHelper { - - private SharingMenuHelper() { - // utility class -> private constructor - } - - 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; - } -} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareRequest.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareRequest.kt index 1f3c9843d..12a378df6 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareRequest.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/CreateShareRequest.kt @@ -28,5 +28,8 @@ data class CreateShareRequest( val permissions: Int?, @Expose - val note: String? + val note: String?, + + @Expose + val attributes: String? ) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/QuickPermissionModel.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/QuickPermission.kt similarity index 58% rename from app/src/main/java/it/niedermann/owncloud/notes/share/model/QuickPermissionModel.kt rename to app/src/main/java/it/niedermann/owncloud/notes/share/model/QuickPermission.kt index 6a7bec0e8..312762ff1 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/model/QuickPermissionModel.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/QuickPermission.kt @@ -6,4 +6,7 @@ */ package it.niedermann.owncloud.notes.share.model -data class QuickPermissionModel(val permissionName: String, val isSelected: Boolean) +/** + * Represents a selectable quick permission option in the UI. + */ +data class QuickPermission(val type: QuickPermissionType, var isSelected: Boolean) diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/QuickPermissionType.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/QuickPermissionType.kt new file mode 100644 index 000000000..a25e31649 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/QuickPermissionType.kt @@ -0,0 +1,46 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2015-2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.share.model + +import android.content.Context +import com.owncloud.android.lib.resources.shares.OCShare +import it.niedermann.owncloud.notes.R + +/** + * Represents predefined quick permission types for sharing a note. + * + * Each type defines: + * - An icon resource ([iconId]) + * - A string resource ([textId]) + * - A corresponding permission flag used by [OCShare] + * + * These types are typically used to simplify permission selection in the UI. + */ +enum class QuickPermissionType(val iconId: Int, val textId: Int) { + NONE(R.drawable.ic_unknown, R.string.share_permission_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); + + fun getText(context: Context): String = context.getString(textId) + + fun getPermissionFlag(): Int = when (this) { + NONE -> OCShare.NO_PERMISSION + VIEW_ONLY -> OCShare.READ_PERMISSION_FLAG + CAN_EDIT -> OCShare.MAXIMUM_PERMISSIONS_FOR_FILE + } + + fun getAvailablePermissions(): List { + val permissions = listOf(VIEW_ONLY, CAN_EDIT) + + return permissions.map { type -> + QuickPermission( + type = type, + isSelected = (type == this) + ) + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateShareRequest.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateShareRequest.kt index 67c88f174..ec9f8c10f 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateShareRequest.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/model/UpdateShareRequest.kt @@ -6,45 +6,93 @@ */ package it.niedermann.owncloud.notes.share.model +import com.google.gson.Gson import com.google.gson.annotations.Expose +import com.google.gson.annotations.SerializedName +import com.google.gson.reflect.TypeToken +import com.owncloud.android.lib.resources.shares.ShareType +import it.niedermann.owncloud.notes.shared.model.Capabilities data class UpdateShareRequest( @Expose - val share_id: Int, + @SerializedName("permissions") + val permissions: Int? = null, @Expose - val permissions: Int?, + @SerializedName("password") + val password: String? = null, @Expose - val password: String, + @SerializedName("note") + val note: String? = null, @Expose - val publicUpload: String, + @SerializedName("label") + val label: String? = null, @Expose - val expireDate: String?, + @SerializedName("expireDate") + val expireDate: String? = null, @Expose - val note: String, + @SerializedName("hideDownload") + val hideDownload: String? = null, - /** - * Array of ShareAttributes data class in JSON format - */ @Expose - val attributes: String?, + @SerializedName("attributes") + val attributes: String? = null +) { + companion object { + fun createAttributes(capabilities: Capabilities, allowDownloadAndSync: Boolean, type: ShareType?): String { + if (type == null || (type != ShareType.INTERNAL && type != ShareType.USER)) { + return "[]" + } - @Expose - val sendMail: String -) + val shouldUseShareAttributesV2 = (capabilities.nextcloudMajorVersion?.toInt() ?: 0) >= 30 + + val shareAttributes = arrayOf( + if (shouldUseShareAttributesV2) { + ShareAttributesV2( + scope = "permissions", + key = "download", + value = allowDownloadAndSync + ) + } else { + ShareAttributesV1( + scope = "permissions", + key = "download", + enabled = allowDownloadAndSync + ) + } + ) + + return Gson().toJson(shareAttributes) + } + } +} data class ShareAttributesV2( var scope: String, var key: String, var value: Boolean -) +) { + companion object { + fun getAttributes(json: String): List { + val type = object : TypeToken>() {}.type + return Gson().fromJson(json, type) + } + } +} data class ShareAttributesV1( var scope: String, var key: String, var enabled: Boolean -) +) { + companion object { + fun getAttributes(json: String): List { + val typeV1 = object : TypeToken>() {}.type + return Gson().fromJson(json, typeV1) + } + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/share/repository/ShareRepository.kt b/app/src/main/java/it/niedermann/owncloud/notes/share/repository/ShareRepository.kt index 8b7c7e478..442fd8b5e 100644 --- a/app/src/main/java/it/niedermann/owncloud/notes/share/repository/ShareRepository.kt +++ b/app/src/main/java/it/niedermann/owncloud/notes/share/repository/ShareRepository.kt @@ -23,7 +23,6 @@ import it.niedermann.owncloud.notes.share.model.CreateShareRequest import it.niedermann.owncloud.notes.share.model.CreateShareResponse import it.niedermann.owncloud.notes.share.model.ShareAttributesV1 import it.niedermann.owncloud.notes.share.model.ShareAttributesV2 -import it.niedermann.owncloud.notes.share.model.SharePasswordRequest import it.niedermann.owncloud.notes.share.model.UpdateSharePermissionRequest import it.niedermann.owncloud.notes.share.model.UpdateShareRequest import it.niedermann.owncloud.notes.share.model.toOCShareList @@ -34,9 +33,7 @@ import it.niedermann.owncloud.notes.shared.model.OcsResponse import it.niedermann.owncloud.notes.shared.util.StringConstants import it.niedermann.owncloud.notes.shared.util.extensions.getErrorMessage import it.niedermann.owncloud.notes.shared.util.extensions.toExpirationDateLong -import it.niedermann.owncloud.notes.shared.util.extensions.toExpirationDateString import org.json.JSONObject -import java.util.Date class ShareRepository(private val applicationContext: Context, private val account: SingleSignOnAccount) { @@ -68,29 +65,16 @@ class ShareRepository(private val applicationContext: Context, private val accou } } - fun getShareEntitiesForSpecificNote(note: Note): List { - val path = getNotePath(note) - return notesRepository.getShareEntities(path) - } - - private fun getExpirationDate(chosenExpDateInMills: Long): String? { - if (chosenExpDateInMills == -1L) { - return null - } - - return Date(chosenExpDateInMills).toExpirationDateString() - } - fun getCapabilities(): Capabilities = notesRepository.capabilities // region API calls - fun getSharesForNotesAndSaveShareEntities() { + fun fetchSharesForNotesAndSaveShareEntities() { val notesPathResponseResult = getNotesPathResponseResult() ?: return val notesPath = notesPathResponseResult.notesPath val remotePath = "/$notesPath" val shareAPI = apiProvider.getShareAPI(applicationContext, account) - val call = shareAPI.getSharesForSpecificNote(remotePath) + val call = shareAPI.fetchSharesForSpecificNote(remotePath) val entities = arrayListOf() try { @@ -111,6 +95,8 @@ class ShareRepository(private val applicationContext: Context, private val accou val displayNameOwner = map?.get("displayname_owner") as? String val url = map?.get("url") as? String val expirationDateString = map?.get("expiration") as? String + val permissions = map?.get("permissions") as? Double + val attributes = map?.get("attributes") as? String id?.toInt()?.let { val entity = ShareEntity( @@ -125,7 +111,9 @@ class ShareRepository(private val applicationContext: Context, private val accou uid_owner = uidOwner, displayname_owner = displayNameOwner, url = url, - expiration_date = expirationDateString?.toExpirationDateLong() + expiration_date = expirationDateString?.toExpirationDateLong(), + permissions = permissions, + attributes = attributes ) entities.add(entity) @@ -139,6 +127,22 @@ class ShareRepository(private val applicationContext: Context, private val accou } } + fun getShareByPathAndDisplayName(share: OCShare): ShareEntity? { + return notesRepository.getShareByPathAndDisplayName(share) + } + + fun isAllowDownloadAndSync(share: OCShare): Boolean { + val entity = getShareByPathAndDisplayName(share) + val attributes = entity?.attributes ?: return false + val capabilities = notesRepository.capabilities + val shouldUseShareAttributesV2 = (capabilities.nextcloudMajorVersion?.toInt() ?: 0) >= 30 + return if (shouldUseShareAttributesV2) { + ShareAttributesV2.getAttributes(attributes).first().value + } else { + ShareAttributesV1.getAttributes(attributes).first().enabled + } + } + private fun LinkedTreeMap<*, *>.getList(key: String): ArrayList<*>? = this[key] as? ArrayList<*> /** @@ -153,7 +157,7 @@ class ShareRepository(private val applicationContext: Context, private val accou */ fun getSharees(searchString: String, page: Int, perPage: Int): ArrayList { val shareAPI = apiProvider.getShareAPI(applicationContext, account) - val call = shareAPI.getSharees( + val call = shareAPI.fetchSharees( search = searchString, page = page.toString(), perPage = perPage.toString() @@ -202,112 +206,31 @@ class ShareRepository(private val applicationContext: Context, private val accou } } - fun getUpdateShareRequest( - downloadPermission: Boolean, - share: OCShare?, - noteText: String, - password: String, - sendEmail: Boolean, - chosenExpDateInMills: Long, - permission: Int - ): UpdateShareRequest { - val capabilities = getCapabilities() - val shouldUseShareAttributesV2 = (capabilities.nextcloudMajorVersion?.toInt() ?: 0) >= 30 - - val shareAttributes = arrayOf( - if (shouldUseShareAttributesV2) { - ShareAttributesV2( - scope = "permissions", - key = "download", - value = downloadPermission - ) - } else { - ShareAttributesV1( - scope = "permissions", - key = "download", - enabled = downloadPermission - ) - } - ) - - val attributes = gson.toJson(shareAttributes) - - return UpdateShareRequest( - share_id = share!!.id.toInt(), - permissions = if (permission == -1) null else permission, - password = password, - publicUpload = "false", - expireDate = getExpirationDate(chosenExpDateInMills), - note = noteText, - attributes = attributes, - sendMail = sendEmail.toString() - ) - } - - /** - * Fetches all shares for the given file or folder identified by its remote ID. - * - * @param remoteId The remote file ID on the server for which to retrieve shares. - * @return A list of [OCShare] objects if the request is successful, or `null` if the request fails or an exception - * occurs. - */ - fun getShares(remoteId: Long): List? { - val shareAPI = apiProvider.getShareAPI(applicationContext, account) - val call = shareAPI.getShares(remoteId) - val response = call.execute() - - return try { - if (response.isSuccessful) { - val result = - response.body()?.ocs?.data ?: throw RuntimeException("No shares available") - result.toOCShareList() - } else { - Log_OC.d(tag, "Failed to getShares: ${response.errorBody()?.string()}") - null - } - } catch (e: Exception) { - Log_OC.d(tag, "Exception while getShares: $e") - null - } - } - - fun sendEmail(shareId: Long, requestBody: SharePasswordRequest): Boolean { - val shareAPI = apiProvider.getShareAPI(applicationContext, account) - val call = shareAPI.sendEmail(shareId, requestBody) - val response = call.execute() - - return try { - if (response.isSuccessful) { - true - } else { - Log_OC.d(tag, "Failed to send-email: ${response.errorBody()?.string()}") - false - } - } catch (e: Exception) { - Log_OC.d(tag, "Exception while send-email: $e") - false - } + fun fetchSharesFromNote(note: Note): List { + val sharesWithMe = fetchShares(note, sharedWithMe = true) + val sharesWithOthers = fetchShares(note, sharedWithMe = false) + return sharesWithOthers + sharesWithMe } - fun getShareFromNote(note: Note): List? { - val shareAPI = apiProvider.getShareAPI(applicationContext, account) - val path = getNotePath(note) ?: return null - val call = shareAPI.getShareFromNote(path) + private fun fetchShares(note: Note, sharedWithMe: Boolean): List { + val api = apiProvider.getShareAPI(applicationContext, account) + val path = getNotePath(note) ?: return emptyList() + val call = api.fetchSharesFromNote(path, sharedWithMe) val response = call.execute() return try { if (response.isSuccessful) { val body = response.body() Log_OC.d(tag, "Response successful: $body") - body?.ocs?.data?.toOCShareList() + body?.ocs?.data?.toOCShareList() ?: emptyList() } else { val errorBody = response.errorBody()?.string() Log_OC.d(tag, "Response failed: $errorBody") - null + emptyList() } } catch (e: Exception) { Log_OC.d(tag, "Exception while getting share from note: ", e) - null + emptyList() } } @@ -323,6 +246,9 @@ class ShareRepository(private val applicationContext: Context, private val accou updateNote(note) } + // delete previously stored shared + notesRepository.deleteShareById(share.id.toInt()) + Log_OC.d(tag, "Share removed successfully.") } else { Log_OC.d(tag, "Failed to remove share: ${response.errorBody()?.string()}") @@ -353,8 +279,9 @@ class ShareRepository(private val applicationContext: Context, private val accou message = applicationContext.getString(R.string.note_share_created) ) } else { - Log_OC.d(tag, "Failed to update share: ${response.errorBody()?.string()}") - ApiResult.Error(message = response.getErrorMessage() ?: "") + val message = response.errorBody()?.string() + Log_OC.d(tag, "Failed to update share: $message") + ApiResult.Error(message = message ?: "") } } catch (e: Exception) { Log_OC.d(tag, "Exception while updating share", e) @@ -364,6 +291,7 @@ class ShareRepository(private val applicationContext: Context, private val accou fun updateNote(note: Note) = notesRepository.updateNote(note) + @JvmOverloads fun addShare( note: Note, shareType: ShareType, @@ -371,7 +299,8 @@ class ShareRepository(private val applicationContext: Context, private val accou publicUpload: String = "false", password: String = "", permissions: Int = 0, - shareNote: String = "" + shareNote: String = "", + attributes: String = "[]" ): ApiResult?> { val defaultErrorMessage = applicationContext.getString(R.string.note_share_activity_cannot_created) @@ -389,7 +318,8 @@ class ShareRepository(private val applicationContext: Context, private val accou publicUpload = publicUpload, password = password, permissions = permissions, - note = shareNote + note = shareNote, + attributes = attributes ) val shareAPI = apiProvider.getShareAPI(applicationContext, account) @@ -405,10 +335,7 @@ class ShareRepository(private val applicationContext: Context, private val accou message = applicationContext.getString(R.string.note_share_created) ) } else { - val errorMessage = response.getErrorMessage() - if (errorMessage == null) { - return ApiResult.Error(message = defaultErrorMessage) - } + val errorMessage = response.getErrorMessage() ?: return ApiResult.Error(message = defaultErrorMessage) Log_OC.d(tag, "Response failed: $errorMessage") ApiResult.Error(message = errorMessage) } diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/DateUtil.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/DateUtil.kt new file mode 100644 index 000000000..d9ae95a34 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/DateUtil.kt @@ -0,0 +1,35 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2015-2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.util + +import it.niedermann.owncloud.notes.shared.util.extensions.toExpirationDateString +import java.util.Date + +/** + * Utility object for handling date formatting. + * + */ +object DateUtil { + + /** + * Converts a timestamp in milliseconds to a formatted expiration date string. + * + * @param chosenExpDateInMills The expiration date represented as milliseconds + * since the Unix epoch (January 1, 1970). If the value is `-1L`, it is treated + * as "no expiration date" and `null` is returned. + * + * @return A formatted expiration date string generated via + * [toExpirationDateString], or `null` if no expiration date is set. + */ + fun getExpirationDate(chosenExpDateInMills: Long): String? { + if (chosenExpDateInMills == -1L) { + return null + } + + return Date(chosenExpDateInMills).toExpirationDateString() + } +} diff --git a/app/src/main/java/it/niedermann/owncloud/notes/util/OCShareExtensions.kt b/app/src/main/java/it/niedermann/owncloud/notes/util/OCShareExtensions.kt new file mode 100644 index 000000000..f67b19624 --- /dev/null +++ b/app/src/main/java/it/niedermann/owncloud/notes/util/OCShareExtensions.kt @@ -0,0 +1,21 @@ +/* + * Nextcloud Notes - Android Client + * + * SPDX-FileCopyrightText: 2015-2026 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: GPL-3.0-or-later + */ +package it.niedermann.owncloud.notes.util + +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.lib.resources.shares.ShareType + +fun ShareType.isPublicOrMail(): Boolean = (this == ShareType.PUBLIC_LINK || this == ShareType.EMAIL) + +fun OCShare?.remainingDownloadLimit(): Int? { + val downloadLimit = this?.fileDownloadLimit ?: return null + return if (downloadLimit.limit > 0) { + downloadLimit.limit - downloadLimit.count + } else { + null + } +} diff --git a/app/src/main/res/drawable/ic_eye.xml b/app/src/main/res/drawable/ic_eye.xml new file mode 100644 index 000000000..c62b99ba8 --- /dev/null +++ b/app/src/main/res/drawable/ic_eye.xml @@ -0,0 +1,16 @@ + + + + diff --git a/app/src/main/res/drawable/ic_unknown.xml b/app/src/main/res/drawable/ic_unknown.xml new file mode 100644 index 000000000..09be873b0 --- /dev/null +++ b/app/src/main/res/drawable/ic_unknown.xml @@ -0,0 +1,17 @@ + + + + diff --git a/app/src/main/res/layout/activity_note_share_detail.xml b/app/src/main/res/layout/activity_note_share_detail.xml index a782f5b60..42be708e2 100644 --- a/app/src/main/res/layout/activity_note_share_detail.xml +++ b/app/src/main/res/layout/activity_note_share_detail.xml @@ -65,46 +65,21 @@ android:orientation="vertical"> - - - - - - + + - + android:textSize="@dimen/note_font_size_small" + android:visibility="gone" + tools:text="5 downloads remaining" + tools:visibility="visible" /> + app:drawableTint="@color/defaultBrand" + tools:text="View only" /> - + app:icon="@drawable/ic_content_copy" + app:iconSize="@dimen/iconized_single_line_item_icon_size" + app:iconTint="@color/icon_color_default" /> - + android:minHeight="@dimen/minimum_size_for_touchable_area" + app:icon="@drawable/ic_dots_vertical" + app:iconSize="@dimen/iconized_single_line_item_icon_size" + app:iconTint="@color/icon_color_default" /> diff --git a/app/src/main/res/layout/item_note_share_action.xml b/app/src/main/res/layout/item_note_share_action.xml index b222c2653..581038942 100644 --- a/app/src/main/res/layout/item_note_share_action.xml +++ b/app/src/main/res/layout/item_note_share_action.xml @@ -140,11 +140,12 @@ app:tint="@color/defaultBrand" /> diff --git a/app/src/main/res/layout/item_share_link_share.xml b/app/src/main/res/layout/item_share_link_share.xml index d5d6daa1b..6536a799b 100644 --- a/app/src/main/res/layout/item_share_link_share.xml +++ b/app/src/main/res/layout/item_share_link_share.xml @@ -1,13 +1,12 @@ - - + app:tint="@color/text_color" /> + tools:text="Share label" + tools:visibility="visible" /> + android:textSize="@dimen/note_font_size_minimum" + android:visibility="gone" + tools:text="5 downloads remaining" + tools:visibility="visible" /> + app:drawableTint="@color/defaultBrand" + tools:text="View only" /> - - - @string/link_share_view_only - @string/link_share_allow_upload_and_editing - @string/link_share_file_drop - - - @string/link_share_view_only - @string/link_share_editing - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3dfe25173..6f4bd215d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -69,8 +69,9 @@ - Advanced Settings + This password looks a bit weak. For better security, try using a longer password with a mix of letters, numbers, and symbols. Hide download + Allow download and sync Note to recipient Note Next @@ -109,7 +110,8 @@ Internal share link only works for users with access to this folder Internal share link only works for users with access to this file Share internal link - Delete Link + Delete link + Delete share Settings Send new email Sharing @@ -135,6 +137,9 @@ File drop (upload only) Could not retrieve shares View only + Unknown + + File request Can edit File drop Secure file drop