mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 13:53:12 -06:00
Merge QA: a fresh panel no longer skips its first item; rotated wall screenshots
This commit is contained in:
commit
32189d1784
|
|
@ -544,7 +544,10 @@ class PlaylistController(
|
|||
retryRunnable = Runnable {
|
||||
if (isRunning && items.isNotEmpty()) {
|
||||
if (firstActiveIndex() < 0) { showNothingScheduled(); return@Runnable }
|
||||
val idx = PlaylistSelection.nextPlayableIndex(items.size, currentIndex) { playableNow(it) }
|
||||
// Include currentIndex when nothing is on screen: there it is the intended START,
|
||||
// not a position that has had its turn. Skipping it dropped item 1 on every cold
|
||||
// start, because a fresh panel always gets the playlist before the media.
|
||||
val idx = PlaylistSelection.recheckIndex(items.size, currentIndex, hasContentOnScreen) { playableNow(it) }
|
||||
if (idx >= 0) { currentIndex = idx; playCurrentItem() } else onContentNotReady()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,39 @@ object PlaylistSelection {
|
|||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* First playable index AT OR AFTER [from] (wrapping), or -1 if none. A negative [from] means
|
||||
* "no position yet" and starts at 0 rather than wrapping onto the last item.
|
||||
*/
|
||||
fun playableFromIndex(size: Int, from: Int, isPlayable: (Int) -> Boolean): Int {
|
||||
if (size <= 0) return -1
|
||||
val start = if (from < 0) 0 else from % size
|
||||
for (i in 0 until size) {
|
||||
val idx = (start + i) % size
|
||||
if (isPlayable(idx)) return idx
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Which item a content re-check should play once something finally becomes ready.
|
||||
*
|
||||
* [hasContentOnScreen] is the entire distinction, and getting it wrong costs the operator the
|
||||
* first item of their playlist. When content IS up, currentIndex is a real position that has
|
||||
* already had its turn, so the scan must move PAST it. When nothing is up, currentIndex is only
|
||||
* where playback INTENDED to begin — updatePlaylist() seeds it to 0 for a playlist that has not
|
||||
* started yet — so it has never been shown, and advancing past it silently drops item 1 from the
|
||||
* first pass through the playlist.
|
||||
*
|
||||
* That is the cold-start case on every fresh panel: the playlist arrives before its media has
|
||||
* downloaded, start() finds nothing playable, and the re-check three seconds later is what
|
||||
* actually begins playback. On a two-item playlist it looks exactly like "only one of the two
|
||||
* ever plays" until the list wraps.
|
||||
*/
|
||||
fun recheckIndex(size: Int, from: Int, hasContentOnScreen: Boolean, isPlayable: (Int) -> Boolean): Int =
|
||||
if (hasContentOnScreen) nextPlayableIndex(size, from, isPlayable)
|
||||
else playableFromIndex(size, from, isPlayable)
|
||||
|
||||
enum class NonePlayable { KEEP_CURRENT, SHOW_WAITING }
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -66,16 +66,25 @@ class ScreenshotCapture {
|
|||
if (tv.isAvailable && tv.visibility == View.VISIBLE) {
|
||||
val tvBitmap = tv.bitmap
|
||||
if (tvBitmap != null) {
|
||||
val loc = IntArray(2)
|
||||
tv.getLocationInWindow(loc)
|
||||
val rootLoc = IntArray(2)
|
||||
view.getLocationInWindow(rootLoc)
|
||||
val x = (loc[0] - rootLoc[0]).toFloat()
|
||||
val y = (loc[1] - rootLoc[1]).toFloat()
|
||||
val destRect = Rect(x.toInt(), y.toInt(), x.toInt() + tv.width, y.toInt() + tv.height)
|
||||
canvas.drawBitmap(tvBitmap, null, destRect, null)
|
||||
// Place the frame through the SAME transform chain the hierarchy was drawn
|
||||
// with, rather than an axis-aligned rect at getLocationInWindow().
|
||||
//
|
||||
// #236 gave a video-wall panel a mounting rotation, which puts a real
|
||||
// rotation on an ancestor of this TextureView. An axis-aligned rect cannot
|
||||
// express that, so the frame was pasted un-rotated at a position that fell
|
||||
// outside the capture bitmap entirely — and what the dashboard received was
|
||||
// the plain black that view.draw() leaves wherever a TextureView is, i.e. a
|
||||
// panel that looks dead while it is happily playing. Verified on the
|
||||
// emulator: at rotation 90 every remote screenshot of a video came back
|
||||
// #010101 with zero variance, while rotation 0 was correct.
|
||||
val m = matrixTo(tv, view)
|
||||
// The surface bitmap is not required to match the view's size.
|
||||
if (tvBitmap.width > 0 && tvBitmap.height > 0) {
|
||||
m.preScale(tv.width.toFloat() / tvBitmap.width, tv.height.toFloat() / tvBitmap.height)
|
||||
}
|
||||
canvas.drawBitmap(tvBitmap, m, null)
|
||||
tvBitmap.recycle()
|
||||
Log.d("ScreenshotCapture", "Composited TextureView at ($x,$y) size=${tv.width}x${tv.height}")
|
||||
Log.d("ScreenshotCapture", "Composited TextureView ${tv.width}x${tv.height} via $m")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -114,6 +123,28 @@ class ScreenshotCapture {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The matrix mapping [view]'s own coordinates into [ancestor]'s, by walking up the parent chain
|
||||
* and concatenating each step the way the framework does when it draws a child: the child's own
|
||||
* matrix (rotation/scale/translation about its pivot) followed by its layout offset.
|
||||
*
|
||||
* Stops at [ancestor], or at the top of the View chain if it is never reached — a partial chain
|
||||
* still places the frame better than ignoring the transform completely.
|
||||
*/
|
||||
private fun matrixTo(view: View, ancestor: View): android.graphics.Matrix {
|
||||
val out = android.graphics.Matrix()
|
||||
var v: View = view
|
||||
while (true) {
|
||||
val local = android.graphics.Matrix(v.matrix) // translationX/Y + rotation about pivot
|
||||
local.postTranslate(v.left.toFloat(), v.top.toFloat())
|
||||
out.postConcat(local) // out = local * out (child-first)
|
||||
val parent = v.parent
|
||||
if (parent !is View || parent === ancestor) break
|
||||
v = parent
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private fun findAllTextureViews(view: View, result: MutableList<TextureView>) {
|
||||
if (view is TextureView) {
|
||||
result.add(view)
|
||||
|
|
|
|||
|
|
@ -57,4 +57,49 @@ class PlaylistSelectionTest {
|
|||
@Test fun `a single downloaded item loops instead of blanking`() {
|
||||
assertEquals(0, PlaylistSelection.nextPlayableIndex(1, 0, readyPredicate(0)))
|
||||
}
|
||||
|
||||
// ===== the cold-start re-check: item 1 of the playlist must not be skipped =====
|
||||
//
|
||||
// Observed on the emulator: a freshly paired panel gets its playlist BEFORE the media has
|
||||
// downloaded, so start() finds nothing playable and the 3-second content re-check is what
|
||||
// actually begins playback. updatePlaylist() has already seeded currentIndex = 0, and the
|
||||
// re-check used to advance PAST it — so a 4-item playlist played 1,2,3,0 on its first pass and
|
||||
// a 2-item playlist looked like "only one of the two ever plays".
|
||||
|
||||
@Test fun `a cold-start re-check begins at the seeded index instead of skipping past it`() {
|
||||
// currentIndex seeded to 0, nothing on screen yet, everything now downloaded.
|
||||
assertEquals("item 0 has never played — it must not be skipped", 0,
|
||||
PlaylistSelection.recheckIndex(4, from = 0, hasContentOnScreen = false, isPlayable = readyPredicate(0, 1, 2, 3)))
|
||||
}
|
||||
|
||||
@Test fun `a re-check with content already on screen still advances past the current item`() {
|
||||
// The other half of the rule: a real position has had its turn, so we must move on.
|
||||
assertEquals(1,
|
||||
PlaylistSelection.recheckIndex(4, from = 0, hasContentOnScreen = true, isPlayable = readyPredicate(0, 1, 2, 3)))
|
||||
}
|
||||
|
||||
@Test fun `a cold-start re-check still skips an item whose content is not downloaded`() {
|
||||
// Item 0 is still downloading; the panel starts on the first item it can actually show.
|
||||
assertEquals(2,
|
||||
PlaylistSelection.recheckIndex(4, from = 0, hasContentOnScreen = false, isPlayable = readyPredicate(2, 3)))
|
||||
}
|
||||
|
||||
@Test fun `a cold-start re-check with no position yet starts at the top, not the last item`() {
|
||||
// currentIndex is -1 before updatePlaylist seeds it; wrapping onto the last item here would
|
||||
// start a fresh panel at the END of its playlist.
|
||||
assertEquals(0,
|
||||
PlaylistSelection.recheckIndex(3, from = -1, hasContentOnScreen = false, isPlayable = readyPredicate(0, 1, 2)))
|
||||
}
|
||||
|
||||
@Test fun `a cold-start re-check returns -1 while nothing is downloaded`() {
|
||||
assertEquals(-1,
|
||||
PlaylistSelection.recheckIndex(3, from = 0, hasContentOnScreen = false, isPlayable = readyPredicate()))
|
||||
}
|
||||
|
||||
@Test fun `playableFromIndex is inclusive of its start and wraps`() {
|
||||
assertEquals(1, PlaylistSelection.playableFromIndex(4, 1, readyPredicate(1, 3)))
|
||||
assertEquals(3, PlaylistSelection.playableFromIndex(4, 2, readyPredicate(1, 3)))
|
||||
assertEquals(1, PlaylistSelection.playableFromIndex(4, 3, readyPredicate(1))) // wraps
|
||||
assertEquals(-1, PlaylistSelection.playableFromIndex(0, 0, readyPredicate(0)))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue