A modern, dark‑first video player for Android built around one thing most players do badly: looping. Loop a whole video seamlessly, or a precise A–B segment, pop the video out into a floating window, and browse everything on your device from a clean Material 3 library.
Package:
com.loopr.player· minSdk 26 (Android 8.0) · targetSdk 35
- Gapless single‑video loop — repeats the current video seamlessly (
REPEAT_MODE_ONE, same decoder, no re‑buffer at the seam). - A–B loop — set point A and point B and Loopr loops just that segment, jumping back to A as soon as playback passes B. A/B markers and the looped region are drawn right on the seek bar.
- Plays your whole library as a queue with ⏮ / ⏭ previous/next.
- Opening a video from a file manager or another app enqueues the whole containing folder (sorted by name), so ⏮ / ⏭ traverse the folder instead of stopping at the single file — no matter how the launching app references the file (a MediaStore item, a Storage‑Access‑Framework/documents URI, the app's own FileProvider, or a file path). A launching app can also hand the folder over directly by attaching it as intent
ClipData, which covers folders MediaStore doesn't index at all (e.g. under a.nomedia) and needs no media permission. Loopr plays exactly the list it is handed, so a sending app must attach the file's real containing folder — never a cache or staging directory it copied the file into, whose other entries aren't siblings at all. Falls back to single‑file playback — and says why — when the folder can't be read. - Repeat mode cycles Off → One → All, and a Shuffle toggle — both remembered between sessions.
- Optional multiple‑players mode — open several videos in their own instances and play them at once (off by default). Applies to videos opened from other apps too: with the mode on, a video handed over by a file manager gets its own window instead of taking over the player already running, so a video floating in PiP keeps playing.
- Variable speed (0.25×–2×), mute, resize (Fit / Crop / Stretch) and rotate.
- Picture‑in‑Picture — float the video over other apps, with the correct aspect ratio and ⏮ / play‑pause / ⏭ controls, so you can skip through the folder without expanding back to full screen. Auto‑enters PiP when you press Home mid‑playback.
- Floating windows (optional) — Android allows only one PiP window per device, so for several videos at once Loopr draws its own: up to three overlay windows, each dragged and pinch‑resized where you want it. Each carries its own folder queue (⏮ / ⏭ traverse it) and its own A–B loop, so a segment you set before floating keeps looping in the window, marked with an A–B badge. Tap ⛶ and it goes back to full screen with the queue, position, speed, mute, resize and A–B intact. One notification covers the lot, with Close all. If a window ever stops showing video while the player carries on — a picture can die without anything reporting an error — Loopr notices and works back through it: it re‑seats the surface, then rebuilds the window from scratch, then reloads the video where it was, and closes the window with a reason only if none of that works. It watches three things: the frames being decoded, the frames actually reaching the screen, and whether playback is running at all — a window can fail in any of those three ways and the other two signals can't see it. If a video does fail, Loopr retries it where it stopped, then moves past it if it fails again, rather than leaving the window stranded. Needs the "Display over other apps" permission; without it Loopr falls back to the system PiP window and says so.
- Tap to show/hide controls (auto‑hide while playing).
- Double‑tap left/right edge to seek ∓10s; double‑tap centre to play/pause.
- Vertical swipe — brightness on the left half, volume on the right.
- Horizontal swipe to scrub, with a live position read‑out.
- Grid of all device videos via MediaStore, with async thumbnails, durations and file sizes.
- Runtime media permission flow.
- Material 3 UI with light / dark / follow‑system themes — defaults to dark.
Add screenshots here (docs/ folder) — library grid, player with A–B markers, and PiP window.
| Area | Choice |
|---|---|
| Language | Kotlin |
| Media | AndroidX Media3 / ExoPlayer 1.4.1 |
| UI | Material 3 (Views + ViewBinding), ConstraintLayout, RecyclerView |
| Async | Kotlin Coroutines |
| Build | Gradle 8.11.1, Android Gradle Plugin 8.7.3, JDK 17 |
No third‑party analytics, ads, or network calls — Loopr only reads the videos already on your device.
- JDK 17
- Android SDK with platform 35 and build‑tools 34.0.0
- A
local.propertiespointing at your SDK:sdk.dir=/path/to/Android/Sdk
./gradlew assembleDebug
# output: app/build/outputs/apk/debug/app-debug.apk- Generate a keystore:
keytool -genkeypair -v -keystore loopr-release.jks -alias loopr \ -keyalg RSA -keysize 2048 -validity 10000 - Copy
keystore.properties.exampletokeystore.propertiesand fill in your passwords. - Build:
./gradlew assembleRelease # output: app/build/outputs/apk/release/app-release.apk
keystore.propertiesand*.jksare git‑ignored on purpose — keep your signing secrets out of version control.
adb install -r app/build/outputs/apk/release/app-release.apkThe floating windows are overlays drawn by a service, so no accessibility dump or adb shell input
can reach them — their drag and pinch‑resize are covered by an on‑device test that injects real
multi‑pointer events and reads the window geometry back from dumpsys window:
adb shell appops set com.loopr.player SYSTEM_ALERT_WINDOW allow # the test needs the permission
./gradlew connectedDebugAndroidTestapp/src/main/
├── java/com/loopr/player/
│ ├── MainActivity.kt # video library: MediaStore query, grid, permissions, theme
│ ├── PlayerActivity.kt # ExoPlayer, custom controls, A–B loop, queue, PiP, gestures
│ ├── FloatingPlayerService.kt # foreground service owning up to 3 floating windows
│ ├── FloatingWindow.kt # one overlay window: own player, queue, A–B, drag/pinch
│ ├── FloatingHandoff.kt # state passed between the activity and a floating window
│ ├── VideoAdapter.kt # RecyclerView grid adapter
│ ├── VideoItem.kt # video model
│ ├── ThumbnailLoader.kt # async thumbnail loading + LRU cache
│ ├── AbSeekBar.kt # SeekBar that draws the A/B markers and loop region
│ └── ThemeManager.kt # light/dark/system theme persistence
└── res/ # layouts, Material 3 themes, vector icons, adaptive launcher
- Whole video: the Repeat chip sets ExoPlayer's
REPEAT_MODE_ONE, which loops the item without tearing down the decoder — so the seam is effectively gapless. - A–B segment: setting A and B starts a watcher that polls the playback position every 30 ms and seeks back to A the moment it passes B (or the video ends). The queue item itself is left alone — nothing is rebuilt or replaced — so clearing A–B is instant and your place in the queue is never disturbed. The poll pauses while you're scrubbing. A–B belongs to the video it was set on: moving to another one clears it, and B must come after A.
- Per‑folder browsing in the library grid
- Playback position memory / resume
- Subtitle (SRT) support
- Background audio mode
MIT — see LICENSE.