fix: recalculate grid span count after rotation#4894
Conversation
|
Hi @himu-gupta! Thanks for opening this PR! 🙌🏻 Some improvements here before the CR:
Let us know if you have any doubts and we will help you! I will start with the code review when all changes are done 🍻 |
4a35526 to
bc59543
Compare
Signed-off-by: Himanshu Gupta <himu.gupta23@gmail.com>
Signed-off-by: Himanshu Gupta <himu.gupta23@gmail.com>
bc59543 to
c889d0b
Compare
Thanks @joragua, I have done the requested changes. |
|
Thanks for your contribution @himu-gupta!! Let me tell you how the process is. Basically, there are two phases after the PR is correct (signed commits, separated changelog, conventional commits correct...):
For sure, all CI checks must be green as well, otherwise merging will be blocked. Code review will start soon, keep tuned!! |
Thanks for the insight |
|
@joragua is anything pending from my end? |
|
I'll take care of it as soon as possible and I'll ping you once the CR is ready! Stay tuned! 🙌🏻 @himu-gupta |
joragua
left a comment
There was a problem hiding this comment.
CR finished! 🚀 Some comments here @himu-gupta
Signed-off-by: Himanshu Gupta <himu.gupta23@gmail.com>
Signed-off-by: Himanshu Gupta <himu.gupta23@gmail.com>
|
Thanks @joragua, addressed the CR in two new commits on top without force-pushing: |
joragua
left a comment
There was a problem hiding this comment.
Just two easy comments and it's ready! Thanks for your patience @himu-gupta!
Would it be possible to squash the two commits about calens into a single one? We use to have only one commit for the calens entry in each PR. Also, I'd rename the last commit to make it more descriptive. The idea would be to have only three commits in this PR:
fix: recalculate grid span count after rotationchore: add calens filerefactor: remove unnecessary grid recalculation logic
| The grid mode column count has been recalculated after device rotation, keeping | ||
| file tiles in the correct layout for the new orientation. |
There was a problem hiding this comment.
I'd re-write the description like this:
| The grid mode column count has been recalculated after device rotation, keeping | |
| file tiles in the correct layout for the new orientation. | |
| The grid mode column count has been recalculated after device rotation, keeping | |
| file layouts aligned with the new orientation. |
DeepDiver1975
left a comment
There was a problem hiding this comment.
Maintainer review (automated, on behalf of @DeepDiver1975) — fix looks correct and addresses #4884 well.
Correctness — verified
FileDisplayActivityandFolderPickerActivityboth declareandroid:configChanges="orientation|screenSize"in the manifest, so on rotation the activity is not recreated andonConfigurationChangedis the right (and only) hook. CallingupdateRecyclerViewLayoutForCurrentViewType()there is the correct fix.- Setting
this.viewType = viewTypeinonViewTypeListeneris more than a refactor — it fixes a real latent bug: theviewTypefield was previously only set ininitViews()and never updated when the user toggled list/grid. Without this, a rotation after toggling would recompute the span count for the stale view type. Good catch. viewTypeislateinitbut is always initialized ininitViews()before the view exists, so reading it inonConfigurationChangedis safe (config changes can't precede view creation). No lifecycle/NPE risk.- The extracted
when (viewType)over the two-value enum is exhaustive (noelseneeded) and idiomatic Kotlin. Behavior inonViewTypeListeneris unchanged vs. the previous inlineif.
Nit (non-blocking)
- For a
StaggeredGridLayoutManagerspan-count change,notifyItemRangeChanged(0, itemCount)works, butlayoutManager.invalidateSpanAssignments()(optionally followed byrequestLayout()) is the more robust idiom for guaranteeing stale span gaps are cleared after a column-count change. Since the change is verified against the reported overlap and the previous reviewer confirmednotifyItemRangeChangedis sufficient, I'm fine leaving it. Worth a mental note if any residual overlap is ever reported. - The PR description mentions "Invalidate staggered-grid span assignments", but the code does not call
invalidateSpanAssignments()— it relies onnotifyItemRangeChanged. Minor wording mismatch only; no code change needed.
Changelog — changelog/unreleased/4894 is present and matches the calens convention (TEMPLATE.md): Bugfix: type ≤ 80 chars, present-perfect-passive body, issue + PR URLs. ✅
Note: @joragua's earlier change-requests (filename → 4894, present-perfect-passive description, PR link) appear addressed in the current diff. No new blocking issues from my side; this is good to go once the maintainer's outstanding commit-squash request is satisfied.
|
ey @himu-gupta how are you? don't hesitate to ping us if you need nothing. The current PR is almost ready (just a couple of tiny comments that @joragua suggested) to move to QA. Let us know! same for the other PR you opened. |
dj4oC
left a comment
There was a problem hiding this comment.
Fixes issue #4884: recalculates the RecyclerView grid span count on onConfigurationChanged (not just on view-type toggle) so grid layout stays correct after device rotation.
Findings
- Security: no findings (no network/IPC/credential/deserialization surface touched — see inline discussion)
- Stability: no findings (
viewTypeis always initialized before any reachableonConfigurationChangedcall, per manifestconfigChanges+ fragment lifecycle ordering; no threading issues) - Performance: 1 finding — see inline suggestion (unconditional adapter rebind fires on more than just rotation)
- Test coverage: 0% on touched lines, below the 85% bar — test proposed inline
Verdict
Changes requested (test coverage gap; performance nit is non-blocking but recommended)
Note: I did not take the existing "automated maintainer review" comment from @DeepDiver1975 on this thread at face value — each of the four passes above (security/stability/performance/coverage) formed its own independent judgment from the diff and surrounding code.
🤖 Automated review by Claude Code (security · stability · performance · coverage)
Generated by Claude Code
| layoutManager.spanCount = when (viewType) { | ||
| ViewType.VIEW_TYPE_LIST -> 1 | ||
| ViewType.VIEW_TYPE_GRID -> ColumnQuantity(requireContext(), R.layout.grid_item).calculateNoOfColumns() | ||
| } | ||
| fileListAdapter.notifyItemRangeChanged(0, fileListAdapter.itemCount) |
There was a problem hiding this comment.
Performance (minor-moderate, non-blocking): this now runs on every onConfigurationChanged, not just rotation — FileDisplayActivity/FolderPickerActivity declare configChanges covering keyboard visibility and uiMode (dark/light) too. In VIEW_TYPE_LIST, spanCount is always 1 and never actually changes, so those triggers still force a full notifyItemRangeChanged(0, itemCount) rebind. FileListAdapter.onBindViewHolder does a synchronous ThumbnailsCacheManager.getBitmapFromDiskCache() disk read per row, so this is real (if currently small) main-thread I/O cost on every qualifying config change, not just rotation.
Suggest guarding the rebind on an actual span-count change:
| layoutManager.spanCount = when (viewType) { | |
| ViewType.VIEW_TYPE_LIST -> 1 | |
| ViewType.VIEW_TYPE_GRID -> ColumnQuantity(requireContext(), R.layout.grid_item).calculateNoOfColumns() | |
| } | |
| fileListAdapter.notifyItemRangeChanged(0, fileListAdapter.itemCount) | |
| val newSpanCount = when (viewType) { | |
| ViewType.VIEW_TYPE_LIST -> 1 | |
| ViewType.VIEW_TYPE_GRID -> ColumnQuantity(requireContext(), R.layout.grid_item).calculateNoOfColumns() | |
| } | |
| if (newSpanCount != layoutManager.spanCount) { | |
| layoutManager.spanCount = newSpanCount | |
| fileListAdapter.notifyItemRangeChanged(0, fileListAdapter.itemCount) | |
| } |
Generated by Claude Code
There was a problem hiding this comment.
Test coverage: 0% on the lines this PR touches (onConfigurationChanged, onViewTypeListener, the new updateRecyclerViewLayoutForCurrentViewType()). No test file exists for MainFileListFragment at all (checked src/test and src/androidTest), and the PR's "Verification" section only lists manual compileDebugKotlin/assembleDebug runs — that proves it compiles, not that the rotation fix behaves or stays fixed. This is UI-visible behavior (the bug being fixed, #4884, is itself a rotation/layout regression), so per house rules it needs Espresso/Robolectric coverage — below the 85% bar required to approve.
Proposed test (Espresso, matching this repo's existing androidTest Koin + mockk(relaxed = true) + ActivityScenario conventions used for sibling fragments):
package com.owncloud.android.presentation.files.filelist
import android.content.res.Configuration
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
import androidx.test.core.app.ActivityScenario.launch
import androidx.test.core.app.ApplicationProvider
import com.owncloud.android.R
import com.owncloud.android.domain.files.model.FileListOption
import com.owncloud.android.presentation.files.ViewType
import com.owncloud.android.presentation.files.operations.FileOperationsViewModel
import com.owncloud.android.sharing.shares.ui.TestShareFileActivity
import com.owncloud.android.testutil.OC_ACCOUNT
import com.owncloud.android.testutil.OC_FOLDER
import io.mockk.mockk
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.core.context.startKoin
import org.koin.core.context.stopKoin
import org.koin.dsl.module
class MainFileListFragmentRotationTest {
private lateinit var fragment: MainFileListFragment
private lateinit var mainFileListViewModel: MainFileListViewModel
@Before
fun setUp() {
val context = ApplicationProvider.getApplicationContext<android.content.Context>()
mainFileListViewModel = mockk(relaxed = true)
stopKoin()
startKoin {
context
allowOverride(override = true)
modules(module {
viewModel { mainFileListViewModel }
viewModel { mockk<FileOperationsViewModel>(relaxed = true) }
})
}
fragment = MainFileListFragment.newInstance(
initialFolderToDisplay = OC_FOLDER,
fileListOption = FileListOption.ALL_FILES,
accountName = OC_ACCOUNT.name,
)
launch(TestShareFileActivity::class.java).onActivity { it.startFragment(fragment) }
}
private fun currentSpanCount(): Int =
(fragment.view!!.findViewById<RecyclerView>(R.id.recyclerViewMainFileList)
.layoutManager as StaggeredGridLayoutManager).spanCount
@Test
fun rotating_in_grid_mode_recalculates_span_count() {
fragment.onViewTypeListener(ViewType.VIEW_TYPE_GRID)
fragment.onConfigurationChanged(Configuration(fragment.resources.configuration).apply {
orientation = Configuration.ORIENTATION_LANDSCAPE
})
assertTrue("expected grid span > 1 after rotation", currentSpanCount() > 1)
}
@Test
fun rotating_in_list_mode_keeps_span_count_at_one() {
fragment.onViewTypeListener(ViewType.VIEW_TYPE_LIST)
fragment.onConfigurationChanged(Configuration(fragment.resources.configuration).apply {
orientation = Configuration.ORIENTATION_LANDSCAPE
})
assertEquals(1, currentSpanCount())
}
@Test
fun toggling_view_type_is_remembered_across_subsequent_rotation() {
fragment.onViewTypeListener(ViewType.VIEW_TYPE_LIST)
fragment.onViewTypeListener(ViewType.VIEW_TYPE_GRID)
fragment.onConfigurationChanged(fragment.resources.configuration)
assertTrue("view type toggle should persist and drive grid span on rotation", currentSpanCount() > 1)
}
}Security and stability passes found nothing blocking (see review summary).
Generated by Claude Code
🤖 Automated review by Claude Code (security · stability · performance · coverage) Generated by Claude Code |
Summary
Fixes #4884
Verification
JAVA_HOME="/Applications/Android Studio.app/Contents/jbr/Contents/Home" ANDROID_HOME="/Users/himu/Library/Android/sdk" ./gradlew --no-daemon --console=plain :owncloudApp:compileOriginalDebugKotlingit diff --check