diff --git a/app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.kt b/app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.kt index 4b88be021d4d..d606e81407c1 100644 --- a/app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.kt +++ b/app/src/androidTest/java/com/nextcloud/client/FileDisplayActivityIT.kt @@ -35,6 +35,7 @@ import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.operations.CreateFolderOperation import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.adapter.OCFileListItemViewHolder +import com.owncloud.android.ui.fragment.util.SharePermissionManager import org.junit.Assert import org.junit.Rule import org.junit.Test @@ -68,7 +69,7 @@ class FileDisplayActivityIT : AbstractOnServerIT() { "admin", false, "", - OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER + SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FOLDER ).execute(client).isSuccess ) diff --git a/app/src/androidTest/java/com/nextcloud/utils/SharePermissionManagerTest.kt b/app/src/androidTest/java/com/nextcloud/utils/SharePermissionManagerTest.kt index 071adc9915c3..5e087045844e 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/SharePermissionManagerTest.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/SharePermissionManagerTest.kt @@ -85,7 +85,7 @@ class SharePermissionManagerTest { // region Helper Method Tests @Test fun testCanEditShouldReturnTrueIfAllPermissionsPresent() { - val share = createShare(OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER, isFolder = true) + val share = createShare(SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FOLDER, isFolder = true) assertTrue(SharePermissionManager.canEdit(share)) } @@ -129,7 +129,7 @@ class SharePermissionManagerTest { @Test fun testGetMaximumPermissionForFolder() { assertEquals( - OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER, + SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FOLDER, SharePermissionManager.getMaximumPermission(isFolder = true) ) } @@ -137,7 +137,7 @@ class SharePermissionManagerTest { @Test fun testGetMaximumPermissionForFile() { assertEquals( - OCShare.MAXIMUM_PERMISSIONS_FOR_FILE, + SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE, SharePermissionManager.getMaximumPermission(isFolder = false) ) } @@ -146,7 +146,7 @@ class SharePermissionManagerTest { // region GetSelectedTypeTests @Test fun testGetSelectedTypeShouldReturnCanEditWhenFullPermissionsGiven() { - val share = createShare(OCShare.MAXIMUM_PERMISSIONS_FOR_FILE) + val share = createShare(SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE) assertEquals(QuickPermissionType.CAN_EDIT, SharePermissionManager.getSelectedType(share, encrypted = false)) } diff --git a/app/src/androidTest/java/com/nmc/android/ui/FileSharingIT.kt b/app/src/androidTest/java/com/nmc/android/ui/FileSharingIT.kt new file mode 100644 index 000000000000..6761c6ff3e79 --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/ui/FileSharingIT.kt @@ -0,0 +1,395 @@ +package com.nmc.android.ui + +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.Espresso.pressBack +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.contrib.RecyclerViewActions +import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition +import androidx.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.isEnabled +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import com.nextcloud.test.RetryTestRule +import com.nextcloud.test.TestActivity +import com.nmc.android.ui.RecyclerViewAssertions.clickChildViewWithId +import com.nmc.android.ui.RecyclerViewAssertions.withRecyclerView +import com.owncloud.android.AbstractIT +import com.owncloud.android.R +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.lib.resources.shares.OCShare +import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.ui.fragment.FileDetailFragment +import com.owncloud.android.ui.fragment.util.SharePermissionManager +import org.hamcrest.Matchers.not +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Rule +import org.junit.Test + +class FileSharingIT : AbstractIT() { + @get:Rule + val activityScenarioRule = ActivityScenarioRule(TestActivity::class.java) + + @get:Rule + val retryRule = RetryTestRule() + + lateinit var file: OCFile + lateinit var folder: OCFile + + @Before + fun before() { + activityScenarioRule.scenario.onActivity { + file = OCFile("/test.md").apply { + remoteId = "00000001" + parentId = it.storageManager.getFileByEncryptedRemotePath("/").fileId + permissions = OCFile.PERMISSION_CAN_RESHARE + } + + folder = OCFile("/test").apply { + setFolder() + remoteId = "00000002" + parentId = it.storageManager.getFileByEncryptedRemotePath("/").fileId + permissions = OCFile.PERMISSION_CAN_RESHARE + } + } + } + + private fun show(file: OCFile) { + val fragment = FileDetailFragment.newInstance(file, user, 0) + + activityScenarioRule.scenario.onActivity { + it.addFragment(fragment) + } + + waitForIdleSync() + + shortSleep() + } + + @Test + fun validateUiOfFileDetailFragment() { + show(file) + + onView(withId(R.id.filename)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.filename)).check(matches(withText("test.md"))) + onView(withId(R.id.favorite)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.size)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.file_separator)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.last_modification_timestamp)).check(matches(isCompletelyDisplayed())) + } + + private fun validateCommonUI() { + onView(withId(R.id.sharing_heading_title)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.sharing_heading_title)).check(matches(withText("Send link by mail"))) + + onView(withId(R.id.searchView)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.searchView)).check(matches(isEnabled())) + onView(withId(R.id.pick_contact_email_btn)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.pick_contact_email_btn)).check(matches(isEnabled())) + + onView(withId(R.id.or_section_layout)).check(matches(isCompletelyDisplayed())) + + onView(withId(R.id.link_share_section_heading)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.link_share_section_heading)).check(matches(withText("Copy link"))) + + onView(withId(R.id.share_create_new_link)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.share_create_new_link)).check(matches(withText("Create new link"))) + + onView(withId(R.id.shared_with_divider)).check(matches(isCompletelyDisplayed())) + + onView(withId(R.id.tv_your_shares)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_your_shares)).check(matches(withText("Shared with"))) + } + + @Test + fun validateUiForEmptyShares() { + show(file) + + validateCommonUI() + + onView(withId(R.id.linkSharesList)).check(matches(not(isDisplayed()))) + + onView(withId(R.id.sharesList)).check(matches(not(isDisplayed()))) + + onView(withId(R.id.tv_empty_shares)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_empty_shares)).check(matches(withText("You have not yet shared your file/folder. Share to give others access."))) + } + + @Test + fun validateUiForFileWithShares() { + activityScenarioRule.scenario.onActivity { + OCShare(file.decryptedRemotePath).apply { + remoteId = 1 + shareType = ShareType.USER + sharedWithDisplayName = "Admin" + permissions = OCShare.READ_PERMISSION_FLAG + userId = getUserId(user) + it.storageManager.saveShare(this) + } + + OCShare(file.decryptedRemotePath).apply { + remoteId = 3 + shareType = ShareType.EMAIL + permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE + sharedWithDisplayName = "johndoe@gmail.com" + userId = getUserId(user) + it.storageManager.saveShare(this) + } + + OCShare(file.decryptedRemotePath).apply { + remoteId = 4 + shareType = ShareType.PUBLIC_LINK + permissions = OCShare.READ_PERMISSION_FLAG + label = "Customer" + it.storageManager.saveShare(this) + } + + OCShare(file.decryptedRemotePath).apply { + remoteId = 5 + shareType = ShareType.PUBLIC_LINK + permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE + label = "Colleagues" + it.storageManager.saveShare(this) + } + + } + show(file) + + validateCommonUI() + + onView(withId(R.id.linkSharesList)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.sharesList)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_empty_shares)).check(matches(not(isDisplayed()))) + } + + @Test + fun validateUiWithResharingNotAllowed() { + file = file.apply { + permissions = "" + ownerDisplayName = "John Doe" + ownerId = "JohnDoe" + note = "Shared for testing purpose." + } + show(file) + + onView(withId(R.id.tv_resharing_info)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_resharing_info)).check(matches(withText("This file / folder was shared with you by John Doe"))) + + onView(withId(R.id.tv_resharing_status)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_resharing_status)).check(matches(withText("Resharing is not allowed."))) + + onView(withId(R.id.searchView)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.searchView)).check(matches(not(isEnabled()))) + onView(withId(R.id.pick_contact_email_btn)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.pick_contact_email_btn)).check(matches(not(isEnabled()))) + + onView(withId(R.id.or_section_layout)).check(matches(not(isDisplayed()))) + onView(withId(R.id.link_share_section_heading)).check(matches(not(isDisplayed()))) + onView(withId(R.id.linkSharesList)).check(matches(not(isDisplayed()))) + onView(withId(R.id.share_create_new_link)).check(matches(not(isDisplayed()))) + onView(withId(R.id.shared_with_divider)).check(matches(not(isDisplayed()))) + onView(withId(R.id.tv_your_shares)).check(matches(not(isDisplayed()))) + onView(withId(R.id.sharesList)).check(matches(not(isDisplayed()))) + onView(withId(R.id.tv_empty_shares)).check(matches(not(isDisplayed()))) + } + + @Test + fun validateUiWithResharingAllowed() { + file = file.apply { + ownerDisplayName = "John Doe" + ownerId = "JohnDoe" + } + show(file) + + validateCommonUI() + + onView(withId(R.id.tv_resharing_info)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_resharing_info)).check(matches(withText("This file / folder was shared with you by John Doe"))) + + onView(withId(R.id.tv_resharing_status)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_resharing_status)).check(matches(withText("Resharing is allowed."))) + + onView(withId(R.id.linkSharesList)).check(matches(not(isDisplayed()))) + onView(withId(R.id.sharesList)).check(matches(not(isDisplayed()))) + onView(withId(R.id.tv_empty_shares)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_empty_shares)).check(matches(withText("You have not yet shared your file/folder. Share to give others access."))) + } + + @Test + fun validateQuickPermissionDialogForFiles() { + val sharesList: MutableList = mutableListOf() + + activityScenarioRule.scenario.onActivity { it -> + OCShare(file.decryptedRemotePath).apply { + remoteId = 1 + shareType = ShareType.USER + sharedWithDisplayName = "Admin" + permissions = OCShare.READ_PERMISSION_FLAG + userId = getUserId(user) + isFolder = false + it.storageManager.saveShare(this) + } + + OCShare(file.decryptedRemotePath).apply { + remoteId = 3 + shareType = ShareType.EMAIL + sharedWithDisplayName = "johndoe@gmail.com" + permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE + userId = getUserId(user) + isFolder = false + it.storageManager.saveShare(this) + } + + OCShare(file.decryptedRemotePath).apply { + remoteId = 4 + shareType = ShareType.PUBLIC_LINK + permissions = OCShare.READ_PERMISSION_FLAG + userId = getUserId(user) + label = "Customer" + isFolder = false + it.storageManager.saveShare(this) + } + + //get other shares + sharesList.addAll(it.storageManager.getSharesWithForAFile(file.remotePath, user.accountName)) + + //get public link shares + sharesList.addAll(it.storageManager.getSharesByPathAndType(file.remotePath, ShareType.PUBLIC_LINK, "")) + + sharesList.sortByDescending { share -> share.shareType } + } + + + assertEquals(3, sharesList.size) + + show(file) + + for (i in sharesList.indices) { + val share = sharesList[i] + //since for public link the quick permission button is disabled + if (share.shareType == ShareType.PUBLIC_LINK) { + continue + } + showQuickPermissionDialogAndValidate(i, file.isFolder, share) + pressBack() + } + } + + @Test + fun validateQuickPermissionDialogForFolder() { + val sharesList: MutableList = mutableListOf() + + activityScenarioRule.scenario.onActivity { it -> + OCShare(folder.decryptedRemotePath).apply { + remoteId = 1 + shareType = ShareType.USER + sharedWithDisplayName = "Admin" + permissions = OCShare.CREATE_PERMISSION_FLAG + userId = getUserId(user) + isFolder = true + it.storageManager.saveShare(this) + } + + OCShare(folder.decryptedRemotePath).apply { + remoteId = 3 + shareType = ShareType.EMAIL + sharedWithDisplayName = "johndoe@gmail.com" + permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FOLDER + userId = getUserId(user) + isFolder = true + it.storageManager.saveShare(this) + } + + OCShare(folder.decryptedRemotePath).apply { + remoteId = 4 + shareType = ShareType.PUBLIC_LINK + permissions = OCShare.READ_PERMISSION_FLAG + userId = getUserId(user) + label = "Customer" + isFolder = true + it.storageManager.saveShare(this) + } + + //get other shares + sharesList.addAll(it.storageManager.getSharesWithForAFile(folder.remotePath, user.accountName)) + + //get public link shares + sharesList.addAll(it.storageManager.getSharesByPathAndType(folder.remotePath, ShareType.PUBLIC_LINK, "")) + + sharesList.sortByDescending { share -> share.shareType } + } + + + assertEquals(3, sharesList.size) + + show(folder) + + for (i in sharesList.indices) { + val share = sharesList[i] + showQuickPermissionDialogAndValidate(i, folder.isFolder, share) + pressBack() + } + } + + private fun showQuickPermissionDialogAndValidate(index: Int, isFolder: Boolean, ocShare: OCShare) { + onView(withId(R.id.sharesList)).perform( + actionOnItemAtPosition( + index, + clickChildViewWithId(if (ocShare.shareType == ShareType.USER) R.id.share_name_layout else R.id.share_by_link_container) + ) + ) + + val permissionList = permissionList(isFolder, ocShare.shareType!!) + + for (i in permissionList.indices) { + // Scroll to the item at position i + onView(withId(R.id.rv_quick_share_permissions)).perform( + RecyclerViewActions.scrollToPosition( + i + ) + ) + + val permissionTextView = onView( + withRecyclerView(R.id.rv_quick_share_permissions) + .atPositionOnView(i, R.id.tv_quick_share_name) + ) + permissionTextView.check(matches(withText(permissionList[i]))) + + val permissionCheckView = onView( + withRecyclerView(R.id.rv_quick_share_permissions) + .atPositionOnView(i, R.id.tv_quick_share_check_icon) + ) + if ((permissionList[i] == "Read only" && SharePermissionManager.isViewOnly(ocShare)) + || (permissionList[i] == "Can edit" && SharePermissionManager.canEdit(ocShare)) + || (permissionList[i] == "Filedrop only" && SharePermissionManager.isFileRequest(ocShare)) + ) { + permissionCheckView.check(matches(isDisplayed())) + } + } + } + + @After + override fun after() { + activityScenarioRule.scenario.onActivity { + it.storageManager.cleanShares() + it.finish() + } + super.after() + } + + companion object { + private val filePermissionList = listOf("Read only", "Can edit") + private val folderExternalAndLinkSharePermissionList = listOf("Read only", "Can edit", "Filedrop only") + private val folderOtherSharePermissionList = listOf("Read only", "Can edit") + + fun permissionList(isFolder: Boolean, shareType: ShareType): List = + if (isFolder) { + if (shareType == ShareType.PUBLIC_LINK || shareType == ShareType.EMAIL) folderExternalAndLinkSharePermissionList + else folderOtherSharePermissionList + } else filePermissionList + } +} diff --git a/app/src/androidTest/java/com/nmc/android/ui/RecyclerViewAssertions.kt b/app/src/androidTest/java/com/nmc/android/ui/RecyclerViewAssertions.kt new file mode 100644 index 000000000000..040b165f9e82 --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/ui/RecyclerViewAssertions.kt @@ -0,0 +1,81 @@ +package com.nmc.android.ui + +import android.content.res.Resources +import android.view.View +import androidx.recyclerview.widget.RecyclerView +import androidx.test.espresso.UiController +import androidx.test.espresso.ViewAction +import org.hamcrest.Description +import org.hamcrest.Matcher +import org.hamcrest.TypeSafeMatcher + +object RecyclerViewAssertions { + + fun clickChildViewWithId(id: Int): ViewAction { + return object : ViewAction { + override fun getConstraints(): Matcher? { + return null + } + + override fun getDescription(): String { + return "Click on a child view with specified id." + } + + override fun perform(uiController: UiController?, view: View) { + val v: View = view.findViewById(id) + v.performClick() + } + } + } + + fun withRecyclerView(recyclerViewId: Int): RecyclerViewMatcher { + return RecyclerViewMatcher(recyclerViewId) + } + + class RecyclerViewMatcher(private val recyclerViewId: Int) { + fun atPosition(position: Int): Matcher { + return atPositionOnView(position, -1) + } + + fun atPositionOnView(position: Int, targetViewId: Int): Matcher { + return object : TypeSafeMatcher() { + var resources: Resources? = null + var childView: View? = null + + override fun describeTo(description: Description?) { + var idDescription = recyclerViewId.toString() + resources?.let { + idDescription = try { + resources!!.getResourceName(recyclerViewId) + } catch (exception: Resources.NotFoundException) { + "$recyclerViewId (resource name not found)" + } + } + + description?.appendText("with id: $idDescription") + } + + override fun matchesSafely(view: View?): Boolean { + resources = view?.resources + + if (childView == null) { + val recyclerView = view?.rootView?.findViewById(recyclerViewId) + + if (recyclerView != null && recyclerView.id == recyclerViewId) { + childView = recyclerView.findViewHolderForAdapterPosition(position)?.itemView + } else { + return false + } + } + + return if (targetViewId == -1) { + view == childView + } else { + val targetView = childView?.findViewById(targetViewId) + view == targetView + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/owncloud/android/operations/GetSharesForFileOperationIT.kt b/app/src/androidTest/java/com/owncloud/android/operations/GetSharesForFileOperationIT.kt index 1d268ce743db..b6cacba07c37 100644 --- a/app/src/androidTest/java/com/owncloud/android/operations/GetSharesForFileOperationIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/operations/GetSharesForFileOperationIT.kt @@ -12,6 +12,7 @@ import com.owncloud.android.lib.resources.files.CreateFolderRemoteOperation import com.owncloud.android.lib.resources.shares.CreateShareRemoteOperation import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.ui.fragment.util.SharePermissionManager import junit.framework.TestCase import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue @@ -32,7 +33,7 @@ class GetSharesForFileOperationIT : AbstractOnServerIT() { "admin", false, "", - OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER + SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FOLDER ) .execute(client).isSuccess ) diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt index c3a0db94d87c..d2a89f0c90c3 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt @@ -34,8 +34,6 @@ import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.OCShare.Companion.CREATE_PERMISSION_FLAG import com.owncloud.android.lib.resources.shares.OCShare.Companion.DELETE_PERMISSION_FLAG -import com.owncloud.android.lib.resources.shares.OCShare.Companion.MAXIMUM_PERMISSIONS_FOR_FILE -import com.owncloud.android.lib.resources.shares.OCShare.Companion.MAXIMUM_PERMISSIONS_FOR_FOLDER import com.owncloud.android.lib.resources.shares.OCShare.Companion.NO_PERMISSION import com.owncloud.android.lib.resources.shares.OCShare.Companion.READ_PERMISSION_FLAG import com.owncloud.android.lib.resources.shares.OCShare.Companion.SHARE_PERMISSION_FLAG @@ -180,7 +178,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { remoteId = 1 shareType = ShareType.USER sharedWithDisplayName = "Admin" - permissions = MAXIMUM_PERMISSIONS_FOR_FILE + permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE userId = getUserId(user) activity.storageManager.saveShare(this) } @@ -189,7 +187,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { remoteId = 2 shareType = ShareType.GROUP sharedWithDisplayName = "Group" - permissions = MAXIMUM_PERMISSIONS_FOR_FILE + permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE userId = getUserId(user) activity.storageManager.saveShare(this) } @@ -338,31 +336,33 @@ class FileDetailSharingFragmentIT : AbstractIT() { // validate view shown on screen onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isDisplayed())) onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.file_drop_radio_button)).check(matches(isDisplayed())) onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isDisplayed())) onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isDisplayed())) onView(ViewMatchers.withId(R.id.share_process_change_name_switch)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.share_process_allow_resharing_checkbox)).check(matches(not(isDisplayed()))) + onView(ViewMatchers.withId(R.id.share_process_download_limit_switch)).check(matches(not(isDisplayed()))) // read-only onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isChecked())) onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.file_drop_radio_button)).check(matches(isNotChecked())) goBack() // upload and editing - publicShare.permissions = MAXIMUM_PERMISSIONS_FOR_FOLDER + publicShare.permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FOLDER openAdvancedPermissions(sut, publicShare) onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isNotChecked())) onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isChecked())) - onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.file_drop_radio_button)).check(matches(isNotChecked())) goBack() - // file request + // file drop publicShare.permissions = 4 openAdvancedPermissions(sut, publicShare) onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isNotChecked())) onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isChecked())) + onView(ViewMatchers.withId(R.id.file_drop_radio_button)).check(matches(isChecked())) goBack() // password protection @@ -378,7 +378,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { // hide download publicShare.isHideFileDownload = true - publicShare.permissions = MAXIMUM_PERMISSIONS_FOR_FOLDER + publicShare.permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FOLDER openAdvancedPermissions(sut, publicShare) onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isChecked())) goBack() @@ -485,12 +485,12 @@ class FileDetailSharingFragmentIT : AbstractIT() { // validate view shown on screen onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isDisplayed())) onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isDisplayed())) - onView( - ViewMatchers.withId(R.id.file_request_radio_button) - ).check(matches(not(isDisplayed()))) + onView(ViewMatchers.withId(R.id.file_drop_radio_button)).check(matches(not(isDisplayed()))) onView(ViewMatchers.withId(R.id.share_process_hide_download_checkbox)).check(matches(isDisplayed())) onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(isDisplayed())) onView(ViewMatchers.withId(R.id.share_process_change_name_switch)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.share_process_allow_resharing_checkbox)).check(matches(not(isDisplayed()))) + onView(ViewMatchers.withId(R.id.share_process_download_limit_switch)).check(matches(isDisplayed())) // read-only publicShare.permissions = 17 // from server @@ -499,7 +499,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { goBack() // editing - publicShare.permissions = MAXIMUM_PERMISSIONS_FOR_FILE // from server + publicShare.permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE // from server openAdvancedPermissions(sut, publicShare) onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isNotChecked())) onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isChecked())) @@ -618,7 +618,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isDisplayed())) onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isDisplayed())) onView( - ViewMatchers.withId(R.id.file_request_radio_button) + ViewMatchers.withId(R.id.file_drop_radio_button) ).check(matches(not(isDisplayed()))) onView( ViewMatchers.withId(R.id.share_process_hide_download_checkbox) @@ -629,6 +629,8 @@ class FileDetailSharingFragmentIT : AbstractIT() { onView( ViewMatchers.withId(R.id.share_process_change_name_switch) ).check(matches(not(isDisplayed()))) + onView(ViewMatchers.withId(R.id.share_process_allow_resharing_checkbox)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.share_process_download_limit_switch)).check(matches(not(isDisplayed()))) // read-only userShare.permissions = 17 // from server @@ -637,7 +639,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { goBack() // editing - userShare.permissions = MAXIMUM_PERMISSIONS_FOR_FILE // from server + userShare.permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE // from server openAdvancedPermissions(sut, userShare) onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isNotChecked())) onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isChecked())) @@ -750,7 +752,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { // validate view shown on screen onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isDisplayed())) onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isDisplayed())) - onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isDisplayed())) + onView(ViewMatchers.withId(R.id.file_drop_radio_button)).check(matches(isDisplayed())) onView( ViewMatchers.withId(R.id.share_process_hide_download_checkbox) ).check(matches(not(isDisplayed()))) @@ -760,20 +762,21 @@ class FileDetailSharingFragmentIT : AbstractIT() { onView( ViewMatchers.withId(R.id.share_process_change_name_switch) ).check(matches(not(isDisplayed()))) + onView(ViewMatchers.withId(R.id.share_process_download_limit_switch)).check(matches(not(isDisplayed()))) // read-only userShare.permissions = 17 // from server onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isChecked())) onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.file_drop_radio_button)).check(matches(isNotChecked())) goBack() // allow upload & editing - userShare.permissions = MAXIMUM_PERMISSIONS_FOR_FOLDER // from server + userShare.permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FOLDER // from server openAdvancedPermissions(sut, userShare) onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isNotChecked())) onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isChecked())) - onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isNotChecked())) + onView(ViewMatchers.withId(R.id.file_drop_radio_button)).check(matches(isNotChecked())) goBack() // file request @@ -781,7 +784,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { openAdvancedPermissions(sut, userShare) onView(ViewMatchers.withId(R.id.view_only_radio_button)).check(matches(isNotChecked())) onView(ViewMatchers.withId(R.id.can_edit_radio_button)).check(matches(isNotChecked())) - onView(ViewMatchers.withId(R.id.file_request_radio_button)).check(matches(isChecked())) + onView(ViewMatchers.withId(R.id.file_drop_radio_button)).check(matches(isChecked())) goBack() // set expiration date @@ -887,7 +890,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { @Test fun testUploadAndEditingSharePermissions() { val testCases = mapOf( - MAXIMUM_PERMISSIONS_FOR_FOLDER to true, + SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FOLDER to true, NO_PERMISSION to false, READ_PERMISSION_FLAG to false, CREATE_PERMISSION_FLAG to false, @@ -910,8 +913,8 @@ class FileDetailSharingFragmentIT : AbstractIT() { CREATE_PERMISSION_FLAG to false, DELETE_PERMISSION_FLAG to false, SHARE_PERMISSION_FLAG to false, - MAXIMUM_PERMISSIONS_FOR_FOLDER to false, - MAXIMUM_PERMISSIONS_FOR_FILE to false + SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FOLDER to false, + SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE to false ) val share = OCShare() @@ -929,8 +932,8 @@ class FileDetailSharingFragmentIT : AbstractIT() { READ_PERMISSION_FLAG to false, DELETE_PERMISSION_FLAG to false, SHARE_PERMISSION_FLAG to false, - MAXIMUM_PERMISSIONS_FOR_FOLDER to false, - MAXIMUM_PERMISSIONS_FOR_FILE to false + SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FOLDER to false, + SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE to false ) val share = OCShare().apply { diff --git a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt index 8c2996f75f6c..072753e39e1d 100644 --- a/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt +++ b/app/src/androidTest/java/com/owncloud/android/ui/fragment/SharedListFragmentIT.kt @@ -15,6 +15,7 @@ import com.owncloud.android.AbstractIT import com.owncloud.android.datamodel.OCFile import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.ShareType +import com.owncloud.android.ui.fragment.util.SharePermissionManager import com.owncloud.android.utils.ScreenshotTest import org.junit.Before import org.junit.Rule @@ -94,7 +95,7 @@ internal class SharedListFragmentIT : AbstractIT() { remoteId = 1 shareType = ShareType.USER sharedWithDisplayName = "Admin" - permissions = OCShare.MAXIMUM_PERMISSIONS_FOR_FILE + permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE userId = getUserId(user) sharedDate = 1188206955 mimetype = "image/png" @@ -105,7 +106,7 @@ internal class SharedListFragmentIT : AbstractIT() { remoteId = 2 shareType = ShareType.GROUP sharedWithDisplayName = "Group" - permissions = OCShare.MAXIMUM_PERMISSIONS_FOR_FILE + permissions = SharePermissionManager.CAN_EDIT_PERMISSIONS_FOR_FILE userId = getUserId(user) sharedDate = 1188206955 mimetype = "image/png" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 73493014a302..991097d59bb2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -598,7 +598,8 @@ android:label="@string/share_dialog_title" android:launchMode="singleTop" android:theme="@style/Theme.ownCloud.Dialog.NoTitle" - android:windowSoftInputMode="adjustResize"> + android:configChanges="orientation|screenSize" + android:windowSoftInputMode="adjustPan"> diff --git a/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt b/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt new file mode 100644 index 000000000000..a3b8a1149948 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/CheckableThemeUtils.kt @@ -0,0 +1,117 @@ +package com.nmc.android.utils + +import android.content.res.ColorStateList +import androidx.appcompat.widget.AppCompatCheckBox +import androidx.appcompat.widget.SwitchCompat +import androidx.core.content.res.ResourcesCompat +import com.owncloud.android.R + +object CheckableThemeUtils { + @JvmStatic + fun tintCheckbox(vararg checkBoxes: AppCompatCheckBox) { + for (checkBox in checkBoxes) { + val checkEnabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_checked_enabled, + checkBox.context.theme + ) + val checkDisabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_checked_disabled, + checkBox.context.theme + ) + val uncheckEnabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_unchecked_enabled, + checkBox.context.theme + ) + val uncheckDisabled = ResourcesCompat.getColor( + checkBox.context.resources, + R.color.checkbox_unchecked_disabled, + checkBox.context.theme + ) + + val states = arrayOf( + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_enabled, android.R.attr.state_checked), + intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_enabled, -android.R.attr.state_checked) + ) + val colors = intArrayOf( + checkEnabled, + checkDisabled, + uncheckEnabled, + uncheckDisabled + ) + checkBox.buttonTintList = ColorStateList(states, colors) + } + } + + @JvmStatic + @JvmOverloads + fun tintSwitch(switchView: SwitchCompat, color: Int = 0, colorText: Boolean = false) { + if (colorText) { + switchView.setTextColor(color) + } + + val states = arrayOf( + intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked), + intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_enabled) + ) + + val thumbColorCheckedEnabled = ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_thumb_checked_enabled, + switchView.context.theme + ) + val thumbColorUncheckedEnabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_thumb_unchecked_enabled, + switchView.context.theme + ) + val thumbColorDisabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_thumb_disabled, + switchView.context.theme + ) + + val thumbColors = intArrayOf( + thumbColorCheckedEnabled, + thumbColorUncheckedEnabled, + thumbColorDisabled + ) + val thumbColorStateList = ColorStateList(states, thumbColors) + + val trackColorCheckedEnabled = ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_track_checked_enabled, + switchView.context.theme + ) + val trackColorUncheckedEnabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_track_unchecked_enabled, + switchView.context.theme + ) + val trackColorDisabled = + ResourcesCompat.getColor( + switchView.context.resources, + R.color.switch_track_disabled, + switchView.context.theme + ) + + val trackColors = intArrayOf( + trackColorCheckedEnabled, + trackColorUncheckedEnabled, + trackColorDisabled + ) + + val trackColorStateList = ColorStateList(states, trackColors) + + switchView.thumbTintList = thumbColorStateList + switchView.trackTintList = trackColorStateList + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/utils/DisplayUtils.kt b/app/src/main/java/com/nmc/android/utils/DisplayUtils.kt new file mode 100644 index 000000000000..f58e93c4252a --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/DisplayUtils.kt @@ -0,0 +1,18 @@ +package com.nmc.android.utils + +import android.content.res.Configuration +import com.owncloud.android.MainApp +import com.owncloud.android.R + +object DisplayUtils { + + @JvmStatic + fun isShowDividerForList(): Boolean = isTablet() || isLandscapeOrientation() + + @JvmStatic + fun isTablet(): Boolean = MainApp.getAppContext().resources.getBoolean(R.bool.isTablet) + + @JvmStatic + fun isLandscapeOrientation(): Boolean = + MainApp.getAppContext().resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/utils/KeyboardUtils.java b/app/src/main/java/com/nmc/android/utils/KeyboardUtils.java new file mode 100644 index 000000000000..ec43ac2dd3a8 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/KeyboardUtils.java @@ -0,0 +1,21 @@ +package com.nmc.android.utils; + +import android.app.Activity; +import android.content.Context; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +public class KeyboardUtils { + + public static void showSoftKeyboard(Context context, View view) { + view.requestFocus(); + InputMethodManager imm = (InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + } + + public static void hideKeyboardFrom(Context context, View view) { + view.clearFocus(); + InputMethodManager imm = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/utils/SearchViewThemeUtils.kt b/app/src/main/java/com/nmc/android/utils/SearchViewThemeUtils.kt new file mode 100644 index 000000000000..4bd092e97633 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/SearchViewThemeUtils.kt @@ -0,0 +1,23 @@ +package com.nmc.android.utils + +import android.content.Context +import android.widget.ImageView +import androidx.appcompat.widget.AppCompatAutoCompleteTextView +import androidx.appcompat.widget.SearchView +import com.owncloud.android.R + +object SearchViewThemeUtils { + fun themeSearchView(context: Context, searchView: SearchView) { + val fontColor = context.resources.getColor(R.color.fontAppbar, null) + val editText: AppCompatAutoCompleteTextView = searchView.findViewById(R.id.search_src_text) + editText.textSize = 16F + editText.setTextColor(fontColor) + editText.highlightColor = context.resources.getColor(R.color.et_highlight_color, null) + editText.setHintTextColor(context.resources.getColor(R.color.fontSecondaryAppbar, null)) + val closeButton: ImageView = searchView.findViewById(R.id.search_close_btn) + closeButton.setColorFilter(fontColor) + val searchButton: ImageView = searchView.findViewById(R.id.search_button) + searchButton.setImageResource(R.drawable.ic_search) + searchButton.setColorFilter(fontColor) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/nmc/android/utils/TextViewUtils.kt b/app/src/main/java/com/nmc/android/utils/TextViewUtils.kt new file mode 100644 index 000000000000..6b81001676a6 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/TextViewUtils.kt @@ -0,0 +1,47 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Your Name + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +package com.nmc.android.utils + +import android.content.res.Resources +import android.text.Layout +import android.util.DisplayMetrics +import android.view.ViewTreeObserver +import android.widget.TextView + +interface EllipsizeListener { + fun onResult(isEllipsized: Boolean) +} + +object TextViewUtils { + + @JvmStatic + fun isTextEllipsized(textView: TextView, listener: EllipsizeListener) { + // check for devices density smaller than 320dpi + // NMC-4347 fix for smaller devices + if (Resources.getSystem().displayMetrics.densityDpi <= DisplayMetrics.DENSITY_XHIGH) { + listener.onResult(true) + return + } + + textView.viewTreeObserver.addOnGlobalLayoutListener(object : ViewTreeObserver.OnGlobalLayoutListener { + override fun onGlobalLayout() { + textView.viewTreeObserver.removeOnGlobalLayoutListener(this) + + val layout: Layout? = textView.layout + val isEllipsized = layout?.let { + for (i in 0 until it.lineCount) { + if (it.getEllipsisCount(i) > 0) return@let true + } + false + } ?: false + + listener.onResult(isEllipsized) + } + }) + } +} diff --git a/app/src/main/java/com/owncloud/android/datamodel/quickPermission/QuickPermissionType.kt b/app/src/main/java/com/owncloud/android/datamodel/quickPermission/QuickPermissionType.kt index 51dfbb5bf23c..609bfeaa70e2 100644 --- a/app/src/main/java/com/owncloud/android/datamodel/quickPermission/QuickPermissionType.kt +++ b/app/src/main/java/com/owncloud/android/datamodel/quickPermission/QuickPermissionType.kt @@ -17,9 +17,9 @@ enum class QuickPermissionType( val textId: Int ) { NONE(R.drawable.ic_unknown, R.string.unknown), - VIEW_ONLY(R.drawable.ic_eye, R.string.share_permission_view_only), + VIEW_ONLY(R.drawable.ic_eye, R.string.share_permission_read_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), + FILE_REQUEST(R.drawable.ic_file_request, R.string.share_permission_file_drop), 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); @@ -32,7 +32,9 @@ enum class QuickPermissionType( hasFileRequestPermission: Boolean, selectedType: QuickPermissionType ): List { - val permissions = listOf(VIEW_ONLY, CAN_EDIT, FILE_REQUEST, CUSTOM_PERMISSIONS) + // NMC Customization: not required Custom Permissions + // File Request is File Drop + val permissions = listOf(VIEW_ONLY, CAN_EDIT, FILE_REQUEST) val result = if (hasFileRequestPermission) permissions else permissions.filter { it != FILE_REQUEST } return result.map { type -> diff --git a/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java index 68a5751938cb..0c4692534626 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java @@ -232,7 +232,9 @@ protected RemoteOperationResult run(OwnCloudClient client) { // once creating share link update other information UpdateShareInfoOperation updateShareInfoOperation = new UpdateShareInfoOperation(share, getStorageManager()); - updateShareInfoOperation.setExpirationDateInMillis(expirationDateInMillis); + if (expirationDateInMillis > 0) { + updateShareInfoOperation.setExpirationDateInMillis(expirationDateInMillis); + } updateShareInfoOperation.setHideFileDownload(hideFileDownload); updateShareInfoOperation.setNote(noteMessage); updateShareInfoOperation.setLabel(label); diff --git a/app/src/main/java/com/owncloud/android/operations/UpdateShareInfoOperation.java b/app/src/main/java/com/owncloud/android/operations/UpdateShareInfoOperation.java index 1c078c20045b..1d853aced57b 100644 --- a/app/src/main/java/com/owncloud/android/operations/UpdateShareInfoOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/UpdateShareInfoOperation.java @@ -16,10 +16,14 @@ import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.RemoteOperation; import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.common.utils.Log_OC; import com.owncloud.android.lib.resources.shares.GetShareRemoteOperation; import com.owncloud.android.lib.resources.shares.OCShare; +import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.lib.resources.shares.UpdateShareRemoteOperation; import com.owncloud.android.operations.common.SyncOperation; +import com.owncloud.android.operations.share_download_limit.DeleteShareDownloadLimitRemoteOperation; +import com.owncloud.android.operations.share_download_limit.UpdateShareDownloadLimitRemoteOperation; /** @@ -27,6 +31,7 @@ */ public class UpdateShareInfoOperation extends SyncOperation { + private static final String TAG = UpdateShareInfoOperation.class.getSimpleName(); private OCShare share; private long shareId; private long expirationDateInMillis; @@ -36,6 +41,8 @@ public class UpdateShareInfoOperation extends SyncOperation { private String password; private String label; private String attributes; + //download limit for link share + private long downloadLimit; /** * Constructor @@ -107,6 +114,9 @@ protected RemoteOperationResult run(OwnCloudClient client) { if (result.isSuccess() && shareId > 0) { OCShare ocShare = (OCShare) result.getData().get(0); ocShare.setPasswordProtected(!TextUtils.isEmpty(password)); + + executeShareDownloadLimitOperation(client, ocShare); + getStorageManager().saveShare(ocShare); } @@ -115,6 +125,44 @@ protected RemoteOperationResult run(OwnCloudClient client) { return result; } + /** + * method will be used to update or delete the download limit for the particular share + * + * @param client + * @param ocShare share object + */ + private void executeShareDownloadLimitOperation(OwnCloudClient client, OCShare ocShare) { + //if share type is of Link Share then only we have to update the download limit if configured by user + if (ocShare.getShareType() == ShareType.PUBLIC_LINK && !ocShare.isFolder()) { + + //if download limit it greater than 0 then update the limit + //else delete the download limit + if (downloadLimit > 0) { + //api will update the download limit for the particular share + UpdateShareDownloadLimitRemoteOperation updateShareDownloadLimitRemoteOperation = + new UpdateShareDownloadLimitRemoteOperation(ocShare.getToken(), downloadLimit); + + RemoteOperationResult downloadLimitOp = + updateShareDownloadLimitRemoteOperation.execute(client); + if (downloadLimitOp.isSuccess()) { + Log_OC.d(TAG, "Download limit updated for the share."); + Log_OC.d(TAG, "Download limit " + downloadLimit); + } + } else { + //api will delete the download limit for the particular share + DeleteShareDownloadLimitRemoteOperation limitRemoteOperation = + new DeleteShareDownloadLimitRemoteOperation(ocShare.getToken()); + + RemoteOperationResult deleteDownloadLimitOp = + limitRemoteOperation.execute(client); + if (deleteDownloadLimitOp.isSuccess()) { + Log_OC.d(TAG, "Download limit delete for the share."); + } + } + + } + } + public void setExpirationDateInMillis(long expirationDateInMillis) { this.expirationDateInMillis = expirationDateInMillis; } @@ -142,5 +190,9 @@ public void setPassword(String password) { public void setLabel(String label) { this.label = label; } + + public void setDownloadLimit(long downloadLimit) { + this.downloadLimit = downloadLimit; + } } diff --git a/app/src/main/java/com/owncloud/android/operations/share_download_limit/DeleteShareDownloadLimitRemoteOperation.java b/app/src/main/java/com/owncloud/android/operations/share_download_limit/DeleteShareDownloadLimitRemoteOperation.java new file mode 100644 index 000000000000..303042240c60 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/share_download_limit/DeleteShareDownloadLimitRemoteOperation.java @@ -0,0 +1,90 @@ +/** + * ownCloud Android client application + * + * @author TSI-mc Copyright (C) 2021 TSI-mc + *

+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + *

+ * You should have received a copy of the GNU General Public License along with this program. If not, see + * . + */ + +package com.owncloud.android.operations.share_download_limit; + +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.operations.RemoteOperation; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.common.utils.Log_OC; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.methods.DeleteMethod; + +/** + * class to delete the download limit for the link share + * this has to be executed when user has toggled off the download limit + *

+ * API : //DELETE to /ocs/v2.php/apps/files_downloadlimit/{share_token}/limit + */ +public class DeleteShareDownloadLimitRemoteOperation extends RemoteOperation { + + private static final String TAG = DeleteShareDownloadLimitRemoteOperation.class.getSimpleName(); + + private final String shareToken; + + public DeleteShareDownloadLimitRemoteOperation(String shareToken) { + this.shareToken = shareToken; + } + + @Override + protected RemoteOperationResult run(OwnCloudClient client) { + RemoteOperationResult result; + int status; + + DeleteMethod deleteMethod = null; + + try { + // Post Method + deleteMethod = new DeleteMethod(client.getBaseUri() + ShareDownloadLimitUtils.INSTANCE.getDownloadLimitApiPath(shareToken)); + + deleteMethod.addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE); + + status = client.executeMethod(deleteMethod); + + if (isSuccess(status)) { + String response = deleteMethod.getResponseBodyAsString(); + + Log_OC.d(TAG, "Delete Download Limit response: " + response); + + DownloadLimitXMLParser parser = new DownloadLimitXMLParser(); + result = parser.parse(true, response); + + if (result.isSuccess()) { + return result; + } + + } else { + result = new RemoteOperationResult<>(false, deleteMethod); + } + + } catch (Exception e) { + result = new RemoteOperationResult<>(e); + Log_OC.e(TAG, "Exception while deleting share download limit", e); + + } finally { + if (deleteMethod != null) { + deleteMethod.releaseConnection(); + } + } + return result; + } + + private boolean isSuccess(int status) { + return status == HttpStatus.SC_OK || status == HttpStatus.SC_BAD_REQUEST; + } + +} diff --git a/app/src/main/java/com/owncloud/android/operations/share_download_limit/DownloadLimitResponse.java b/app/src/main/java/com/owncloud/android/operations/share_download_limit/DownloadLimitResponse.java new file mode 100644 index 000000000000..f54edb17fc60 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/share_download_limit/DownloadLimitResponse.java @@ -0,0 +1,37 @@ +package com.owncloud.android.operations.share_download_limit; + +/** + * response from the Get download limit api + * + * + * + * ok + * 200 + * OK + * + * + * 5 + * 0 + * + * + */ +public class DownloadLimitResponse { + private long limit; + private long count; + + public long getLimit() { + return limit; + } + + public void setLimit(long limit) { + this.limit = limit; + } + + public long getCount() { + return count; + } + + public void setCount(long count) { + this.count = count; + } +} diff --git a/app/src/main/java/com/owncloud/android/operations/share_download_limit/DownloadLimitXMLParser.java b/app/src/main/java/com/owncloud/android/operations/share_download_limit/DownloadLimitXMLParser.java new file mode 100644 index 000000000000..07121f52e110 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/share_download_limit/DownloadLimitXMLParser.java @@ -0,0 +1,323 @@ +package com.owncloud.android.operations.share_download_limit; + +import android.util.Xml; + +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.common.utils.Log_OC; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +/** + * class to parse the Download Limit api XML response This class code referenced from java in NC library + */ +public class DownloadLimitXMLParser { + private static final String TAG = DownloadLimitXMLParser.class.getSimpleName(); + + // No namespaces + private static final String ns = null; + + // NODES for XML Parser + private static final String NODE_OCS = "ocs"; + + private static final String NODE_META = "meta"; + private static final String NODE_STATUS = "status"; + private static final String NODE_STATUS_CODE = "statuscode"; + private static final String NODE_MESSAGE = "message"; + + private static final String NODE_DATA = "data"; + private static final String NODE_LIMIT = "limit"; + private static final String NODE_COUNT = "count"; + + private static final int SUCCESS = 100; + private static final int OK = 200; + private static final int ERROR_WRONG_PARAMETER = 400; + private static final int ERROR_FORBIDDEN = 403; + private static final int ERROR_NOT_FOUND = 404; + + private String mStatus; + private int mStatusCode; + private String mMessage = ""; + + // Getters and Setters + public String getStatus() { + return mStatus; + } + + public void setStatus(String status) { + this.mStatus = status; + } + + public int getStatusCode() { + return mStatusCode; + } + + public void setStatusCode(int statusCode) { + this.mStatusCode = statusCode; + } + + public String getMessage() { + return mMessage; + } + + public boolean isSuccess() { + return mStatusCode == SUCCESS || mStatusCode == OK; + } + + public boolean isForbidden() { + return mStatusCode == ERROR_FORBIDDEN; + } + + public boolean isNotFound() { + return mStatusCode == ERROR_NOT_FOUND; + } + + public boolean isWrongParameter() { + return mStatusCode == ERROR_WRONG_PARAMETER; + } + + public void setMessage(String message) { + this.mMessage = message; + } + + /** + * method to parse the Download limit response + * @param isGet check if parsing has to do for GET api or not + * because the parsing will depend on that + * if API is GET then response will have tag else it wont have + * @param serverResponse + * @return + */ + public RemoteOperationResult parse(boolean isGet, String serverResponse) { + if (serverResponse == null || serverResponse.length() == 0) { + return new RemoteOperationResult<>(RemoteOperationResult.ResultCode.WRONG_SERVER_RESPONSE); + } + + RemoteOperationResult result; + try { + // Parse xml response and obtain the list of downloadLimitResponse + InputStream is = new ByteArrayInputStream(serverResponse.getBytes()); + + DownloadLimitResponse downloadLimitResponse = parseXMLResponse(is); + + if (isSuccess()) { + if (downloadLimitResponse != null && isGet) { + result = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.OK); + result.setResultData(downloadLimitResponse); + } else if (!isGet) { + result = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.OK); + } else { + result = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.WRONG_SERVER_RESPONSE); + Log_OC.e(TAG, "Successful status with no share in the response"); + } + } else if (isWrongParameter()) { + result = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.SHARE_WRONG_PARAMETER); + result.setMessage(getMessage()); + } else if (isNotFound()) { + result = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.SHARE_NOT_FOUND); + result.setMessage(getMessage()); + } else if (isForbidden()) { + result = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.SHARE_FORBIDDEN); + result.setMessage(getMessage()); + } else { + result = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.WRONG_SERVER_RESPONSE); + result.setMessage(getMessage()); + } + + } catch (XmlPullParserException e) { + Log_OC.e(TAG, "Error parsing response from server ", e); + result = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.WRONG_SERVER_RESPONSE); + + } catch (IOException e) { + Log_OC.e(TAG, "Error reading response from server ", e); + result = new RemoteOperationResult<>(RemoteOperationResult.ResultCode.WRONG_SERVER_RESPONSE); + } + + return result; + } + + /** + * Parse is as response of Share API + * + * @param is InputStream to parse + * @return List of ShareRemoteFiles + * @throws XmlPullParserException + * @throws IOException + */ + private DownloadLimitResponse parseXMLResponse(InputStream is) throws XmlPullParserException, IOException { + try { + // XMLPullParser + XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); + factory.setNamespaceAware(true); + + XmlPullParser parser = Xml.newPullParser(); + parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); + parser.setInput(is, null); + parser.nextTag(); + return readOCS(parser); + + } finally { + is.close(); + } + } + + /** + * Parse OCS node + * + * @param parser + * @return List of ShareRemoteFiles + * @throws XmlPullParserException + * @throws IOException + */ + private DownloadLimitResponse readOCS(XmlPullParser parser) throws XmlPullParserException, + IOException { + DownloadLimitResponse downloadLimitResponse = new DownloadLimitResponse(); + parser.require(XmlPullParser.START_TAG, ns, NODE_OCS); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + String name = parser.getName(); + // read NODE_META and NODE_DATA + if (NODE_META.equalsIgnoreCase(name)) { + readMeta(parser); + } else if (NODE_DATA.equalsIgnoreCase(name)) { + downloadLimitResponse = readData(parser); + } else { + skip(parser); + } + + } + return downloadLimitResponse; + + + } + + /** + * Parse Meta node + * + * @param parser + * @throws XmlPullParserException + * @throws IOException + */ + private void readMeta(XmlPullParser parser) throws XmlPullParserException, IOException { + parser.require(XmlPullParser.START_TAG, ns, NODE_META); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + String name = parser.getName(); + + if (NODE_STATUS.equalsIgnoreCase(name)) { + setStatus(readNode(parser, NODE_STATUS)); + + } else if (NODE_STATUS_CODE.equalsIgnoreCase(name)) { + setStatusCode(Integer.parseInt(readNode(parser, NODE_STATUS_CODE))); + + } else if (NODE_MESSAGE.equalsIgnoreCase(name)) { + setMessage(readNode(parser, NODE_MESSAGE)); + + } else { + skip(parser); + } + + } + } + + /** + * Parse Data node + * + * @param parser + * @return + * @throws XmlPullParserException + * @throws IOException + */ + private DownloadLimitResponse readData(XmlPullParser parser) throws XmlPullParserException, + IOException { + DownloadLimitResponse downloadLimitResponse = new DownloadLimitResponse(); + + parser.require(XmlPullParser.START_TAG, ns, NODE_DATA); + //Log_OC.d(TAG, "---- NODE DATA ---"); + while (parser.next() != XmlPullParser.END_TAG) { + if (parser.getEventType() != XmlPullParser.START_TAG) { + continue; + } + String name = parser.getName(); + if (NODE_LIMIT.equalsIgnoreCase(name)) { + downloadLimitResponse.setLimit(Long.parseLong(readNode(parser, NODE_LIMIT))); + } else if (NODE_COUNT.equalsIgnoreCase(name)) { + downloadLimitResponse.setCount(Long.parseLong(readNode(parser, NODE_COUNT))); + } else { + skip(parser); + } + } + + return downloadLimitResponse; + } + + + /** + * Parse a node, to obtain its text. Needs readText method + * + * @param parser + * @param node + * @return Text of the node + * @throws XmlPullParserException + * @throws IOException + */ + private String readNode(XmlPullParser parser, String node) throws XmlPullParserException, + IOException { + parser.require(XmlPullParser.START_TAG, ns, node); + String value = readText(parser); + //Log_OC.d(TAG, "node= " + node + ", value= " + value); + parser.require(XmlPullParser.END_TAG, ns, node); + return value; + } + + + /** + * Read the text from a node + * + * @param parser + * @return Text of the node + * @throws IOException + * @throws XmlPullParserException + */ + private String readText(XmlPullParser parser) throws IOException, XmlPullParserException { + String result = ""; + if (parser.next() == XmlPullParser.TEXT) { + result = parser.getText(); + parser.nextTag(); + } + return result; + } + + /** + * Skip tags in parser procedure + * + * @param parser + * @throws XmlPullParserException + * @throws IOException + */ + private void skip(XmlPullParser parser) throws XmlPullParserException, IOException { + if (parser.getEventType() != XmlPullParser.START_TAG) { + throw new IllegalStateException(); + } + int depth = 1; + while (depth != 0) { + switch (parser.next()) { + case XmlPullParser.END_TAG: + depth--; + break; + case XmlPullParser.START_TAG: + depth++; + break; + } + } + } +} diff --git a/app/src/main/java/com/owncloud/android/operations/share_download_limit/GetShareDownloadLimitOperation.java b/app/src/main/java/com/owncloud/android/operations/share_download_limit/GetShareDownloadLimitOperation.java new file mode 100644 index 000000000000..f10036961823 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/share_download_limit/GetShareDownloadLimitOperation.java @@ -0,0 +1,90 @@ +/** + * ownCloud Android client application + * + * @author TSI-mc Copyright (C) 2021 TSI-mc + *

+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + *

+ * You should have received a copy of the GNU General Public License along with this program. If not, see + * . + */ + +package com.owncloud.android.operations.share_download_limit; + +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.operations.RemoteOperation; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.common.utils.Log_OC; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.methods.GetMethod; + +/** + * class to fetch the download limit for the link share it requires share token to fetch the data + *

+ * API : //GET to /ocs/v2.php/apps/files_downloadlimit/{share_token}/limit + */ +public class GetShareDownloadLimitOperation extends RemoteOperation { + + private static final String TAG = GetShareDownloadLimitOperation.class.getSimpleName(); + + //share token from OCShare + private final String shareToken; + + public GetShareDownloadLimitOperation(String shareToken) { + this.shareToken = shareToken; + } + + @Override + protected RemoteOperationResult run(OwnCloudClient client) { + RemoteOperationResult result = null; + int status = -1; + + GetMethod get = null; + + try { + // Get Method + get = new GetMethod(client.getBaseUri() + ShareDownloadLimitUtils.INSTANCE.getDownloadLimitApiPath(shareToken)); + + get.addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE); + + status = client.executeMethod(get); + + if (isSuccess(status)) { + String response = get.getResponseBodyAsString(); + + Log_OC.d(TAG, "Get Download Limit response: " + response); + + DownloadLimitXMLParser parser = new DownloadLimitXMLParser(); + result = parser.parse(true, response); + + if (result.isSuccess()) { + Log_OC.d(TAG, "Got " + result.getResultData() + " Response"); + } + + } else { + result = new RemoteOperationResult(false, get); + } + + } catch (Exception e) { + result = new RemoteOperationResult(e); + Log_OC.e(TAG, "Exception while getting share download limit", e); + + } finally { + if (get != null) { + get.releaseConnection(); + } + } + return result; + } + + private boolean isSuccess(int status) { + return (status == HttpStatus.SC_OK); + } + +} diff --git a/app/src/main/java/com/owncloud/android/operations/share_download_limit/ShareDownloadLimitUtils.kt b/app/src/main/java/com/owncloud/android/operations/share_download_limit/ShareDownloadLimitUtils.kt new file mode 100644 index 000000000000..4c0780a9a532 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/share_download_limit/ShareDownloadLimitUtils.kt @@ -0,0 +1,30 @@ +/** + * ownCloud Android client application + * + * @author TSI-mc Copyright (C) 2021 TSI-mc + *

+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + *

+ * You should have received a copy of the GNU General Public License along with this program. If not, see + * . + */ + +package com.owncloud.android.operations.share_download_limit + +object ShareDownloadLimitUtils { + + private const val SHARE_TOKEN_PATH = "{share_token}" + + //ocs route + //replace the {share_token} + private const val SHARE_DOWNLOAD_LIMIT_API_PATH = "/ocs/v2.php/apps/files_downloadlimit/$SHARE_TOKEN_PATH/limit" + + fun getDownloadLimitApiPath(shareToken: String) : String{ + return SHARE_DOWNLOAD_LIMIT_API_PATH.replace(SHARE_TOKEN_PATH, shareToken) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/operations/share_download_limit/UpdateShareDownloadLimitRemoteOperation.java b/app/src/main/java/com/owncloud/android/operations/share_download_limit/UpdateShareDownloadLimitRemoteOperation.java new file mode 100644 index 000000000000..f177a515e612 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/operations/share_download_limit/UpdateShareDownloadLimitRemoteOperation.java @@ -0,0 +1,114 @@ +/** + * ownCloud Android client application + * + * @author TSI-mc Copyright (C) 2021 TSI-mc + *

+ * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public + * License version 2, as published by the Free Software Foundation. + *

+ * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied + * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more + * details. + *

+ * You should have received a copy of the GNU General Public License along with this program. If not, see + * . + */ + +package com.owncloud.android.operations.share_download_limit; + +import android.util.Pair; + +import com.owncloud.android.lib.common.OwnCloudClient; +import com.owncloud.android.lib.common.operations.RemoteOperation; +import com.owncloud.android.lib.common.operations.RemoteOperationResult; +import com.owncloud.android.lib.common.utils.Log_OC; + +import org.apache.commons.httpclient.HttpStatus; +import org.apache.commons.httpclient.methods.PutMethod; +import org.apache.commons.httpclient.methods.StringRequestEntity; + +import java.util.ArrayList; +import java.util.List; + +/** + * class to update the download limit for the link share + *

+ * API : //PUT to /ocs/v2.php/apps/files_downloadlimit/{share_token}/limit + *

+ * Body: {"token" : "Bpd4oEAgPqn3AbG", "limit" : 5} + */ +public class UpdateShareDownloadLimitRemoteOperation extends RemoteOperation { + + private static final String TAG = UpdateShareDownloadLimitRemoteOperation.class.getSimpleName(); + + private static final String PARAM_TOKEN = "token"; + private static final String PARAM_LIMIT = "limit"; + + private static final String ENTITY_CONTENT_TYPE = "application/x-www-form-urlencoded"; + private static final String ENTITY_CHARSET = "UTF-8"; + + private final String shareToken; + private final long downloadLimit; + + public UpdateShareDownloadLimitRemoteOperation(String shareToken, long downloadLimit) { + this.shareToken = shareToken; + this.downloadLimit = downloadLimit; + } + + @Override + protected RemoteOperationResult run(OwnCloudClient client) { + RemoteOperationResult result; + int status; + + PutMethod put = null; + + try { + // Post Method + put = new PutMethod(client.getBaseUri() + ShareDownloadLimitUtils.INSTANCE.getDownloadLimitApiPath(shareToken)); + + put.addRequestHeader(OCS_API_HEADER, OCS_API_HEADER_VALUE); + List> parametersToUpdate = new ArrayList<>(); + parametersToUpdate.add(new Pair<>(PARAM_TOKEN, shareToken)); + parametersToUpdate.add(new Pair<>(PARAM_LIMIT, String.valueOf(downloadLimit))); + + for (Pair parameter : parametersToUpdate) { + put.setRequestEntity(new StringRequestEntity(parameter.first + "=" + parameter.second, + ENTITY_CONTENT_TYPE, + ENTITY_CHARSET)); + } + + status = client.executeMethod(put); + + if (isSuccess(status)) { + String response = put.getResponseBodyAsString(); + + Log_OC.d(TAG, "Download Limit response: " + response); + + DownloadLimitXMLParser parser = new DownloadLimitXMLParser(); + result = parser.parse(true, response); + + if (result.isSuccess()) { + return result; + } + + } else { + result = new RemoteOperationResult<>(false, put); + } + + } catch (Exception e) { + result = new RemoteOperationResult<>(e); + Log_OC.e(TAG, "Exception while updating share download limit", e); + + } finally { + if (put != null) { + put.releaseConnection(); + } + } + return result; + } + + private boolean isSuccess(int status) { + return status == HttpStatus.SC_OK || status == HttpStatus.SC_BAD_REQUEST; + } + +} diff --git a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java index fc4c05dd4b77..a8ad045a4cd5 100644 --- a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java +++ b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java @@ -81,7 +81,7 @@ public class UsersAndGroupsSearchProvider extends ContentProvider { BaseColumns._ID, SearchManager.SUGGEST_COLUMN_TEXT_1, SearchManager.SUGGEST_COLUMN_TEXT_2, - SearchManager.SUGGEST_COLUMN_ICON_1, + // SearchManager.SUGGEST_COLUMN_ICON_1, SearchManager.SUGGEST_COLUMN_INTENT_DATA }; @@ -301,7 +301,11 @@ private Cursor searchForUsersOrGroups(Uri uri) { displayName = userName; subline = (status.getMessage() == null || status.getMessage().isEmpty()) ? null : status.getMessage(); - Uri.Builder builder = Uri.parse("content://" + AUTHORITY + "/icon").buildUpon(); + icon = R.drawable.ic_internal_share; + + // Commented for NMC customization + // uncomment the below code to show icon with initials + /*Uri.Builder builder = Uri.parse("content://" + AUTHORITY + "/icon").buildUpon(); builder.appendQueryParameter("shareWith", shareWith); builder.appendQueryParameter("displayName", displayName); @@ -311,7 +315,7 @@ private Cursor searchForUsersOrGroups(Uri uri) { builder.appendQueryParameter("icon", status.getIcon()); } - icon = builder.build(); + icon = builder.build();*/ dataUri = Uri.withAppendedPath(userBaseUri, shareWith); break; @@ -340,12 +344,22 @@ private Cursor searchForUsersOrGroups(Uri uri) { } if (displayName != null && dataUri != null) { - response.newRow() - .add(count++) // BaseColumns._ID - .add(displayName) // SearchManager.SUGGEST_COLUMN_TEXT_1 - .add(subline) // SearchManager.SUGGEST_COLUMN_TEXT_2 - .add(icon) // SearchManager.SUGGEST_COLUMN_ICON_1 - .add(dataUri); + //if display name is empty set sublime as primary text + if (displayName.equals("")) { + response.newRow() + .add(count++) // BaseColumns._ID + .add(subline) // SearchManager.SUGGEST_COLUMN_TEXT_1 + .add(displayName) // SearchManager.SUGGEST_COLUMN_TEXT_2 + //.add(icon) // SearchManager.SUGGEST_COLUMN_ICON_1 + .add(dataUri); + } else { + response.newRow() + .add(count++) // BaseColumns._ID + .add(displayName) // SearchManager.SUGGEST_COLUMN_TEXT_1 + .add(subline) // SearchManager.SUGGEST_COLUMN_TEXT_2 + //.add(icon) // SearchManager.SUGGEST_COLUMN_ICON_1 + .add(dataUri); + } } } @@ -427,7 +441,8 @@ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) thr } catch (FileNotFoundException e) { Log_OC.e(TAG, "File not found: " + e.getMessage()); } - + } catch (OutOfMemoryError oome) { + Log_OC.e(TAG, "Out of memory"); } catch (Exception e) { Log_OC.e(TAG, "Error opening file: " + e.getMessage()); } diff --git a/app/src/main/java/com/owncloud/android/services/OperationsService.java b/app/src/main/java/com/owncloud/android/services/OperationsService.java index d2b8a9c75dae..c7bd974f10dc 100644 --- a/app/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/app/src/main/java/com/owncloud/android/services/OperationsService.java @@ -64,6 +64,7 @@ import com.owncloud.android.operations.UpdateShareInfoOperation; import com.owncloud.android.operations.UpdateSharePermissionsOperation; import com.owncloud.android.operations.UpdateShareViaLinkOperation; +import com.owncloud.android.operations.share_download_limit.GetShareDownloadLimitOperation; import java.io.IOException; import java.util.Optional; @@ -99,6 +100,8 @@ public class OperationsService extends Service { public static final String EXTRA_SHARE_HIDE_FILE_DOWNLOAD = "HIDE_FILE_DOWNLOAD"; public static final String EXTRA_SHARE_ID = "SHARE_ID"; public static final String EXTRA_SHARE_NOTE = "SHARE_NOTE"; + public static final String EXTRA_SHARE_TOKEN = "SHARE_TOKEN"; + public static final String EXTRA_SHARE_DOWNLOAD_LIMIT = "SHARE_DOWNLOAD_LIMIT"; 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"; @@ -121,6 +124,7 @@ public class OperationsService extends Service { public static final String ACTION_MOVE_FILE = "MOVE_FILE"; public static final String ACTION_COPY_FILE = "COPY_FILE"; public static final String ACTION_CHECK_CURRENT_CREDENTIALS = "CHECK_CURRENT_CREDENTIALS"; + public static final String ACTION_GET_SHARE_DOWNLOAD_LIMIT = "GET_SHARE_DOWNLOAD_LIMIT"; public static final String ACTION_RESTORE_VERSION = "RESTORE_VERSION"; public static final String ACTION_UPDATE_FILES_DOWNLOAD_LIMIT = "UPDATE_FILES_DOWNLOAD_LIMIT"; @@ -653,6 +657,12 @@ private Pair newOperation(Intent operationIntent) { String shareAttributes = operationIntent.getStringExtra(EXTRA_SHARE_ATTRIBUTES); updateShare.setAttributes(shareAttributes); + // download limit for link share type + if (operationIntent.hasExtra(EXTRA_SHARE_DOWNLOAD_LIMIT)) { + updateShare.setDownloadLimit(operationIntent.getLongExtra(EXTRA_SHARE_DOWNLOAD_LIMIT, + 0L)); + } + operation = updateShare; } break; @@ -749,6 +759,13 @@ private Pair newOperation(Intent operationIntent) { fileVersion.getFileName()); break; + case ACTION_GET_SHARE_DOWNLOAD_LIMIT: + String shareToken = operationIntent.getStringExtra(EXTRA_SHARE_TOKEN); + if (!TextUtils.isEmpty(shareToken)) { + operation = new GetShareDownloadLimitOperation(shareToken); + } + break; + case ACTION_UPDATE_FILES_DOWNLOAD_LIMIT: shareId = operationIntent.getLongExtra(EXTRA_SHARE_ID, -1); int newLimit = operationIntent.getIntExtra(EXTRA_FILES_DOWNLOAD_LIMIT, -1); diff --git a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java index b6c93efd0df4..99ca32565456 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/FileActivity.java @@ -77,6 +77,8 @@ import com.owncloud.android.operations.UpdateSharePermissionsOperation; import com.owncloud.android.operations.UpdateShareViaLinkOperation; import com.owncloud.android.providers.UsersAndGroupsSearchConfig; +import com.owncloud.android.operations.share_download_limit.DownloadLimitResponse; +import com.owncloud.android.operations.share_download_limit.GetShareDownloadLimitOperation; import com.owncloud.android.providers.UsersAndGroupsSearchProvider; import com.owncloud.android.services.OperationsService; import com.owncloud.android.services.OperationsService.OperationsServiceBinder; @@ -88,6 +90,7 @@ import com.owncloud.android.ui.dialog.SslUntrustedCertDialog; import com.owncloud.android.ui.fragment.FileDetailFragment; import com.owncloud.android.ui.fragment.FileDetailSharingFragment; +import com.owncloud.android.ui.fragment.FileDetailsSharingProcessFragment; import com.owncloud.android.ui.fragment.OCFileListFragment; import com.owncloud.android.ui.fragment.filesRepository.FilesRepository; import com.owncloud.android.ui.fragment.filesRepository.RemoteFilesRepository; @@ -396,7 +399,8 @@ public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationRe operation instanceof UnshareOperation || operation instanceof SynchronizeFolderOperation || operation instanceof UpdateShareViaLinkOperation || - operation instanceof UpdateSharePermissionsOperation + operation instanceof UpdateSharePermissionsOperation || + operation instanceof UpdateShareInfoOperation ) { if (result.isSuccess()) { updateFileFromDB(); @@ -436,6 +440,8 @@ public void onRemoteOperationFinish(RemoteOperation operation, RemoteOperationRe onUpdateShareInformation(result, R.string.unsharing_failed); } else if (operation instanceof UpdateNoteForShareOperation) { onUpdateNoteForShareOperationFinish(result); + } else if (operation instanceof GetShareDownloadLimitOperation) { + onShareDownloadLimitFetched(result); } } @@ -815,7 +821,6 @@ public void refreshList() { private void onCreateShareViaLinkOperationFinish(CreateShareViaLinkOperation operation, RemoteOperationResult result) { FileDetailSharingFragment sharingFragment = getShareFileFragment(); - final Fragment fileListFragment = getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES); if (result.isSuccess()) { updateFileFromDB(); @@ -842,6 +847,8 @@ private void onCreateShareViaLinkOperationFinish(CreateShareViaLinkOperation ope sharingFragment.onUpdateShareInformation(result, file); } + // this has to be here to avoid the crash when creating link from inside of FileDetailSharingFragment + Fragment fileListFragment = getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES); if (fileListFragment instanceof OCFileListFragment ocFileListFragment && file != null) { if (ocFileListFragment.getAdapterFiles().contains(file)) { ocFileListFragment.updateOCFile(file); @@ -881,6 +888,22 @@ private void onCreateShareViaLinkOperationFinish(CreateShareViaLinkOperation ope } } + /** + * method will be called when download limit is fetched + * + * @param result + */ + private void onShareDownloadLimitFetched(RemoteOperationResult result) { + FileDetailSharingFragment sharingFragment = getShareFileFragment(); + + if (result.isSuccess() && sharingFragment != null && result.isSuccess() && result.getResultData() != null + && result.getResultData() instanceof DownloadLimitResponse) { + onLinkShareDownloadLimitFetched(((DownloadLimitResponse) result.getResultData()).getLimit(), + ((DownloadLimitResponse) result.getResultData()).getCount()); + + } + } + /** * Shortcut to get access to the {@link FileDetailSharingFragment} instance, if any * @@ -948,11 +971,17 @@ private OCFile getFileFromDetailFragment() { * @param shareType */ protected void doShareWith(String shareeName, ShareType shareType) { - FileDetailFragment fragment = getFileDetailFragment(); + Fragment fragment = getFileDetailFragment(); if (fragment != null) { - fragment.initiateSharingProcess(shareeName, - shareType, - usersAndGroupsSearchConfig.getSearchOnlyUsers()); + ((FileDetailFragment) fragment).initiateSharingProcess(shareeName, + shareType, + usersAndGroupsSearchConfig.getSearchOnlyUsers()); + } else { + //if user sharing from Preview Image Fragment + fragment = getSupportFragmentManager().findFragmentByTag(ShareActivity.TAG_SHARE_FRAGMENT); + if (fragment != null) { + ((FileDetailSharingFragment) fragment).initiateSharingProcess(shareeName, shareType, usersAndGroupsSearchConfig.getSearchOnlyUsers()); + } } } @@ -966,9 +995,23 @@ protected void doShareWith(String shareeName, ShareType shareType) { @Override public void editExistingShare(OCShare share, int screenTypePermission, boolean isReshareShown, boolean isExpiryDateShown) { - FileDetailFragment fragment = getFileDetailFragment(); + Fragment fragment = getFileDetailFragment(); if (fragment != null) { - fragment.editExistingShare(share, screenTypePermission, isReshareShown, isExpiryDateShown); + ((FileDetailFragment) fragment).editExistingShare(share, screenTypePermission, isReshareShown, isExpiryDateShown); + } else { + //if user editing from Preview Image Fragment + fragment = getSupportFragmentManager().findFragmentByTag(ShareActivity.TAG_SHARE_FRAGMENT); + if (fragment != null) { + ((FileDetailSharingFragment) fragment).editExistingShare(share, screenTypePermission, isReshareShown, isExpiryDateShown); + } + } + } + + @Override + public void onLinkShareDownloadLimitFetched(long downloadLimit, long downloadCount) { + Fragment fileDetailsSharingProcessFragment = getSupportFragmentManager().findFragmentByTag(FileDetailsSharingProcessFragment.TAG); + if (fileDetailsSharingProcessFragment != null) { + ((FileDetailsSharingProcessFragment) fileDetailsSharingProcessFragment).onLinkShareDownloadLimitFetched(downloadLimit, downloadCount); } } @@ -979,7 +1022,7 @@ public void editExistingShare(OCShare share, int screenTypePermission, boolean i public void onShareProcessClosed() { FileDetailFragment fragment = getFileDetailFragment(); if (fragment != null) { - fragment.showHideFragmentView(false); + //do something } } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java index 3c6b724e553f..5f04b79c7006 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ShareActivity.java @@ -32,6 +32,7 @@ import com.owncloud.android.operations.GetSharesForFileOperation; import com.owncloud.android.ui.fragment.FileDetailSharingFragment; import com.owncloud.android.ui.fragment.FileDetailsSharingProcessFragment; +import com.owncloud.android.ui.fragment.util.SharePermissionManager; import com.owncloud.android.utils.DisplayUtils; import com.owncloud.android.utils.MimeTypeUtil; @@ -132,11 +133,12 @@ protected void onStart() { @Override protected void doShareWith(String shareeName, ShareType shareType) { - getSupportFragmentManager().beginTransaction().replace(R.id.share_fragment_container, + getSupportFragmentManager().beginTransaction().add(R.id.share_fragment_container, FileDetailsSharingProcessFragment.newInstance(getFile(), shareeName, shareType, - false), + false, + SharePermissionManager.canEditFile(getUser().get(), getStorageManager().getCapability(getUser().get()), getFile(), editorUtils)), FileDetailsSharingProcessFragment.TAG) .commit(); } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.java b/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.java index 15e35a76093f..bc24f420109a 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/LinkShareViewHolder.java @@ -18,11 +18,10 @@ import android.text.TextUtils; import android.view.View; -import com.nextcloud.android.lib.resources.files.FileDownloadLimit; import com.nextcloud.utils.mdm.MDMConfig; +import com.nmc.android.utils.TextViewUtils; 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.SharePermissionManager; @@ -37,6 +36,7 @@ class LinkShareViewHolder extends RecyclerView.ViewHolder { private Context context; private ViewThemeUtils viewThemeUtils; private boolean encrypted; + private boolean isTextFile; public LinkShareViewHolder(@NonNull View itemView) { super(itemView); @@ -45,29 +45,32 @@ public LinkShareViewHolder(@NonNull View itemView) { public LinkShareViewHolder(FileDetailsShareLinkShareItemBinding binding, Context context, final ViewThemeUtils viewThemeUtils, - boolean encrypted) { + boolean encrypted, + boolean isTextFile) { this(binding.getRoot()); this.binding = binding; this.context = context; this.viewThemeUtils = viewThemeUtils; this.encrypted = encrypted; + this.isTextFile = isTextFile; } public void bind(OCShare publicShare, ShareeListAdapterListener listener, int position) { if (ShareType.EMAIL == publicShare.getShareType()) { - final var res = context.getResources(); binding.name.setText(publicShare.getSharedWithDisplayName()); - - final var emailDrawable = ResourcesCompat.getDrawable(res, R.drawable.ic_email, null); - binding.icon.setImageDrawable(emailDrawable); + binding.icon.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), + R.drawable.ic_external_share, + null)); + binding.overflowMenu.setVisibility(View.VISIBLE); binding.copyLink.setVisibility(View.GONE); + binding.detailText.setVisibility(View.GONE); } else { String label = publicShare.getLabel(); 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); + binding.name.setText(R.string.share_permission_file_drop); } else if (SharePermissionManager.INSTANCE.isSecureFileDrop(publicShare) && encrypted) { binding.name.setText(R.string.share_permission_secure_file_drop); } else { @@ -76,23 +79,16 @@ public void bind(OCShare publicShare, ShareeListAdapterListener listener, int po binding.name.setText((position == 0) ? context.getString(textRes) : context.getString(textRes, arg)); } - } - viewThemeUtils.platform.colorImageViewBackgroundAndIcon(binding.icon); + binding.overflowMenu.setVisibility(View.GONE); + binding.copyLink.setVisibility(View.VISIBLE); + binding.detailText.setVisibility(View.VISIBLE); - FileDownloadLimit downloadLimit = publicShare.getFileDownloadLimit(); - if (downloadLimit != null && downloadLimit.getLimit() > 0) { - int remaining = downloadLimit.getLimit() - downloadLimit.getCount(); - String text = context.getResources().getQuantityString(R.plurals.share_download_limit_description, remaining, remaining); - - binding.subline.setText(text); - binding.subline.setVisibility(View.VISIBLE); - } else { - binding.subline.setVisibility(View.GONE); } - QuickPermissionType quickPermissionType = SharePermissionManager.INSTANCE.getSelectedType(publicShare, encrypted); - setPermissionName(publicShare, quickPermissionType.getText(context)); + setPermissionName(publicShare, SharePermissionManager.getPermissionName(context, publicShare)); + showHideCalendarIcon(publicShare.getExpirationDate()); + showHidePasswordIcon(publicShare.isPasswordProtected()); binding.overflowMenu.setOnClickListener(v -> listener.showSharingMenuActionSheet(publicShare)); if (!SharePermissionManager.INSTANCE.isSecureFileDrop(publicShare) && !encrypted) { @@ -104,16 +100,46 @@ public void bind(OCShare publicShare, ShareeListAdapterListener listener, int po } else { binding.copyLink.setVisibility(View.GONE); } + binding.detailText.setOnClickListener(v -> listener.showSharingMenuActionSheet(publicShare)); } private void setPermissionName(OCShare publicShare, String permissionName) { if (TextUtils.isEmpty(permissionName) || (SharePermissionManager.INSTANCE.isSecureFileDrop(publicShare) && encrypted)) { - binding.permissionName.setVisibility(View.GONE); + binding.quickPermissionLayout.permissionLayout.setVisibility(View.GONE); return; } - binding.permissionName.setText(permissionName); - binding.permissionName.setVisibility(View.VISIBLE); - viewThemeUtils.androidx.colorPrimaryTextViewElement(binding.permissionName); + binding.quickPermissionLayout.permissionName.setText(permissionName); + TextViewUtils.isTextEllipsized(binding.quickPermissionLayout.permissionName, isEllipsized -> { + if(isEllipsized) { + binding.quickPermissionLayout.permissionName.setText(SharePermissionManager.getShortPermissionName(context, permissionName)); + } + }); + setPermissionTypeIcon(permissionName); + binding.quickPermissionLayout.permissionLayout.setVisibility(View.VISIBLE); + } + + private void showHideCalendarIcon(long expirationDate) { + binding.quickPermissionLayout.calendarPermissionIcon.setVisibility(expirationDate > 0 ? View.VISIBLE : View.GONE); + } + + private void showHidePasswordIcon(boolean isPasswordProtected) { + binding.quickPermissionLayout.passwordPermissionIcon.setVisibility(isPasswordProtected ? View.VISIBLE : View.GONE); + } + + private void setPermissionTypeIcon(String permissionName) { + if (permissionName.equalsIgnoreCase(context.getResources().getString(R.string.share_quick_permission_can_edit))) { + binding.quickPermissionLayout.permissionTypeIcon.setImageResource(R.drawable.ic_sharing_edit); + binding.quickPermissionLayout.permissionTypeIcon.setVisibility(View.VISIBLE); + } else if (permissionName.equalsIgnoreCase(context.getResources().getString(R.string.share_quick_permission_can_view))) { + binding.quickPermissionLayout.permissionTypeIcon.setImageResource(R.drawable.ic_sharing_read_only); + binding.quickPermissionLayout.permissionTypeIcon.setVisibility(View.VISIBLE); + } else if (permissionName.equalsIgnoreCase(context.getResources().getString(R.string.share_permission_secure_file_drop)) + || permissionName.equalsIgnoreCase(context.getResources().getString(R.string.share_quick_permission_can_upload))) { + binding.quickPermissionLayout.permissionTypeIcon.setImageResource(R.drawable.ic_sharing_file_drop); + binding.quickPermissionLayout.permissionTypeIcon.setVisibility(View.VISIBLE); + } else { + binding.quickPermissionLayout.permissionTypeIcon.setVisibility(View.GONE); + } } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt index 44e9fa7e8ee8..d534cb665727 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt @@ -22,6 +22,7 @@ interface ListViewHolder { val localFileIndicator: ImageView val imageFileName: TextView? val shared: ImageView + val sharedMessage: TextView? val checkbox: ImageView val itemLayout: View val unreadComments: ImageView diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 41f92f77fe75..feba5eadfec7 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java @@ -589,7 +589,8 @@ private void bindListGridItemViewHolder(ListGridItemViewHolder holder, OCFile fi private void bindListItemViewHolder(ListItemViewHolder holder, OCFile file) { if ((file.isSharedWithMe() || file.isSharedWithSharee()) && !isMultiSelect() && !gridView && !hideItemOptions) { - holder.getSharedAvatars().setVisibility(View.VISIBLE); + //visibility gone as view not required for NMC + holder.getSharedAvatars().setVisibility(View.GONE); holder.getSharedAvatars().removeAllViews(); String fileOwner = file.getOwnerId(); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt index e292006ec5f0..3dd75c5a8008 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListDelegate.kt @@ -13,6 +13,8 @@ import android.os.AsyncTask import android.view.View import android.widget.ImageView import androidx.core.content.ContextCompat +import android.widget.TextView +import androidx.appcompat.content.res.AppCompatResources import androidx.core.content.res.ResourcesCompat import androidx.core.graphics.drawable.toDrawable import com.elyeproj.loaderviewlibrary.LoaderImageView @@ -265,7 +267,7 @@ class OCFileListDelegate( if (shouldHideShare) { gridViewHolder.shared.visibility = View.GONE } else { - configureSharedIconView(gridViewHolder, file) + showShareIcon(gridViewHolder, file) } if (!file.isOfflineOperation && !file.isFolder) { @@ -385,39 +387,74 @@ class OCFileListDelegate( } } - private fun configureSharedIconView(gridViewHolder: ListViewHolder, file: OCFile) { - val result = getShareIconIdAndContentDescriptionId(gridViewHolder, file) - - gridViewHolder.shared.run { - if (result == null) { - visibility = View.GONE - return - } - - setImageResource(result.first) - contentDescription = context.getString(result.second) - visibility = View.VISIBLE - setOnClickListener { ocFileListFragmentInterface.onShareIconClick(file) } - } - } - - @Suppress("ReturnCount") - private fun getShareIconIdAndContentDescriptionId(holder: ListViewHolder, file: OCFile): Pair? { + private fun showShareIcon(gridViewHolder: ListViewHolder, file: OCFile) { + val sharedIconView = gridViewHolder.shared if (!MDMConfig.sharingSupport(context)) { - return null + sharedIconView.visibility = View.GONE + return } - - if (file.isOfflineOperation) return null - - if (holder !is OCFileListItemViewHolder && file.unreadCommentsCount != 0) return null - - return when { - file.isSharedWithSharee || file.isSharedWithMe -> { - if (showShareAvatar) null else R.drawable.shared_via_users to R.string.shared_icon_shared + //Initialising Textview for Message and setting its visibility + //only applicable for list item + val sharedMessageView: TextView? = gridViewHolder.sharedMessage + sharedMessageView?.visibility = if (com.nmc.android.utils.DisplayUtils.isShowDividerForList()) View.VISIBLE else View.GONE + + if (gridViewHolder is OCFileListItemViewHolder || file.unreadCommentsCount == 0) { + sharedIconView.visibility = View.VISIBLE + when { + file.isSharedWithMe -> { + val sharedWithMeColor = ResourcesCompat.getColor( + context.resources, + R.color.shared_with_me_color, null + ) + val shareWithMeIcon = AppCompatResources.getDrawable(context, R.drawable.ic_shared_with_me)?.mutate() + val shareWithMeTintedIcon = + viewThemeUtils.platform.colorDrawable(shareWithMeIcon!!, sharedWithMeColor) + sharedIconView.setImageDrawable(shareWithMeTintedIcon) + sharedIconView.contentDescription = context.getString(R.string.shared_icon_shared) + //Added Code For Message Text + sharedMessageView?.text = context.resources.getString(R.string.placeholder_receivedMessage) + sharedMessageView?.setTextColor(sharedWithMeColor) + } + file.isSharedWithSharee -> { + val shareIcon = viewThemeUtils.platform.colorDrawable( + AppCompatResources.getDrawable(context, R.drawable.ic_share)?.mutate()!!, + context.resources.getColor(R.color.primary, null) + ) + sharedIconView.setImageDrawable(shareIcon) + sharedIconView.contentDescription = context.getString(R.string.shared_icon_shared) + //Added Code For Message Text + sharedMessageView?.text = context.resources.getString(R.string.placeholder_sharedMessage) + sharedMessageView?.setTextColor(context.resources.getColor(R.color.primary, null)) + } + file.isSharedViaLink -> { + val shareIcon = viewThemeUtils.platform.colorDrawable( + AppCompatResources.getDrawable(context, R.drawable.ic_share)?.mutate()!!, + context.resources.getColor(R.color.primary, null) + ) + sharedIconView.setImageDrawable(shareIcon) + sharedIconView.contentDescription = context.getString(R.string.shared_icon_shared_via_link) + //Added Code For Message Text + sharedMessageView?.text = context.resources.getString(R.string.placeholder_sharedMessage) + sharedMessageView?.setTextColor(context.resources.getColor(R.color.primary, null)) + } + file.isEncrypted -> { + sharedIconView.visibility = View.GONE + } + else -> { + val unShareIconColor = ResourcesCompat.getColor( + context.resources, + R.color.list_icon_color, null + ) + val unShareIcon = AppCompatResources.getDrawable(context, R.drawable.ic_share)?.mutate() + val unShareTintedIcon = viewThemeUtils.platform.colorDrawable(unShareIcon!!, unShareIconColor) + sharedIconView.setImageDrawable(unShareTintedIcon) + sharedIconView.contentDescription = context.getString(R.string.shared_icon_share) + sharedMessageView?.visibility = View.GONE + } } - - file.isSharedViaLink -> R.drawable.shared_via_link to R.string.shared_icon_shared_via_link - else -> R.drawable.ic_unshared to R.string.shared_icon_share + sharedIconView.setOnClickListener { ocFileListFragmentInterface.onShareIconClick(file) } + } else { + sharedIconView.visibility = View.GONE } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt index 05ee4731e994..fbc5e5664d37 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt @@ -40,6 +40,8 @@ internal class OCFileListGridItemViewHolder(var binding: GridItemBinding) : get() = null override val shared: ImageView get() = binding.sharedIcon + override val sharedMessage: TextView? + get() = null override val checkbox: ImageView get() = binding.customCheckbox override val itemLayout: View diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt index 38b3310f0bbe..e163745e8227 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt @@ -74,6 +74,8 @@ internal class OCFileListItemViewHolder(private var binding: ListItemBinding) : get() = null override val shared: ImageView get() = binding.sharedIcon + override val sharedMessage: TextView + get() = binding.sharedMessage override val checkbox: ImageView get() = binding.customCheckbox override val itemLayout: View diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt index 7619df6544fb..c92dc905fd54 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt @@ -40,6 +40,8 @@ internal class OCFileListViewHolder(var binding: GridImageBinding) : get() = binding.localFileIndicator override val shared: ImageView get() = binding.sharedIcon + override val sharedMessage: TextView? + get() = null override val checkbox: ImageView get() = binding.customCheckbox override val itemLayout: View diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/QuickSharingPermissionsAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/QuickSharingPermissionsAdapter.kt index 06a444fb01c5..5585ebeedd66 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/QuickSharingPermissionsAdapter.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/QuickSharingPermissionsAdapter.kt @@ -14,7 +14,6 @@ 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.quickPermission.QuickPermission @@ -53,12 +52,12 @@ class QuickSharingPermissionsAdapter( val permissionName = quickPermission.type.getText(context) binding.run { - quickPermissionButton.text = permissionName - quickPermissionButton.iconGravity = MaterialButton.ICON_GRAVITY_START - quickPermissionButton.icon = quickPermission.type.getIcon(context) + tvQuickShareName.text = permissionName if (quickPermission.isSelected) { - viewThemeUtils.material.colorMaterialButtonPrimaryBorderless(quickPermissionButton) + tvQuickShareCheckIcon.visibility = View.VISIBLE + } else { + tvQuickShareCheckIcon.visibility = View.INVISIBLE } } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ShareViewHolder.java b/app/src/main/java/com/owncloud/android/ui/adapter/ShareViewHolder.java index 974ea6d5655c..6742c37fb258 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ShareViewHolder.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ShareViewHolder.java @@ -20,9 +20,9 @@ import com.nextcloud.client.account.User; import com.nextcloud.utils.extensions.ImageViewExtensionsKt; +import com.nmc.android.utils.TextViewUtils; 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.SharePermissionManager; @@ -64,33 +64,33 @@ public void bind(OCShare share, float avatarRadiusDimension) { this.avatarRadiusDimension = avatarRadiusDimension; String name = share.getSharedWithDisplayName(); - + if ("".equals(name) && !"".equals(share.getShareWith())) { name = share.getShareWith(); } - + binding.icon.setTag(null); 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); + // 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); + // viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context); break; case CIRCLE: - viewThemeUtils.files.createAvatar(share.getShareType(), binding.icon, context); + // 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()); + // 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); + /* float avatarRadius = context.getResources().getDimension(R.dimen.list_item_avatar_icon_radius); if (share.getShareWith() != null) { DisplayUtils.setAvatar(user, @@ -101,11 +101,11 @@ public void bind(OCShare share, context.getResources(), binding.icon, context); - } - - binding.icon.setOnClickListener(v -> listener.showProfileBottomSheet(user, share.getShareWith())); + } */ + // Not required for NMC as per NMC-3097 + // binding.icon.setOnClickListener(v -> listener.showProfileBottomSheet(user, share.getShareWith())); default: - setImage(binding.icon, name); + // setImage(binding.icon, name); break; } } @@ -116,8 +116,9 @@ public void bind(OCShare share, share.getUserId() != null && share.getUserId().equalsIgnoreCase(userId)) { binding.overflowMenu.setVisibility(View.VISIBLE); - QuickPermissionType quickPermissionType = SharePermissionManager.INSTANCE.getSelectedType(share, encrypted); - setPermissionName(quickPermissionType.getText(context)); + setPermissionName(SharePermissionManager.getPermissionName(context, share)); + showHideCalendarIcon(share.getExpirationDate()); + showHidePasswordIcon(share.isPasswordProtected()); // bind listener to edit privileges binding.overflowMenu.setOnClickListener(v -> listener.showSharingMenuActionSheet(share)); @@ -129,13 +130,43 @@ public void bind(OCShare share, private void setPermissionName(String permissionName) { if (!TextUtils.isEmpty(permissionName)) { - binding.permissionName.setText(permissionName); - binding.permissionName.setVisibility(View.VISIBLE); + binding.quickPermissionLayout.permissionName.setText(permissionName); + TextViewUtils.isTextEllipsized(binding.quickPermissionLayout.permissionName, isEllipsized -> { + if(isEllipsized) { + binding.quickPermissionLayout.permissionName.setText(SharePermissionManager.getShortPermissionName(context, permissionName)); + } + }); + setPermissionTypeIcon(permissionName); + binding.quickPermissionLayout.permissionLayout.setVisibility(View.VISIBLE); } else { - binding.permissionName.setVisibility(View.GONE); + binding.quickPermissionLayout.permissionLayout.setVisibility(View.GONE); } } + private void setPermissionTypeIcon(String permissionName) { + if (permissionName.equalsIgnoreCase(context.getResources().getString(R.string.share_quick_permission_can_edit))) { + binding.quickPermissionLayout.permissionTypeIcon.setImageResource(R.drawable.ic_sharing_edit); + binding.quickPermissionLayout.permissionTypeIcon.setVisibility(View.VISIBLE); + } else if (permissionName.equalsIgnoreCase(context.getResources().getString(R.string.share_quick_permission_can_view))) { + binding.quickPermissionLayout.permissionTypeIcon.setImageResource(R.drawable.ic_sharing_read_only); + binding.quickPermissionLayout.permissionTypeIcon.setVisibility(View.VISIBLE); + } else if (permissionName.equalsIgnoreCase(context.getResources().getString(R.string.share_permission_secure_file_drop)) + || permissionName.equalsIgnoreCase(context.getResources().getString(R.string.share_quick_permission_can_upload))) { + binding.quickPermissionLayout.permissionTypeIcon.setImageResource(R.drawable.ic_sharing_file_drop); + binding.quickPermissionLayout.permissionTypeIcon.setVisibility(View.VISIBLE); + } else { + binding.quickPermissionLayout.permissionTypeIcon.setVisibility(View.GONE); + } + } + + private void showHideCalendarIcon(long expirationDate) { + binding.quickPermissionLayout.calendarPermissionIcon.setVisibility(expirationDate > 0 ? View.VISIBLE : View.GONE); + } + + private void showHidePasswordIcon(boolean isPasswordProtected) { + binding.quickPermissionLayout.passwordPermissionIcon.setVisibility(isPasswordProtected ? View.VISIBLE : View.GONE); + } + private void setImage(ImageView avatar, String name) { if (TextUtils.isEmpty(name)) { setUserImage(avatar); diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.java index 16be239081ff..e5a57d2905e9 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ShareeListAdapter.java @@ -25,7 +25,6 @@ import com.owncloud.android.databinding.FileDetailsSharePublicLinkAddNewItemBinding; import com.owncloud.android.databinding.FileDetailsShareSecureFileDropAddNewItemBinding; import com.owncloud.android.databinding.FileDetailsShareShareItemBinding; -import com.owncloud.android.datamodel.SharesType; import com.owncloud.android.lib.resources.shares.OCShare; import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.ui.activity.FileActivity; @@ -52,8 +51,10 @@ public class ShareeListAdapter extends RecyclerView.Adapter shares, @@ -61,8 +62,7 @@ public ShareeListAdapter(FileActivity fileActivity, String userId, User user, final ViewThemeUtils viewThemeUtils, - boolean encrypted, - SharesType sharesType) { + boolean encrypted) { this.fileActivity = fileActivity; this.shares = shares; this.listener = listener; @@ -70,7 +70,6 @@ public ShareeListAdapter(FileActivity fileActivity, this.user = user; this.viewThemeUtils = viewThemeUtils; this.encrypted = encrypted; - this.sharesType = sharesType; avatarRadiusDimension = fileActivity.getResources().getDimension(R.dimen.user_icon_radius); @@ -114,7 +113,7 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int switch (ShareType.fromValue(viewType)) { case PUBLIC_LINK, EMAIL -> { final var binding = FileDetailsShareLinkShareItemBinding.inflate(parentViewGroup, parent, false); - return new LinkShareViewHolder(binding, fileActivity, viewThemeUtils, encrypted); + return new LinkShareViewHolder(binding, fileActivity, viewThemeUtils, encrypted, isTextFile); } case NEW_PUBLIC_LINK -> { if (encrypted) { @@ -226,6 +225,10 @@ public boolean shouldCallGeneratedCallback(String tag, Object callContext) { return false; } + public void setTextFile(boolean textFile) { + isTextFile = textFile; + } + public void remove(OCShare share) { int position = shares.indexOf(share); if (position != -1) { @@ -254,13 +257,6 @@ protected final void sortShares() { shares = links; shares.addAll(users); - - // add internal share link at end - if (!encrypted && sharesType == SharesType.INTERNAL) { - final OCShare ocShare = new OCShare(); - ocShare.setShareType(ShareType.INTERNAL); - shares.add(ocShare); - } } public List getShares() { diff --git a/app/src/main/java/com/owncloud/android/ui/dialog/SendShareDialog.kt b/app/src/main/java/com/owncloud/android/ui/dialog/SendShareDialog.kt index c45d2f854fe1..29d8a2b45e0c 100644 --- a/app/src/main/java/com/owncloud/android/ui/dialog/SendShareDialog.kt +++ b/app/src/main/java/com/owncloud/android/ui/dialog/SendShareDialog.kt @@ -23,7 +23,6 @@ import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.snackbar.Snackbar -import com.nextcloud.android.common.ui.theme.utils.ColorRole import com.nextcloud.client.di.Injectable import com.nextcloud.client.utils.IntentUtil.createSendIntent import com.nextcloud.utils.extensions.getParcelableArgument @@ -81,7 +80,6 @@ class SendShareDialog : BottomSheetDialogFragment(R.layout.send_share_fragment), binding.btnLink.visibility = View.GONE } - applyTintColor() setupBottomSheetBehaviour() checkButtonVisibilities() setupSendButtonRecyclerView() @@ -109,12 +107,6 @@ class SendShareDialog : BottomSheetDialogFragment(R.layout.send_share_fragment), bottomSheetDialog.behavior.skipCollapsed = true } - private fun applyTintColor() { - viewThemeUtils?.material?.colorMaterialButtonPrimaryFilled(binding.btnLink) - viewThemeUtils?.material?.colorMaterialButtonPrimaryFilled(binding.btnShare) - viewThemeUtils?.platform?.colorViewBackground(binding.bottomSheet, ColorRole.SURFACE) - } - @Suppress("MagicNumber") private fun checkButtonVisibilities() { if (hideNcSharingOptions) { @@ -138,6 +130,9 @@ class SendShareDialog : BottomSheetDialogFragment(R.layout.send_share_fragment), } private fun shareByLink() { + // NMC Customization + isPeopleShareClicked = false + val fileOperationsHelper = (requireActivity() as FileActivity).fileOperationsHelper if (file?.isSharedViaLink == true) { @@ -222,6 +217,9 @@ class SendShareDialog : BottomSheetDialogFragment(R.layout.send_share_fragment), } private fun shareFile(file: OCFile?) { + // NMC Customization + isPeopleShareClicked = true + dismiss() if (activity is FileDisplayActivity) { @@ -248,6 +246,11 @@ class SendShareDialog : BottomSheetDialogFragment(R.layout.send_share_fragment), const val PACKAGE_NAME = "PACKAGE_NAME" const val ACTIVITY_NAME = "ACTIVITY_NAME" + // TODO: 06/21/23 remove this condition after Comments section included + // flag to avoid crash during creating new link share for a file for which link share already exist + @JvmField + var isPeopleShareClicked = false + @JvmStatic fun newInstance(file: OCFile?, hideNcSharingOptions: Boolean, capability: OCCapability): SendShareDialog { val dialogFragment = SendShareDialog() diff --git a/app/src/main/java/com/owncloud/android/ui/events/ShareSearchViewFocusEvent.kt b/app/src/main/java/com/owncloud/android/ui/events/ShareSearchViewFocusEvent.kt new file mode 100644 index 000000000000..0c695ef103b1 --- /dev/null +++ b/app/src/main/java/com/owncloud/android/ui/events/ShareSearchViewFocusEvent.kt @@ -0,0 +1,28 @@ +/* + * Nextcloud Android client application + * + * @author TSI-mc + * Copyright (C) 2022 TSI-mc + * Copyright (C) 2022 Nextcloud GmbH + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package com.owncloud.android.ui.events + +/** + * Event for search view focus while sharing a file/folder + * this event will be used to hide the view only for landscape mode so that user will have more space + */ +class ShareSearchViewFocusEvent(val hasFocus: Boolean) \ No newline at end of file diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java index 3ad6b09a1386..d843a8777ee6 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailFragment.java @@ -13,6 +13,7 @@ import android.content.res.ColorStateList; import android.graphics.Bitmap; import android.graphics.Color; +import android.graphics.PorterDuff; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; @@ -22,7 +23,6 @@ import com.google.android.material.chip.Chip; import com.google.android.material.floatingactionbutton.FloatingActionButton; -import com.google.android.material.tabs.TabLayout; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.di.Injectable; @@ -34,16 +34,17 @@ import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.model.WorkerState; import com.nextcloud.model.WorkerStateLiveData; +import com.nextcloud.utils.EditorUtils; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; import com.nextcloud.utils.MenuUtils; import com.nextcloud.utils.extensions.BundleExtensionsKt; import com.nextcloud.utils.extensions.FileExtensionsKt; -import com.nextcloud.utils.mdm.MDMConfig; import com.owncloud.android.MainApp; import com.owncloud.android.R; import com.owncloud.android.databinding.FileDetailsFragmentBinding; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; +import com.owncloud.android.datamodel.SyncedFolderProvider; import com.owncloud.android.datamodel.ThumbnailsCacheManager; import com.owncloud.android.lib.common.OwnCloudClient; import com.owncloud.android.lib.common.operations.RemoteOperationResult; @@ -55,13 +56,14 @@ import com.owncloud.android.ui.activity.DrawerActivity; import com.owncloud.android.ui.activity.FileDisplayActivity; import com.owncloud.android.ui.activity.ToolbarActivity; -import com.owncloud.android.ui.adapter.FileDetailTabAdapter; import com.owncloud.android.ui.adapter.progressListener.DownloadProgressListener; import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment; import com.owncloud.android.ui.dialog.RenameFileDialogFragment; +import com.owncloud.android.ui.dialog.SendShareDialog; import com.owncloud.android.ui.events.FavoriteEvent; +import com.owncloud.android.ui.events.ShareSearchViewFocusEvent; +import com.owncloud.android.ui.fragment.util.SharePermissionManager; import com.owncloud.android.utils.DisplayUtils; -import com.owncloud.android.utils.EncryptionUtils; import com.owncloud.android.utils.MimeTypeUtil; import com.owncloud.android.utils.theme.ViewThemeUtils; @@ -81,7 +83,6 @@ import androidx.annotation.Nullable; import androidx.core.content.res.ResourcesCompat; import androidx.fragment.app.FragmentManager; -import androidx.viewpager2.widget.ViewPager2; /** * This Fragment is used to display the details about a file. @@ -90,6 +91,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener, private static final String TAG = FileDetailFragment.class.getSimpleName(); private static final String FTAG_CONFIRMATION = "REMOVE_CONFIRMATION_FRAGMENT"; static final String FTAG_RENAME_FILE = "RENAME_FILE_FRAGMENT"; + private static final String FTAG_SHARING = "SHARING_DETAILS_FRAGMENT"; private static final String ARG_FILE = "FILE"; private static final String ARG_PARENT_FOLDER = "PARENT_FOLDER"; @@ -112,6 +114,8 @@ public class FileDetailFragment extends FileFragment implements OnClickListener, @Inject FileDataStorageManager storageManager; @Inject ViewThemeUtils viewThemeUtils; @Inject BackgroundJobManager backgroundJobManager; + @Inject EditorUtils editorUtils; + @Inject SyncedFolderProvider syncedFolderProvider; /** * Public factory method to create new FileDetailFragment instances. @@ -173,12 +177,7 @@ public FileDetailSharingFragment getFileDetailSharingFragment() { if (binding == null) { return null; } - - if (binding.pager.getAdapter() instanceof FileDetailTabAdapter adapter) { - return adapter.getFileDetailSharingFragment(); - } - - return null; + return (FileDetailSharingFragment)requireActivity().getSupportFragmentManager().findFragmentByTag(FTAG_SHARING); } /** @@ -187,10 +186,6 @@ public FileDetailSharingFragment getFileDetailSharingFragment() { * @return reference to the {@link FileDetailActivitiesFragment} */ public FileDetailActivitiesFragment getFileDetailActivitiesFragment() { - if (binding.pager.getAdapter() instanceof FileDetailTabAdapter adapter) { - return adapter.getFileDetailActivitiesFragment(); - } - return null; } @@ -283,12 +278,11 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat } } - @Override - public void onDestroy() { - if (getActivity() instanceof DrawerActivity drawerActivity) { - drawerActivity.showBottomNavigationBar(true); - } - super.onDestroy(); + private void replaceSharingFragment() { + requireActivity().getSupportFragmentManager().beginTransaction() + .replace(R.id.sharing_frame_container, + FileDetailSharingFragment.newInstance(getFile(), user), + FTAG_SHARING).commit(); } private void onOverflowIconClicked() { @@ -320,82 +314,6 @@ private void onOverflowIconClicked() { .show(fragmentManager, "actions"); } - private void setupViewPager() { - binding.tabLayout.removeAllTabs(); - - binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.drawer_item_activities).setIcon(R.drawable.ic_activity)); - - - if (showSharingTab()) { - binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.share_dialog_title).setIcon(R.drawable.shared_via_users)); - } - - if (MimeTypeUtil.isImage(getFile())) { - binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.filedetails_details).setIcon(R.drawable.image_32dp)); - } - - viewThemeUtils.material.themeTabLayout(binding.tabLayout); - - final FileDetailTabAdapter adapter = new FileDetailTabAdapter(requireActivity(), - getFile(), - user, - showSharingTab()); - binding.pager.setAdapter(adapter); - - binding.pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - final FileDetailActivitiesFragment fragment = getFileDetailActivitiesFragment(); - if (activeTab == 0 && fragment != null) { - fragment.markCommentsAsRead(); - } - super.onPageScrolled(position, positionOffset, positionOffsetPixels); - } - - @Override - public void onPageSelected(int position) { - super.onPageSelected(position); - if (binding != null) { - final var tab = binding.tabLayout.getTabAt(position); - if (tab != null) { - tab.select(); - } - } - } - }); - - binding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { - @Override - public void onTabSelected(TabLayout.Tab tab) { - binding.pager.setCurrentItem(tab.getPosition()); - if (tab.getPosition() == 0) { - final FileDetailActivitiesFragment fragment = getFileDetailActivitiesFragment(); - if (fragment != null) { - fragment.markCommentsAsRead(); - } - } - } - - @Override - public void onTabUnselected(TabLayout.Tab tab) { - // unused at the moment - } - - @Override - public void onTabReselected(TabLayout.Tab tab) { - // unused at the moment - } - }); - - binding.tabLayout.post(() -> { - if (binding != null) { - TabLayout.Tab tab = binding.tabLayout.getTabAt(activeTab); - if (tab == null) return; - tab.select(); - } - }); - } - @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); @@ -600,7 +518,8 @@ public void updateFileDetails(boolean transferring, boolean refresh) { fabMain.hide(); } - binding.syncBlock.setVisibility(file.isFolder() ? View.VISIBLE : View.GONE); + // NMC: not required + /* binding.syncBlock.setVisibility(file.isFolder() ? View.VISIBLE : View.GONE); if (file.isInternalFolderSync()) { binding.folderSyncButton.setChecked(file.isInternalFolderSync()); @@ -609,10 +528,13 @@ public void updateFileDetails(boolean transferring, boolean refresh) { binding.folderSyncButton.setChecked(true); binding.folderSyncButton.setEnabled(false); } - } + }*/ } - setupViewPager(); + // TODO: 06/21/23 remove this condition after Comments section included + if (SendShareDialog.isPeopleShareClicked) { + replaceSharingFragment(); + } final var view = getView(); if (view != null) { view.invalidate(); @@ -644,13 +566,19 @@ private void setFileModificationTimestamp(OCFile file, boolean showDetailedTimes private void setFavoriteIconStatus(boolean isFavorite) { if (isFavorite) { - binding.favorite.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_star, null)); + binding.favorite.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.favorite, null)); binding.favorite.setContentDescription(getString(R.string.unset_favorite)); } else { binding.favorite.setImageDrawable(ResourcesCompat.getDrawable(getResources(), R.drawable.ic_star_outline, null)); binding.favorite.setContentDescription(getString(R.string.favorite)); + + //NMC Customization + binding.favorite.getDrawable().mutate().setColorFilter(requireContext() + .getResources() + .getColor(R.color.list_item_lastmod_and_filesize_text, null), + PorterDuff.Mode.SRC_IN); } } @@ -830,32 +758,14 @@ public void initiateSharingProcess(String shareeName, return; } - final var fileShareDetailFragment = FileDetailsSharingProcessFragment.newInstance(file, shareeName, shareType, secureShare); + final var fileShareDetailFragment = FileDetailsSharingProcessFragment.newInstance(file, shareeName, shareType, secureShare, SharePermissionManager.canEditFile(user, storageManager.getCapability(user), getFile(), editorUtils)); requireActivity() .getSupportFragmentManager() .beginTransaction() - .add(R.id.sharing_frame_container, fileShareDetailFragment, FileDetailsSharingProcessFragment.TAG) + .replace(R.id.sharing_frame_container, fileShareDetailFragment, FileDetailsSharingProcessFragment.TAG) + .addToBackStack(null) .commit(); - - showHideFragmentView(true); - } - - /** - * method will handle the views need to be hidden when sharing process fragment shows - * - * @param isFragmentReplaced - */ - public void showHideFragmentView(boolean isFragmentReplaced) { - binding.tabLayout.setVisibility(isFragmentReplaced ? View.GONE : View.VISIBLE); - binding.pager.setVisibility(isFragmentReplaced ? View.GONE : View.VISIBLE); - binding.sharingFrameContainer.setVisibility(isFragmentReplaced ? View.VISIBLE : View.GONE); - FloatingActionButton mFabMain = requireActivity().findViewById(R.id.fab_main); - if (isFragmentReplaced) { - mFabMain.hide(); - } else { - mFabMain.show(); - } } /** @@ -868,12 +778,13 @@ public void showHideFragmentView(boolean isFragmentReplaced) { */ public void editExistingShare(OCShare share, int screenTypePermission, boolean isReshareShown, boolean isExpiryDateShown) { - requireActivity().getSupportFragmentManager().beginTransaction().add(R.id.sharing_frame_container, + requireActivity().getSupportFragmentManager().beginTransaction().replace(R.id.sharing_frame_container, FileDetailsSharingProcessFragment.newInstance(share, screenTypePermission, isReshareShown, - isExpiryDateShown), + isExpiryDateShown, + SharePermissionManager.canEditFile(user, storageManager.getCapability(user), getFile(), editorUtils)), FileDetailsSharingProcessFragment.TAG) + .addToBackStack(null) .commit(); - showHideFragmentView(true); } @Subscribe(threadMode = ThreadMode.BACKGROUND) @@ -898,21 +809,14 @@ public void onMessageEvent(FavoriteEvent event) { } } - private boolean showSharingTab() { - if (!MDMConfig.INSTANCE.shareViaLink(requireContext()) && !MDMConfig.INSTANCE.shareViaUser(requireContext())) { - return false; - } - - if (getFile().isEncrypted()) { - if (parentFolder == null) { - parentFolder = storageManager.getFileById(getFile().getParentId()); - } - // sharing not allowed for encrypted files, thus only show first tab (activities) - // sharing not allowed for encrypted subfolders - return EncryptionUtils.supportsSecureFiledrop(getFile(), user) && !parentFolder.isEncrypted(); - } else { - // unencrypted files/folders - return true; - } + /** + * hide the view for landscape mode to have more space for the user to type in search view + * {@link FileDetailSharingFragment#scrollToSearchViewPosition(boolean)} + * + * @param event + */ + @Subscribe(threadMode = ThreadMode.MAIN) + public void onMessageEvent(ShareSearchViewFocusEvent event) { + binding.shareDetailFileContainer.setVisibility(event.getHasFocus() ? View.GONE : View.VISIBLE); } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java index bc4bc399b938..484edc783ebb 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingFragment.java @@ -16,35 +16,37 @@ import android.Manifest; import android.accounts.AccountManager; +import android.annotation.SuppressLint; import android.app.Activity; import android.app.SearchManager; import android.content.Context; import android.content.Intent; +import android.content.res.Configuration; import android.database.Cursor; +import android.graphics.Typeface; import android.graphics.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract; -import android.text.InputType; import android.text.TextUtils; +import android.text.style.StyleSpan; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import com.google.android.material.appbar.AppBarLayout; import com.nextcloud.client.account.User; import com.nextcloud.client.account.UserAccountManager; import com.nextcloud.client.di.Injectable; import com.nextcloud.client.network.ClientFactory; +import com.nextcloud.utils.EditorUtils; 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.nmc.android.utils.SearchViewThemeUtils; import com.owncloud.android.R; import com.owncloud.android.databinding.FileDetailsSharingFragmentBinding; import com.owncloud.android.datamodel.FileDataStorageManager; import com.owncloud.android.datamodel.OCFile; -import com.owncloud.android.datamodel.SharesType; import com.owncloud.android.lib.common.OwnCloudAccount; import com.owncloud.android.lib.common.operations.RemoteOperationResult; import com.owncloud.android.lib.common.utils.Log_OC; @@ -60,13 +62,17 @@ import com.owncloud.android.ui.adapter.ShareeListAdapterListener; import com.owncloud.android.ui.asynctasks.RetrieveHoverCardAsyncTask; import com.owncloud.android.ui.dialog.SharePasswordDialogFragment; +import com.owncloud.android.ui.events.ShareSearchViewFocusEvent; import com.owncloud.android.ui.fragment.util.FileDetailSharingFragmentHelper; +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; import com.owncloud.android.utils.PermissionUtil; import com.owncloud.android.utils.theme.ViewThemeUtils; +import org.greenrobot.eventbus.EventBus; + import java.util.ArrayList; import java.util.List; @@ -101,13 +107,12 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda private FileDetailsSharingFragmentBinding binding; private OnEditShareListener onEditShareListener; - - private ShareeListAdapter internalShareeListAdapter; - - private ShareeListAdapter externalShareeListAdapter; + + private boolean isSearchViewFocused; @Inject UserAccountManager accountManager; @Inject ClientFactory clientFactory; + @Inject EditorUtils editorUtils; @Inject ViewThemeUtils viewThemeUtils; @Inject UsersAndGroupsSearchConfig searchConfig; @@ -159,6 +164,7 @@ public void onActivityCreated(Bundle savedInstanceState) { refreshSharesFromDB(); } + @SuppressLint("ClickableViewAccessibility") @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { binding = FileDetailsSharingFragmentBinding.inflate(inflater, container, false); @@ -170,37 +176,38 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, String userId = accountManager.getUserData(user.toPlatformAccount(), com.owncloud.android.lib.common.accounts.AccountUtils.Constants.KEY_USER_ID); - internalShareeListAdapter = new ShareeListAdapter(fileActivity, - new ArrayList<>(), - this, - userId, - user, - viewThemeUtils, - file.isEncrypted(), - SharesType.INTERNAL); + binding.linkSharesList.setAdapter(new ShareeListAdapter(fileActivity, + new ArrayList<>(), + this, + userId, + user, + viewThemeUtils, + file.isEncrypted())); - internalShareeListAdapter.setHasStableIds(true); + binding.linkSharesList.setLayoutManager(new LinearLayoutManager(requireContext())); - binding.sharesListInternal.setAdapter(internalShareeListAdapter); + binding.sharesList.setAdapter(new ShareeListAdapter(fileActivity, + new ArrayList<>(), + this, + userId, + user, + viewThemeUtils, + file.isEncrypted())); - binding.sharesListInternal.setLayoutManager(new LinearLayoutManager(requireContext())); + binding.sharesList.setLayoutManager(new LinearLayoutManager(requireContext())); - externalShareeListAdapter = new ShareeListAdapter(fileActivity, - new ArrayList<>(), - this, - userId, - user, - viewThemeUtils, - file.isEncrypted(), - SharesType.EXTERNAL); + binding.pickContactEmailBtn.setOnClickListener(v -> checkContactPermission()); - externalShareeListAdapter.setHasStableIds(true); - - binding.sharesListExternal.setAdapter(externalShareeListAdapter); + binding.shareCreateNewLink.setOnClickListener(v -> createPublicShareLink()); - binding.sharesListExternal.setLayoutManager(new LinearLayoutManager(requireContext())); + //remove focus from search view on click of root view + binding.shareContainer.setOnClickListener(v -> binding.searchView.clearFocus()); - binding.pickContactEmailBtn.setOnClickListener(v -> checkContactPermission()); + //enable-disable scrollview scrolling + binding.fileDetailsNestedScrollView.setOnTouchListener((view1, motionEvent) -> { + //true means disable the scrolling and false means enable the scrolling + return com.nmc.android.utils.DisplayUtils.isLandscapeOrientation() && isSearchViewFocused; + }); setupView(); @@ -241,68 +248,75 @@ public void onStop() { private void setupView() { setShareWithYou(); + } - OCFile parentFile = fileDataStorageManager.getFileById(file.getParentId()); - + private void setUpSearchView() { FileDetailSharingFragmentHelper.setupSearchView( (SearchManager) fileActivity.getSystemService(Context.SEARCH_SERVICE), binding.searchView, fileActivity.getComponentName()); - viewThemeUtils.androidx.themeToolbarSearchView(binding.searchView); - viewThemeUtils.material.colorMaterialTextButton(binding.sharesListInternalShowAll); - binding.sharesListInternalShowAll.setOnClickListener(view -> { - internalShareeListAdapter.toggleShowAll(); - int textRes = internalShareeListAdapter.isShowAll() ? R.string.show_less : R.string.show_all; - binding.sharesListInternalShowAll.setText(textRes); - }); + SearchViewThemeUtils.INSTANCE.themeSearchView(requireContext(), binding.searchView); + + binding.searchView.setQueryHint(getResources().getString(R.string.share_search)); + binding.searchView.setVisibility(View.VISIBLE); + binding.pickContactEmailBtn.setVisibility(View.VISIBLE); - viewThemeUtils.material.colorMaterialTextButton(binding.sharesListExternalShowAll); - binding.sharesListExternalShowAll.setOnClickListener(view -> { - externalShareeListAdapter.toggleShowAll(); - int textRes = externalShareeListAdapter.isShowAll() ? R.string.show_less : R.string.show_all; - binding.sharesListExternalShowAll.setText(textRes); + binding.searchView.setOnQueryTextFocusChangeListener((view, hasFocus) -> { + isSearchViewFocused = hasFocus; + scrollToSearchViewPosition(false); }); - if (file.canReshare() && !FileDetailSharingFragmentHelper.isPublicShareDisabled(capabilities)) { - if (file.isEncrypted() || (parentFile != null && parentFile.isEncrypted())) { - if (file.getE2eCounter() == -1) { - // V1 cannot share - binding.searchContainer.setVisibility(View.GONE); - binding.createLink.setVisibility(View.GONE); - } else { - binding.createLink.setText(R.string.add_new_secure_file_drop); - binding.searchView.setQueryHint(getResources().getString(R.string.secure_share_search)); + } + + /** + * @param isDeviceRotated true when user rotated the device and false when user is already in landscape mode + */ + private void scrollToSearchViewPosition(boolean isDeviceRotated) { + if (com.nmc.android.utils.DisplayUtils.isLandscapeOrientation()) { + if (isSearchViewFocused) { + binding.fileDetailsNestedScrollView.post(() -> { + //ignore the warning because there can be case that the scrollview can be null + if (binding.fileDetailsNestedScrollView == null) { + return; + } - if (file.isSharedViaLink()) { - binding.searchView.setQueryHint(getResources().getString(R.string.share_not_allowed_when_file_drop)); - binding.searchView.setInputType(InputType.TYPE_NULL); - disableSearchView(binding.searchView); + //need to hide app bar to have more space in landscape mode while search view is focused + hideAppBar(); + + //send the event to hide the share top view to have more space + //need to use this here else white view will be visible for sometime + EventBus.getDefault().post(new ShareSearchViewFocusEvent(isSearchViewFocused)); + + if (isDeviceRotated) { + //during the rotation we need to use getTop() method for proper alignment of search view + //-25 just to avoid blank space at top + binding.fileDetailsNestedScrollView.smoothScrollTo(0, binding.searchView.getTop() - 20); + } else { + //when user is already in landscape mode and search view gets focus + //we need to user getBottom() method for proper alignment of search view + //-100 just to avoid blank space at top + binding.fileDetailsNestedScrollView.smoothScrollTo(0, binding.searchView.getBottom() - 100); } - } + }); } else { - binding.createLink.setText(R.string.create_link); - binding.searchView.setQueryHint(getResources().getString(R.string.share_search_internal)); + //send the event to show the share top view again + EventBus.getDefault().post(new ShareSearchViewFocusEvent(isSearchViewFocused)); } - - binding.createLink.setOnClickListener(v -> createPublicShareLink()); - } else { - binding.searchView.setQueryHint(getResources().getString(R.string.resharing_is_not_allowed)); - binding.createLink.setVisibility(View.GONE); - binding.externalSharesHeadline.setVisibility(View.GONE); - binding.searchView.setInputType(InputType.TYPE_NULL); - binding.pickContactEmailBtn.setVisibility(View.GONE); - disableSearchView(binding.searchView); - binding.createLink.setOnClickListener(null); + //in portrait mode we need to see the layout everytime + //send the event to show the share top view + EventBus.getDefault().post(new ShareSearchViewFocusEvent(false)); } - - checkShareViaUser(); } - private void checkShareViaUser() { - if (!MDMConfig.INSTANCE.shareViaUser(requireContext())) { - binding.searchContainer.setVisibility(View.GONE); + private void hideAppBar() { + if (requireActivity() instanceof FileDisplayActivity) { + AppBarLayout appBarLayout = requireActivity().findViewById(R.id.appbar); + + if (appBarLayout != null) { + appBarLayout.setExpanded(false, true); + } } } @@ -316,13 +330,54 @@ private void disableSearchView(View view) { } } + /** + * will be called from FileActivity when user is sharing from PreviewImageFragment + * + * @param shareeName + * @param shareType + */ + public void initiateSharingProcess(String shareeName, ShareType shareType, boolean secureShare) { + requireActivity().getSupportFragmentManager().beginTransaction().replace(R.id.share_fragment_container, + FileDetailsSharingProcessFragment.newInstance(file, + shareeName, + shareType, + secureShare, + SharePermissionManager.canEditFile(user, capabilities, file, editorUtils)), + FileDetailsSharingProcessFragment.TAG) + .addToBackStack(null) + .commit(); + } + + /** + * open the new sharing screen process to modify the created share this will be called from PreviewImageFragment + * + * @param share + * @param screenTypePermission + * @param isReshareShown + * @param isExpiryDateShown + */ + public void editExistingShare(OCShare share, int screenTypePermission, boolean isReshareShown, + boolean isExpiryDateShown) { + requireActivity().getSupportFragmentManager().beginTransaction().replace(R.id.share_fragment_container, + FileDetailsSharingProcessFragment.newInstance(share, screenTypePermission, isReshareShown, + isExpiryDateShown, SharePermissionManager.canEditFile(user, capabilities, file, editorUtils)), + FileDetailsSharingProcessFragment.TAG) + .addToBackStack(null) + .commit(); + } + private void setShareWithYou() { + setUpSearchView(); if (accountManager.userOwnsFile(file, user)) { - binding.sharedWithYouContainer.setVisibility(View.GONE); + binding.tvResharingInfo.setVisibility(View.GONE); + binding.tvResharingStatus.setVisibility(View.GONE); } else { - binding.sharedWithYouUsername.setText( - String.format(getString(R.string.shared_with_you_by), file.getOwnerDisplayName())); - DisplayUtils.setAvatar(user, + binding.tvResharingInfo.setText( + DisplayUtils.createTextWithSpan( + String.format(getString(R.string.resharing_user_info), file.getOwnerDisplayName()), + file.getOwnerDisplayName(), + new StyleSpan(Typeface.BOLD))); + /* DisplayUtils.setAvatar(user, file.getOwnerId(), this, getResources().getDimension( @@ -330,16 +385,27 @@ private void setShareWithYou() { getResources(), binding.sharedWithYouAvatar, getContext()); - binding.sharedWithYouAvatar.setVisibility(View.VISIBLE); - - String note = file.getNote(); + binding.sharedWithYouAvatar.setVisibility(View.VISIBLE);*/ - if (!TextUtils.isEmpty(note)) { - binding.sharedWithYouNote.setText(file.getNote()); - binding.sharedWithYouNoteContainer.setVisibility(View.VISIBLE); + if (file.canReshare()) { + binding.tvResharingStatus.setText(getResources().getString(R.string.reshare_allowed)); } else { - binding.sharedWithYouNoteContainer.setVisibility(View.GONE); + binding.orSectionLayout.setVisibility(View.GONE); + binding.linkShareSectionHeading.setVisibility(View.GONE); + binding.linkSharesList.setVisibility(View.GONE); + binding.shareCreateNewLink.setVisibility(View.GONE); + + binding.sharedWithDivider.setVisibility(View.GONE); + binding.tvYourShares.setVisibility(View.GONE); + binding.sharesList.setVisibility(View.GONE); + binding.tvEmptyShares.setVisibility(View.GONE); + + binding.tvResharingStatus.setText(getResources().getString(R.string.reshare_not_allowed)); + + disableSearchView(binding.searchContainer); } + binding.tvResharingStatus.setVisibility(View.VISIBLE); + binding.tvResharingInfo.setVisibility(View.VISIBLE); } } @@ -395,6 +461,8 @@ public void copyLink(OCShare share) { } else { ClipboardUtil.copyToClipboard(requireActivity(), share.getShareLink()); } + // NMC: send link after copying it to clipboard + sendLink(share); } } @@ -511,49 +579,64 @@ public void refreshSharesFromDB() { file = newFile; } - if (internalShareeListAdapter == null) { + ShareeListAdapter adapter = (ShareeListAdapter) binding.sharesList.getAdapter(); + + if (adapter == null) { DisplayUtils.showSnackMessage(getView(), getString(R.string.could_not_retrieve_shares)); return; } + adapter.removeAll(); - internalShareeListAdapter.removeAll(); + //update flag in adapter + adapter.setTextFile(SharePermissionManager.canEditFile(user, + capabilities, file, editorUtils)); // to show share with users/groups info List shares = fileDataStorageManager.getSharesWithForAFile(file.getRemotePath(), user.getAccountName()); - List internalShares = new ArrayList<>(); - List externalShares = new ArrayList<>(); - - for (OCShare share : shares) { - if (share.getShareType() != null) { - switch (share.getShareType()) { - case PUBLIC_LINK: - case FEDERATED_GROUP: - case FEDERATED: - case EMAIL: - externalShares.add(share); - break; - - default: - internalShares.add(share); - break; - } - } + adapter.addShares(shares); + + showHideEmailShareView(shares == null || shares.isEmpty()); + + if (FileDetailSharingFragmentHelper.isPublicShareDisabled(capabilities) || !file.canReshare()) { + return; } - - internalShareeListAdapter.addShares(internalShares); - ViewExtensionsKt.setVisibleIf(binding.sharesListInternalShowAll, internalShareeListAdapter.getShares().size() > 3); - addExternalAndPublicShares(externalShares); - ViewExtensionsKt.setVisibleIf(binding.sharesListExternalShowAll, externalShareeListAdapter.getShares().size() > 3); + ShareeListAdapter linkAdapter = (ShareeListAdapter) binding.linkSharesList.getAdapter(); + + if (linkAdapter == null) { + DisplayUtils.showSnackMessage(getView(), getString(R.string.could_not_retrieve_shares)); + return; + } + linkAdapter.getShares().clear(); + + //update flag in adapter + linkAdapter.setTextFile(SharePermissionManager.canEditFile(user, + capabilities, file, editorUtils)); + + // Get public share + List publicShares = fileDataStorageManager.getSharesByPathAndType(file.getRemotePath(), + ShareType.PUBLIC_LINK, + ""); + + linkAdapter.addShares(publicShares); + + showHideLinkShareView(publicShares == null || publicShares.isEmpty()); } - private void addExternalAndPublicShares(List 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 showHideLinkShareView(boolean isEmptyList) { + binding.linkSharesList.setVisibility(isEmptyList ? View.GONE : View.VISIBLE); + } + + private void showHideEmailShareView(boolean isEmptyList) { + binding.sharesList.setVisibility(isEmptyList ? View.GONE : View.VISIBLE); + // additional check to hide the empty shares if file cannot be shared + if (!file.canReshare()) { + binding.tvEmptyShares.setVisibility(View.GONE); + return; + } + binding.tvEmptyShares.setVisibility(isEmptyList ? View.VISIBLE : View.GONE); } private void checkContactPermission() { @@ -617,7 +700,8 @@ public void onSaveInstanceState(@NonNull Bundle outState) { @Override public void avatarGenerated(Drawable avatarDrawable, Object callContext) { - binding.sharedWithYouAvatar.setImageDrawable(avatarDrawable); + // NMC: not required + // binding.sharedWithYouAvatar.setImageDrawable(avatarDrawable); } @Override @@ -636,6 +720,11 @@ public void search(String query) { searchView.setQuery(query, true); } + @Override + public void openIn(OCShare share) { + fileOperationsHelper.sendShareFile(file, true); + } + @Override public void advancedPermissions(OCShare share) { modifyExistingShare(share, FileDetailsSharingProcessFragment.SCREEN_TYPE_PERMISSION); @@ -650,9 +739,9 @@ public void sendNewEmail(OCShare share) { public void unShare(OCShare share) { unShareWith(share); - if (binding.sharesListInternal.getAdapter() instanceof ShareeListAdapter adapter) { + if (binding.linkSharesList.getAdapter() instanceof ShareeListAdapter adapter) { adapter.remove(share); - } else if (binding.sharesListExternal.getAdapter() instanceof ShareeListAdapter adapter) { + } else if (binding.sharesList.getAdapter() instanceof ShareeListAdapter adapter) { adapter.remove(share); } else { DisplayUtils.showSnackMessage(getView(), getString(R.string.failed_update_ui)); @@ -668,11 +757,6 @@ public void sendLink(OCShare share) { } } - @Override - public void addAnotherLink(OCShare share) { - createPublicShareLink(); - } - private void modifyExistingShare(OCShare share, int screenTypePermission) { onEditShareListener.editExistingShare(share, screenTypePermission, !isReshareForbidden(share), capabilities.getVersion().isNewerOrEqual(OwnCloudVersion.nextcloud_18)); @@ -683,11 +767,6 @@ public void onQuickPermissionChanged(OCShare share, int permission) { 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 requestContactPermissionLauncher = registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { @@ -725,5 +804,16 @@ void editExistingShare(OCShare share, int screenTypePermission, boolean isReshar boolean isExpiryDateShown); void onShareProcessClosed(); + + void onLinkShareDownloadLimitFetched(long downloadLimit, long downloadCount); + } + + @Override + public void onConfigurationChanged(@NonNull Configuration newConfig) { + super.onConfigurationChanged(newConfig); + + //when user is in portrait mode and search view is focused and keyboard is open + //so when user rotate the device we have to fix the search view properly in landscape mode + scrollToSearchViewPosition(true); } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingMenuBottomSheetDialog.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingMenuBottomSheetDialog.java index 86b21f332872..0a08d6f50a78 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingMenuBottomSheetDialog.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailSharingMenuBottomSheetDialog.java @@ -57,13 +57,6 @@ protected void onCreate(Bundle savedInstanceState) { getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } - viewThemeUtils.platform.themeDialog(binding.getRoot()); - - 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(); setupClickListener(); @@ -75,12 +68,18 @@ protected void onCreate(Bundle savedInstanceState) { } private void updateUI() { + if (ocShare.isFolder()) { + binding.menuShareOpenIn.setVisibility(View.GONE); + } else { + binding.menuShareOpenIn.setVisibility(View.VISIBLE); + } + if (ocShare.getShareType() == ShareType.PUBLIC_LINK) { if (MDMConfig.INSTANCE.sendFilesSupport(getContext())) { - binding.menuShareSendLink.setVisibility(View.VISIBLE); + binding.menuShareSendNewEmail.setVisibility(View.GONE); } } else { - binding.menuShareSendLink.setVisibility(View.GONE); + binding.menuShareSendNewEmail.setVisibility(View.VISIBLE); } if (SharePermissionManager.INSTANCE.isSecureFileDrop(ocShare) && encrypted) { @@ -89,6 +88,11 @@ private void updateUI() { } private void setupClickListener() { + binding.menuShareOpenIn.setOnClickListener(v -> { + actions.openIn(ocShare); + dismiss(); + }); + binding.menuShareAdvancedPermissions.setOnClickListener(v -> { actions.advancedPermissions(ocShare); dismiss(); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingMenuBottomSheetActions.java b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingMenuBottomSheetActions.java index 241eabb1f2a7..163b66329775 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingMenuBottomSheetActions.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingMenuBottomSheetActions.java @@ -18,6 +18,10 @@ * Actions interface to be implemented by any class that makes use of {@link FileDetailSharingMenuBottomSheetDialog}. */ public interface FileDetailsSharingMenuBottomSheetActions { + /** + * open sharing options only applicable for files + */ + void openIn(OCShare share); /** * open advanced permission for selected share @@ -39,8 +43,4 @@ public interface FileDetailsSharingMenuBottomSheetActions { */ void sendLink(OCShare share); - /** - * create another link only valid for {@link ShareType#PUBLIC_LINK} - */ - void addAnotherLink(OCShare share); } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt index f63e1c6f93b9..8dfd63d31d53 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt @@ -10,17 +10,21 @@ package com.owncloud.android.ui.fragment import android.content.Context import android.content.res.Configuration import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.text.TextUtils import android.view.LayoutInflater +import android.view.MotionEvent import android.view.View import android.view.ViewGroup +import android.widget.RadioGroup 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.nmc.android.utils.CheckableThemeUtils import com.owncloud.android.R import com.owncloud.android.databinding.FileDetailsSharingProcessFragmentBinding import com.owncloud.android.datamodel.OCFile @@ -36,6 +40,7 @@ import com.owncloud.android.ui.helpers.FileOperationsHelper import com.owncloud.android.utils.ClipboardUtil import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.theme.CapabilityUtils +import com.owncloud.android.utils.KeyboardUtils import com.owncloud.android.utils.theme.ViewThemeUtils import java.text.SimpleDateFormat import java.util.Date @@ -53,7 +58,8 @@ import javax.inject.Inject class FileDetailsSharingProcessFragment : Fragment(), Injectable, - ExpirationDatePickerDialogFragment.OnExpiryDateListener { + ExpirationDatePickerDialogFragment.OnExpiryDateListener, + RadioGroup.OnCheckedChangeListener { companion object { const val TAG = "FileDetailsSharingProcessFragment" @@ -65,11 +71,11 @@ class FileDetailsSharingProcessFragment : private const val ARG_RESHARE_SHOWN = "arg_reshare_shown" private const val ARG_EXP_DATE_SHOWN = "arg_exp_date_shown" private const val ARG_SECURE_SHARE = "secure_share" + private const val ARG_IS_TEXT_FILE = "is_text_file" // 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 @@ -79,13 +85,15 @@ class FileDetailsSharingProcessFragment : file: OCFile, shareeName: String, shareType: ShareType, - secureShare: Boolean + secureShare: Boolean, + isTextFile: Boolean ): FileDetailsSharingProcessFragment { val bundle = Bundle().apply { putParcelable(ARG_OCFILE, file) putSerializable(ARG_SHARE_TYPE, shareType) putString(ARG_SHAREE_NAME, shareeName) putBoolean(ARG_SECURE_SHARE, secureShare) + putBoolean(ARG_IS_TEXT_FILE, isTextFile) } return FileDetailsSharingProcessFragment().apply { @@ -101,13 +109,15 @@ class FileDetailsSharingProcessFragment : share: OCShare, screenType: Int, isReshareShown: Boolean, - isExpirationDateShown: Boolean + isExpirationDateShown: Boolean, + isTextFile: Boolean ): FileDetailsSharingProcessFragment { val bundle = Bundle().apply { putParcelable(ARG_OCSHARE, share) putInt(ARG_SCREEN_TYPE, screenType) putBoolean(ARG_RESHARE_SHOWN, isReshareShown) putBoolean(ARG_EXP_DATE_SHOWN, isExpirationDateShown) + putBoolean(ARG_IS_TEXT_FILE, isTextFile) } return FileDetailsSharingProcessFragment().apply { @@ -119,6 +129,9 @@ class FileDetailsSharingProcessFragment : @Inject lateinit var viewThemeUtils: ViewThemeUtils + @Inject + lateinit var keyboardUtils: KeyboardUtils + private lateinit var onEditShareListener: FileDetailSharingFragment.OnEditShareListener private lateinit var binding: FileDetailsSharingProcessFragmentBinding @@ -131,11 +144,13 @@ class FileDetailsSharingProcessFragment : private var shareProcessStep = SCREEN_TYPE_PERMISSION // default screen type private var permission = OCShare.NO_PERMISSION // no permission private var chosenExpDateInMills: Long = -1 // for no expiry date + private var isTextFile: Boolean = false private var share: OCShare? = null 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 isDownloadCountFetched: Boolean = false private lateinit var capabilities: OCCapability @@ -160,9 +175,8 @@ class FileDetailsSharingProcessFragment : requireNotNull(fileActivity) { "FileActivity may not be null" } - permission = share?.permissions - ?: capabilities.defaultPermissions - ?: SharePermissionManager.getMaximumPermission(isFolder()) + // NMC customization: default read only permission + permission = share?.permissions ?: OCShare.READ_PERMISSION_FLAG } private fun initArguments() { @@ -181,6 +195,17 @@ class FileDetailsSharingProcessFragment : isReShareShown = it.getBoolean(ARG_RESHARE_SHOWN, true) isExpDateShown = it.getBoolean(ARG_EXP_DATE_SHOWN, true) isSecureShare = it.getBoolean(ARG_SECURE_SHARE, false) + isTextFile = it.getBoolean(ARG_IS_TEXT_FILE, false) + } + } + + // Updating Hide Download enable/disable on selection of FileDrop + override fun onCheckedChanged(group: RadioGroup?, checkId: Int) { + if (binding.fileDropRadioButton.id == checkId) { + binding.shareProcessHideDownloadCheckbox.isChecked = true + binding.shareProcessHideDownloadCheckbox.isEnabled = false + } else { + binding.shareProcessHideDownloadCheckbox.isEnabled = true } } @@ -194,15 +219,23 @@ class FileDetailsSharingProcessFragment : super.onViewCreated(view, savedInstanceState) if (isShareProcessStepIsPermission()) { setupUI() + setVisibilitiesOfShareOption() + toggleNextButtonAvailability(isAnyShareOptionChecked()) } else { + // NMC Customization: for note share directly enable button + toggleNextButtonAvailability(true) updateViewForNoteScreenType() } + //Set default value to 0 for download count + if (!isDownloadCountFetched) { + binding.shareProcessRemainingDownloadCountTv.text = + String.format(resources.getString(R.string.download_text), "0") + } + binding.shareRadioGroup.setOnCheckedChangeListener(this) implementClickEvents() setCheckboxStates() themeView() - setVisibilitiesOfShareOption() - toggleNextButtonAvailability(isAnyShareOptionChecked()) logShareInfo() } @@ -219,57 +252,18 @@ class FileDetailsSharingProcessFragment : private fun setVisibilitiesOfShareOption() { binding.run { - shareAllowDownloadAndSyncCheckbox.setVisibleIf(!isPublicShare()) - fileRequestRadioButton.setVisibleIf(canSetFileRequest()) + shareProcessHideDownloadCheckbox.setVisibleIf(!isPublicShare()) + fileDropRadioButton.setVisibleIf(canSetFileRequest()) } } private fun themeView() { - viewThemeUtils.platform.run { - binding.run { - colorTextView(shareProcessEditShareLink) - colorTextView(shareCustomPermissionsText) - - themeRadioButton(viewOnlyRadioButton) - themeRadioButton(canEditRadioButton) - themeRadioButton(customPermissionRadioButton) - - if (!isPublicShare()) { - themeCheckbox(shareAllowDownloadAndSyncCheckbox) - } - - if (canSetFileRequest()) { - themeRadioButton(fileRequestRadioButton) - } - - themeCheckbox(shareReadCheckbox) - themeCheckbox(shareCreateCheckbox) - themeCheckbox(shareEditCheckbox) - themeCheckbox(shareCheckbox) - themeCheckbox(shareDeleteCheckbox) - } - } - - 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) - } - } + CheckableThemeUtils.tintSwitch(binding.shareProcessSetPasswordSwitch) + CheckableThemeUtils.tintSwitch(binding.shareProcessAllowResharingCheckbox) + CheckableThemeUtils.tintSwitch(binding.shareProcessSetExpDateSwitch) + CheckableThemeUtils.tintSwitch(binding.shareProcessHideDownloadCheckbox) + CheckableThemeUtils.tintSwitch(binding.shareProcessChangeNameSwitch) + CheckableThemeUtils.tintSwitch(binding.shareProcessDownloadLimitSwitch) } override fun onConfigurationChanged(newConfig: Configuration) { @@ -287,8 +281,8 @@ class FileDetailsSharingProcessFragment : private fun setupUI() { binding.run { + viewOnlyRadioButton.isChecked = true shareProcessGroupOne.visibility = View.VISIBLE - shareProcessEditShareLink.visibility = View.VISIBLE shareProcessGroupTwo.visibility = View.GONE } @@ -296,6 +290,7 @@ class FileDetailsSharingProcessFragment : // show or hide expiry date binding.shareProcessSetExpDateSwitch.setVisibleIf(isExpDateShown && !isSecureShare) + binding.dividerSharingExpDate.setVisibleIf(isExpDateShown && !isSecureShare) shareProcessStep = SCREEN_TYPE_PERMISSION } @@ -307,21 +302,12 @@ class FileDetailsSharingProcessFragment : } } - 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() { @@ -336,33 +322,19 @@ class FileDetailsSharingProcessFragment : } private fun updateViewForUpdate() { + binding.shareProcessBtnNext.text = requireContext().resources.getString(R.string.common_confirm) + if (share?.isFolder == true) updateViewForFolder() else updateViewForFile() selectRadioButtonAccordingToPermission() - if (isShareProcessStepIsCustomPermission()) { - selectCustomPermissionLayout() - } - 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 (isPublicShare()) { - R.string.share_copy_link - } else { - R.string.common_confirm - } - ) - updateViewForShareType() binding.shareProcessSetPasswordSwitch.isChecked = share?.isPasswordProtected == true showPasswordInput(binding.shareProcessSetPasswordSwitch.isChecked) updateExpirationDateView() showExpirationDateInput(binding.shareProcessSetExpDateSwitch.isChecked) - updateFileDownloadLimitView() - showFileDownloadLimitInput(binding.shareProcessSetDownloadLimitSwitch.isChecked) } private fun selectRadioButtonAccordingToPermission() { @@ -378,11 +350,7 @@ class FileDetailsSharingProcessFragment : } QuickPermissionType.FILE_REQUEST -> { - fileRequestRadioButton.isChecked = true - } - - QuickPermissionType.CUSTOM_PERMISSIONS -> { - selectCustomPermissionLayout() + fileDropRadioButton.isChecked = true } else -> Unit @@ -390,11 +358,6 @@ class FileDetailsSharingProcessFragment : } } - private fun selectCustomPermissionLayout() { - binding.customPermissionRadioButton.isChecked = true - binding.customPermissionLayout.setVisibilityWithAnimation(true) - } - private fun updateViewForShareType() { when (shareType) { ShareType.EMAIL -> { @@ -412,54 +375,98 @@ class FileDetailsSharingProcessFragment : } private fun updateViewForExternalShare() { - binding.run { - shareProcessChangeNameSwitch.visibility = View.GONE - shareProcessChangeNameContainer.visibility = View.GONE - updateViewForExternalAndLinkShare() - } + hideLinkLabelViews() + updateViewForExternalAndLinkShare() } private fun updateViewForLinkShare() { updateViewForExternalAndLinkShare() binding.run { shareProcessChangeNameSwitch.visibility = View.VISIBLE + dividerSharingChangeName.visibility = View.VISIBLE if (share != null) { - shareProcessChangeName.setText(share?.label) + shareProcessChangeNameEt.setText(share?.label) shareProcessChangeNameSwitch.isChecked = !TextUtils.isEmpty(share?.label) } - shareReadCheckbox.isEnabled = isFolder() showChangeNameInput(shareProcessChangeNameSwitch.isChecked) } + + //download limit will only be available for files + if (share?.isFolder == false || file?.isFolder == false) { + binding.shareProcessDownloadLimitSwitch.visibility = View.VISIBLE + binding.dividerSharingDownloadLimit.visibility = View.VISIBLE + + //fetch the download limit for link share + fetchDownloadLimitForShareLink() + } else { + binding.shareProcessDownloadLimitSwitch.visibility = View.GONE + binding.dividerSharingDownloadLimit.visibility = View.GONE + } + + //the input for download limit will be hidden initially + //and can be visible back or no depending on the api result + //from the download limit api + binding.shareProcessDownloadLimitEt.visibility = View.GONE + binding.shareProcessRemainingDownloadCountTv.visibility = View.GONE } private fun updateViewForInternalShare() { + hideLinkLabelViews() binding.run { - shareProcessChangeNameSwitch.visibility = View.GONE - shareProcessChangeNameContainer.visibility = View.GONE shareProcessHideDownloadCheckbox.visibility = View.GONE - shareCheckbox.setVisibleIf(!isSecureShare) + shareProcessChangeNameSwitch.visibility = View.GONE + shareProcessAllowResharingCheckbox.setVisibleIf(!isSecureShare) shareProcessSetPasswordSwitch.visibility = View.GONE if (share != null) { if (!isReShareShown) { - shareCheckbox.visibility = View.GONE + shareProcessAllowResharingCheckbox.visibility = View.GONE } - shareCheckbox.isChecked = SharePermissionManager.canReshare(share) + shareProcessAllowResharingCheckbox.isChecked = SharePermissionManager.canReshare(share) + if (share?.isFolder == true) { + hideFileDropView() + } + } else if (file?.isFolder == true) { + hideFileDropView() } } } + private fun hideFileDropView() { + //no file drop for internal share due to 403 bad request api issue + binding.fileDropRadioButton.visibility = View.GONE + binding.shareFileDropInfo.visibility = View.GONE + } + + private fun hideLinkLabelViews() { + binding.shareProcessChangeNameSwitch.visibility = View.GONE + binding.shareProcessChangeNameEt.visibility = View.GONE + binding.dividerSharingChangeName.visibility = View.GONE + + binding.shareProcessDownloadLimitSwitch.visibility = View.GONE + binding.shareProcessDownloadLimitEt.visibility = View.GONE + binding.shareProcessRemainingDownloadCountTv.visibility = View.GONE + binding.dividerSharingDownloadLimit.visibility = View.GONE + } + private fun updateViewForExternalAndLinkShare() { binding.run { shareProcessHideDownloadCheckbox.visibility = View.VISIBLE - shareCheckbox.visibility = View.GONE + dividerSharingHideDownload.visibility = View.VISIBLE + shareProcessAllowResharingCheckbox.visibility = View.GONE + shareProcessAllowResharingInfo.visibility = View.GONE + dividerSharingAllowResharing.visibility = View.GONE shareProcessSetPasswordSwitch.visibility = View.VISIBLE + dividerSharingEnterPassword.visibility = View.VISIBLE if (share != null) { if (SharePermissionManager.isFileRequest(share)) { - shareProcessHideDownloadCheckbox.visibility = View.GONE + shareProcessHideDownloadCheckbox.isChecked = true + shareProcessHideDownloadCheckbox.isEnabled = false + dividerSharingHideDownload.visibility = View.GONE } else { - shareProcessHideDownloadCheckbox.visibility = View.VISIBLE + shareProcessHideDownloadCheckbox.isEnabled = true + dividerSharingHideDownload.visibility = View.VISIBLE shareProcessHideDownloadCheckbox.isChecked = share?.isHideFileDownload == true } } @@ -479,31 +486,23 @@ class FileDetailsSharingProcessFragment : } } - private fun updateFileDownloadLimitView() { - if (canSetDownloadLimit()) { - binding.shareProcessSetDownloadLimitSwitch.visibility = View.VISIBLE - - val currentDownloadLimit = share?.fileDownloadLimit?.limit ?: capabilities.filesDownloadLimitDefault - if (currentDownloadLimit > 0) { - binding.shareProcessSetDownloadLimitSwitch.isChecked = true - showFileDownloadLimitInput(true) - binding.shareProcessSetDownloadLimitInput.setText("$currentDownloadLimit") - } - } - } - private fun updateViewForFile() { binding.run { canEditRadioButton.text = getString(R.string.link_share_editing) + fileDropRadioButton.visibility = View.GONE + shareFileDropInfo.visibility = View.GONE } } private fun updateViewForFolder() { binding.run { - canEditRadioButton.text = getString(R.string.share_permission_can_edit) - + canEditRadioButton.text = getString(R.string.link_share_allow_upload_and_editing) + fileDropRadioButton.visibility = View.VISIBLE + shareFileDropInfo.visibility = View.VISIBLE if (isSecureShare) { - shareCheckbox.visibility = View.GONE + fileDropRadioButton.visibility = View.GONE + shareFileDropInfo.visibility = View.GONE + shareProcessAllowResharingCheckbox.visibility = View.GONE shareProcessSetExpDateSwitch.visibility = View.GONE } } @@ -512,17 +511,15 @@ class FileDetailsSharingProcessFragment : 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) + shareProcessBtnNext.text = getString(R.string.send_email) noteText.setText(share?.note) } else { shareProcessBtnNext.text = getString(R.string.send_share) noteText.setText(R.string.empty) } shareProcessStep = SCREEN_TYPE_NOTE - shareProcessBtnNext.performClick() } } // endregion @@ -546,15 +543,22 @@ class FileDetailsSharingProcessFragment : shareProcessSetExpDateSwitch.setOnCheckedChangeListener { _, isChecked -> showExpirationDateInput(isChecked) } - shareProcessSetDownloadLimitSwitch.setOnCheckedChangeListener { _, isChecked -> - showFileDownloadLimitInput(isChecked) - } shareProcessChangeNameSwitch.setOnCheckedChangeListener { _, isChecked -> showChangeNameInput(isChecked) } shareProcessSelectExpDate.setOnClickListener { showExpirationDateDialog() } + shareProcessDownloadLimitSwitch.setOnCheckedChangeListener { _, isChecked -> + showDownloadLimitInput(isChecked) + } + noteText.setOnTouchListener { view, event -> + view.parent.requestDisallowInterceptTouchEvent(true) + if ((event.action and MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) { + view.parent.requestDisallowInterceptTouchEvent(false) + } + return@setOnTouchListener false + } // region RadioButtons shareRadioGroup.setOnCheckedChangeListener { _, optionId -> @@ -567,14 +571,13 @@ class FileDetailsSharingProcessFragment : permission = SharePermissionManager.getMaximumPermission(isFolder()) } - R.id.file_request_radio_button -> { + R.id.file_drop_radio_button -> { permission = OCShare.CREATE_PERMISSION_FLAG } } - val isCustomPermissionSelected = (optionId == R.id.custom_permission_radio_button) - customPermissionLayout.setVisibilityWithAnimation(isCustomPermissionSelected) - toggleNextButtonAvailability(true) + // NMC customization: after every permission check toggle allow reshare + togglePermission(binding.shareProcessAllowResharingCheckbox.isChecked, OCShare.SHARE_PERMISSION_FLAG) } // endregion } @@ -582,20 +585,10 @@ class FileDetailsSharingProcessFragment : 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 - } + fileDropRadioButton.isChecked + } || permission != OCShare.NO_PERMISSION } private fun toggleNextButtonAvailability(value: Boolean) { @@ -611,25 +604,10 @@ class FileDetailsSharingProcessFragment : 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 - } - } + shareProcessAllowResharingCheckbox.isChecked = hasPermission(currentPermissions, OCShare.SHARE_PERMISSION_FLAG) if (!isPublicShare()) { - shareAllowDownloadAndSyncCheckbox.isChecked = isAllowDownloadAndSyncEnabled(share) + shareProcessHideDownloadCheckbox.isChecked = isAllowDownloadAndSyncEnabled(share) } } } @@ -639,11 +617,7 @@ class FileDetailsSharingProcessFragment : 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 + binding.shareProcessAllowResharingCheckbox to OCShare.SHARE_PERMISSION_FLAG, ) checkboxes.forEach { (checkbox, flag) -> @@ -651,7 +625,7 @@ class FileDetailsSharingProcessFragment : } if (!isPublicShare()) { - binding.shareAllowDownloadAndSyncCheckbox.setOnCheckedChangeListener { _, isChecked -> + binding.shareProcessHideDownloadCheckbox.setOnCheckedChangeListener { _, isChecked -> val result = SharePermissionManager.toggleAllowDownloadAndSync(isChecked, share) share?.attributes = result downloadAttribute = result @@ -677,14 +651,31 @@ class FileDetailsSharingProcessFragment : } private fun showChangeNameInput(isChecked: Boolean) { - binding.shareProcessChangeNameContainer.setVisibleIf(isChecked) + binding.shareProcessChangeNameEt.setVisibleIf(isChecked) + + if (!isChecked) { + binding.shareProcessChangeNameEt.setText(R.string.empty) + // hide keyboard when user unchecks + hideKeyboard() + } + } + private fun showDownloadLimitInput(isChecked: Boolean) { + binding.shareProcessDownloadLimitEt.visibility = if (isChecked) View.VISIBLE else View.GONE + binding.shareProcessRemainingDownloadCountTv.visibility = if (isChecked) View.VISIBLE else View.GONE if (!isChecked) { - binding.shareProcessChangeName.setText(R.string.empty) + binding.shareProcessDownloadLimitEt.setText(R.string.empty) + if (!isDownloadCountFetched) { + binding.shareProcessRemainingDownloadCountTv.text = String.format(resources.getString(R.string.download_text), "0") + } + //hide keyboard when user unchecks + hideKeyboard() } } private fun onCancelClick() { + // hide keyboard when user clicks cancel button + hideKeyboard() // if modifying the existing share then on back press remove the current fragment if (share != null) { removeCurrentFragment() @@ -705,6 +696,13 @@ class FileDetailsSharingProcessFragment : binding.shareProcessSelectExpDate.setVisibleIf(isChecked) binding.shareProcessExpDateDivider.setVisibleIf(isChecked) + //update margin of divider when switch is enabled/disabled + val margin = if (isChecked) requireContext().resources.getDimensionPixelOffset(R.dimen.standard_half_margin) + else 0 + val param = binding.dividerSharingExpDate.layoutParams as ViewGroup.MarginLayoutParams + param.setMargins(0, margin, 0, 0) + binding.dividerSharingExpDate.layoutParams = param + // reset the expiration date if switch is unchecked if (!isChecked) { chosenExpDateInMills = -1 @@ -712,27 +710,29 @@ class FileDetailsSharingProcessFragment : } } - private fun showFileDownloadLimitInput(isChecked: Boolean) { - binding.shareProcessSetDownloadLimitInputContainer.setVisibleIf(isChecked) - - // reset download limit if switch is unchecked - if (!isChecked) { - binding.shareProcessSetDownloadLimitInput.setText(R.string.empty) - } - } - private fun showPasswordInput(isChecked: Boolean) { - binding.shareProcessEnterPasswordContainer.setVisibleIf(isChecked) + binding.shareProcessEnterPassword.setVisibleIf(isChecked) // reset the password if switch is unchecked if (!isChecked) { binding.shareProcessEnterPassword.setText(R.string.empty) + // hide keyboard when user unchecks + hideKeyboard() + } + } + + private fun hideKeyboard() { + if (this::binding.isInitialized) { + keyboardUtils.hideKeyboardFrom(requireContext(), binding.root) } } + /** + * remove the fragment and pop it from backstack because we are adding it to backstack + */ private fun removeCurrentFragment() { - onEditShareListener.onShareProcessClosed() - fileActivity?.supportFragmentManager?.beginTransaction()?.remove(this)?.commit() + requireActivity().supportFragmentManager.beginTransaction().remove(this).commit() + requireActivity().supportFragmentManager.popBackStack() } /** @@ -740,6 +740,7 @@ class FileDetailsSharingProcessFragment : */ @Suppress("ReturnCount") private fun validateShareProcessFirst() { + hideKeyboard() if (permission == OCShare.NO_PERMISSION) { DisplayUtils.showSnackMessage(binding.root, R.string.no_share_permission_selected) return @@ -760,12 +761,23 @@ class FileDetailsSharingProcessFragment : } if (binding.shareProcessChangeNameSwitch.isChecked && - binding.shareProcessChangeName.text?.trim().isNullOrEmpty() + binding.shareProcessChangeNameEt.text?.trim().isNullOrEmpty() ) { DisplayUtils.showSnackMessage(binding.root, R.string.label_empty) return } + if (binding.shareProcessDownloadLimitSwitch.isChecked) { + val downloadLimit = binding.shareProcessDownloadLimitEt.text?.trim() + if (downloadLimit.isNullOrEmpty()) { + DisplayUtils.showSnackMessage(binding.root, R.string.download_limit_empty) + return + } else if (downloadLimit.toString().toLong() <= 0) { + DisplayUtils.showSnackMessage(binding.root, R.string.download_limit_zero) + return + } + } + // if modifying existing share information then execute the process if (share != null) { updateShare() @@ -820,32 +832,17 @@ class FileDetailsSharingProcessFragment : binding.shareProcessHideDownloadCheckbox.isChecked, binding.shareProcessEnterPassword.text.toString().trim(), chosenExpDateInMills, - binding.shareProcessChangeName.text.toString().trim() - ) - - if (canSetDownloadLimit()) { - setDownloadLimit() - } - - // copy the share link if available - if (!TextUtils.isEmpty(share?.shareLink)) { - ClipboardUtil.copyToClipboard(requireActivity(), share?.shareLink) - } - } - - private fun setDownloadLimit() { - val downloadLimitInput = binding.shareProcessSetDownloadLimitInput.text.toString().trim() - val downloadLimit = - if (binding.shareProcessSetDownloadLimitSwitch.isChecked && downloadLimitInput.isNotEmpty()) { - downloadLimitInput.toInt() - } else { - 0 + binding.shareProcessChangeNameEt.text.toString().trim(), + binding.shareProcessDownloadLimitEt.text.toString().trim() + ) + // copy the share link if available + if (!TextUtils.isEmpty(share?.shareLink)) { + ClipboardUtil.copyToClipboard(requireActivity(), share?.shareLink) } - - fileOperationsHelper?.updateFilesDownloadLimit(share, downloadLimit) - } + } private fun createShare(noteText: String) { + hideKeyboard() fileOperationsHelper?.shareFileWithSharee( file, shareeName, @@ -856,11 +853,24 @@ class FileDetailsSharingProcessFragment : chosenExpDateInMills, noteText, downloadAttribute, - binding.shareProcessChangeName.text.toString().trim(), + binding.shareProcessChangeNameEt.text.toString().trim(), true ) } + /** + * fetch the download limit for the link share + * the response will be received in FileActivity --> onRemoteOperationFinish() method + */ + private fun fetchDownloadLimitForShareLink() { + //need to call this method in handler else to show progress dialog it will throw exception + Handler(Looper.getMainLooper()).post { + share?.let { + fileOperationsHelper?.getShareDownloadLimit(it.token) + } + } + } + /** * method will be called from DrawerActivity on back press to handle screen backstack */ @@ -880,14 +890,20 @@ class FileDetailsSharingProcessFragment : binding.shareProcessSetExpDateSwitch.isChecked = false } + /** + * will be called when download limit is fetched + */ + fun onLinkShareDownloadLimitFetched(downloadLimit: Long, downloadCount: Long) { + binding.shareProcessDownloadLimitSwitch.isChecked = downloadLimit > 0 + showDownloadLimitInput(binding.shareProcessDownloadLimitSwitch.isChecked) + binding.shareProcessDownloadLimitEt.setText(if (downloadLimit > 0) downloadLimit.toString() else "") + binding.shareProcessRemainingDownloadCountTv.text = String.format(resources.getString(R.string.download_text), downloadCount.toString()) + isDownloadCountFetched = true + } + // region Helpers private fun isShareProcessStepIsPermission(): Boolean = ( - shareProcessStep == SCREEN_TYPE_PERMISSION || - isShareProcessStepIsCustomPermission() - ) - - private fun isShareProcessStepIsCustomPermission(): Boolean = - (shareProcessStep == SCREEN_TYPE_PERMISSION_WITH_CUSTOM_PERMISSION) + shareProcessStep == SCREEN_TYPE_PERMISSION) private fun isShareProcessStepIsNote(): Boolean = (shareProcessStep == SCREEN_TYPE_NOTE) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java index 5caa73a41921..18ad6f8ce833 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/OCFileListFragment.java @@ -96,6 +96,7 @@ import com.owncloud.android.ui.dialog.RenameFileDialogFragment; import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment; import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment; +import com.owncloud.android.ui.dialog.SendShareDialog; import com.owncloud.android.ui.events.ChangeMenuEvent; import com.owncloud.android.ui.events.CommentsEvent; import com.owncloud.android.ui.events.EncryptionEvent; @@ -636,6 +637,8 @@ public void createRichWorkspace() { @Override public void onShareIconClick(OCFile file) { + //NMC Customization + SendShareDialog.isPeopleShareClicked = true; if (file.isFolder()) { mContainerActivity.showDetails(file, 1); } else { @@ -1368,6 +1371,8 @@ public boolean onFileActionChosen(@IdRes final int itemId, Set checkedFi OCFile singleFile = checkedFiles.iterator().next(); if (itemId == R.id.action_send_share_file) { + //NMC Customization + SendShareDialog.isPeopleShareClicked = true; mContainerActivity.getFileOperationsHelper().sendShareFile(singleFile); return true; } else if (itemId == R.id.action_open_file_with) { @@ -1396,7 +1401,8 @@ public boolean onFileActionChosen(@IdRes final int itemId, Set checkedFi if (mActiveActionMode != null) { mActiveActionMode.finish(); } - + //NMC Customization + SendShareDialog.isPeopleShareClicked = true; mContainerActivity.showDetails(singleFile); mContainerActivity.showSortListGroup(false); return true; diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/QuickSharingPermissionsBottomSheetDialog.java b/app/src/main/java/com/owncloud/android/ui/fragment/QuickSharingPermissionsBottomSheetDialog.java index f662522ccfb2..ff03f177bf5d 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/QuickSharingPermissionsBottomSheetDialog.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/QuickSharingPermissionsBottomSheetDialog.java @@ -33,9 +33,8 @@ import androidx.recyclerview.widget.LinearLayoutManager; 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 static com.owncloud.android.lib.resources.shares.OCShare.SHARE_PERMISSION_FLAG; /** * File Details Quick Sharing permissions options {@link Dialog} styled as a bottom sheet for main actions. @@ -71,8 +70,6 @@ protected void onCreate(Bundle savedInstanceState) { getWindow().setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); } - viewThemeUtils.platform.themeDialog(binding.getRoot()); - setUpRecyclerView(); setOnShowListener(d -> BottomSheetBehavior.from((View) binding.getRoot().getParent()) @@ -87,8 +84,8 @@ private void setUpRecyclerView() { new QuickSharingPermissionsAdapter.QuickSharingPermissionViewHolder.OnPermissionChangeListener() { @Override public void onCustomPermissionSelected() { + // NMC Customizations: No action will be required dismiss(); - actions.openShareDetailWithCustomPermissions(ocShare); } @Override @@ -117,13 +114,19 @@ private void handlePermissionChanged(List quickPermissionList, 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 = SharePermissionManager.INSTANCE.getMaximumPermission(ocShare.isFolder()); + } else if (permissionName.equalsIgnoreCase(res.getString(R.string.share_permission_read_only))) { permissionFlag = READ_PERMISSION_FLAG; - } else if (permissionName.equalsIgnoreCase(res.getString(R.string.share_permission_file_request))) { + } else if (permissionName.equalsIgnoreCase(res.getString(R.string.share_permission_file_drop))) { permissionFlag = CREATE_PERMISSION_FLAG + READ_PERMISSION_FLAG; } + // NMC Customization: after permission change check if share already has reshare allowed + // if allowed then toggle permission flag + if (SharePermissionManager.INSTANCE.canReshare(ocShare)) { + permissionFlag = SharePermissionManager.INSTANCE.togglePermission(true, permissionFlag, SHARE_PERMISSION_FLAG); + } + actions.onQuickPermissionChanged(ocShare, permissionFlag); dismiss(); @@ -146,7 +149,5 @@ protected void onStop() { public interface QuickPermissionSharingBottomSheetActions { void onQuickPermissionChanged(OCShare share, int permission); - - void openShareDetailWithCustomPermissions(OCShare share); } } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/util/SharePermissionManager.kt b/app/src/main/java/com/owncloud/android/ui/fragment/util/SharePermissionManager.kt index 14e8a7dbddc0..34bcda74b8ec 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/util/SharePermissionManager.kt +++ b/app/src/main/java/com/owncloud/android/ui/fragment/util/SharePermissionManager.kt @@ -7,16 +7,30 @@ package com.owncloud.android.ui.fragment.util +import android.content.Context +import com.nextcloud.client.account.User +import com.nextcloud.utils.EditorUtils +import com.owncloud.android.R +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.attributes.ShareAttributes import com.owncloud.android.lib.resources.shares.attributes.ShareAttributesJsonHandler import com.owncloud.android.lib.resources.shares.attributes.getDownloadAttribute +import com.owncloud.android.lib.resources.status.OCCapability import com.owncloud.android.ui.fragment.FileDetailsSharingProcessFragment.Companion.TAG object SharePermissionManager { + // NMC Customization + // updated Edit permissions for folder and files + // because the MAXIMUM_PERMISSIONS_FOR_FILE and MAXIMUM_PERMISSIONS_FOR_FOLDER permission in OCShare + // are not valid due to functionality changes + const val CAN_EDIT_PERMISSIONS_FOR_FILE = OCShare.READ_PERMISSION_FLAG + OCShare.UPDATE_PERMISSION_FLAG + const val CAN_EDIT_PERMISSIONS_FOR_FOLDER = + CAN_EDIT_PERMISSIONS_FOR_FILE + OCShare.CREATE_PERMISSION_FLAG + OCShare.DELETE_PERMISSION_FLAG + // region Permission change fun togglePermission(isChecked: Boolean, permission: Int, permissionFlag: Int): Int { Log_OC.d(TAG, "togglePermission before: $permission") @@ -103,15 +117,23 @@ object SharePermissionManager { } fun isViewOnly(share: OCShare?): Boolean { - return share?.permissions != OCShare.NO_PERMISSION && share?.permissions == OCShare.READ_PERMISSION_FLAG + // NMC Customization: add check for Allow Reshare permission + if (share == null) { + return false + } + return share.permissions != OCShare.NO_PERMISSION && ((share.permissions and OCShare.Companion.SHARE_PERMISSION_FLAG.inv()) == OCShare.READ_PERMISSION_FLAG) } fun isFileRequest(share: OCShare?): Boolean { - if (share?.isFolder == false) { + // NMC Customization: add check for Allow Reshare permission + if (share == null) { + return false + } + if (!share.isFolder) { return false } - return share?.permissions != OCShare.NO_PERMISSION && share?.permissions == OCShare.CREATE_PERMISSION_FLAG + return share.permissions != OCShare.NO_PERMISSION && ((share.permissions and OCShare.Companion.SHARE_PERMISSION_FLAG.inv()) == OCShare.CREATE_PERMISSION_FLAG) } fun isSecureFileDrop(share: OCShare?): Boolean { @@ -167,11 +189,76 @@ object SharePermissionManager { } fun getMaximumPermission(isFolder: Boolean): Int { + // NMC Customization: for NMC no full permission is required + // we use our custom permission return if (isFolder) { - OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER + CAN_EDIT_PERMISSIONS_FOR_FOLDER } else { - OCShare.MAXIMUM_PERMISSIONS_FOR_FILE + CAN_EDIT_PERMISSIONS_FOR_FILE } } // endregion + + // NMC method + /** + * method to check if the file should not be a text file or any of the office files + * this method will be used during sharing process to disable/enable edit option + */ + @JvmStatic + fun canEditFile( + user: User, + capability: OCCapability, file: OCFile, + editorUtils: EditorUtils + ): Boolean { + //if OCFile is folder then no need to check further direct return true + //as edit permission should be available for folder restriction is only applicable for files + + if (file.isFolder) { + return true + } + + //check for text files like .md, .txt, etc + val isTextFile = editorUtils.isEditorAvailable(user, file.mimeType) && !file.isEncrypted + + //check for office files like .docx, .pptx, .xls, etc + val isOfficeFile = + capability.richDocumentsMimeTypeList != null && capability.richDocumentsMimeTypeList!!.contains(file.mimeType) && + capability.richDocumentsDirectEditing.isTrue && !file.isEncrypted + + return isTextFile || isOfficeFile + } + + // function to provide quick permission label for NMC + @JvmStatic + fun getPermissionName(context: Context, share: OCShare?): String? { + if (canEdit(share)) { + return context.resources.getString(R.string.share_quick_permission_can_edit) + } else if (isViewOnly(share)) { + return context.resources.getString(R.string.share_quick_permission_can_view) + } else if (isFileRequest(share)) { + return context.resources.getString(R.string.share_quick_permission_can_upload) + } + return null + } + + // function to provide quick permission short label for NMC + // will be used in small screen devices where text overlaps + @JvmStatic + fun getShortPermissionName(context: Context, permissionName: String): String { + return when (permissionName) { + context.resources.getString(R.string.share_quick_permission_can_edit) -> { + context.resources.getString(R.string.share_quick_permission_can_edit_short) + } + + context.resources.getString(R.string.share_quick_permission_can_view) -> { + context.resources.getString(R.string.share_quick_permission_can_view_short) + } + + context.resources.getString(R.string.share_quick_permission_can_upload) -> { + context.resources.getString(R.string.share_quick_permission_can_upload_short) + } + + else -> permissionName + } + } } diff --git a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java index 5acc339d4949..29b4cd5d7f43 100755 --- a/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java +++ b/app/src/main/java/com/owncloud/android/ui/helpers/FileOperationsHelper.java @@ -601,9 +601,10 @@ public void unShareShare(OCFile file, OCShare share) { private void queueShareIntent(Intent shareIntent) { // Unshare the file - mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(shareIntent); - - fileActivity.showLoadingDialog(fileActivity.getApplicationContext().getString(R.string.wait_a_moment)); + if(fileActivity.getOperationsServiceBinder() != null) { + mWaitingForOpId = fileActivity.getOperationsServiceBinder().queueNewOperation(shareIntent); + fileActivity.showLoadingDialog(fileActivity.getApplicationContext().getString(R.string.wait_a_moment)); + } } /** @@ -755,7 +756,8 @@ public void updateShareInformation(OCShare share, boolean hideFileDownload, String password, long expirationTimeInMillis, - String label) { + String label, + String downloadLimit) { final var id = share.getId(); final var attributes = share.getAttributes(); @@ -777,6 +779,26 @@ public void updateShareInformation(OCShare share, 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); + + //download limit for link share type + updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_DOWNLOAD_LIMIT, + (downloadLimit == null || downloadLimit.isEmpty()) ? 0 : + Long.parseLong(downloadLimit)); + + queueShareIntent(updateShareIntent); + } + + /** + * method to fetch the download limit for the particular share Note: Download limit is only for Link share type + * + * @param shareToken of the OCShare + */ + public void getShareDownloadLimit(String shareToken) { + Intent updateShareIntent = new Intent(fileActivity, OperationsService.class); + updateShareIntent.setAction(OperationsService.ACTION_GET_SHARE_DOWNLOAD_LIMIT); + updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); + updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_TOKEN, shareToken); + queueShareIntent(updateShareIntent); } diff --git a/app/src/main/java/com/owncloud/android/utils/KeyboardUtils.kt b/app/src/main/java/com/owncloud/android/utils/KeyboardUtils.kt index 08bde79d95ad..74b7548131b7 100644 --- a/app/src/main/java/com/owncloud/android/utils/KeyboardUtils.kt +++ b/app/src/main/java/com/owncloud/android/utils/KeyboardUtils.kt @@ -9,6 +9,10 @@ package com.owncloud.android.utils import android.view.Window +import android.app.Activity +import android.content.Context +import android.view.View +import android.view.inputmethod.InputMethodManager import android.widget.EditText import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -22,4 +26,10 @@ class KeyboardUtils @Inject constructor() { WindowCompat.getInsetsController(window, editText).show(WindowInsetsCompat.Type.ime()) } } + + fun hideKeyboardFrom(context: Context, view: View) { + view.clearFocus() + val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(view.windowToken, 0) + } } diff --git a/app/src/main/res/color/share_contact_icon_color.xml b/app/src/main/res/color/share_contact_icon_color.xml new file mode 100644 index 000000000000..bc179324d20c --- /dev/null +++ b/app/src/main/res/color/share_contact_icon_color.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-night/ic_internal_share.xml b/app/src/main/res/drawable-night/ic_internal_share.xml new file mode 100644 index 000000000000..1a3f4dbf20f0 --- /dev/null +++ b/app/src/main/res/drawable-night/ic_internal_share.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_calendar.xml b/app/src/main/res/drawable/ic_calendar.xml new file mode 100644 index 000000000000..708aae2ccb04 --- /dev/null +++ b/app/src/main/res/drawable/ic_calendar.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_clipboard.xml b/app/src/main/res/drawable/ic_clipboard.xml new file mode 100644 index 000000000000..9827d48041c1 --- /dev/null +++ b/app/src/main/res/drawable/ic_clipboard.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_contact_book.xml b/app/src/main/res/drawable/ic_contact_book.xml index 1ca1a279994b..9301b5665e24 100644 --- a/app/src/main/res/drawable/ic_contact_book.xml +++ b/app/src/main/res/drawable/ic_contact_book.xml @@ -7,10 +7,12 @@ - + diff --git a/app/src/main/res/drawable/ic_external_share.xml b/app/src/main/res/drawable/ic_external_share.xml new file mode 100644 index 000000000000..8c4f995ba247 --- /dev/null +++ b/app/src/main/res/drawable/ic_external_share.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_internal_share.xml b/app/src/main/res/drawable/ic_internal_share.xml new file mode 100644 index 000000000000..e6b2da36f6d0 --- /dev/null +++ b/app/src/main/res/drawable/ic_internal_share.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_open_in.xml b/app/src/main/res/drawable/ic_open_in.xml new file mode 100644 index 000000000000..de3b7b7e2cea --- /dev/null +++ b/app/src/main/res/drawable/ic_open_in.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_pencil_edit.xml b/app/src/main/res/drawable/ic_pencil_edit.xml new file mode 100644 index 000000000000..a1089345a7b3 --- /dev/null +++ b/app/src/main/res/drawable/ic_pencil_edit.xml @@ -0,0 +1,12 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_shared.xml b/app/src/main/res/drawable/ic_shared.xml new file mode 100644 index 000000000000..a7aa8ad46e52 --- /dev/null +++ b/app/src/main/res/drawable/ic_shared.xml @@ -0,0 +1,12 @@ + + + diff --git a/app/src/main/res/drawable/ic_shared_with_me.xml b/app/src/main/res/drawable/ic_shared_with_me.xml new file mode 100644 index 000000000000..24e5a42e0c32 --- /dev/null +++ b/app/src/main/res/drawable/ic_shared_with_me.xml @@ -0,0 +1,13 @@ + + + + diff --git a/app/src/main/res/drawable/ic_sharing_edit.xml b/app/src/main/res/drawable/ic_sharing_edit.xml new file mode 100644 index 000000000000..b4e40bc75bea --- /dev/null +++ b/app/src/main/res/drawable/ic_sharing_edit.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_sharing_file_drop.xml b/app/src/main/res/drawable/ic_sharing_file_drop.xml new file mode 100644 index 000000000000..0dea8a079dad --- /dev/null +++ b/app/src/main/res/drawable/ic_sharing_file_drop.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_sharing_quick_permission_arrow.xml b/app/src/main/res/drawable/ic_sharing_quick_permission_arrow.xml new file mode 100644 index 000000000000..44e00e17aabd --- /dev/null +++ b/app/src/main/res/drawable/ic_sharing_quick_permission_arrow.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_sharing_read_only.xml b/app/src/main/res/drawable/ic_sharing_read_only.xml new file mode 100644 index 000000000000..cfef11314c6d --- /dev/null +++ b/app/src/main/res/drawable/ic_sharing_read_only.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/drawable/share_et_bg.xml b/app/src/main/res/drawable/share_et_bg.xml new file mode 100644 index 000000000000..10e93e811aae --- /dev/null +++ b/app/src/main/res/drawable/share_et_bg.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/share_search_background.xml b/app/src/main/res/drawable/share_search_background.xml new file mode 100644 index 000000000000..bd0eb50d4e67 --- /dev/null +++ b/app/src/main/res/drawable/share_search_background.xml @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/sharing_email_warning_bg.xml b/app/src/main/res/drawable/sharing_email_warning_bg.xml new file mode 100644 index 000000000000..b4c4f72141cd --- /dev/null +++ b/app/src/main/res/drawable/sharing_email_warning_bg.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/layout/file_details_fragment.xml b/app/src/main/res/layout/file_details_fragment.xml index c64365632d41..38ff8167462b 100644 --- a/app/src/main/res/layout/file_details_fragment.xml +++ b/app/src/main/res/layout/file_details_fragment.xml @@ -18,6 +18,7 @@ android:orientation="vertical"> @@ -35,8 +36,8 @@ android:layout_height="wrap_content" android:ellipsize="middle" android:text="@string/placeholder_filename" - android:textColor="@color/text_color" - android:textSize="20sp" + android:textColor="@color/share_title_txt_color" + android:textSize="@dimen/txt_size_14sp" android:textStyle="bold" tools:text="@string/placeholder_filename" /> @@ -49,8 +50,8 @@ + android:textColor="@color/share_subtitle_txt_color" + android:textSize="@dimen/two_line_secondary_text_size" /> + app:iconTint="@color/grey_60" /> @@ -174,7 +176,8 @@ android:id="@+id/syncBlock" android:layout_width="match_parent" android:orientation="vertical" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:visibility="gone"> - - - - + android:background="@color/divider_color" /> diff --git a/app/src/main/res/layout/file_details_share_link_share_item.xml b/app/src/main/res/layout/file_details_share_link_share_item.xml index ea32fd599b39..ae00b18574bd 100644 --- a/app/src/main/res/layout/file_details_share_link_share_item.xml +++ b/app/src/main/res/layout/file_details_share_link_share_item.xml @@ -9,92 +9,100 @@ ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> - - + android:layout_marginTop="@dimen/standard_quarter_margin" + android:orientation="vertical"> + android:layout_width="match_parent" + android:layout_height="@dimen/sharee_list_item_size" + android:orientation="horizontal"> - + android:layout_marginStart="@dimen/standard_margin" + android:layout_marginTop="7dp" + android:layout_marginEnd="@dimen/standard_margin" + android:contentDescription="@string/share" + android:src="@drawable/shared_via_link" + android:visibility="gone" + app:tint="@color/text_color" /> - + + + + + + + + + android:textSize="@dimen/txt_size_15sp" /> - - + android:layout_height="match_parent" + android:contentDescription="@string/copy_link" + android:paddingStart="@dimen/standard_padding" + android:paddingEnd="@dimen/standard_padding" + android:scaleType="fitCenter" + android:src="@drawable/ic_share" + app:tint="@color/primary" /> - + - + + diff --git a/app/src/main/res/layout/file_details_share_public_link_add_new_item.xml b/app/src/main/res/layout/file_details_share_public_link_add_new_item.xml index 466cdcddc3d3..49b78772f9fb 100644 --- a/app/src/main/res/layout/file_details_share_public_link_add_new_item.xml +++ b/app/src/main/res/layout/file_details_share_public_link_add_new_item.xml @@ -14,8 +14,8 @@ android:orientation="horizontal"> - - + android:layout_width="match_parent" + android:layout_height="@dimen/sharee_list_item_size" + android:orientation="horizontal" + android:weightSum="1"> - + android:layout_marginStart="@dimen/standard_margin" + android:layout_marginLeft="@dimen/standard_margin" + android:layout_marginTop="@dimen/alternate_half_margin" + android:layout_marginEnd="@dimen/standard_margin" + android:layout_marginRight="@dimen/standard_margin" + android:contentDescription="@string/user_icon" + android:src="@drawable/ic_internal_share" + android:visibility="gone" + app:tint="@color/text_color" /> - + android:layout_gravity="center_vertical" + android:layout_marginStart="@dimen/standard_margin" + android:layout_weight="1" + android:orientation="vertical"> + + + + + + + - + diff --git a/app/src/main/res/layout/file_details_sharing_fragment.xml b/app/src/main/res/layout/file_details_sharing_fragment.xml index adb3a65bf77e..69dcc5d344ac 100644 --- a/app/src/main/res/layout/file_details_sharing_fragment.xml +++ b/app/src/main/res/layout/file_details_sharing_fragment.xml @@ -8,186 +8,191 @@ + android:layout_height="match_parent"> - - - - - - - - - - - - - - - - - + android:paddingRight="@dimen/standard_padding" + android:text="@string/sharing_heading" + android:textColor="@color/share_title_txt_color" + android:textSize="@dimen/txt_size_16sp" + android:textStyle="bold" /> + + + + - - + android:layout_marginTop="@dimen/standard_margin" + android:layout_marginBottom="@dimen/standard_margin" + android:gravity="center_vertical" + android:orientation="horizontal"> + android:layout_width="@dimen/share_search_height" + android:layout_height="@dimen/share_search_height" + android:layout_marginEnd="@dimen/standard_margin" + android:background="@drawable/share_search_background" + app:srcCompat="@drawable/ic_contact_book" + app:tint="@color/share_contact_icon_color" /> - + + + + + + + + + + + android:layout_marginStart="@dimen/standard_margin" + android:layout_marginTop="@dimen/standard_quarter_margin" + android:layout_marginEnd="@dimen/standard_margin" + android:text="@string/copy_link" + android:textColor="@color/text_color" + android:textSize="@dimen/txt_size_17sp" + android:textStyle="bold" /> - - + android:dividerHeight="1dp" /> + android:layout_height="@dimen/create_link_button_height" + android:layout_margin="@dimen/standard_margin" + android:text="@string/create_link" + android:textColor="@color/text_color" + android:textStyle="normal" + android:typeface="normal" /> - - + android:layout_height="0.5dp" + android:layout_marginTop="@dimen/standard_margin" + android:layout_marginBottom="@dimen/standard_margin" + android:background="@color/divider_color" /> - + android:layout_marginStart="@dimen/standard_margin" + android:layout_marginTop="@dimen/standard_quarter_margin" + android:layout_marginEnd="@dimen/standard_margin" + android:text="@string/shared_with_heading" + android:textColor="@color/text_color" + android:textSize="@dimen/txt_size_17sp" + android:textStyle="bold" /> + android:dividerHeight="1dp" /> - + - + android:layout_height="80dp" /> diff --git a/app/src/main/res/layout/file_details_sharing_menu_bottom_sheet_fragment.xml b/app/src/main/res/layout/file_details_sharing_menu_bottom_sheet_fragment.xml index f0f1543e0fc8..d4ddad829a54 100644 --- a/app/src/main/res/layout/file_details_sharing_menu_bottom_sheet_fragment.xml +++ b/app/src/main/res/layout/file_details_sharing_menu_bottom_sheet_fragment.xml @@ -12,17 +12,50 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" + android:background="@color/bottom_sheet_bg_color" android:orientation="vertical" - android:paddingTop="@dimen/dialog_padding" - android:background="@color/bg_default"> + android:paddingTop="@dimen/standard_padding"> + + + + + + + + @@ -52,7 +85,8 @@ @@ -84,7 +118,8 @@ @@ -118,7 +153,8 @@ diff --git a/app/src/main/res/layout/file_details_sharing_process_fragment.xml b/app/src/main/res/layout/file_details_sharing_process_fragment.xml index 60ac7edbebd0..1e9ebabd654a 100644 --- a/app/src/main/res/layout/file_details_sharing_process_fragment.xml +++ b/app/src/main/res/layout/file_details_sharing_process_fragment.xml @@ -6,181 +6,278 @@ ~ SPDX-FileCopyrightText: 2021 Nextcloud GmbH ~ SPDX-License-Identifier: AGPL-3.0-or-later OR GPL-2.0-only --> - - + android:layout_height="wrap_content" + android:minHeight="400dp"> + + + android:textColor="@color/share_title_txt_color" + android:textSize="@dimen/txt_size_16sp" + android:textStyle="bold" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/tv_sharing_title" /> + + + android:layout_gravity="end" + android:orientation="vertical" + android:paddingLeft="@dimen/standard_padding" + android:paddingRight="@dimen/standard_padding" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/divider_sharing_permissions"> + android:background="?android:selectableItemBackground" + android:button="@null" + android:drawableEnd="?android:attr/listChoiceIndicatorSingle" + android:text="@string/link_share_read_only" + android:textColor="@color/share_txt_color" + android:textSize="@dimen/txt_size_15sp" /> + android:background="?android:selectableItemBackground" + android:button="@null" + android:drawableEnd="?android:attr/listChoiceIndicatorSingle" + android:text="@string/link_share_allow_upload_and_editing" + android:textColor="@color/share_txt_color" + android:textSize="@dimen/txt_size_15sp" /> - - + android:background="?android:selectableItemBackground" + android:button="@null" + android:drawableEnd="?android:attr/listChoiceIndicatorSingle" + android:text="@string/link_share_file_drop" + android:textColor="@color/share_txt_color" + android:textSize="@dimen/txt_size_15sp" /> - + + + + + + - - - + android:layout_height="1dp" + android:background="@color/list_divider_background" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/share_process_hide_download_checkbox" /> - - - - - - - + - + - + - - + + - - - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/share_process_set_password_switch" + tools:visibility="visible" /> - + + android:layout_marginLeft="@dimen/standard_margin" + android:layout_marginRight="@dimen/standard_margin" + android:text="@string/share_no_expiration_date_label" + android:textColor="@color/share_txt_color" + android:textSize="@dimen/txt_size_15sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/divider_sharing_enter_password" /> + android:layout_marginLeft="@dimen/share_exp_date_divider_margin" + android:layout_marginRight="@dimen/share_exp_date_divider_margin" + android:background="@color/share_et_divider" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/share_process_select_exp_date" /> + + + android:layout_marginLeft="@dimen/standard_margin" + android:layout_marginRight="@dimen/standard_margin" + android:text="@string/link_label" + android:textColor="@color/share_txt_color" + android:textSize="@dimen/txt_size_15sp" + android:visibility="gone" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/divider_sharing_exp_date" + tools:visibility="visible" /> - - - - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/share_process_change_name_switch" + tools:visibility="visible" /> - + + + + - - - - - - - + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/share_process_download_limit_et" + tools:visibility="visible" /> - + + android:visibility="visible" + app:constraint_referenced_ids="share_process_exp_date_divider, + share_radio_group, share_process_permission_title, + share_process_advance_permission_title, share_process_hide_download_checkbox, + share_process_allow_resharing_checkbox, share_process_set_password_switch, + share_process_set_exp_date_switch, share_process_enter_password, + share_process_select_exp_date, share_process_change_name_switch, + share_process_change_name_et, divider_sharing_adv_permissions, + divider_sharing_permissions, divider_sharing_allow_resharing, + divider_sharing_change_name, divider_sharing_enter_password, + divider_sharing_exp_date,divider_sharing_hide_download, + share_file_drop_info, share_process_allow_resharing_info, + share_process_download_limit_switch, divider_sharing_download_limit, + share_process_download_limit_et,share_process_remaining_download_count_tv" /> - - + + + android:layout_marginRight="@dimen/standard_margin" + android:background="@drawable/share_et_bg" + android:gravity="top" + android:importantForAutofill="no" + android:inputType="textCapSentences|textMultiLine|textNoSuggestions" + android:overScrollMode="always" + android:padding="@dimen/standard_padding" + android:scrollbarStyle="insideInset" + android:scrollbars="vertical" + android:textColor="@color/share_txt_color" + android:textColorHighlight="@color/et_highlight_color" + android:textColorHint="@color/grey_60" + android:textSize="@dimen/txt_size_15sp" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/share_process_message_title" /> - - - + - - - - - - + android:visibility="gone" + app:constraint_referenced_ids="share_process_message_title, note_text" /> - + - + - + - + + diff --git a/app/src/main/res/layout/item_quick_share_permissions.xml b/app/src/main/res/layout/item_quick_share_permissions.xml index d40064e490be..4abbc64b3c34 100644 --- a/app/src/main/res/layout/item_quick_share_permissions.xml +++ b/app/src/main/res/layout/item_quick_share_permissions.xml @@ -2,23 +2,39 @@ - + \ No newline at end of file + android:padding="@dimen/standard_padding"> + + + + + + diff --git a/app/src/main/res/layout/list_item.xml b/app/src/main/res/layout/list_item.xml index d275c2b82932..c2f549b71491 100644 --- a/app/src/main/res/layout/list_item.xml +++ b/app/src/main/res/layout/list_item.xml @@ -235,18 +235,32 @@ android:visibility="gone" tools:visibility="visible" /> + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-de/nmc_sharing_strings.xml b/app/src/main/res/values-de/nmc_sharing_strings.xml new file mode 100644 index 000000000000..e3b9a12c02a5 --- /dev/null +++ b/app/src/main/res/values-de/nmc_sharing_strings.xml @@ -0,0 +1,55 @@ + + + Freigabe aufheben + Erstellen erlauben + Löschen erlauben + Bearbeitung erlauben + Nur Lesen + Link zum Ordner + Link zur Datei + Passwortschutz (%1$s) + Sie müssen das Ablaufdatum auswählen. + Bitte Anmerkung eingeben. + Email senden + Öffnen mit… + Sie können Links erstellen oder Freigaben per Mail versenden. Wenn Sie + MagentaCLOUD Nutzer einladen, bieten sich Ihnen mehr Möglichkeiten der Zusammenarbeit. + Ihre Nachricht + persönliche Freigabe per E-Mail + Neuen Link erstellen + Ihre Freigaben + Linkbezeichnung + Ihre Linkbezeichnung + Bei der Sammelbox ist nur das Hochladen erlaubt. Nur Sie sehen Dateien und Ordner die hochgeladen worden sind. + Password Protection + Expiration Date + Sie haben ihre Datei / ihren Ordner noch nicht geteilt. Teilen Sie um anderen Zugriff zu geben. + Download Limit + Das Feld für das Download-Limit darf nicht leer sein. + Downlimit eingeben + Downloads: %s + Der Wert für das Downloadlimit sollte größer als 0 sein. + Sie teilen mit einer/einem MagentaCLOUD Nutzer(in). Sie können ihr oder ihm erlauben, den Ordner oder die Dateien weiterzuteilen. + Der Passwortschutz ist aktiviert. Sie müssen dem Empfänger das Passwort + selbst mitteilen.\n\nWenn Sie die Freigabe über die MagentaCLOUD verschicken und das Passwort in den + Nachrichtentext eintragen, wird es unverschlüsselt im Klartext übertragen. + Empfangen + Geteilt + Link per E-Mail versenden + Weiterteilen wurde nicht erlaubt. + Weiterteilen ist erlaubt. + Dieser Datei / dieser Ordner wurde mit ihnen geteilt von %s + oder + Geteilt mit + Details + Jeder kann nur anzeigen + Nur anzeigen + Jeder kann bearbeiten + Nur bearbeiten + Jeder kann nur hochladen + Nur hochladen + Erweiterte Berechtigungen + Hochladen & Bearbeiten + Sammelbox + Weiterteilen erlauben + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 54cf74fbc08d..f31003430243 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -10,7 +10,7 @@ Bearbeiten Alle Benachrichtigungen löschen Papierkorb leeren - Senden/Teilen + Teilen Kachelansicht Listenansicht Kontakte und Kalender wiederherstellen @@ -211,7 +211,6 @@ URL konnte nicht abgerufen werden Erstellen Verzeichnis konnte nicht erstellt werden - Link erstellen Neu Neues Dokument Neuer Ordner @@ -511,7 +510,7 @@ Letzte Sicherung:%1$s Link Link-Name - Bearbeitung + Bearbeiten Layout der Liste Weitere Ergebnisse laden Es befinden sich keine Dateien in diesem Ordner. @@ -582,7 +581,7 @@ Nur ein Konto zulässig Keine App zum Öffnen von PDFs verfügbar Keine App zum Senden der ausgewählten Dateien verfügbar - Bitte mindestens eine Berechtigung zum Teilen auswählen. + Bitte wählen Sie mindestens eine Berechtigung zum Teilen aus. Notiz konnte nicht versandt werden Notiz-Symbol Aktion konnte nicht ausgeführt werden @@ -753,7 +752,7 @@ Kontolöschung anfordern Löschung anfordern Beim Diensteanbieter die dauerhafte Löschung des Kontos anfordern - Richtlinien oder Berechtigungen verhindern das Weiterteilen + Resharing/Wiederteilen ist nicht erlaubt. Datei wiederherstellen Letzte Sicherung wiederherstellen Gelöschte Datei wiederherstellen @@ -819,21 +818,21 @@ diese Datei zu teilen Passwort eingeben (optional) Passwort eingeben - Teile Link (%1$s) + Link \"%1$s\" Ablaufdatum setzen Passwort setzen Erneutes teilen ist für die sichere Dateiablage nicht zugelassen Bitte mindestens eine Freigabeoption wählen, bevor Sie fortfahren. - Kann bearbeiten - Dateianfrage + Kann Bearbeiten + Sammelbox Sichere Dateiablage - Nur anzeigen + Nur lesen Berechtigungen zum Teilen Teilen Lesen %1$s (remote) %1$s (Unterhaltung) - Name, Federated-Cloud-ID oder E-Mail-Adresse … + Kontaktname oder E-Mail Benutzer oder Teams hinzufügen Neue E-Mail senden Notiz an Empfänger diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index db1e1d218038..e51fe9edd7db 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -39,4 +39,69 @@ @android:color/white #101418 + + + #FFFFFF + @color/grey_30 + @color/grey_30 + #CCCCCC + @color/grey_70 + @color/grey_80 + #2D2D2D + @color/grey_70 + @color/grey_70 + + + @color/grey_80 + @color/grey_0 + + + @color/grey_80 + @color/grey_0 + + + @color/grey_60 + @color/grey_0 + @color/grey_0 + @color/grey_30 + #FFFFFF + @color/grey_30 + @color/grey_80 + #FFFFFF + + + @color/grey_80 + @color/grey_30 + @color/grey_0 + + + @color/grey_80 + @color/grey_0 + @color/grey_80 + + + @color/grey_70 + @color/grey_60 + + + @color/grey_70 + @color/grey_70 + + + #FFFFFF + @color/grey_30 + @color/grey_0 + @color/grey_0 + @color/grey_0 + @color/grey_0 + @color/grey_60 + @color/grey_0 + #FFFFFF + #7d94f9 + + + #121212 + @color/grey_0 + @color/grey_80 + @color/grey_80 diff --git a/app/src/main/res/values-sw480dp/bool.xml b/app/src/main/res/values-sw480dp/bool.xml new file mode 100644 index 000000000000..8e66f10e898c --- /dev/null +++ b/app/src/main/res/values-sw480dp/bool.xml @@ -0,0 +1,4 @@ + + + true + diff --git a/app/src/main/res/values/bool.xml b/app/src/main/res/values/bool.xml new file mode 100644 index 000000000000..c2dcd8baf0ea --- /dev/null +++ b/app/src/main/res/values/bool.xml @@ -0,0 +1,4 @@ + + + false + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 4095ec7508a1..360a594a91a3 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -78,4 +78,94 @@ #A5A5A5 #F7F9FF + + + #191919 + @color/primary + #191919 + #191919 + @color/grey_30 + @android:color/white + #FFFFFF + @color/grey_0 + #CCCCCC + #77c4ff + #B3FFFFFF + @color/grey_10 + + + #101010 + #F2F2F2 + #E5E5E5 + #B2B2B2 + #666666 + #4C4C4C + #333333 + + + @color/design_snackbar_background_color + @color/white + + + #FFFFFF + #191919 + + + @color/grey_0 + #191919 + @color/primary + #191919 + @color/primary + @color/grey_30 + @color/white + #191919 + + + #FFFFFF + #191919 + #191919 + + + #FFFFFF + #191919 + #FFFFFF + + + @color/primary + #F399C7 + #FFFFFF + @color/grey_30 + @color/grey_10 + @color/grey_0 + + + @color/primary + @color/grey_30 + @color/grey_30 + #CCCCCC + + + #191919 + @color/grey_30 + #191919 + #191919 + #191919 + #191919 + @color/grey_30 + #191919 + #000000 + #191919 + #F6E5EB + #C16F81 + #0D39DF + #0099ff + #2238df + + + @color/grey_0 + #191919 + @color/grey_0 + @color/grey_30 + #77b6bb + #5077b6bb diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 000000000000..3a987ca9bef3 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,32 @@ + + + 4dp + 16dp + 24dp + 6dp + 18sp + 15sp + 15dp + 56dp + 86dp + 80dp + 11sp + 30dp + 55dp + 258dp + 17sp + 20dp + 160dp + 50dp + 150dp + 55dp + 48dp + 48dp + 24dp + 26dp + 20sp + 145dp + 1dp + 13sp + 44dp + \ No newline at end of file diff --git a/app/src/main/res/values/nmc_sharing_strings.xml b/app/src/main/res/values/nmc_sharing_strings.xml new file mode 100644 index 000000000000..a735dfeca81f --- /dev/null +++ b/app/src/main/res/values/nmc_sharing_strings.xml @@ -0,0 +1,54 @@ + + + Unshare + Allow creating + Allow deleting + Allow editing + Read only + Link to folder + Link to file + Password protect (%1$s) + You must select expiration date. + Please enter note. + Send email + Open in… + You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration. + Your Message + Personal share by mail + Create new link + Your Shares + Link Label + Your custom link label + With File drop, only uploading is allowed. Only you can see files and folders that have been uploaded. + Password Protection + Expiration Date + You have not yet shared your file/folder. Share to give others access. + Download Limit + Download limit cannot be empty. + Enter download limit + Downloads: %s + Download limit should be greater than 0. + You are sharing with a MagentaCLOUD user and you can allow her or him to reshare. + Password protection has been enabled. You have to provide the password to + the recipient.\n\nIf you send a share via MagentaCLOUD and paste the password in this message, it will be + transmitted unencrypted in plaintext. + Received + Shared + Send link by mail + Resharing is not allowed. + Resharing is allowed. + This file / folder was shared with you by %s + or + Shared with + Detail + Everyone can only view + Only view + Everyone can edit + Can edit + Everyone can just upload + Just upload + Advanced Permissions + Allow upload and editing + File drop (upload only) + Allow resharing + \ No newline at end of file diff --git a/app/src/main/res/values/nmc_sharing_styles.xml b/app/src/main/res/values/nmc_sharing_styles.xml new file mode 100644 index 000000000000..a9d2e8f20905 --- /dev/null +++ b/app/src/main/res/values/nmc_sharing_styles.xml @@ -0,0 +1,14 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cf1866a5a7d4..82659162ac99 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -624,7 +624,7 @@ Share with… Unset - Name, Federated Cloud ID or email address… + Contact name or email Add users and teams Secure share… @@ -1072,8 +1072,8 @@ New %1$s %2$s - File request - View only + Filedrop only + Read only Can edit Secure file drop @@ -1094,7 +1094,7 @@ Note icon Add new public share link New name - Share link (%1$s) + Link \'%1$s\' Share link Custom permissions Read @@ -1172,7 +1172,7 @@ Create Please select one template Please choose a template and enter a file name. - Share permissions + Permissions Next Send share Please select at least one permission to share. @@ -1340,7 +1340,6 @@ Show less Internal shares External shares - Create link This folder is best viewed in %1$s. Open in %1$s This folder is already included in the parent folder’s sync, which may cause duplicate uploads diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 08ebf4b5f8c7..fcc4daae5b10 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -271,6 +271,7 @@