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..f99082077f00 --- /dev/null +++ b/app/src/androidTest/java/com/nmc/android/ui/FileSharingIT.kt @@ -0,0 +1,396 @@ +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.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.FileDetailSharingFragment +import com.owncloud.android.ui.fragment.util.SharingMenuHelper +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() + } + + private fun getFragment(): FileDetailSharingFragment? { + var fragment: FileDetailSharingFragment? = null + activityScenarioRule.scenario.onActivity { + fragment = + it.supportFragmentManager.findFragmentByTag("SHARING_DETAILS_FRAGMENT") as FileDetailSharingFragment + } + return fragment + } + + @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())) + } + + @Test + fun validateUiForEmptyShares() { + show(file) + + onView(withId(R.id.shared_with_you_container)).check(matches(not(isCompletelyDisplayed()))) + onView(withId(R.id.tv_sharing_details_message)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_sharing_details_message)).check(matches(withText("You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."))) + onView(withId(R.id.searchView)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.pick_contact_email_btn)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.label_personal_share)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.label_personal_share)).check(matches(withText("Personal share by mail"))) + + onView(withId(R.id.share_create_new_link)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.share_create_new_link)).check(matches(withText("Create Link"))) + + 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(isCompletelyDisplayed())) + onView(withId(R.id.tv_empty_shares)).check(matches(withText("No shares created yet."))) + } + + @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 = SharingMenuHelper.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 = SharingMenuHelper.CAN_EDIT_PERMISSIONS_FOR_FILE + label = "Colleagues" + it.storageManager.saveShare(this) + } + + } + show(file) + + onView(withId(R.id.shared_with_you_container)).check(matches(not(isCompletelyDisplayed()))) + onView(withId(R.id.tv_sharing_details_message)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_sharing_details_message)).check(matches(withText("You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."))) + onView(withId(R.id.searchView)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.pick_contact_email_btn)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.label_personal_share)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.label_personal_share)).check(matches(withText("Personal share by mail"))) + + onView(withId(R.id.share_create_new_link)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.share_create_new_link)).check(matches(withText("Create Link"))) + + onView(withId(R.id.tv_your_shares)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_your_shares)).check(matches(withText("Your Shares"))) + 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.shared_with_you_container)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_sharing_details_message)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_sharing_details_message)).check(matches(withText("Resharing is not allowed."))) + onView(withId(R.id.shared_with_you_username)).check(matches(withText("Shared with you by John Doe"))) + onView(withId(R.id.shared_with_you_note)).check(matches(withText("Shared for testing purpose."))) + onView(withId(R.id.shared_with_you_avatar)).check(matches(isCompletelyDisplayed())) + + onView(withId(R.id.searchView)).check(matches(not(isDisplayed()))) + onView(withId(R.id.pick_contact_email_btn)).check(matches(not(isDisplayed()))) + onView(withId(R.id.label_personal_share)).check(matches(not(isDisplayed()))) + onView(withId(R.id.share_create_new_link)).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) + + onView(withId(R.id.shared_with_you_container)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_sharing_details_message)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.tv_sharing_details_message)).check(matches(withText("Resharing is allowed. You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration."))) + onView(withId(R.id.shared_with_you_username)).check(matches(withText("Shared with you by John Doe"))) + onView(withId(R.id.shared_with_you_avatar)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.shared_with_you_note_container)).check(matches(not(isDisplayed()))) + + onView(withId(R.id.searchView)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.pick_contact_email_btn)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.label_personal_share)).check(matches(isCompletelyDisplayed())) + onView(withId(R.id.share_create_new_link)).check(matches(isCompletelyDisplayed())) + 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(isCompletelyDisplayed())) + onView(withId(R.id.tv_empty_shares)).check(matches(withText("No shares created yet."))) + } + + @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 = SharingMenuHelper.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 = SharingMenuHelper.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" && SharingMenuHelper.isReadOnly(ocShare)) + || (permissionList[i] == "Can edit" && SharingMenuHelper.isUploadAndEditingAllowed(ocShare)) + || (permissionList[i] == "Filedrop only" && SharingMenuHelper.isFileDrop(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/ui/fragment/FileDetailSharingFragmentIT.kt b/app/src/androidTest/java/com/owncloud/android/ui/fragment/FileDetailSharingFragmentIT.kt index 3b794bce1c4e..8e9e2fe74bed 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 @@ -255,6 +255,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { 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.share_process_permission_read_only)).check(matches(isChecked())) @@ -383,6 +384,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { 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 @@ -500,6 +502,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(not(isDisplayed()))) 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 @@ -623,6 +626,7 @@ class FileDetailSharingFragmentIT : AbstractIT() { onView(ViewMatchers.withId(R.id.share_process_set_password_switch)).check(matches(not(isDisplayed()))) 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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8b1b9cc71c6c..f965b20f51dc 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -500,7 +500,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..dc2267f10164 --- /dev/null +++ b/app/src/main/java/com/nmc/android/utils/SearchViewThemeUtils.kt @@ -0,0 +1,22 @@ +package com.nmc.android.utils + +import android.content.Context +import android.widget.ImageView +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: SearchView.SearchAutoComplete = 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/owncloud/android/operations/CreateShareWithShareeOperation.java b/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java index bcae519d076a..ca57f9b2b1ee 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java @@ -224,7 +224,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 e69a426b75c9..53a9eb854593 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; @@ -35,6 +40,8 @@ public class UpdateShareInfoOperation extends SyncOperation { private int permissions = -1; private String password; private String label; + //download limit for link share + private long downloadLimit; /** * Constructor @@ -105,6 +112,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); } @@ -113,6 +123,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; } @@ -136,5 +184,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 cb278552daa1..bd2e5a548921 100644 --- a/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java +++ b/app/src/main/java/com/owncloud/android/providers/UsersAndGroupsSearchProvider.java @@ -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 cb3d53642968..10dbea6e109e 100644 --- a/app/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/app/src/main/java/com/owncloud/android/services/OperationsService.java @@ -62,6 +62,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.Iterator; @@ -98,6 +99,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 ACTION_CREATE_SHARE_VIA_LINK = "CREATE_SHARE_VIA_LINK"; @@ -118,6 +121,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"; private ServiceHandler mOperationsHandler; @@ -638,6 +642,12 @@ private Pair newOperation(Intent operationIntent) { updateShare.setLabel(operationIntent.getStringExtra(EXTRA_SHARE_PUBLIC_LABEL)); } + //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; @@ -733,6 +743,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; + default: // do nothing break; 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 f398dd6d8c3d..1e377f63dd46 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 @@ -69,6 +69,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; @@ -80,6 +82,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.helpers.FileOperationsHelper; import com.owncloud.android.ui.preview.PreviewImageActivity; @@ -362,7 +365,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(); @@ -400,6 +404,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); } } @@ -776,7 +782,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(); @@ -800,6 +805,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 && file != null) { ((OCFileListFragment) fileListFragment).updateOCFile(file); } @@ -835,6 +842,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 * @@ -902,11 +925,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()); + } } } @@ -920,9 +949,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); } } @@ -933,7 +976,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 b415d94f4455..60031603f17f 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.SharingMenuHelper; 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, + SharingMenuHelper.canEditFile(this, getUser().get(), getStorageManager().getCapability(getUser().get()), getFile(), editorUtils)), FileDetailsSharingProcessFragment.TAG) .commit(); } diff --git a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java index b018647ed0e2..54897338abe9 100644 --- a/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java +++ b/app/src/main/java/com/owncloud/android/ui/activity/ToolbarActivity.java @@ -63,6 +63,7 @@ public abstract class ToolbarActivity extends BaseActivity implements Injectable private TextView mInfoBoxMessage; protected AppCompatSpinner mToolbarSpinner; private boolean isHomeSearchToolbarShow = false; + private ImageView mToolbarBackIcon; @Inject public ThemeColorUtils themeColorUtils; @Inject public ThemeUtils themeUtils; @@ -81,6 +82,7 @@ private void setupToolbar(boolean isHomeSearchToolbarShow, boolean showSortListB mHomeSearchToolbar = findViewById(R.id.home_toolbar); mMenuButton = findViewById(R.id.menu_button); mSearchText = findViewById(R.id.search_text); + mToolbarBackIcon = findViewById(R.id.toolbar_back_icon); mSwitchAccountButton = findViewById(R.id.switch_account_button); if (showSortListButtonGroup) { @@ -102,6 +104,8 @@ private void setupToolbar(boolean isHomeSearchToolbarShow, boolean showSortListB viewThemeUtils.material.colorToolbarOverflowIcon(mToolbar); viewThemeUtils.platform.themeStatusBar(this); viewThemeUtils.material.colorMaterialTextButton(mSwitchAccountButton); + + mToolbarBackIcon.setOnClickListener(v -> onBackPressed()); } public void setupToolbarShowOnlyMenuButtonAndTitle(String title, View.OnClickListener toggleDrawer) { @@ -257,10 +261,20 @@ public boolean sortListGroupVisibility(){ public void setPreviewImageBitmap(Bitmap bitmap) { if (mPreviewImage != null) { mPreviewImage.setImageBitmap(bitmap); + resetPreviewImageConfiguration(); setPreviewImageVisibility(true); } } + /** + * reset preview image configuration if required the scale type and padding are changing in sharing screen so to + * reset it call this method + */ + private void resetPreviewImageConfiguration() { + mPreviewImage.setScaleType(ImageView.ScaleType.CENTER_CROP); + mPreviewImage.setPadding(0, 0, 0, 0); + } + /** * Change the drawable for the toolbar's preview image. * @@ -269,10 +283,35 @@ public void setPreviewImageBitmap(Bitmap bitmap) { public void setPreviewImageDrawable(Drawable drawable) { if (mPreviewImage != null) { mPreviewImage.setImageDrawable(drawable); + resetPreviewImageConfiguration(); setPreviewImageVisibility(true); } } + /** + * method to show/hide the toolbar custom back icon currently this is only showing for sharing details screen for + * thumbnail images + * + * @param show + */ + public void showToolbarBackImage(boolean show) { + if (mToolbarBackIcon == null) { + return; + } + ActionBar actionBar = getSupportActionBar(); + if (show) { + mToolbarBackIcon.setVisibility(View.VISIBLE); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(false); + } + } else { + mToolbarBackIcon.setVisibility(View.GONE); + if (actionBar != null) { + actionBar.setDisplayHomeAsUpEnabled(true); + } + } + } + /** * get the toolbar's preview image view. */ @@ -284,6 +323,15 @@ public FrameLayout getPreviewImageContainer() { return mPreviewImageContainer; } + /** + * method will expand the toolbar using app bar if its hidden due to scrolling + */ + public void expandToolbar() { + if (mAppBar != null) { + mAppBar.setExpanded(true, true); + } + } + public void updateToolbarSubtitle(@NonNull String subtitle) { ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { 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 302ba09fdbdb..67c2c8a7ebee 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 @@ -15,7 +15,7 @@ package com.owncloud.android.ui.adapter; import android.content.Context; -import android.graphics.PorterDuff; +import android.content.res.ColorStateList; import android.text.TextUtils; import android.view.View; @@ -28,12 +28,14 @@ import androidx.annotation.NonNull; import androidx.core.content.res.ResourcesCompat; +import androidx.core.widget.TextViewCompat; import androidx.recyclerview.widget.RecyclerView; class LinkShareViewHolder extends RecyclerView.ViewHolder { private FileDetailsShareLinkShareItemBinding binding; private Context context; private ViewThemeUtils viewThemeUtils; + private boolean isTextFile; public LinkShareViewHolder(@NonNull View itemView) { super(itemView); @@ -41,25 +43,22 @@ public LinkShareViewHolder(@NonNull View itemView) { public LinkShareViewHolder(FileDetailsShareLinkShareItemBinding binding, Context context, - final ViewThemeUtils viewThemeUtils) { + final ViewThemeUtils viewThemeUtils, + boolean isTextFile) { this(binding.getRoot()); this.binding = binding; this.context = context; this.viewThemeUtils = viewThemeUtils; + this.isTextFile = isTextFile; } public void bind(OCShare publicShare, ShareeListAdapterListener listener) { if (ShareType.EMAIL == publicShare.getShareType()) { binding.name.setText(publicShare.getSharedWithDisplayName()); binding.icon.setImageDrawable(ResourcesCompat.getDrawable(context.getResources(), - R.drawable.ic_email, + R.drawable.ic_external_share, null)); binding.copyLink.setVisibility(View.GONE); - - binding.icon.getBackground().setColorFilter(context.getResources().getColor(R.color.nc_grey), - PorterDuff.Mode.SRC_IN); - binding.icon.getDrawable().mutate().setColorFilter(context.getResources().getColor(R.color.icon_on_nc_grey), - PorterDuff.Mode.SRC_IN); } else { if (!TextUtils.isEmpty(publicShare.getLabel())) { String text = String.format(context.getString(R.string.share_link_with_label), publicShare.getLabel()); @@ -72,24 +71,40 @@ public void bind(OCShare publicShare, ShareeListAdapterListener listener) { } } - viewThemeUtils.platform.colorImageViewBackgroundAndIcon(binding.icon); } String permissionName = SharingMenuHelper.getPermissionName(context, publicShare); - setPermissionName(publicShare, permissionName); + setPermissionName(publicShare, permissionName, listener); binding.copyLink.setOnClickListener(v -> listener.copyLink(publicShare)); binding.overflowMenu.setOnClickListener(v -> listener.showSharingMenuActionSheet(publicShare)); - if (!SharingMenuHelper.isSecureFileDrop(publicShare)) { - binding.shareByLinkContainer.setOnClickListener(v -> listener.showPermissionsDialog(publicShare)); - } } - private void setPermissionName(OCShare publicShare, String permissionName) { + private void setPermissionName(OCShare publicShare, String permissionName, ShareeListAdapterListener listener) { + ColorStateList colorStateList = new ColorStateList( + new int[][]{ + new int[]{-android.R.attr.state_enabled}, + new int[]{android.R.attr.state_enabled}, + }, + new int[]{ + ResourcesCompat.getColor(context.getResources(), R.color.share_disabled_txt_color, + null), + ResourcesCompat.getColor(context.getResources(), R.color.primary, + null) + } + ); + TextViewCompat.setCompoundDrawableTintList(binding.permissionName, colorStateList); + binding.permissionName.setTextColor(colorStateList); + if (!TextUtils.isEmpty(permissionName) && !SharingMenuHelper.isSecureFileDrop(publicShare)) { + if (permissionName.equalsIgnoreCase(context.getResources().getString(R.string.share_permission_read_only)) && !isTextFile) { + binding.permissionName.setEnabled(false); + } else { + binding.permissionName.setEnabled(true); + binding.shareByLinkContainer.setOnClickListener(v -> listener.showPermissionsDialog(publicShare)); + } binding.permissionName.setText(permissionName); binding.permissionName.setVisibility(View.VISIBLE); - viewThemeUtils.androidx.colorPrimaryTextViewElement(binding.permissionName); } else { binding.permissionName.setVisibility(View.GONE); } diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt index 23f7a9b29ebc..8e800540ab56 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ListGridImageViewHolder.kt @@ -22,6 +22,7 @@ interface ListGridImageViewHolder { 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 cd99bf1a2dc7..ce7a791e369a 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 @@ -534,7 +534,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 a9ec7237ef9f..20d4572fcc5e 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 @@ -14,6 +14,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 com.elyeproj.loaderviewlibrary.LoaderImageView import com.nextcloud.android.common.ui.theme.utils.ColorRole @@ -363,22 +365,64 @@ class OCFileListDelegate( private fun showShareIcon(gridViewHolder: ListGridImageViewHolder, file: OCFile) { val sharedIconView = gridViewHolder.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 - if (file.isSharedWithSharee || file.isSharedWithMe) { - if (showShareAvatar) { - sharedIconView.visibility = View.GONE - } else { - sharedIconView.visibility = View.VISIBLE - sharedIconView.setImageResource(R.drawable.shared_via_users) + 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) + 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_shared)!!, + 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_shared)!!, + 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_unshared) + val unShareTintedIcon = viewThemeUtils.platform.colorDrawable(unShareIcon!!, unShareIconColor) + sharedIconView.setImageDrawable(unShareTintedIcon) + sharedIconView.contentDescription = context.getString(R.string.shared_icon_share) + sharedMessageView?.visibility = View.GONE } - } else if (file.isSharedViaLink) { - sharedIconView.setImageResource(R.drawable.shared_via_link) - sharedIconView.contentDescription = context.getString(R.string.shared_icon_shared_via_link) - } else { - sharedIconView.setImageResource(R.drawable.ic_unshared) - sharedIconView.contentDescription = context.getString(R.string.shared_icon_share) } sharedIconView.setOnClickListener { ocFileListFragmentInterface.onShareIconClick(file) } } else { diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt index dd04c90eabc1..f697505678ac 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridImageViewHolder.kt @@ -40,6 +40,8 @@ internal class OCFileListGridImageViewHolder(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/OCFileListGridItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt index e8134be86c7d..1dc7a85142ff 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 57c431ba1fff..638753c6c4f6 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/QuickSharingPermissionsAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/QuickSharingPermissionsAdapter.kt index 2b3c8a1a0de7..3f72ecb6a8e6 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 @@ -51,7 +51,6 @@ class QuickSharingPermissionsAdapter( fun bindData(quickPermissionModel: QuickPermissionModel) { binding.tvQuickShareName.text = quickPermissionModel.permissionName if (quickPermissionModel.isSelected) { - viewThemeUtils.platform.colorImageView(binding.tvQuickShareCheckIcon) binding.tvQuickShareCheckIcon.visibility = View.VISIBLE } else { binding.tvQuickShareCheckIcon.visibility = View.INVISIBLE 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 08e120c8ff82..965c5fddf6fc 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 @@ -65,22 +65,22 @@ public void bind(OCShare share, 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(), R.drawable.ic_user); + //setImage(binding.icon, share.getSharedWithDisplayName(), R.drawable.ic_user); break; case USER: binding.icon.setTag(share.getShareWith()); - float avatarRadius = context.getResources().getDimension(R.dimen.list_item_avatar_icon_radius); + /* float avatarRadius = context.getResources().getDimension(R.dimen.list_item_avatar_icon_radius); DisplayUtils.setAvatar(user, share.getShareWith(), share.getSharedWithDisplayName(), @@ -88,15 +88,17 @@ public void bind(OCShare share, avatarRadius, context.getResources(), binding.icon, - context); + 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, R.drawable.ic_user); + //setImage(binding.icon, name, R.drawable.ic_user); break; } binding.name.setText(name); + binding.icon.setImageResource(R.drawable.ic_internal_share); if (share.getShareWith().equalsIgnoreCase(userId) || share.getUserId().equalsIgnoreCase(userId)) { binding.overflowMenu.setVisibility(View.VISIBLE); 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 3b7d70b973b5..39243c19d6fe 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 @@ -50,6 +50,7 @@ public class ShareeListAdapter extends RecyclerView.Adapter shares, @@ -86,7 +87,8 @@ public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int parent, false), fileActivity, - viewThemeUtils); + viewThemeUtils, + isTextFile); } case NEW_PUBLIC_LINK -> { if (encrypted) { @@ -174,6 +176,10 @@ public boolean shouldCallGeneratedCallback(String tag, Object callContext) { return false; } + public void setTextFile(boolean textFile) { + isTextFile = textFile; + } + @SuppressLint("NotifyDataSetChanged") public void remove(OCShare share) { shares.remove(share); @@ -200,13 +206,6 @@ protected final void sortShares() { shares = links; shares.addAll(users); - - // add internal share link at end - if (!encrypted) { - 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 59f29a15a614..e6ac284aef25 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 @@ -74,7 +73,6 @@ class SendShareDialog : BottomSheetDialogFragment(R.layout.send_share_fragment), binding.btnShare.setOnClickListener { shareFile(file) } binding.btnLink.setOnClickListener { shareByLink() } - applyTintColor() setupBottomSheetBehaviour() checkButtonVisibilities() setupSendButtonRecyclerView() @@ -98,12 +96,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) { @@ -127,6 +119,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) { @@ -210,6 +205,9 @@ class SendShareDialog : BottomSheetDialogFragment(R.layout.send_share_fragment), } private fun shareFile(file: OCFile?) { + // NMC Customization + isPeopleShareClicked = true + dismiss() if (activity is FileDisplayActivity) { @@ -236,6 +234,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 71b991cb1069..0548088b6900 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 @@ -12,17 +12,19 @@ import android.content.Context; import android.content.res.ColorStateList; import android.graphics.Bitmap; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; import android.os.Bundle; import android.view.LayoutInflater; import android.view.Menu; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; +import android.widget.ImageView; import android.widget.ProgressBar; 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; @@ -32,6 +34,7 @@ import com.nextcloud.client.network.ClientFactory; import com.nextcloud.client.network.ConnectivityService; import com.nextcloud.client.preferences.AppPreferences; +import com.nextcloud.utils.EditorUtils; import com.nextcloud.ui.fileactions.FileActionsBottomSheet; import com.nextcloud.utils.MenuUtils; import com.nextcloud.utils.extensions.BundleExtensionsKt; @@ -40,6 +43,7 @@ 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.network.OnDatatransferProgressListener; @@ -50,12 +54,13 @@ import com.owncloud.android.lib.resources.shares.ShareType; 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.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.SharingMenuHelper; 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; @@ -73,6 +78,7 @@ import androidx.annotation.IdRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import androidx.fragment.app.FragmentManager; @@ -83,6 +89,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"; @@ -92,6 +99,10 @@ public class FileDetailFragment extends FileFragment implements OnClickListener, private User user; private OCFile parentFolder; private boolean previewLoaded; + /** + * variable to check if custom back icon on toolbar has to be shown + */ + private boolean isCustomBackIcon; private FileDetailsFragmentBinding binding; private ProgressListener progressListener; @@ -105,6 +116,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. @@ -166,7 +179,7 @@ public FileDetailSharingFragment getFileDetailSharingFragment() { if (binding == null) { return null; } - return ((FileDetailTabAdapter) binding.pager.getAdapter()).getFileDetailSharingFragment(); + return (FileDetailSharingFragment)requireActivity().getSupportFragmentManager().findFragmentByTag(FTAG_SHARING); } /** @@ -175,7 +188,8 @@ public FileDetailSharingFragment getFileDetailSharingFragment() { * @return reference to the {@link FileDetailActivitiesFragment} */ public FileDetailActivitiesFragment getFileDetailActivitiesFragment() { - return ((FileDetailTabAdapter) binding.pager.getAdapter()).getFileDetailActivitiesFragment(); + //return ((FileDetailTabAdapter) binding.pager.getAdapter()).getFileDetailActivitiesFragment(); + return null; } public void goBackToOCFileListFragment() { @@ -221,6 +235,11 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, return null; } + FloatingActionButton fabMain = requireActivity().findViewById(R.id.fab_main); + if (fabMain != null) { + fabMain.hide(); + } + if (getFile().getTags().isEmpty()) { binding.tagsGroup.setVisibility(View.GONE); } else { @@ -255,6 +274,13 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat } } + private void replaceSharingFragment() { + requireActivity().getSupportFragmentManager().beginTransaction() + .replace(R.id.sharing_frame_container, + FileDetailSharingFragment.newInstance(getFile(), user), + FTAG_SHARING).commit(); + } + private void onOverflowIconClicked() { final OCFile file = getFile(); final List additionalFilter = new ArrayList<>( @@ -280,66 +306,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(getFragmentManager(), - getFile(), - user, - showSharingTab()); - binding.pager.setAdapter(adapter); - binding.pager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(binding.tabLayout) { - @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); - } - }); - 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 - } - }); - - TabLayout.Tab tab = binding.tabLayout.getTabAt(activeTab); - if (tab != null) { - tab.select(); - } - } - @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); @@ -362,10 +328,18 @@ public void onResume() { if (previewLoaded) { toolbarActivity.setPreviewImageVisibility(true); } + showHideCustomBackButton(); } } + //show custom back button for image previews + private void showHideCustomBackButton() { + if (toolbarActivity != null) { + toolbarActivity.showToolbarBackImage(isCustomBackIcon); + } + } + @Override public void onPause() { super.onPause(); @@ -377,6 +351,7 @@ public void onStop() { if (toolbarActivity != null) { toolbarActivity.hidePreviewImage(); + toolbarActivity.showToolbarBackImage(false); } EventBus.getDefault().unregister(this); @@ -505,11 +480,7 @@ public void updateFileDetails(boolean transferring, boolean refresh) { OCFile file = getFile(); // set file details - if (MimeTypeUtil.isImage(file)) { - binding.filename.setText(file.getFileName()); - } else { - binding.filename.setVisibility(View.GONE); - } + binding.filename.setText(file.getFileName()); binding.size.setText(DisplayUtils.bytesToHumanReadable(file.getFileLength())); boolean showDetailedTimestamp = preferences.isShowDetailedTimestampEnabled(); @@ -540,7 +511,10 @@ public void updateFileDetails(boolean transferring, boolean refresh) { } } - setupViewPager(); + // TODO: 06/21/23 remove this condition after Comments section included + if (SendShareDialog.isPeopleShareClicked) { + replaceSharingFragment(); + } getView().invalidate(); } @@ -556,11 +530,17 @@ 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)); } else { binding.favorite.setImageDrawable(ResourcesCompat.getDrawable(getResources(), - R.drawable.ic_star_outline, - null)); + R.drawable.ic_star_outline, + null)); + + //NMC Customization + binding.favorite.getDrawable().mutate().setColorFilter(requireContext() + .getResources() + .getColor(R.color.list_item_lastmod_and_filesize_text, null), + PorterDuff.Mode.SRC_IN); } } @@ -581,56 +561,123 @@ private boolean readyToShow() { private void setFilePreview(OCFile file) { Bitmap resizedImage; - if (toolbarActivity != null && MimeTypeUtil.isImage(file)) { - String tagId = ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + getFile().getRemoteId(); - resizedImage = ThumbnailsCacheManager.getBitmapFromDiskCache(tagId); + if (toolbarActivity != null) { + if (file.isFolder()) { + boolean isAutoUploadFolder = SyncedFolderProvider.isAutoUploadFolder(syncedFolderProvider, file, user); - if (resizedImage != null && !file.isUpdateThumbnailNeeded()) { - toolbarActivity.setPreviewImageBitmap(resizedImage); - previewLoaded = true; - } else { - // show thumbnail while loading resized image - Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( - ThumbnailsCacheManager.PREFIX_THUMBNAIL + getFile().getRemoteId()); + Integer overlayIconId = file.getFileOverlayIconId(isAutoUploadFolder); + // NMC Customization: No overlay icon will be used. Directly using folder icons + toolbarActivity.setPreviewImageDrawable(ContextCompat.getDrawable(requireContext(), overlayIconId)); - if (thumbnail != null) { - toolbarActivity.setPreviewImageBitmap(thumbnail); - } else { - thumbnail = ThumbnailsCacheManager.mDefaultImg; - } + int leftRightPadding = requireContext().getResources().getDimensionPixelSize(R.dimen.standard_padding); + updatePreviewImageUI(leftRightPadding); - // generate new resized image - if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(getFile(), toolbarActivity.getPreviewImageView()) && - containerActivity.getStorageManager() != null) { - final ThumbnailsCacheManager.ResizedImageGenerationTask task = - new ThumbnailsCacheManager.ResizedImageGenerationTask(this, - toolbarActivity.getPreviewImageView(), - toolbarActivity.getPreviewImageContainer(), - containerActivity.getStorageManager(), - connectivityService, - containerActivity.getStorageManager().getUser(), - getResources().getColor(R.color.background_color_inverse, - requireContext().getTheme()) - ); - - if (resizedImage == null) { - resizedImage = thumbnail; + previewLoaded = true; + isCustomBackIcon = false; + } else { + if (file.getRemoteId() != null && file.isPreviewAvailable()) { + String tagId = ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE + getFile().getRemoteId(); + resizedImage = ThumbnailsCacheManager.getBitmapFromDiskCache(tagId); + + if (resizedImage != null && !file.isUpdateThumbnailNeeded()) { + toolbarActivity.setPreviewImageBitmap(resizedImage); + toolbarActivity.showToolbarBackImage(true); + previewLoaded = true; + isCustomBackIcon = true; + } else { + // show thumbnail while loading resized image + Bitmap thumbnail = ThumbnailsCacheManager.getBitmapFromDiskCache( + ThumbnailsCacheManager.PREFIX_THUMBNAIL + getFile().getRemoteId()); + + if (thumbnail != null) { + toolbarActivity.setPreviewImageBitmap(thumbnail); + toolbarActivity.showToolbarBackImage(true); + previewLoaded = true; + isCustomBackIcon = true; + } else { + Drawable drawable = MimeTypeUtil.getFileTypeIcon(file.getMimeType(), + file.getFileName(), + requireContext(), + viewThemeUtils); + if (drawable == null) { + thumbnail = ThumbnailsCacheManager.mDefaultImg; + toolbarActivity.setPreviewImageBitmap(thumbnail); + } else { + toolbarActivity.setPreviewImageDrawable(drawable); + previewLoaded = true; + isCustomBackIcon = false; + } + updatePreviewImageUIForFiles(); + } + + if (MimeTypeUtil.isImage(file)) { + // generate new resized image + if (ThumbnailsCacheManager.cancelPotentialThumbnailWork(getFile(), toolbarActivity.getPreviewImageView()) && + containerActivity.getStorageManager() != null) { + final ThumbnailsCacheManager.ResizedImageGenerationTask task = + new ThumbnailsCacheManager.ResizedImageGenerationTask(this, + toolbarActivity.getPreviewImageView(), + toolbarActivity.getPreviewImageContainer(), + containerActivity.getStorageManager(), + connectivityService, + containerActivity.getStorageManager().getUser(), + getResources().getColor(R.color.background_color_inverse, + requireContext().getTheme()) + ); + + if (resizedImage == null) { + resizedImage = thumbnail; + } + + final ThumbnailsCacheManager.AsyncResizedImageDrawable asyncDrawable = + new ThumbnailsCacheManager.AsyncResizedImageDrawable( + MainApp.getAppContext().getResources(), + resizedImage, + task + ); + + toolbarActivity.setPreviewImageDrawable(asyncDrawable); + toolbarActivity.showToolbarBackImage(true); + previewLoaded = true; + isCustomBackIcon = true; + task.execute(getFile()); + } + } } - - final ThumbnailsCacheManager.AsyncResizedImageDrawable asyncDrawable = - new ThumbnailsCacheManager.AsyncResizedImageDrawable( - MainApp.getAppContext().getResources(), - resizedImage, - task - ); - - toolbarActivity.setPreviewImageDrawable(asyncDrawable); + } else { + toolbarActivity.setPreviewImageDrawable(MimeTypeUtil.getFileTypeIcon(file.getMimeType(), + file.getFileName(), + requireContext(), + viewThemeUtils)); + updatePreviewImageUIForFiles(); previewLoaded = true; - task.execute(getFile()); + isCustomBackIcon = false; } } } else { previewLoaded = false; + isCustomBackIcon = false; + } + showHideCustomBackButton(); + } + + /** + * update preview image for files we are taking different paddings for files and folders + */ + private void updatePreviewImageUIForFiles() { + int leftRightPadding = requireContext().getResources().getDimensionPixelSize(R.dimen.standard_half_padding); + updatePreviewImageUI(leftRightPadding); + } + + /** + * change scale type and padding for folders and files without thumbnails + */ + private void updatePreviewImageUI(int leftRightPadding) { + if (toolbarActivity != null && toolbarActivity.getPreviewImageView() != null) { + toolbarActivity.getPreviewImageView().setScaleType(ImageView.ScaleType.FIT_START); + int topPadding = requireContext().getResources().getDimensionPixelSize(R.dimen.activity_row_layout_height); + int bottomPadding = requireContext().getResources().getDimensionPixelSize(R.dimen.standard_padding); + toolbarActivity.getPreviewImageView().setPadding(leftRightPadding, topPadding, leftRightPadding, bottomPadding); } } @@ -731,32 +778,15 @@ private void showEmptyContent() { public void initiateSharingProcess(String shareeName, ShareType shareType, boolean secureShare) { - requireActivity().getSupportFragmentManager().beginTransaction().add(R.id.sharing_frame_container, + requireActivity().getSupportFragmentManager().beginTransaction().replace(R.id.sharing_frame_container, FileDetailsSharingProcessFragment.newInstance(getFile(), shareeName, shareType, - secureShare), + secureShare, + SharingMenuHelper.canEditFile(requireActivity(), user, storageManager.getCapability(user), getFile(), editorUtils)), 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(); - } } /** @@ -769,12 +799,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, + SharingMenuHelper.canEditFile(requireActivity(), user, storageManager.getCapability(user), getFile(), editorUtils)), FileDetailsSharingProcessFragment.TAG) + .addToBackStack(null) .commit(); - showHideFragmentView(true); } @Subscribe(threadMode = ThreadMode.BACKGROUND) @@ -799,22 +830,14 @@ public void onMessageEvent(FavoriteEvent event) { } } - private boolean showSharingTab() { - if (getFile().isEncrypted()) { - if (parentFolder == null) { - parentFolder = storageManager.getFileById(getFile().getParentId()); - } - if (EncryptionUtils.supportsSecureFiledrop(getFile(), user) && !parentFolder.isEncrypted()) { - return true; - } else { - // sharing not allowed for encrypted files, thus only show first tab (activities) - // sharing not allowed for encrypted subfolders - return false; - } - } 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 68241a44e69e..df6f01663561 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,26 +16,30 @@ 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.drawable.Drawable; import android.net.Uri; import android.os.Bundle; import android.provider.ContactsContract; -import android.text.InputType; import android.text.TextUtils; 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.extensions.BundleExtensionsKt; +import com.nextcloud.utils.EditorUtils; +import com.nmc.android.utils.SearchViewThemeUtils; import com.owncloud.android.R; import com.owncloud.android.databinding.FileDetailsSharingFragmentBinding; import com.owncloud.android.datamodel.FileDataStorageManager; @@ -55,13 +59,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.SharingMenuHelper; 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; @@ -95,8 +103,11 @@ public class FileDetailSharingFragment extends Fragment implements ShareeListAda private OnEditShareListener onEditShareListener; + private boolean isSearchViewFocused; + @Inject UserAccountManager accountManager; @Inject ClientFactory clientFactory; + @Inject EditorUtils editorUtils; @Inject ViewThemeUtils viewThemeUtils; @Inject UsersAndGroupsSearchConfig searchConfig; @@ -148,6 +159,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); @@ -171,6 +183,17 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, binding.pickContactEmailBtn.setOnClickListener(v -> checkContactPermission()); + binding.shareCreateNewLink.setOnClickListener(v -> createPublicShareLink()); + + //remove focus from search view on click of root view + binding.shareContainer.setOnClickListener(v -> binding.searchView.clearFocus()); + + //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(); return binding.getRoot(); @@ -209,58 +232,125 @@ 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); + SearchViewThemeUtils.INSTANCE.themeSearchView(requireContext(), binding.searchView); - if (file.canReshare()) { - if (file.isEncrypted() || (parentFile != null && parentFile.isEncrypted())) { - if (file.getE2eCounter() == -1) { - // V1 cannot share - binding.searchContainer.setVisibility(View.GONE); - } else { - binding.searchView.setQueryHint(getResources().getString(R.string.secure_share_search)); + binding.searchView.setQueryHint(getResources().getString(R.string.share_search)); + binding.searchView.setVisibility(View.VISIBLE); + binding.labelPersonalShare.setVisibility(View.VISIBLE); + binding.pickContactEmailBtn.setVisibility(View.VISIBLE); + + binding.searchView.setOnQueryTextFocusChangeListener((view, hasFocus) -> { + isSearchViewFocused = hasFocus; + scrollToSearchViewPosition(false); + }); + + } - 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); + /** + * @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; } - } + + //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.searchView.setQueryHint(getResources().getString(R.string.share_search)); + //send the event to show the share top view again + EventBus.getDefault().post(new ShareSearchViewFocusEvent(isSearchViewFocused)); } } else { - binding.searchView.setQueryHint(getResources().getString(R.string.reshare_not_allowed)); - binding.searchView.setInputType(InputType.TYPE_NULL); - binding.pickContactEmailBtn.setVisibility(View.GONE); - disableSearchView(binding.searchView); + //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)); } } - private void disableSearchView(View view) { - view.setEnabled(false); + private void hideAppBar() { + if (requireActivity() instanceof FileDisplayActivity) { + AppBarLayout appBarLayout = requireActivity().findViewById(R.id.appbar); - if (view instanceof ViewGroup viewGroup) { - for (int i = 0; i < viewGroup.getChildCount(); i++) { - disableSearchView(viewGroup.getChildAt(i)); + if (appBarLayout != null) { + appBarLayout.setExpanded(false, true); } } } + /** + * 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, + SharingMenuHelper.canEditFile(requireActivity(), 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, SharingMenuHelper.canEditFile(requireActivity(), user, capabilities, file, editorUtils)), + FileDetailsSharingProcessFragment.TAG) + .addToBackStack(null) + .commit(); + } + private void setShareWithYou() { if (accountManager.userOwnsFile(file, user)) { binding.sharedWithYouContainer.setVisibility(View.GONE); + binding.shareCreateNewLink.setVisibility(View.VISIBLE); + binding.tvSharingDetailsMessage.setText(getResources().getString(R.string.sharing_description)); + setUpSearchView(); } else { binding.sharedWithYouUsername.setText( String.format(getString(R.string.shared_with_you_by), file.getOwnerDisplayName())); - DisplayUtils.setAvatar(user, + /* DisplayUtils.setAvatar(user, file.getOwnerId(), this, getResources().getDimension( @@ -268,16 +358,28 @@ private void setShareWithYou() { getResources(), binding.sharedWithYouAvatar, getContext()); - binding.sharedWithYouAvatar.setVisibility(View.VISIBLE); + binding.sharedWithYouAvatar.setVisibility(View.VISIBLE);*/ String note = file.getNote(); + //NMC Customization --> Share with me note container is not required if (!TextUtils.isEmpty(note)) { binding.sharedWithYouNote.setText(file.getNote()); - binding.sharedWithYouNoteContainer.setVisibility(View.VISIBLE); + binding.sharedWithYouNoteContainer.setVisibility(View.GONE); } else { binding.sharedWithYouNoteContainer.setVisibility(View.GONE); } + + if (file.canReshare()) { + binding.tvSharingDetailsMessage.setText(getResources().getString(R.string.reshare_allowed) + " " + getResources().getString(R.string.sharing_description)); + setUpSearchView(); + } else { + binding.searchView.setVisibility(View.GONE); + binding.labelPersonalShare.setVisibility(View.GONE); + binding.pickContactEmailBtn.setVisibility(View.GONE); + binding.shareCreateNewLink.setVisibility(View.GONE); + binding.tvSharingDetailsMessage.setText(getResources().getString(R.string.reshare_not_allowed)); + } } } @@ -454,6 +556,10 @@ public void refreshSharesFromDB() { } adapter.getShares().clear(); + //update flag in adapter + adapter.setTextFile(SharingMenuHelper.canEditFile(requireActivity(), user, + capabilities, file, editorUtils)); + // to show share with users/groups info List shares = fileDataStorageManager.getSharesWithForAFile(file.getRemotePath(), user.getAccountName()); @@ -469,16 +575,25 @@ public void refreshSharesFromDB() { ShareType.PUBLIC_LINK, ""); - if (publicShares.isEmpty() && containsNoNewPublicShare(adapter.getShares()) && + // NMC Customization: Not required for NMC + /*if (publicShares.isEmpty() && containsNoNewPublicShare(adapter.getShares()) && (!file.isEncrypted() || capabilities.getEndToEndEncryption().isTrue())) { final OCShare ocShare = new OCShare(); ocShare.setShareType(ShareType.NEW_PUBLIC_LINK); publicShares.add(ocShare); } else { adapter.removeNewPublicShare(); - } + }*/ adapter.addShares(publicShares); + + showHideView((shares == null || shares.isEmpty()) && (publicShares == null || publicShares.isEmpty())); + } + + private void showHideView(boolean isEmptyList) { + binding.sharesList.setVisibility(isEmptyList ? View.GONE : View.VISIBLE); + binding.tvYourShares.setVisibility(isEmptyList ? View.GONE : View.VISIBLE); + binding.tvEmptyShares.setVisibility(isEmptyList ? View.VISIBLE : View.GONE); } private void checkContactPermission() { @@ -567,6 +682,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); @@ -598,11 +718,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)); @@ -650,5 +765,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 3724765a4fd6..63ae080c6106 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 @@ -51,14 +51,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.menuIconAddAnotherLink); - viewThemeUtils.platform.colorImageView(binding.menuIconAdvancedPermissions); - viewThemeUtils.platform.colorImageView(binding.menuIconSendLink); - viewThemeUtils.platform.colorImageView(binding.menuIconUnshare); - viewThemeUtils.platform.colorImageView(binding.menuIconSendNewEmail); - updateUI(); setupClickListener(); @@ -70,12 +62,16 @@ 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) { - binding.menuShareAddAnotherLink.setVisibility(View.VISIBLE); - binding.menuShareSendLink.setVisibility(View.VISIBLE); + binding.menuShareSendNewEmail.setVisibility(View.GONE); } else { - binding.menuShareAddAnotherLink.setVisibility(View.GONE); - binding.menuShareSendLink.setVisibility(View.GONE); + binding.menuShareSendNewEmail.setVisibility(View.VISIBLE); } if (SharingMenuHelper.isSecureFileDrop(ocShare)) { @@ -85,6 +81,11 @@ private void updateUI() { } private void setupClickListener() { + binding.menuShareOpenIn.setOnClickListener(v -> { + actions.openIn(ocShare); + dismiss(); + }); + binding.menuShareAdvancedPermissions.setOnClickListener(v -> { actions.advancedPermissions(ocShare); dismiss(); @@ -105,10 +106,6 @@ private void setupClickListener() { dismiss(); }); - binding.menuShareAddAnotherLink.setOnClickListener(v -> { - actions.addAnotherLink(ocShare); - dismiss(); - }); } @Override 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 178343c9ece2..0bb14e6edc9e 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 01c6fd321a54..83db86773549 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 @@ -13,14 +13,19 @@ 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.nmc.android.utils.CheckableThemeUtils import com.owncloud.android.R import com.owncloud.android.databinding.FileDetailsSharingProcessFragmentBinding import com.owncloud.android.datamodel.OCFile @@ -28,11 +33,13 @@ import com.owncloud.android.lib.resources.shares.OCShare import com.owncloud.android.lib.resources.shares.SharePermissionsBuilder import com.owncloud.android.lib.resources.shares.ShareType import com.owncloud.android.ui.activity.FileActivity +import com.owncloud.android.ui.activity.ToolbarActivity import com.owncloud.android.ui.dialog.ExpirationDatePickerDialogFragment import com.owncloud.android.ui.fragment.util.SharingMenuHelper import com.owncloud.android.ui.helpers.FileOperationsHelper import com.owncloud.android.utils.ClipboardUtil import com.owncloud.android.utils.DisplayUtils +import com.owncloud.android.utils.KeyboardUtils import com.owncloud.android.utils.theme.ViewThemeUtils import java.text.SimpleDateFormat import java.util.Date @@ -50,7 +57,8 @@ import javax.inject.Inject class FileDetailsSharingProcessFragment : Fragment(), Injectable, - ExpirationDatePickerDialogFragment.OnExpiryDateListener { + ExpirationDatePickerDialogFragment.OnExpiryDateListener, + RadioGroup.OnCheckedChangeListener { companion object { const val TAG = "FileDetailsSharingProcessFragment" @@ -62,6 +70,7 @@ 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 @@ -75,13 +84,15 @@ class FileDetailsSharingProcessFragment : file: OCFile, shareeName: String, shareType: ShareType, - secureShare: Boolean + secureShare: Boolean, + isTextFile: Boolean ): FileDetailsSharingProcessFragment { val args = Bundle() args.putParcelable(ARG_OCFILE, file) args.putSerializable(ARG_SHARE_TYPE, shareType) args.putString(ARG_SHAREE_NAME, shareeName) args.putBoolean(ARG_SECURE_SHARE, secureShare) + args.putBoolean(ARG_IS_TEXT_FILE, isTextFile) val fragment = FileDetailsSharingProcessFragment() fragment.arguments = args return fragment @@ -91,13 +102,20 @@ class FileDetailsSharingProcessFragment : * fragment instance to be called while modifying existing share information */ @JvmStatic - fun newInstance(share: OCShare, screenType: Int, isReshareShown: Boolean, isExpirationDateShown: Boolean): + fun newInstance( + share: OCShare, + screenType: Int, + isReshareShown: Boolean, + isExpirationDateShown: Boolean, + isTextFile: Boolean + ): FileDetailsSharingProcessFragment { val args = Bundle() args.putParcelable(ARG_OCSHARE, share) args.putInt(ARG_SCREEN_TYPE, screenType) args.putBoolean(ARG_RESHARE_SHOWN, isReshareShown) args.putBoolean(ARG_EXP_DATE_SHOWN, isExpirationDateShown) + args.putBoolean(ARG_IS_TEXT_FILE, isTextFile) val fragment = FileDetailsSharingProcessFragment() fragment.arguments = args return fragment @@ -106,6 +124,8 @@ class FileDetailsSharingProcessFragment : @Inject lateinit var viewThemeUtils: ViewThemeUtils + @Inject + lateinit var keyboardUtils: KeyboardUtils private lateinit var onEditShareListener: FileDetailSharingFragment.OnEditShareListener @@ -119,14 +139,23 @@ 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 var expirationDatePickerFragment: ExpirationDatePickerDialogFragment? = null + private var isHideDownloadCheckedReadOnly: Boolean = false + private var isHideDownloadCheckedUploadEdit: Boolean = false + + private var isFileDropSelected: Boolean = false + private var isReadOnlySelected: Boolean = false + private var isUploadEditingSelected: Boolean = false + override fun onAttach(context: Context) { super.onAttach(context) try { @@ -154,6 +183,7 @@ 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) } fileActivity = activity as FileActivity? @@ -161,6 +191,27 @@ class FileDetailsSharingProcessFragment : requireNotNull(fileActivity) { "FileActivity may not be null" } } + // Updating Hide Download enable/disable on selection of FileDrop + override fun onCheckedChanged(group: RadioGroup?, checkId: Int) { + if (binding.shareProcessPermissionFileDrop.id == checkId) { + isFileDropSelected = true + binding.shareProcessHideDownloadCheckbox.isChecked = true + binding.shareProcessHideDownloadCheckbox.isEnabled = false + } else { + isFileDropSelected = false + binding.shareProcessHideDownloadCheckbox.isEnabled = true + if (binding.shareProcessPermissionReadOnly.id == checkId) { + isReadOnlySelected = true + isUploadEditingSelected = false + binding.shareProcessHideDownloadCheckbox.isChecked = isHideDownloadCheckedReadOnly + } else if (binding.shareProcessPermissionUploadEditing.id == checkId) { + isReadOnlySelected = false + isUploadEditingSelected = true + binding.shareProcessHideDownloadCheckbox.isChecked = isHideDownloadCheckedUploadEdit + } + } + } + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { binding = FileDetailsSharingProcessFragmentBinding.inflate(inflater, container, false) fileOperationsHelper = fileActivity?.fileOperationsHelper @@ -170,36 +221,43 @@ class FileDetailsSharingProcessFragment : override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) if (shareProcessStep == SCREEN_TYPE_PERMISSION) { + binding.shareProcessPermissionReadOnly.isChecked = true showShareProcessFirst() } else { showShareProcessSecond() } + //Set default value to 0 for download count + if (!isDownloadCountFetched) { + binding.shareProcessRemainingDownloadCountTv.text = + String.format(resources.getString(R.string.download_text), "0") + } + binding.shareProcessPermissionRadioGroup.setOnCheckedChangeListener(this) implementClickEvents() - + binding.shareProcessHideDownloadCheckbox.setOnCheckedChangeListener { _, isChecked -> + if (!isFileDropSelected) { + if (isReadOnlySelected) { + isHideDownloadCheckedReadOnly = isChecked + } else if (isUploadEditingSelected) { + isHideDownloadCheckedUploadEdit = isChecked + } + } + } themeView() } + private fun scrollTopShowToolbar() { + //show the toolbar if it is hidden due to scrolling + if (requireActivity() is ToolbarActivity) { + (requireActivity() as ToolbarActivity).expandToolbar() + } + } private fun themeView() { - viewThemeUtils.platform.colorTextView(binding.shareProcessEditShareLink) - viewThemeUtils.platform.colorTextView(binding.shareProcessAdvancePermissionTitle) - - viewThemeUtils.platform.themeRadioButton(binding.shareProcessPermissionReadOnly) - viewThemeUtils.platform.themeRadioButton(binding.shareProcessPermissionUploadEditing) - viewThemeUtils.platform.themeRadioButton(binding.shareProcessPermissionFileDrop) - - viewThemeUtils.platform.themeCheckbox(binding.shareProcessAllowResharingCheckbox) - - viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessSetPasswordSwitch) - viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessSetExpDateSwitch) - viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessHideDownloadCheckbox) - viewThemeUtils.androidx.colorSwitchCompat(binding.shareProcessChangeNameSwitch) - - viewThemeUtils.material.colorTextInputLayout(binding.shareProcessEnterPasswordContainer) - viewThemeUtils.material.colorTextInputLayout(binding.shareProcessChangeNameContainer) - viewThemeUtils.material.colorTextInputLayout(binding.noteContainer) - - viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.shareProcessBtnNext) - viewThemeUtils.material.colorMaterialButtonPrimaryOutlined(binding.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) { @@ -216,9 +274,10 @@ class FileDetailsSharingProcessFragment : } private fun showShareProcessFirst() { + scrollTopShowToolbar() binding.shareProcessGroupOne.visibility = View.VISIBLE - binding.shareProcessEditShareLink.visibility = View.VISIBLE binding.shareProcessGroupTwo.visibility = View.GONE + binding.tvSetPasswordEmailWarning.visibility = View.GONE if (share != null) { setupModificationUI() @@ -233,13 +292,17 @@ class FileDetailsSharingProcessFragment : // show or hide expiry date if (isExpDateShown && !isSecureShare) { binding.shareProcessSetExpDateSwitch.visibility = View.VISIBLE + binding.dividerSharingExpDate.visibility = View.VISIBLE } else { binding.shareProcessSetExpDateSwitch.visibility = View.GONE + binding.dividerSharingExpDate.visibility = View.GONE } shareProcessStep = SCREEN_TYPE_PERMISSION } private fun setupModificationUI() { + binding.shareProcessBtnNext.text = requireContext().resources.getString(R.string.common_confirm) + if (share?.isFolder == true) updateViewForFolder() else updateViewForFile() // read only / allow upload and editing / file drop @@ -252,19 +315,6 @@ class FileDetailsSharingProcessFragment : } shareType = share?.shareType ?: ShareType.NO_SHARED - - // show different text for link share and other shares - // because we have link to share in Public Link - val resources = requireContext().resources - - binding.shareProcessBtnNext.text = resources.getString( - if (shareType == ShareType.PUBLIC_LINK) { - R.string.share_copy_link - } else { - R.string.common_confirm - } - ) - updateViewForShareType() binding.shareProcessSetPasswordSwitch.isChecked = share?.isPasswordProtected == true showPasswordInput(binding.shareProcessSetPasswordSwitch.isChecked) @@ -303,24 +353,44 @@ class FileDetailsSharingProcessFragment : } private fun updateViewForExternalShare() { - binding.shareProcessChangeNameSwitch.visibility = View.GONE - binding.shareProcessChangeNameContainer.visibility = View.GONE + hideLinkLabelViews() updateViewForExternalAndLinkShare() + updateFileEditingRadioButton() } private fun updateViewForLinkShare() { updateViewForExternalAndLinkShare() binding.shareProcessChangeNameSwitch.visibility = View.VISIBLE + binding.dividerSharingChangeName.visibility = View.VISIBLE if (share != null) { - binding.shareProcessChangeName.setText(share?.label) + binding.shareProcessChangeNameEt.setText(share?.label) binding.shareProcessChangeNameSwitch.isChecked = !TextUtils.isEmpty(share?.label) } showChangeNameInput(binding.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 + + updateFileEditingRadioButton() } private fun updateViewForInternalShare() { - binding.shareProcessChangeNameSwitch.visibility = View.GONE - binding.shareProcessChangeNameContainer.visibility = View.GONE + hideLinkLabelViews() binding.shareProcessHideDownloadCheckbox.visibility = View.GONE if (isSecureShare) { binding.shareProcessAllowResharingCheckbox.visibility = View.GONE @@ -334,6 +404,35 @@ class FileDetailsSharingProcessFragment : binding.shareProcessAllowResharingCheckbox.visibility = View.GONE } binding.shareProcessAllowResharingCheckbox.isChecked = SharingMenuHelper.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.shareProcessPermissionFileDrop.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 updateFileEditingRadioButton() { + if (!isTextFile) { + binding.shareProcessPermissionUploadEditing.isEnabled = false + binding.shareProcessPermissionUploadEditing.setTextColor(resources.getColor(R.color.share_disabled_txt_color)) } } @@ -341,15 +440,22 @@ class FileDetailsSharingProcessFragment : * update views where share type external or link share */ private fun updateViewForExternalAndLinkShare() { - binding.shareProcessHideDownloadCheckbox.visibility = View.VISIBLE + binding.shareProcessHideDownloadCheckbox.isEnabled = true + binding.dividerSharingHideDownload.visibility = View.VISIBLE binding.shareProcessAllowResharingCheckbox.visibility = View.GONE + binding.shareProcessAllowResharingInfo.visibility = View.GONE + binding.dividerSharingAllowResharing.visibility = View.GONE binding.shareProcessSetPasswordSwitch.visibility = View.VISIBLE + binding.dividerSharingEnterPassword.visibility = View.VISIBLE if (share != null) { if (SharingMenuHelper.isFileDrop(share)) { - binding.shareProcessHideDownloadCheckbox.visibility = View.GONE + binding.shareProcessHideDownloadCheckbox.isChecked = true + binding.shareProcessHideDownloadCheckbox.isEnabled = false + binding.dividerSharingHideDownload.visibility = View.GONE } else { - binding.shareProcessHideDownloadCheckbox.visibility = View.VISIBLE + binding.shareProcessHideDownloadCheckbox.isEnabled = true + binding.dividerSharingHideDownload.visibility = View.VISIBLE binding.shareProcessHideDownloadCheckbox.isChecked = share?.isHideFileDownload == true } } @@ -377,14 +483,17 @@ class FileDetailsSharingProcessFragment : binding.shareProcessPermissionUploadEditing.text = requireContext().resources.getString(R.string.link_share_editing) binding.shareProcessPermissionFileDrop.visibility = View.GONE + binding.shareFileDropInfo.visibility = View.GONE } private fun updateViewForFolder() { binding.shareProcessPermissionUploadEditing.text = requireContext().resources.getString(R.string.link_share_allow_upload_and_editing) binding.shareProcessPermissionFileDrop.visibility = View.VISIBLE + binding.shareFileDropInfo.visibility = View.VISIBLE if (isSecureShare) { binding.shareProcessPermissionFileDrop.visibility = View.GONE + binding.shareFileDropInfo.visibility = View.GONE binding.shareProcessAllowResharingCheckbox.visibility = View.GONE binding.shareProcessSetExpDateSwitch.visibility = View.GONE } @@ -394,15 +503,25 @@ class FileDetailsSharingProcessFragment : * update views for screen type Note */ private fun showShareProcessSecond() { + scrollTopShowToolbar() binding.shareProcessGroupOne.visibility = View.GONE - binding.shareProcessEditShareLink.visibility = View.GONE binding.shareProcessGroupTwo.visibility = View.VISIBLE if (share != null) { - binding.shareProcessBtnNext.text = requireContext().resources.getString(R.string.set_note) + binding.shareProcessBtnNext.text = requireContext().resources.getString(R.string.send_email) binding.noteText.setText(share?.note) + + //show the warning label if password protection is enabled + /* binding.tvSetPasswordEmailWarning.visibility = + if (share?.isPasswordProtected == true) View.VISIBLEF + else View.GONE*/ } else { binding.shareProcessBtnNext.text = requireContext().resources.getString(R.string.send_share) binding.noteText.setText("") + + //show the warning label if password protection is enabled + /* binding.tvSetPasswordEmailWarning.visibility = + if (binding.shareProcessSetPasswordSwitch.isChecked) View.VISIBLE + else View.GONE*/ } shareProcessStep = SCREEN_TYPE_NOTE } @@ -430,6 +549,17 @@ class FileDetailsSharingProcessFragment : binding.shareProcessSelectExpDate.setOnClickListener { showExpirationDateDialog() } + binding.shareProcessDownloadLimitSwitch.setOnCheckedChangeListener { _, isChecked -> + showDownloadLimitInput(isChecked) + } + binding.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 + } + } private fun showExpirationDateDialog(chosenDateInMillis: Long = chosenExpDateInMills) { @@ -445,13 +575,30 @@ class FileDetailsSharingProcessFragment : } private fun showChangeNameInput(isChecked: Boolean) { - binding.shareProcessChangeNameContainer.visibility = if (isChecked) View.VISIBLE else View.GONE + binding.shareProcessChangeNameEt.visibility = if (isChecked) View.VISIBLE else View.GONE + if (!isChecked) { + binding.shareProcessChangeNameEt.setText("") + //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("") + binding.shareProcessDownloadLimitEt.setText("") + 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() @@ -471,6 +618,13 @@ class FileDetailsSharingProcessFragment : binding.shareProcessSelectExpDate.visibility = if (isChecked) View.VISIBLE else View.GONE binding.shareProcessExpDateDivider.visibility = if (isChecked) View.VISIBLE else View.GONE + //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 @@ -479,17 +633,28 @@ class FileDetailsSharingProcessFragment : } private fun showPasswordInput(isChecked: Boolean) { - binding.shareProcessEnterPasswordContainer.visibility = if (isChecked) View.VISIBLE else View.GONE + binding.shareProcessEnterPassword.visibility = if (isChecked) View.VISIBLE else View.GONE // reset the password if switch is unchecked if (!isChecked) { binding.shareProcessEnterPassword.setText("") + // 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() } private fun getReSharePermission(): Int { @@ -503,6 +668,7 @@ class FileDetailsSharingProcessFragment : */ @Suppress("ReturnCount") private fun validateShareProcessFirst() { + hideKeyboard() permission = getSelectedPermission() if (permission == OCShare.NO_PERMISSION) { DisplayUtils.showSnackMessage(binding.root, R.string.no_share_permission_selected) @@ -524,12 +690,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() @@ -547,8 +724,8 @@ class FileDetailsSharingProcessFragment : binding.shareProcessAllowResharingCheckbox.isChecked -> getReSharePermission() binding.shareProcessPermissionReadOnly.isChecked -> OCShare.READ_PERMISSION_FLAG binding.shareProcessPermissionUploadEditing.isChecked -> when { - file?.isFolder == true || share?.isFolder == true -> OCShare.MAXIMUM_PERMISSIONS_FOR_FOLDER - else -> OCShare.MAXIMUM_PERMISSIONS_FOR_FILE + file?.isFolder == true || share?.isFolder == true -> SharingMenuHelper.CAN_EDIT_PERMISSIONS_FOR_FOLDER + else -> SharingMenuHelper.CAN_EDIT_PERMISSIONS_FOR_FILE } binding.shareProcessPermissionFileDrop.isChecked -> OCShare.CREATE_PERMISSION_FLAG @@ -562,7 +739,8 @@ class FileDetailsSharingProcessFragment : binding.shareProcessHideDownloadCheckbox.isChecked, binding.shareProcessEnterPassword.text.toString().trim(), chosenExpDateInMills, - binding.shareProcessChangeName.text.toString().trim() + binding.shareProcessChangeNameEt.text.toString().trim(), + binding.shareProcessDownloadLimitEt.text.toString().trim() ) // copy the share link if available if (!TextUtils.isEmpty(share?.shareLink)) { @@ -574,10 +752,14 @@ class FileDetailsSharingProcessFragment : * method to validate step 2 (note screen) information */ private fun validateShareProcessSecond() { - val noteText = binding.noteText.text.toString().trim() + hideKeyboard() // if modifying existing share then directly update the note and send email - if (share != null && share?.note != noteText) { - fileOperationsHelper?.updateNoteToShare(share, noteText) + if (share != null) { + if (TextUtils.isEmpty(binding.noteText.text.toString().trim())) { + DisplayUtils.showSnackMessage(binding.root, R.string.share_link_empty_note_message) + return + } + fileOperationsHelper?.updateNoteToShare(share, binding.noteText.text.toString().trim()) } else { // else create new share fileOperationsHelper?.shareFileWithSharee( @@ -589,14 +771,27 @@ class FileDetailsSharingProcessFragment : .shareProcessHideDownloadCheckbox.isChecked, binding.shareProcessEnterPassword.text.toString().trim(), chosenExpDateInMills, - noteText, - binding.shareProcessChangeName.text.toString().trim(), + binding.noteText.text.toString().trim(), + binding.shareProcessChangeNameEt.text.toString().trim(), true ) } removeCurrentFragment() } + /** + * 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 */ @@ -617,4 +812,15 @@ class FileDetailsSharingProcessFragment : override fun onDateUnSet() { 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 + } } 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 4bd1e91c379c..8045c54a9475 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 @@ -89,6 +89,7 @@ import com.owncloud.android.ui.dialog.CreateFolderDialogFragment; 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.dialog.SetupEncryptionDialogFragment; import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment; import com.owncloud.android.ui.events.ChangeMenuEvent; @@ -577,6 +578,8 @@ public void createRichWorkspace() { @Override public void onShareIconClick(OCFile file) { + //NMC Customization + SendShareDialog.isPeopleShareClicked = true; if (file.isFolder()) { mContainerActivity.showDetails(file, 1); } else { @@ -1154,6 +1157,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) { @@ -1182,7 +1187,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 caee85609712..2748ddee4235 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 @@ -20,6 +20,7 @@ import com.owncloud.android.databinding.QuickSharingPermissionsBottomSheetFragmentBinding; import com.owncloud.android.datamodel.QuickPermissionModel; import com.owncloud.android.lib.resources.shares.OCShare; +import com.owncloud.android.lib.resources.shares.ShareType; import com.owncloud.android.ui.activity.FileActivity; import com.owncloud.android.ui.adapter.QuickSharingPermissionsAdapter; import com.owncloud.android.ui.fragment.util.SharingMenuHelper; @@ -31,8 +32,6 @@ 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; /** @@ -66,8 +65,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()) @@ -103,22 +100,21 @@ public void onDismissSheet() { * @param position */ private void handlePermissionChanged(List quickPermissionModelList, int position) { - if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(fileActivity.getResources().getString(R.string.link_share_allow_upload_and_editing)) - || quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(fileActivity.getResources().getString(R.string.link_share_editing))) { + if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(fileActivity.getResources().getString(R.string.share_permission_can_edit))) { if (ocShare.isFolder()) { actions.onQuickPermissionChanged(ocShare, - MAXIMUM_PERMISSIONS_FOR_FOLDER); + SharingMenuHelper.CAN_EDIT_PERMISSIONS_FOR_FOLDER); } else { actions.onQuickPermissionChanged(ocShare, - MAXIMUM_PERMISSIONS_FOR_FILE); + SharingMenuHelper.CAN_EDIT_PERMISSIONS_FOR_FILE); } } else if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(fileActivity.getResources().getString(R.string - .link_share_view_only))) { + .share_permission_read_only))) { actions.onQuickPermissionChanged(ocShare, READ_PERMISSION_FLAG); } else if (quickPermissionModelList.get(position).getPermissionName().equalsIgnoreCase(fileActivity.getResources().getString(R.string - .link_share_file_drop))) { + .share_permission_file_drop))) { actions.onQuickPermissionChanged(ocShare, CREATE_PERMISSION_FLAG); } @@ -133,8 +129,13 @@ private List getQuickPermissionList() { String[] permissionArray; if (ocShare.isFolder()) { - permissionArray = - fileActivity.getResources().getStringArray(R.array.folder_share_permission_dialog_values); + if (ocShare.getShareType() == ShareType.EMAIL || ocShare.getShareType() == ShareType.PUBLIC_LINK) { + permissionArray = + fileActivity.getResources().getStringArray(R.array.folder_share_permission_dialog_values); + } else { + permissionArray = + fileActivity.getResources().getStringArray(R.array.folder_internal_share_permission_dialog_values); + } } else { permissionArray = fileActivity.getResources().getStringArray(R.array.file_share_permission_dialog_values); @@ -142,7 +143,6 @@ private List getQuickPermissionList() { //get the checked item position int checkedItem = SharingMenuHelper.getPermissionCheckedItem(fileActivity, ocShare, permissionArray); - final List quickPermissionModelList = new ArrayList<>(permissionArray.length); for (int i = 0; i < permissionArray.length; i++) { QuickPermissionModel quickPermissionModel = new QuickPermissionModel(permissionArray[i], checkedItem == i); diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/util/SharingMenuHelper.java b/app/src/main/java/com/owncloud/android/ui/fragment/util/SharingMenuHelper.java index 6f93fa2275fa..748f6ef05d6f 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/util/SharingMenuHelper.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/util/SharingMenuHelper.java @@ -15,24 +15,40 @@ import android.content.res.Resources; import android.view.MenuItem; +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.lib.resources.shares.OCShare; +import com.owncloud.android.lib.resources.status.OCCapability; import java.text.SimpleDateFormat; import java.util.Date; +import androidx.annotation.NonNull; + 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.DELETE_PERMISSION_FLAG; import static com.owncloud.android.lib.resources.shares.OCShare.NO_PERMISSION; import static com.owncloud.android.lib.resources.shares.OCShare.READ_PERMISSION_FLAG; import static com.owncloud.android.lib.resources.shares.OCShare.SHARE_PERMISSION_FLAG; +import static com.owncloud.android.lib.resources.shares.OCShare.UPDATE_PERMISSION_FLAG; /** * Helper calls for visibility logic of the sharing menu. */ public final class SharingMenuHelper { + //updated Edit permissions for folder and files + //because the MAXIMUM_PERMISSIONS_FOR_FILE and MAXIMUM_PERMISSIONS_FOR_FOLDER permission in OCShare + //are not valid anymore due to functionality changes + public static final int CAN_EDIT_PERMISSIONS_FOR_FILE = + READ_PERMISSION_FLAG + UPDATE_PERMISSION_FLAG; + + public static final int CAN_EDIT_PERMISSIONS_FOR_FOLDER = + READ_PERMISSION_FLAG + UPDATE_PERMISSION_FLAG + CREATE_PERMISSION_FLAG + DELETE_PERMISSION_FLAG; + + private SharingMenuHelper() { // utility class -> private constructor } @@ -90,9 +106,9 @@ public static boolean isUploadAndEditingAllowed(OCShare share) { return false; } - return (share.getPermissions() & (share.isFolder() ? MAXIMUM_PERMISSIONS_FOR_FOLDER : - MAXIMUM_PERMISSIONS_FOR_FILE)) == (share.isFolder() ? MAXIMUM_PERMISSIONS_FOR_FOLDER : - MAXIMUM_PERMISSIONS_FOR_FILE); + return (share.getPermissions() & (share.isFolder() ? CAN_EDIT_PERMISSIONS_FOR_FOLDER: + CAN_EDIT_PERMISSIONS_FOR_FILE)) == (share.isFolder() ? CAN_EDIT_PERMISSIONS_FOR_FOLDER : + CAN_EDIT_PERMISSIONS_FOR_FILE); } public static boolean isReadOnly(OCShare share) { @@ -120,13 +136,13 @@ public static boolean isSecureFileDrop(OCShare share) { } public static String getPermissionName(Context context, OCShare share) { - if (SharingMenuHelper.isUploadAndEditingAllowed(share)) { + if (isUploadAndEditingAllowed(share)) { return context.getResources().getString(R.string.share_permission_can_edit); - } else if (SharingMenuHelper.isReadOnly(share)) { - return context.getResources().getString(R.string.share_permission_view_only); + } else if (isReadOnly(share)) { + return context.getResources().getString(R.string.share_permission_read_only); } else if (SharingMenuHelper.isSecureFileDrop(share)) { return context.getResources().getString(R.string.share_permission_secure_file_drop); - } else if (SharingMenuHelper.isFileDrop(share)) { + } else if (isFileDrop(share)) { return context.getResources().getString(R.string.share_permission_file_drop); } return null; @@ -135,18 +151,18 @@ public static String getPermissionName(Context context, OCShare share) { /** * method to get the current checked index from the list of permissions * + * @param context + * @param share + * @param permissionArray + * @return */ public static int getPermissionCheckedItem(Context context, OCShare share, String[] permissionArray) { if (SharingMenuHelper.isUploadAndEditingAllowed(share)) { - if (share.isFolder()) { - return getPermissionIndexFromArray(context, permissionArray, R.string.link_share_allow_upload_and_editing); - } else { - return getPermissionIndexFromArray(context, permissionArray, R.string.link_share_editing); - } + return getPermissionIndexFromArray(context, permissionArray, R.string.share_permission_can_edit); } else if (SharingMenuHelper.isReadOnly(share)) { - return getPermissionIndexFromArray(context, permissionArray, R.string.link_share_view_only); + return getPermissionIndexFromArray(context, permissionArray, R.string.share_permission_read_only); } else if (SharingMenuHelper.isFileDrop(share)) { - return getPermissionIndexFromArray(context, permissionArray, R.string.link_share_file_drop); + return getPermissionIndexFromArray(context, permissionArray, R.string.share_permission_file_drop); } return 0;//default first item selected } @@ -163,4 +179,28 @@ private static int getPermissionIndexFromArray(Context context, String[] permiss public static boolean canReshare(OCShare share) { return (share.getPermissions() & SHARE_PERMISSION_FLAG) > 0; } + + /** + * 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 + */ + public static boolean canEditFile(@NonNull Context context, @NonNull User user, + @NonNull OCCapability capability, @NonNull OCFile file, + @NonNull EditorUtils editorUtils) { + + //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 + boolean isTextFile = editorUtils.isEditorAvailable(user, file.getMimeType()) && !file.isEncrypted(); + + //check for office files like .docx, .pptx, .xls, etc + boolean isOfficeFile = capability.getRichDocumentsMimeTypeList() != null && capability.getRichDocumentsMimeTypeList().contains(file.getMimeType()) && + capability.getRichDocumentsDirectEditing().isTrue() && !file.isEncrypted(); + + return isTextFile || isOfficeFile; + } } 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 6d8efcea33bd..bb6d3208ccee 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 @@ -632,9 +632,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)); + } } /** @@ -773,7 +774,7 @@ public void updateNoteToShare(OCShare share, String note) { */ public void updateShareInformation(OCShare share, int permissions, boolean hideFileDownload, String password, long expirationTimeInMillis, - String label) { + String label, String downloadLimit) { Intent updateShareIntent = new Intent(fileActivity, OperationsService.class); updateShareIntent.setAction(OperationsService.ACTION_UPDATE_SHARE_INFO); updateShareIntent.putExtra(OperationsService.EXTRA_ACCOUNT, fileActivity.getAccount()); @@ -783,6 +784,26 @@ public void updateShareInformation(OCShare share, int permissions, updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PASSWORD, (password == null) ? "" : password); updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_EXPIRATION_DATE_IN_MILLIS, expirationTimeInMillis); updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_PUBLIC_LABEL, (label == null) ? "" : label); + + //download limit for link share type + updateShareIntent.putExtra(OperationsService.EXTRA_SHARE_DOWNLOAD_LIMIT, + (downloadLimit == null || downloadLimit.equals("")) ? 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 f8a742b9a626..87ccf3b7d6b2 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/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/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..2d8cc41165bb --- /dev/null +++ b/app/src/main/res/drawable/share_search_background.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + 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/drawable/toolbar_back_arrow_bg.xml b/app/src/main/res/drawable/toolbar_back_arrow_bg.xml new file mode 100644 index 000000000000..42c6b7aabba2 --- /dev/null +++ b/app/src/main/res/drawable/toolbar_back_arrow_bg.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/layout/file_details_fragment.xml b/app/src/main/res/layout/file_details_fragment.xml index b0d59e7a0d27..cdd64f432dda 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" /> @@ -60,8 +61,8 @@ android:id="@+id/size" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textSize="@dimen/two_line_primary_text_size" - android:textColor="@color/list_item_lastmod_and_filesize_text" + android:textSize="@dimen/txt_size_14sp" + android:textColor="@color/share_subtitle_txt_color" tools:text="@string/placeholder_fileSize" /> + android:textColor="@color/share_subtitle_txt_color" + android:textSize="@dimen/two_line_secondary_text_size" /> + app:iconTint="@color/grey_60" /> @@ -175,24 +177,8 @@ android:layout_height="1dp" android:background="@color/list_divider_background" /> - - - - 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 082b0eb18e76..4f844bf51ce3 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 @@ -14,18 +14,18 @@ android:id="@+id/share_by_link_container" android:layout_width="match_parent" android:layout_height="@dimen/sharee_list_item_size" + android:layout_marginTop="@dimen/standard_margin" android:orientation="horizontal"> + android:textSize="@dimen/txt_size_14sp" /> + android:scaleType="fitStart" + app:tint="@color/primary" + android:paddingStart="@dimen/standard_padding" + android:paddingEnd="@dimen/standard_half_padding" + android:src="@drawable/ic_clipboard" /> 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 dc26ae6d669d..e5eb36d1ba19 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"> + app:tint="@color/text_color" + android:src="@drawable/ic_internal_share" /> 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 2b38784347ce..53eb2d663c93 100644 --- a/app/src/main/res/layout/file_details_sharing_fragment.xml +++ b/app/src/main/res/layout/file_details_sharing_fragment.xml @@ -6,119 +6,197 @@ ~ SPDX-FileCopyrightText: 2018 Andy Scherzinger ~ SPDX-License-Identifier: AGPL-3.0-or-later --> - + android:layout_height="match_parent"> - - - - - - - - + android:layout_height="match_parent" + android:layout_marginTop="@dimen/standard_margin" + android:orientation="vertical"> - - - + - + - - + android:layout_height="wrap_content" + android:orientation="vertical" + android:paddingLeft="@dimen/standard_half_padding" + android:paddingTop="@dimen/standard_quarter_padding" + android:paddingRight="@dimen/standard_padding"> + android:text="@string/shared_with_you_by" + android:textSize="@dimen/two_line_primary_text_size" /> + + + + + + + + + + + + + + + + + - - + - + + + + + + + + + + + + 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 23bce10792f5..b31091ea01c7 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 @@ @@ -150,7 +186,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 08540bc2088c..aa820e27f577 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 @@ -11,6 +11,7 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@color/bg_default" android:focusable="true" android:focusableInTouchMode="true" android:minHeight="400dp"> @@ -18,8 +19,9 @@ @@ -30,116 +32,252 @@ android:minHeight="400dp"> + + + + + 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" /> + + + app:layout_constraintTop_toBottomOf="@+id/share_file_drop_info" /> - + + + + + + + + + + + app:layout_constraintTop_toBottomOf="@+id/divider_sharing_allow_resharing" /> - - - - - + tools:visibility="visible" /> - + + app:layout_constraintTop_toBottomOf="@+id/divider_sharing_enter_password" /> + + + + + + + app:layout_constraintTop_toBottomOf="@+id/divider_sharing_change_name" /> - + - - + app:layout_constraintTop_toBottomOf="@+id/share_process_download_limit_switch" + tools:visibility="visible" /> - + - + + 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" /> + app:layout_constraintTop_toBottomOf="@+id/tv_sharing_title" /> - + app:layout_constraintTop_toBottomOf="@+id/share_process_message_title" /> - - + + app:constraint_referenced_ids="share_process_message_title, note_text" /> + + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toEndOf="@+id/share_process_btn_cancel"/> 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 cd109b830dd5..9877bcf3feea 100644 --- a/app/src/main/res/layout/item_quick_share_permissions.xml +++ b/app/src/main/res/layout/item_quick_share_permissions.xml @@ -9,17 +9,15 @@ + android:layout_height="wrap_content" + android:padding="@dimen/standard_padding"> + + + + Bearbeiten Alle Benachrichtigungen löschen Papierkorb leeren - Senden/Teilen + Teilen Kachelansicht Listenansicht Kontakte und Kalender wiederherstellen @@ -33,6 +33,9 @@ Neue sichere Dateiablage hinzufügen Hinzufügen zu %1$s Erweiterte Einstellungen + Erstellen erlauben + Löschen erlauben + Bearbeitung erlauben Weiterteilen erlauben Basis-URL Proxy-Hostname @@ -476,10 +479,10 @@ Letzte Sicherung:%1$s Link Link-Name - Hochladen und Bearbeiten erlauben - Bearbeitung - Dateien ablegen (nur Hochladen) - Nur anzeigen + Hochladen & Bearbeiten + Sammelbox + Nur Lesen + Bearbeiten Layout der Liste Weitere Ergebnisse laden Es befinden sich keine Dateien in diesem Ordner. @@ -549,7 +552,6 @@ Nur ein Konto zulässig Keine App verfügbar um PDFs anzuzeigen Keine App zum Senden der ausgewählten Dateien verfügbar - Bitte mindestens eine Berechtigung zum Teilen auswählen. Notiz konnte nicht versandt werden Notiz-Symbol Ausführen der Aktion fehlgeschlagen. @@ -693,7 +695,8 @@ Kontolöschung anfordern Löschung anfordern Beim Diensteanbieter die dauerhafte Löschung des Kontos anfordern - Das Weiterverteilen ist nicht erlaubt + Weiterteilen ist nicht erlaubt. + Weiterteilen ist erlaubt. Resharing/Wiederteilen ist nicht erlaubt. Kein verkleinertes Bild verfügbar. Vollbild herunterladen? Datei wiederherstellen @@ -724,7 +727,6 @@ Bitte eine Vorlage auswählen Vorlage auswählen Senden - Freigabe senden Icon für den Senden-Button Setze als Notiz setzen @@ -744,29 +746,29 @@ Der interne Freigabelink funktioniert nur für Benutzer mit Zugriff auf diesen Ordner auf %1$s Link teilen + Link zum Ordner + Link zur Datei Sie müssen ein Passwort eingeben Es ist ein Fehler beim Freigeben der Datei oder des Ordners aufgetreten. Teilen nicht möglich. Bitte prüfen Sie, ob die Datei existiert. 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 Passwortgeschützt - Kann bearbeiten - Dateiablage - Sichere Dateiablage Nur anzeigen Berechtigungen zum Teilen + Freigabe aufheben %1$s (remote) %1$s (Unterhaltung) - Name, Federated-Cloud-ID oder E-Mail-Adresse … - Neue E-Mail senden + Kontaktname oder E-Mail Notiz an Empfänger Einstellungen Download verbergen + Passwortschutz (%1$s) Link teilen Link teilen Entfernen @@ -883,6 +885,7 @@ Es gibt ungelesene Kommentare Verschlüsselung aufheben Von Favoriten entfernen + Freigabe aufheben Es ist ein Fehler beim Entfernen der Freigabe für diese Datei oder den Ordner aufgetreten. Entfernen der Freigabe nicht möglich. Bitte prüfen Sie, ob die Datei existiert. diese Datei nicht mehr zu teilen @@ -1015,6 +1018,44 @@ Inhalte von %1$d Datei konnten nicht synchronisiert werden (Konflikte: %2$d) Inhalte von %1$d Dateien konnten nicht synchronisiert werden (Konflikte: %2$d) + + Nur lesen + Kann Bearbeiten + Sammelbox + Berechtigungen + Erweiterte Berechtigungen + Ihre Nachricht + Freigabe senden + Bitte wählen Sie mindestens eine Berechtigung zum Teilen aus. + Sie müssen das Ablaufdatum auswählen. + Bitte Anmerkung eingeben. + Email senden + Öffnen mit... + Erweiterte Berechtigungen + Neue Email versenden + Download Limit + Das Feld für das Download-Limit darf nicht leer sein. + Downlimit eingeben + Downloads: %s + Der Wert für das Download limit 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 + Sie können Links erstellen oder Freigaben per Mail versenden. Wenn Sie + MagentaCLOUD Nutzer einladen, bieten sich Ihnen mehr Möglichkeiten der Zusammenarbeit. + persönliche Freigabe per E-Mail + 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 + Noch keine Freigaben erstellt. + %1$d Datei im %2$s-Verzeichnis konnten nicht kopiert werden nach %1$d Dateien im %2$s-Verzeichnis konnten nicht kopiert werden nach diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index e9c749b86943..76e4e05ca495 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -36,4 +36,68 @@ #1E1E1E @android:color/white + + + #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 + + + #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/attrs.xml b/app/src/main/res/values/attrs.xml index 7317a84ee0e1..4a65a8bc62a3 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -27,14 +27,19 @@ - @string/link_share_view_only - @string/link_share_allow_upload_and_editing - @string/link_share_file_drop + @string/share_permission_read_only + @string/share_permission_can_edit + @string/share_permission_file_drop + + + + @string/share_permission_read_only + @string/share_permission_can_edit - @string/link_share_view_only - @string/link_share_editing + @string/share_permission_read_only + @string/share_permission_can_edit @string/sub_folder_rule_month 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 9a721eb3e385..248aa18d524c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -76,4 +76,93 @@ @android:color/white #666666 #A5A5A5 + + + #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 + + + @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/strings.xml b/app/src/main/res/values/strings.xml index b1113f748a0e..460f4729d6d4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -414,6 +414,7 @@ Subfolder options + Unshare Unable to share. Please check whether the file exists. An error occurred while trying to share this file or folder. Unable to unshare. Please check whether the file exists. @@ -551,11 +552,12 @@ Share link Send link Password-protected + Unshare Set password Share with… Unset - Name, Federated Cloud ID or email address … + Contact name or email Secure share … %1$s (group) @@ -791,6 +793,9 @@ Name Password Add to %1$s + Allow creating + Allow deleting + Allow editing Upload files Upload from camera Scan document from camera @@ -858,6 +863,7 @@ No App available to handle mail address No App available to handle maps Hide download + Password protect (%1$s) Unread comments exist This icon indicates availability of live photo Failed to load document! @@ -915,7 +921,8 @@ Downloads Avatar from shared user Shared with you by %1$s - Resharing is not allowed + Resharing is not allowed. + Resharing is allowed. Retrieving file… Associated account not found! Logged in as %1$s @@ -1003,10 +1010,11 @@ Add another link Add new public share link New name - Share link (%1$s) + Link \'%1$s\' Share link + Link to folder + Link to file Allow resharing - View only Editing Allow upload and editing File drop (upload only) @@ -1042,6 +1050,36 @@ This might be due to a backup restore on another device. Falling back to default. Please check settings to adjust data storage folder. Close Login with %1$s to %2$s + + Read only + You must select expiration date. + Please enter note. + Send email + Open in… + Advanced permissions + 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 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 + No shares created yet. + 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 + Login via direct link failed! Update Android System WebView Please update the Android System WebView app for a login @@ -1052,12 +1090,12 @@ Create Please select one template Please choose a template and enter a file name. - View only + Read only Can edit - File drop + Filedrop only Secure file drop - Share Permissions - Advanced Settings + Permissions + Advanced Permissions Next Send Share Please select at least one permission to share. diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 1a8b501ee4b7..3b5ea0a9495b 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -178,6 +178,16 @@ bold + + +