Skip to content

Commit 3e16c69

Browse files
committed
NMC-1923: E2EE related changes with test cases.
NMC-1550: Allow to create encrypted folder directly from bottom sheet dialog from NC PR: nextcloud#10782
1 parent 93fd21e commit 3e16c69

28 files changed

+2867
-2036
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Nextcloud Android client application
3+
*
4+
* @author Álvaro Brey Vilas
5+
* Copyright (C) 2022 Álvaro Brey Vilas
6+
* Copyright (C) 2022 Nextcloud GmbH
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU General Public License as published by
10+
* the Free Software Foundation, either version 3 of the License, or
11+
* (at your option) any later version.
12+
*
13+
* This program is distributed in the hope that it will be useful,
14+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
15+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16+
* GNU General Public License for more details.
17+
*
18+
* You should have received a copy of the GNU General Public License
19+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
20+
*/
21+
package com.nmc.android
22+
23+
import androidx.test.core.app.launchActivity
24+
import androidx.test.ext.junit.runners.AndroidJUnit4
25+
import com.nextcloud.client.account.User
26+
import com.nextcloud.client.jobs.download.FileDownloadWorker
27+
import com.nextcloud.client.jobs.upload.FileUploadHelper
28+
import com.nextcloud.test.TestActivity
29+
import com.nextcloud.utils.EditorUtils
30+
import com.owncloud.android.AbstractIT
31+
import com.owncloud.android.R
32+
import com.owncloud.android.datamodel.ArbitraryDataProvider
33+
import com.owncloud.android.datamodel.FileDataStorageManager
34+
import com.owncloud.android.datamodel.OCFile
35+
import com.owncloud.android.files.FileMenuFilter
36+
import com.owncloud.android.lib.resources.status.CapabilityBooleanType
37+
import com.owncloud.android.lib.resources.status.OCCapability
38+
import com.owncloud.android.services.OperationsService
39+
import com.owncloud.android.ui.activity.ComponentsGetter
40+
import com.owncloud.android.utils.MimeType
41+
import io.mockk.MockKAnnotations
42+
import io.mockk.every
43+
import io.mockk.impl.annotations.MockK
44+
import org.junit.Assert.assertFalse
45+
import org.junit.Assert.assertTrue
46+
import org.junit.Before
47+
import org.junit.Test
48+
import org.junit.runner.RunWith
49+
import java.security.SecureRandom
50+
51+
@RunWith(AndroidJUnit4::class)
52+
class FileMenuFilterIT : AbstractIT() {
53+
54+
@MockK
55+
private lateinit var mockComponentsGetter: ComponentsGetter
56+
57+
@MockK
58+
private lateinit var mockStorageManager: FileDataStorageManager
59+
60+
@MockK
61+
private lateinit var mockFileUploaderBinder: FileUploadHelper
62+
63+
@MockK
64+
private lateinit var mockOperationsServiceBinder: OperationsService.OperationsServiceBinder
65+
66+
@MockK
67+
private lateinit var mockFileDownloadProgressListener: FileDownloadWorker.FileDownloadProgressListener
68+
69+
@MockK
70+
private lateinit var mockArbitraryDataProvider: ArbitraryDataProvider
71+
72+
private lateinit var editorUtils: EditorUtils
73+
74+
@Before
75+
fun setup() {
76+
MockKAnnotations.init(this)
77+
every { mockFileUploaderBinder.isUploading(any(), any()) } returns false
78+
every { mockComponentsGetter.fileUploaderHelper } returns mockFileUploaderBinder
79+
every { mockFileDownloadProgressListener.isDownloading(any(), any()) } returns false
80+
every { mockComponentsGetter.fileDownloadProgressListener } returns mockFileDownloadProgressListener
81+
every { mockOperationsServiceBinder.isSynchronizing(any(), any()) } returns false
82+
every { mockComponentsGetter.operationsServiceBinder } returns mockOperationsServiceBinder
83+
every { mockStorageManager.getFileById(any()) } returns OCFile("/")
84+
every { mockStorageManager.getFolderContent(any(), any()) } returns ArrayList<OCFile>()
85+
every { mockArbitraryDataProvider.getValue(any<User>(), any()) } returns ""
86+
editorUtils = EditorUtils(mockArbitraryDataProvider)
87+
}
88+
89+
@Test
90+
fun hide_shareAndFavouriteMenu_encryptedFolder() {
91+
val capability = OCCapability().apply {
92+
endToEndEncryption = CapabilityBooleanType.TRUE
93+
}
94+
95+
val encryptedFolder = OCFile("/encryptedFolder/").apply {
96+
isEncrypted = true
97+
mimeType = MimeType.DIRECTORY
98+
fileLength = SecureRandom().nextLong()
99+
}
100+
101+
configureCapability(capability)
102+
103+
launchActivity<TestActivity>().use {
104+
it.onActivity { activity ->
105+
val filterFactory =
106+
FileMenuFilter.Factory(mockStorageManager, activity, editorUtils)
107+
108+
val sut = filterFactory.newInstance(encryptedFolder, mockComponentsGetter, true, user)
109+
val toHide = sut.getToHide(false)
110+
111+
// encrypted folder
112+
assertTrue(toHide.contains(R.id.action_see_details))
113+
assertTrue(toHide.contains(R.id.action_favorite))
114+
assertTrue(toHide.contains(R.id.action_unset_favorite))
115+
}
116+
}
117+
}
118+
119+
@Test
120+
fun show_shareAndFavouriteMenu_normalFolder() {
121+
val capability = OCCapability().apply {
122+
endToEndEncryption = CapabilityBooleanType.TRUE
123+
}
124+
125+
val normalFolder = OCFile("/folder/").apply {
126+
mimeType = MimeType.DIRECTORY
127+
fileLength = SecureRandom().nextLong()
128+
}
129+
130+
configureCapability(capability)
131+
132+
launchActivity<TestActivity>().use {
133+
it.onActivity { activity ->
134+
val filterFactory =
135+
FileMenuFilter.Factory(mockStorageManager, activity, editorUtils)
136+
137+
val sut = filterFactory.newInstance(normalFolder, mockComponentsGetter, true, user)
138+
val toHide = sut.getToHide(false)
139+
140+
// normal folder
141+
assertFalse(toHide.contains(R.id.action_see_details))
142+
assertFalse(toHide.contains(R.id.action_favorite))
143+
assertTrue(toHide.contains(R.id.action_unset_favorite))
144+
}
145+
}
146+
}
147+
148+
private fun configureCapability(capability: OCCapability) {
149+
every { mockStorageManager.getCapability(any<User>()) } returns capability
150+
every { mockStorageManager.getCapability(any<String>()) } returns capability
151+
}
152+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.nmc.android
2+
3+
import androidx.test.espresso.Espresso.onView
4+
import androidx.test.espresso.assertion.ViewAssertions.matches
5+
import androidx.test.espresso.intent.rule.IntentsTestRule
6+
import androidx.test.espresso.matcher.ViewMatchers.withHint
7+
import androidx.test.espresso.matcher.ViewMatchers.withId
8+
import androidx.test.internal.runner.junit4.statement.UiThreadStatement
9+
import com.nextcloud.test.TestActivity
10+
import com.owncloud.android.AbstractIT
11+
import com.owncloud.android.ui.dialog.setupEncryption.SetupEncryptionDialogFragment
12+
import org.junit.Rule
13+
import org.junit.Test
14+
import com.owncloud.android.R
15+
16+
class SetupEncryptionDialogFragmentIT : AbstractIT() {
17+
18+
@get:Rule
19+
val testActivityRule = IntentsTestRule(TestActivity::class.java, true, false)
20+
21+
@Test
22+
fun validatePassphraseInputHint() {
23+
val activity = testActivityRule.launchActivity(null)
24+
25+
val sut = SetupEncryptionDialogFragment.newInstance(user, 0)
26+
27+
sut.show(activity.supportFragmentManager, "1")
28+
29+
val keyWords = arrayListOf(
30+
"ability",
31+
"able",
32+
"about",
33+
"above",
34+
"absent",
35+
"absorb",
36+
"abstract",
37+
"absurd",
38+
"abuse",
39+
"access",
40+
"accident",
41+
"account",
42+
"accuse"
43+
)
44+
45+
shortSleep()
46+
47+
UiThreadStatement.runOnUiThread {
48+
sut.setMnemonic(keyWords)
49+
sut.showMnemonicInfo()
50+
}
51+
52+
waitForIdleSync()
53+
54+
onView(withId(R.id.encryption_passwordInput)).check(matches(withHint("Passphrase…")))
55+
}
56+
}

app/src/androidTest/java/com/owncloud/android/ui/dialog/DialogFragmentIT.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,7 @@ class DialogFragmentIT : AbstractIT() {
450450

451451
val action: OCFileListBottomSheetActions = object : OCFileListBottomSheetActions {
452452
override fun createFolder() = Unit
453+
override fun createEncryptedFolder() = Unit
453454
override fun uploadFromApp() = Unit
454455
override fun uploadFiles() = Unit
455456
override fun newDocument() = Unit

app/src/androidTest/java/com/owncloud/android/ui/fragment/OCFileListFragmentStaticServerIT.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,15 +411,19 @@ class OCFileListFragmentStaticServerIT : AbstractIT() {
411411
EspressoIdlingResource.increment()
412412
testFolder.richWorkspace = " "
413413
activity.storageManager.saveFile(testFolder)
414-
sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "")
414+
sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "",
415+
showOnlyFolder = false,
416+
hideEncryptedFolder = false)
415417
EspressoIdlingResource.decrement()
416418

417419
Assert.assertFalse(sut.adapter.shouldShowHeader())
418420

419421
EspressoIdlingResource.increment()
420422
testFolder.richWorkspace = null
421423
activity.storageManager.saveFile(testFolder)
422-
sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "")
424+
sut.adapter.swapDirectory(user, testFolder, activity.storageManager, false, "",
425+
showOnlyFolder = false,
426+
hideEncryptedFolder = false)
423427
EspressoIdlingResource.decrement()
424428
Assert.assertFalse(sut.adapter.shouldShowHeader())
425429

app/src/main/java/com/nextcloud/utils/extensions/OCFileExtensions.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@ fun List<OCFile>.limitToPersonalFiles(userId: String): List<OCFile> = filter { f
3131
ownerId == userId && !file.isSharedWithMe && !file.mounted()
3232
} == true
3333
}
34+
35+
// NMC method to filter only folders with/without e2ee folders
36+
fun List<OCFile>.filterByFolder(hideEncryptedFolder: Boolean = false): List<OCFile> =
37+
filter { it.isFolder && (!hideEncryptedFolder || !it.isEncrypted) }

