11/*
22 * Nextcloud - Android Client
33 *
4+ * SPDX-FileCopyrightText: 2026 Philipp Hasper <vcs@hasper.info>
45 * SPDX-FileCopyrightText: 2025 Alper Ozturk <alper.ozturk@nextcloud.com>
56 * SPDX-FileCopyrightText: 2022 Tobias Kaminsky <tobias@kaminsky.me>
67 * SPDX-FileCopyrightText: 2022 Nextcloud GmbH
@@ -12,20 +13,34 @@ import android.graphics.Bitmap
1213import android.graphics.Canvas
1314import android.graphics.Color
1415import android.graphics.Paint
16+ import android.view.View
17+ import android.view.ViewGroup
18+ import android.widget.FrameLayout
19+ import androidx.recyclerview.widget.RecyclerView
1520import androidx.test.core.app.launchActivity
1621import androidx.test.espresso.Espresso.onView
22+ import androidx.test.espresso.UiController
23+ import androidx.test.espresso.ViewAction
1724import androidx.test.espresso.assertion.ViewAssertions.matches
25+ import androidx.test.espresso.contrib.RecyclerViewActions
26+ import androidx.test.espresso.contrib.RecyclerViewActions.actionOnItemAtPosition
27+ import androidx.test.espresso.matcher.ViewMatchers.isAssignableFrom
1828import androidx.test.espresso.matcher.ViewMatchers.isDisplayed
1929import androidx.test.espresso.matcher.ViewMatchers.isRoot
30+ import androidx.test.espresso.matcher.ViewMatchers.withId
2031import com.nextcloud.test.TestActivity
2132import com.owncloud.android.AbstractIT
33+ import com.owncloud.android.R
2234import com.owncloud.android.datamodel.OCFile
2335import com.owncloud.android.datamodel.ThumbnailsCacheManager
2436import com.owncloud.android.datamodel.ThumbnailsCacheManager.PREFIX_RESIZED_IMAGE
2537import com.owncloud.android.lib.common.utils.Log_OC
2638import com.owncloud.android.lib.resources.files.model.ImageDimension
39+ import com.owncloud.android.ui.adapter.GalleryRowHolder
2740import com.owncloud.android.utils.ScreenshotTest
41+ import org.hamcrest.Matchers.allOf
2842import org.junit.After
43+ import org.junit.Assert.assertEquals
2944import org.junit.Assert.assertNotNull
3045import org.junit.Before
3146import org.junit.Test
@@ -85,14 +100,102 @@ class GalleryFragmentIT : AbstractIT() {
85100 }
86101 }
87102
88- private fun createImage (id : Int , width : Int? = null, height : Int? = null) {
103+ @Test
104+ fun multiSelect () {
105+ val imageCount = 100
106+ for (num in 1 .. imageCount) {
107+ // Spread the files over multiple days to also get multiple sections
108+ val secondsPerDay = 1L * 24 * 60 * 60
109+ createImage(10000000 + num * 7 * secondsPerDay, 700 , 300 )
110+ }
111+
112+ // Test that scrolling through the whole list is possible without a crash
113+ launchActivity<TestActivity >().use { scenario ->
114+ lateinit var galleryFragment: GalleryFragment
115+ scenario.onActivity { testActivity ->
116+ galleryFragment = GalleryFragment ()
117+ testActivity.addFragment(galleryFragment)
118+ }
119+ onView(isRoot()).check(matches(isDisplayed()))
120+
121+ onView(withId(R .id.list_root))
122+ .perform(RecyclerViewActions .scrollToLastPosition<GalleryRowHolder >())
123+ .perform(RecyclerViewActions .scrollToPosition<GalleryRowHolder >(0 ))
124+ }
125+
126+ // Test selection of all entries
127+ launchActivity<TestActivity >().use { scenario ->
128+ lateinit var galleryFragment: GalleryFragment
129+ scenario.onActivity { testActivity ->
130+ galleryFragment = GalleryFragment ()
131+ testActivity.addFragment(galleryFragment)
132+ }
133+ onView(isRoot()).check(matches(isDisplayed()))
134+
135+ // get the RecyclerView and itemCount on the UI thread
136+ val recyclerView = findRecyclerViewRecursively(galleryFragment.view)
137+ ? : throw AssertionError (" RecyclerView not found" )
138+ val adapterCount = recyclerView.adapter?.itemCount ? : 0
139+
140+ // Perform the view action on each adapter position (row)
141+ // Match the visible RecyclerView: if there is only one displayed RecyclerView we can match it by class+displayed
142+ val recyclerMatcher = allOf(isAssignableFrom(RecyclerView ::class .java), isDisplayed())
143+
144+ for (pos in 0 until adapterCount) {
145+ onView(recyclerMatcher)
146+ .perform(actionOnItemAtPosition<RecyclerView .ViewHolder >(pos, longClickAllThumbnailsInRow()))
147+ }
148+
149+ val checked = galleryFragment.commonAdapter.getCheckedItems()
150+ assertEquals(imageCount, checked.size)
151+ }
152+ }
153+
154+ /* * Recursively walk view tree to find the first RecyclerView. Runs on the same thread that calls it. */
155+ private fun findRecyclerViewRecursively (root : View ? ): RecyclerView ? {
156+ if (root == null ) return null
157+ if (root is RecyclerView ) return root
158+ if (root !is ViewGroup ) return null
159+ for (i in 0 until root.childCount) {
160+ val child = root.getChildAt(i)
161+ val found = findRecyclerViewRecursively(child)
162+ if (found != null ) return found
163+ }
164+ return null
165+ }
166+
167+ /* *
168+ * For the given row view, long-click each thumbnail inside its FrameLayouts
169+ */
170+ fun longClickAllThumbnailsInRow (): ViewAction = object : ViewAction {
171+ override fun getConstraints () = isDisplayed()
172+
173+ override fun getDescription () = " Long-click all thumbnail ImageViews inside a GalleryRowHolder"
174+
175+ override fun perform (uiController : UiController , view : View ) {
176+ if (view is ViewGroup ) {
177+ // each child of the row is a FrameLayout representing one gallery cell
178+ for (i in 0 until view.childCount) {
179+ val cell = view.getChildAt(i)
180+ if (cell is FrameLayout ) {
181+ // GalleryRowHolder builds FrameLayout with children:
182+ // 0 = shimmer, 1 = thumbnail ImageView, 2 = checkbox
183+ val thumbnail = if (cell.childCount > 1 ) cell.getChildAt(1 ) else cell
184+ thumbnail.performLongClick()
185+ }
186+ }
187+ }
188+ }
189+ }
190+
191+ private fun createImage (id : Long , width : Int? = null, height : Int? = null) {
89192 val defaultSize = ThumbnailsCacheManager .getThumbnailDimension().toFloat()
90193 val file = OCFile (" /$id .png" ).apply {
91- fileId = id.toLong()
194+ fileId = id
92195 remoteId = " $id "
93196 mimeType = " image/png"
94197 isPreviewAvailable = true
95- modificationTimestamp = (1658475504 + id.toLong() ) * 1000
198+ modificationTimestamp = (1658475504 + id) * 1000
96199 imageDimension = ImageDimension (width?.toFloat() ? : defaultSize, height?.toFloat() ? : defaultSize)
97200 storageManager.saveFile(this )
98201 }
0 commit comments