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 d4c5de954adf..51e97cc4ff23 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 e16471d7ff88..fac562470dd1 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -580,7 +580,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 7de040d6fad7..81855ebfcf3f 100644 --- a/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java +++ b/app/src/main/java/com/owncloud/android/operations/CreateShareWithShareeOperation.java @@ -228,7 +228,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 918e5577a0cc..9d9da1128230 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 fc4c05dd4b77..ce05d4363a30 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 648c7ce1b444..5308fb56edf2 100644 --- a/app/src/main/java/com/owncloud/android/services/OperationsService.java +++ b/app/src/main/java/com/owncloud/android/services/OperationsService.java @@ -63,6 +63,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; @@ -99,6 +100,8 @@ public class OperationsService extends Service { public static final String EXTRA_SHARE_HIDE_FILE_DOWNLOAD = "HIDE_FILE_DOWNLOAD"; public static final String EXTRA_SHARE_ID = "SHARE_ID"; public static final String EXTRA_SHARE_NOTE = "SHARE_NOTE"; + public static final String EXTRA_SHARE_TOKEN = "SHARE_TOKEN"; + public static final String EXTRA_SHARE_DOWNLOAD_LIMIT = "SHARE_DOWNLOAD_LIMIT"; public static final String EXTRA_IN_BACKGROUND = "IN_BACKGROUND"; public static final String ACTION_CREATE_SHARE_VIA_LINK = "CREATE_SHARE_VIA_LINK"; @@ -119,6 +122,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; @@ -639,6 +643,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 a32ee12126f3..9edef4488f17 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 @@ -75,6 +75,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; @@ -86,6 +88,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; @@ -383,7 +386,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(); @@ -421,6 +425,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); } } @@ -800,7 +806,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(); @@ -827,6 +832,8 @@ private void onCreateShareViaLinkOperationFinish(CreateShareViaLinkOperation ope sharingFragment.onUpdateShareInformation(result, file); } + // this has to be here to avoid the crash when creating link from inside of FileDetailSharingFragment + Fragment fileListFragment = getSupportFragmentManager().findFragmentByTag(FileDisplayActivity.TAG_LIST_OF_FILES); if (fileListFragment instanceof OCFileListFragment ocFileListFragment && file != null) { if (ocFileListFragment.getAdapterFiles().contains(file)) { ocFileListFragment.updateOCFile(file); @@ -866,6 +873,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 * @@ -933,11 +956,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()); + } } } @@ -951,9 +980,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); } } @@ -964,7 +1007,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 e51fd0869b4e..2bfaee577b97 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 943aad8b25ce..1d9c73405daa 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) { @@ -261,10 +265,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. * @@ -273,10 +287,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. */ @@ -288,6 +327,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 f6666905d842..883c7b857dd2 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/ListViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt index 44e9fa7e8ee8..d534cb665727 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/ListViewHolder.kt @@ -22,6 +22,7 @@ interface ListViewHolder { val localFileIndicator: ImageView val imageFileName: TextView? val shared: ImageView + val sharedMessage: TextView? val checkbox: ImageView val itemLayout: View val unreadComments: ImageView diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListAdapter.java index 2514e3eb0a64..f4dde8a895fb 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 @@ -520,14 +520,16 @@ private void bindListGridItemViewHolder(ListGridItemViewHolder holder, OCFile fi } } - ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation()); + // NMC: don't show share icon for grid view + // ViewExtensionsKt.setVisibleIf(holder.getShared(), !file.isOfflineOperation()); setColorFilterForOfflineOperations(holder, file); } 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 dff220e2a3bd..50a803dd0ee0 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 @@ -367,22 +369,64 @@ class OCFileListDelegate( private fun showShareIcon(gridViewHolder: ListViewHolder, 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/OCFileListGridItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt index 05ee4731e994..fbc5e5664d37 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListGridItemViewHolder.kt @@ -40,6 +40,8 @@ internal class OCFileListGridItemViewHolder(var binding: GridItemBinding) : get() = null override val shared: ImageView get() = binding.sharedIcon + override val sharedMessage: TextView? + get() = null override val checkbox: ImageView get() = binding.customCheckbox override val itemLayout: View diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt index 38b3310f0bbe..e163745e8227 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListItemViewHolder.kt @@ -74,6 +74,8 @@ internal class OCFileListItemViewHolder(private var binding: ListItemBinding) : get() = null override val shared: ImageView get() = binding.sharedIcon + override val sharedMessage: TextView + get() = binding.sharedMessage override val checkbox: ImageView get() = binding.customCheckbox override val itemLayout: View diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt index 7619df6544fb..c92dc905fd54 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt +++ b/app/src/main/java/com/owncloud/android/ui/adapter/OCFileListViewHolder.kt @@ -40,6 +40,8 @@ internal class OCFileListViewHolder(var binding: GridImageBinding) : get() = binding.localFileIndicator override val shared: ImageView get() = binding.sharedIcon + override val sharedMessage: TextView? + get() = null override val checkbox: ImageView get() = binding.customCheckbox override val itemLayout: View diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/QuickSharingPermissionsAdapter.kt b/app/src/main/java/com/owncloud/android/ui/adapter/QuickSharingPermissionsAdapter.kt index 64ffa0350002..42b1c716b206 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 69109c62d69c..dd31e4745c3b 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 2e051086d91b..dea4ae37c265 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 9be56aff7ec6..816bbe9297f4 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 @@ -75,7 +74,6 @@ class SendShareDialog : BottomSheetDialogFragment(R.layout.send_share_fragment), binding.btnShare.setOnClickListener { shareFile(file) } binding.btnLink.setOnClickListener { shareByLink() } - applyTintColor() setupBottomSheetBehaviour() checkButtonVisibilities() setupSendButtonRecyclerView() @@ -99,12 +97,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) { @@ -128,6 +120,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) { @@ -212,6 +207,9 @@ class SendShareDialog : BottomSheetDialogFragment(R.layout.send_share_fragment), } private fun shareFile(file: OCFile?) { + // NMC Customization + isPeopleShareClicked = true + dismiss() if (activity is FileDisplayActivity) { @@ -238,6 +236,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 08150abc4031..27825ae3655c 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; @@ -41,6 +44,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; @@ -51,12 +55,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; @@ -74,6 +79,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; import androidx.viewpager2.widget.ViewPager2; @@ -85,6 +91,7 @@ public class FileDetailFragment extends FileFragment implements OnClickListener, private static final String TAG = FileDetailFragment.class.getSimpleName(); private static final String FTAG_CONFIRMATION = "REMOVE_CONFIRMATION_FRAGMENT"; static final String FTAG_RENAME_FILE = "RENAME_FILE_FRAGMENT"; + private static final String FTAG_SHARING = "SHARING_DETAILS_FRAGMENT"; private static final String ARG_FILE = "FILE"; private static final String ARG_PARENT_FOLDER = "PARENT_FOLDER"; @@ -94,6 +101,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; @@ -107,6 +118,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. @@ -168,12 +181,7 @@ public FileDetailSharingFragment getFileDetailSharingFragment() { if (binding == null) { return null; } - - if (binding.pager.getAdapter() instanceof FileDetailTabAdapter adapter) { - return adapter.getFileDetailSharingFragment(); - } - - return null; + return (FileDetailSharingFragment)requireActivity().getSupportFragmentManager().findFragmentByTag(FTAG_SHARING); } /** @@ -182,10 +190,6 @@ public FileDetailSharingFragment getFileDetailSharingFragment() { * @return reference to the {@link FileDetailActivitiesFragment} */ public FileDetailActivitiesFragment getFileDetailActivitiesFragment() { - if (binding.pager.getAdapter() instanceof FileDetailTabAdapter adapter) { - return adapter.getFileDetailActivitiesFragment(); - } - return null; } @@ -232,6 +236,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 { @@ -267,6 +276,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<>( @@ -296,68 +312,6 @@ private void onOverflowIconClicked() { .show(fragmentManager, "actions"); } - private void setupViewPager() { - binding.tabLayout.removeAllTabs(); - - binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.drawer_item_activities).setIcon(R.drawable.ic_activity)); - - - if (showSharingTab()) { - binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.share_dialog_title).setIcon(R.drawable.shared_via_users)); - } - - if (MimeTypeUtil.isImage(getFile())) { - binding.tabLayout.addTab(binding.tabLayout.newTab().setText(R.string.filedetails_details).setIcon(R.drawable.image_32dp)); - } - - viewThemeUtils.material.themeTabLayout(binding.tabLayout); - - final FileDetailTabAdapter adapter = new FileDetailTabAdapter(requireActivity(), - getFile(), - user, - showSharingTab()); - binding.pager.setAdapter(adapter); - - binding.pager.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { - @Override - public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { - final FileDetailActivitiesFragment fragment = getFileDetailActivitiesFragment(); - if (activeTab == 0 && fragment != null) { - fragment.markCommentsAsRead(); - } - super.onPageScrolled(position, positionOffset, positionOffsetPixels); - } - }); - binding.tabLayout.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { - @Override - public void onTabSelected(TabLayout.Tab tab) { - binding.pager.setCurrentItem(tab.getPosition()); - if (tab.getPosition() == 0) { - final FileDetailActivitiesFragment fragment = getFileDetailActivitiesFragment(); - if (fragment != null) { - fragment.markCommentsAsRead(); - } - } - } - - @Override - public void onTabUnselected(TabLayout.Tab tab) { - // unused at the moment - } - - @Override - public void onTabReselected(TabLayout.Tab tab) { - // unused at the moment - } - }); - - binding.tabLayout.post(() -> { - TabLayout.Tab tab1 = binding.tabLayout.getTabAt(activeTab); - if (tab1 == null) return; - tab1.select(); - }); - } - @Override public void onSaveInstanceState(@NonNull Bundle outState) { super.onSaveInstanceState(outState); @@ -381,10 +335,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(); @@ -396,6 +358,7 @@ public void onStop() { if (toolbarActivity != null) { toolbarActivity.hidePreviewImage(); + toolbarActivity.showToolbarBackImage(false); } EventBus.getDefault().unregister(this); @@ -533,11 +496,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(); @@ -566,8 +525,9 @@ public void updateFileDetails(boolean transferring, boolean refresh) { if (fabMain != null) { fabMain.hide(); } - - binding.syncBlock.setVisibility(file.isFolder() ? View.VISIBLE : View.GONE); + + // NMC: not required + /* binding.syncBlock.setVisibility(file.isFolder() ? View.VISIBLE : View.GONE); if (file.isInternalFolderSync()) { binding.folderSyncButton.setChecked(file.isInternalFolderSync()); @@ -576,10 +536,13 @@ public void updateFileDetails(boolean transferring, boolean refresh) { binding.folderSyncButton.setChecked(true); binding.folderSyncButton.setEnabled(false); } - } + }*/ } - setupViewPager(); + // TODO: 06/21/23 remove this condition after Comments section included + if (SendShareDialog.isPeopleShareClicked) { + replaceSharingFragment(); + } getView().invalidate(); } @@ -595,11 +558,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); } } @@ -620,56 +589,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); } } @@ -770,32 +806,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(); - } } /** @@ -808,12 +827,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) @@ -838,22 +858,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 071faeee4666..9ffae6227ac9 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,27 +16,31 @@ 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.extensions.FileExtensionsKt; +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; @@ -56,13 +60,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; @@ -97,8 +105,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; @@ -150,6 +161,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); @@ -173,6 +185,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(); @@ -211,58 +234,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( @@ -270,16 +360,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)); + } } } @@ -456,6 +558,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()); @@ -471,16 +577,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() { @@ -569,6 +684,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); @@ -600,11 +720,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)); @@ -652,5 +767,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 bf2c1ae72d1c..cbde307cbce8 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 241eabb1f2a7..163b66329775 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingMenuBottomSheetActions.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingMenuBottomSheetActions.java @@ -18,6 +18,10 @@ * Actions interface to be implemented by any class that makes use of {@link FileDetailSharingMenuBottomSheetDialog}. */ public interface FileDetailsSharingMenuBottomSheetActions { + /** + * open sharing options only applicable for files + */ + void openIn(OCShare share); /** * open advanced permission for selected share @@ -39,8 +43,4 @@ public interface FileDetailsSharingMenuBottomSheetActions { */ void sendLink(OCShare share); - /** - * create another link only valid for {@link ShareType#PUBLIC_LINK} - */ - void addAnotherLink(OCShare share); } diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt b/app/src/main/java/com/owncloud/android/ui/fragment/FileDetailsSharingProcessFragment.kt index fc806d4ab8cb..e0035a34b84d 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 @@ -95,13 +106,15 @@ class FileDetailsSharingProcessFragment : share: OCShare, screenType: Int, isReshareShown: Boolean, - isExpirationDateShown: 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 @@ -110,6 +123,8 @@ class FileDetailsSharingProcessFragment : @Inject lateinit var viewThemeUtils: ViewThemeUtils + @Inject + lateinit var keyboardUtils: KeyboardUtils private lateinit var onEditShareListener: FileDetailSharingFragment.OnEditShareListener @@ -123,14 +138,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 { @@ -158,6 +182,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? @@ -165,6 +190,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 @@ -174,36 +220,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) { @@ -220,9 +273,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() @@ -237,13 +291,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 @@ -256,19 +314,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) @@ -307,24 +352,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 @@ -338,6 +403,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)) } } @@ -345,15 +439,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 } } @@ -381,14 +482,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 } @@ -398,15 +502,24 @@ 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(R.string.empty) + //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 } @@ -434,6 +547,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) { @@ -449,13 +573,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.shareProcessChangeName.setText(R.string.empty) + 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.shareProcessDownloadLimitEt.setText(R.string.empty) + if (!isDownloadCountFetched) { + binding.shareProcessRemainingDownloadCountTv.text = String.format(resources.getString(R.string.download_text), "0") + } + //hide keyboard when user unchecks + hideKeyboard() } } private fun onCancelClick() { + // hide keyboard when user clicks cancel button + hideKeyboard() // if modifying the existing share then on back press remove the current fragment if (share != null) { removeCurrentFragment() @@ -475,6 +616,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 @@ -483,17 +631,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(R.string.empty) + // hide keyboard when user unchecks + hideKeyboard() + } + } + + private fun hideKeyboard() { + if (this::binding.isInitialized) { + keyboardUtils.hideKeyboardFrom(requireContext(), binding.root) } } + /** + * remove the fragment and pop it from backstack because we are adding it to backstack + */ private fun removeCurrentFragment() { - onEditShareListener.onShareProcessClosed() - fileActivity?.supportFragmentManager?.beginTransaction()?.remove(this)?.commit() + requireActivity().supportFragmentManager.beginTransaction().remove(this).commit() + requireActivity().supportFragmentManager.popBackStack() } private fun getReSharePermission(): Int { @@ -507,6 +666,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) @@ -528,12 +688,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() @@ -551,8 +722,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 @@ -566,7 +737,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)) { @@ -578,10 +750,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( @@ -593,14 +769,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 */ @@ -621,4 +810,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 2370560a907e..6a4bf2fa2406 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 @@ -94,6 +94,7 @@ import com.owncloud.android.ui.dialog.RemoveFilesDialogFragment; import com.owncloud.android.ui.dialog.RenameFileDialogFragment; import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment; +import com.owncloud.android.ui.dialog.SendShareDialog; import com.owncloud.android.ui.dialog.SyncFileNotEnoughSpaceDialogFragment; import com.owncloud.android.ui.events.ChangeMenuEvent; import com.owncloud.android.ui.events.CommentsEvent; @@ -601,6 +602,8 @@ public void createRichWorkspace() { @Override public void onShareIconClick(OCFile file) { + //NMC Customization + SendShareDialog.isPeopleShareClicked = true; if (file.isFolder()) { mContainerActivity.showDetails(file, 1); } else { @@ -1195,6 +1198,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) { @@ -1223,7 +1228,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 820a6b0a7f22..0836aaa804b3 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 899ff9cd73af..9c6981033420 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 25a5e4a739f4..1249c13c830d 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 @@ -612,9 +612,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)); + } } /** @@ -753,7 +754,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()); @@ -763,6 +764,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 08bde79d95ad..74b7548131b7 100644 --- a/app/src/main/java/com/owncloud/android/utils/KeyboardUtils.kt +++ b/app/src/main/java/com/owncloud/android/utils/KeyboardUtils.kt @@ -9,6 +9,10 @@ package com.owncloud.android.utils import android.view.Window +import android.app.Activity +import android.content.Context +import android.view.View +import android.view.inputmethod.InputMethodManager import android.widget.EditText import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat @@ -22,4 +26,10 @@ class KeyboardUtils @Inject constructor() { WindowCompat.getInsetsController(window, editText).show(WindowInsetsCompat.Type.ime()) } } + + fun hideKeyboardFrom(context: Context, view: View) { + view.clearFocus() + val imm = context.getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager + imm.hideSoftInputFromWindow(view.windowToken, 0) + } } diff --git a/app/src/main/res/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 9466d4638d35..03cb1988aad0 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" /> @@ -174,7 +176,8 @@ android:id="@+id/syncBlock" android:layout_width="match_parent" android:orientation="vertical" - android:layout_height="wrap_content"> + android:layout_height="wrap_content" + android:visibility="gone"> - - - - 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 bfb626772958..bd4d2f5b7908 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 466cdcddc3d3..49b78772f9fb 100644 --- a/app/src/main/res/layout/file_details_share_public_link_add_new_item.xml +++ b/app/src/main/res/layout/file_details_share_public_link_add_new_item.xml @@ -14,8 +14,8 @@ android:orientation="horizontal"> + 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 ae4d37647d39..7bf5e7f01156 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 OR GPL-2.0-only --> - + 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 030f0e84c99f..dcc83a424a44 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 220e61e96532..86d68ab42133 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/tv_sharing_title" /> + + + 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 efa183210751..4abbc64b3c34 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"> + + + + + + Freigabe aufheben + Erstellen erlauben + Löschen erlauben + Bearbeitung erlauben + Nur Lesen + Link zum Ordner + Link zur Datei + Passwortschutz (%1$s) + Sie müssen das Ablaufdatum auswählen. + Bitte Anmerkung eingeben. + Email senden + Öffnen mit… + Sie können Links erstellen oder Freigaben per Mail versenden. Wenn Sie + MagentaCLOUD Nutzer einladen, bieten sich Ihnen mehr Möglichkeiten der Zusammenarbeit. + Ihre Nachricht + persönliche Freigabe per E-Mail + 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. + Download Limit + Das Feld für das Download-Limit darf nicht leer sein. + Downlimit eingeben + Downloads: %s + Der Wert für das Downloadlimit sollte größer als 0 sein. + Sie teilen mit einer/einem MagentaCLOUD Nutzer(in). Sie können ihr oder ihm erlauben, den Ordner oder die Dateien weiterzuteilen. + Der Passwortschutz ist aktiviert. Sie müssen dem Empfänger das Passwort + selbst mitteilen.\n\nWenn Sie die Freigabe über die MagentaCLOUD verschicken und das Passwort in den + Nachrichtentext eintragen, wird es unverschlüsselt im Klartext übertragen. + Empfangen + Geteilt + \ No newline at end of file diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index 1a57ac2816dd..2d0a9b5102a2 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -10,7 +10,7 @@ Bearbeiten Alle Benachrichtigungen löschen Papierkorb leeren - Senden/Teilen + Teilen Kachelansicht Listenansicht Kontakte und Kalender wiederherstellen @@ -32,7 +32,7 @@ Öffentlichen Link zum Teilen hinzufügen Neue sichere Dateiablage hinzufügen Hinzufügen zu %1$s - Erweiterte Einstellungen + Erweiterte Berechtigungen Weiterteilen erlauben Basis-URL Proxy-Hostname @@ -494,10 +494,9 @@ Letzte Sicherung:%1$s Link Link-Name - Hochladen und Bearbeiten erlauben - Bearbeitung - Dateien ablegen (nur Hochladen) - Nur anzeigen + Hochladen & Bearbeiten + Sammelbox + Bearbeiten Layout der Liste Weitere Ergebnisse laden Es befinden sich keine Dateien in diesem Ordner. @@ -568,7 +567,7 @@ 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. + Bitte wählen Sie mindestens eine Berechtigung zum Teilen aus. Notiz konnte nicht versandt werden Notiz-Symbol Ausführen der Aktion fehlgeschlagen. @@ -719,7 +718,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 @@ -776,19 +776,19 @@ 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 + Kann Bearbeiten + Sammelbox Sichere Dateiablage - Nur anzeigen - Berechtigungen zum Teilen + Nur lesen + Berechtigungen %1$s (remote) %1$s (Unterhaltung) - Name, Federated-Cloud-ID oder E-Mail-Adresse … + Kontaktname oder E-Mail Neue E-Mail senden Notiz an Empfänger Einstellungen diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 1ce3f0da4f73..2f79b79712ed 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 108443d3e383..920a049e064d 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 89ed00a08bf2..46992e0a67c6 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -75,4 +75,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/nmc_sharing_strings.xml b/app/src/main/res/values/nmc_sharing_strings.xml new file mode 100644 index 000000000000..113b4929af50 --- /dev/null +++ b/app/src/main/res/values/nmc_sharing_strings.xml @@ -0,0 +1,37 @@ + + + Unshare + Allow creating + Allow deleting + Allow editing + Read only + Link to folder + Link to file + Password protect (%1$s) + You must select expiration date. + Please enter note. + Send email + Open in… + You can create links or send shares by mail. If you invite MagentaCLOUD users, you have more opportunities for collaboration. + Your Message + Personal share by mail + Create 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 + \ No newline at end of file diff --git a/app/src/main/res/values/nmc_sharing_styles.xml b/app/src/main/res/values/nmc_sharing_styles.xml new file mode 100644 index 000000000000..e6b970e412c0 --- /dev/null +++ b/app/src/main/res/values/nmc_sharing_styles.xml @@ -0,0 +1,12 @@ + + + + + \ 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 cee301fe7fd6..1d8e9014777d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -587,7 +587,7 @@ Share with… Unset - Name, Federated Cloud ID or email address … + Contact name or email Secure share … %1$s (group) @@ -953,7 +953,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 @@ -1042,7 +1043,7 @@ Add another link Add new public share link New name - Share link (%1$s) + Link \'%1$s\' Share link Allow resharing View only @@ -1095,12 +1096,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 4c1563085156..e6d93e922069 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -271,6 +271,7 @@