app/src/main/java/com/owncloud/android/datamodel/OCFile.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,14 +623,19 @@ public boolean isHidden() {
623623
}
624624

625625
/**
626+
* The unique fileId for the file within the instance This only works if we have 12 digits for instanceId RemoteId
627+
* is a combination of localId + instanceId If a file has remoteId: 4174305739oc97a8ddfc96, in this 4174305739 is
628+
* localId & oc97a8ddfc96 is instanceId which is of 12 digits
629+
*
626630
* unique fileId for the file within the instance
627631
*/
628632
@SuppressFBWarnings("STT")
629633
public long getLocalId() {
630634
if (localId > 0) {
631635
return localId;
632636
} else if (remoteId != null && remoteId.length() > 8) {
633-
return Long.parseLong(remoteId.substring(0, 8).replaceAll("^0*", ""));
637+
//NMC Customization --> for long remote id's
638+
return Long.parseLong(remoteId.substring(0, remoteId.length() - 12).replaceAll("^0*", ""));
634639
} else {
635640
return -1;
636641
}

app/src/main/java/com/owncloud/android/files/FileMenuFilter.java

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ private void filterPermissionActions(List<Integer> toHide) {
185185

186186

187187
private void filterShareFile(List<Integer> toHide, OCCapability capability) {
188-
if (!isSingleSelection() || containsEncryptedFile() || hasEncryptedParent() ||
188+
if (!isSingleSelection() || containsEncryptedFile() || hasEncryptedParent() || containsEncryptedFolder() ||
189189
(!isShareViaLinkAllowed() && !isShareWithUsersAllowed()) ||
190190
!isShareApiEnabled(capability) || !files.iterator().next().canReshare()) {
191191
toHide.add(R.id.action_send_share_file);
@@ -216,19 +216,21 @@ private void filterSendFiles(List<Integer> toHide, boolean inSingleFileFragment)
216216
}
217217

218218
private void filterDetails(Collection<Integer> toHide) {
219-
if (!isSingleSelection()) {
219+
if (!isSingleSelection() || containsEncryptedFolder() || containsEncryptedFile()) {
220220
toHide.add(R.id.action_see_details);
221221
}
222222
}
223223

224224
private void filterFavorite(List<Integer> toHide, boolean synchronizing) {
225-
if (files.isEmpty() || synchronizing || allFavorites()) {
225+
if (files.isEmpty() || synchronizing || allFavorites() || containsEncryptedFile()
226+
|| containsEncryptedFolder()) {
226227
toHide.add(R.id.action_favorite);
227228
}
228229
}
229230

230231
private void filterUnfavorite(List<Integer> toHide, boolean synchronizing) {
231-
if (files.isEmpty() || synchronizing || allNotFavorites()) {
232+
if (files.isEmpty() || synchronizing || allNotFavorites() || containsEncryptedFile()
233+
|| containsEncryptedFolder()) {
232234
toHide.add(R.id.action_unset_favorite);
233235
}
234236
}
@@ -261,7 +263,7 @@ private void filterUnlock(List<Integer> toHide, boolean fileLockingEnabled) {
261263

262264
private void filterEncrypt(List<Integer> toHide, boolean endToEndEncryptionEnabled) {
263265
if (files.isEmpty() || !isSingleSelection() || isSingleFile() || isEncryptedFolder() || isGroupFolder()
264-
|| !endToEndEncryptionEnabled || !isEmptyFolder() || isShared()) {
266+
|| !endToEndEncryptionEnabled || !isEmptyFolder() || isShared() || isInSubFolder()) {
265267
toHide.add(R.id.action_encrypted);
266268
}
267269
}
@@ -363,8 +365,10 @@ private void filterSelectAll(List<Integer> toHide, boolean inSingleFileFragment)
363365
}
364366

365367
private void filterRemove(List<Integer> toHide, boolean synchronizing) {
366-
if (files.isEmpty() || synchronizing || containsLockedFile()
367-
|| containsEncryptedFolder() || isFolderAndContainsEncryptedFile()) {
368+
if ((files.isEmpty() || synchronizing || containsLockedFile()
369+
|| containsEncryptedFolder() || isFolderAndContainsEncryptedFile())
370+
//show delete option for encrypted sub-folder
371+
&& !hasEncryptedParent()) {
368372
toHide.add(R.id.action_remove_file);
369373
}
370374
}
@@ -605,4 +609,15 @@ private boolean isShared() {
605609
}
606610
return false;
607611
}
612+
613+
private boolean isInSubFolder() {
614+
OCFile folder = files.iterator().next();
615+
OCFile parent = storageManager.getFileById(folder.getParentId());
616+
617+
if (parent == null) {
618+
return false;
619+
}
620+
621+
return !OCFile.ROOT_PATH.equals(parent.getRemotePath());
622+
}
608623
}

0 commit comments

Comments
 (0)