diff options
| author | Danilo M. <danix@danix.xyz> | 2026-10-22 11:25:54 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-10-22 11:25:54 +0200 |
| commit | 2e7672cef12dddfa3c2511904b58d86baf52f6b8 (patch) | |
| tree | d37d9ffe2ca734a83b74e2e42eb046e084fb6ca0 | |
| parent | b86d85b16399827bc64583bee3d0283259020a14 (diff) | |
| download | mygram-droid-2e7672cef12dddfa3c2511904b58d86baf52f6b8.tar.gz mygram-droid-2e7672cef12dddfa3c2511904b58d86baf52f6b8.zip | |
Fix viewer review findings
- Reels paging: drop page callbacks after destroy or a replaced playlist
- Scope Glide loads to views and clear them in onDestroy (no deprecated Glide.with(Activity))
- Stop the player in onStop, re-prepare in onStart
- Photo timer uses real elapsed time and starts when the photo is on screen
- Reels: continue past the old last post when a new page arrives; "No reels" when empty
- Likes: ordered single-thread executor, newer toggle wins on error, no "null" in toast
- Note the unbounded frame grab limitation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| -rw-r--r-- | app/src/main/java/mygram/droid/MainActivity.java | 4 | ||||
| -rw-r--r-- | app/src/main/java/mygram/droid/ViewerActivity.java | 69 |
2 files changed, 59 insertions, 14 deletions
diff --git a/app/src/main/java/mygram/droid/MainActivity.java b/app/src/main/java/mygram/droid/MainActivity.java index 7578f05..3292f90 100644 --- a/app/src/main/java/mygram/droid/MainActivity.java +++ b/app/src/main/java/mygram/droid/MainActivity.java @@ -124,7 +124,7 @@ public final class MainActivity extends Activity { return; ready = true; // Foldergram's logo is AGPLv3: loaded from the instance after login, never bundled. - Glide.with(this).load(Api.resolve(Prefs.server(), "/icon-192.png")).error(R.drawable.ic_home).into(logo); + Glide.with(logo).load(Api.resolve(Prefs.server(), "/icon-192.png")).error(R.drawable.ic_home).into(logo); backgrounds = posts; handler.removeCallbacks(rotate); rotateBackground(); @@ -235,7 +235,7 @@ public final class MainActivity extends Activity { back.animate().cancel(); back.setAlpha(0f); back.bringToFront(); - Glide.with(this).load(p.media.get(0).thumbnailUrl).listener(new RequestListener<Drawable>() { + Glide.with(back).load(p.media.get(0).thumbnailUrl).listener(new RequestListener<Drawable>() { @Override public boolean onLoadFailed(GlideException e, Object model, Target<Drawable> target, boolean first) { return false; diff --git a/app/src/main/java/mygram/droid/ViewerActivity.java b/app/src/main/java/mygram/droid/ViewerActivity.java index e422104..098f817 100644 --- a/app/src/main/java/mygram/droid/ViewerActivity.java +++ b/app/src/main/java/mygram/droid/ViewerActivity.java @@ -21,6 +21,7 @@ import android.media.MediaMetadataRetriever; import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.os.SystemClock; import android.view.Gravity; import android.view.KeyEvent; import android.view.View; @@ -47,6 +48,7 @@ import com.bumptech.glide.request.RequestListener; import com.bumptech.glide.request.target.Target; import java.util.HashMap; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ThreadLocalRandom; @@ -59,6 +61,8 @@ public final class ViewerActivity extends Activity { private static final long SEEK_MS = 10_000, TICK_MS = 100; /** Frame grabs block on the network; keep them off the API pool. */ private static final ExecutorService FRAMES = Executors.newSingleThreadExecutor(); + /** One thread, so like toggles reach the server in order. */ + private static final ExecutorService LIKES = Executors.newSingleThreadExecutor(); private static final ViewGroup.LayoutParams FILL = new ViewGroup.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT); @@ -73,8 +77,8 @@ public final class ViewerActivity extends Activity { private int index, slide; /** Bumped on every slide change; late callbacks for older slides are dropped. */ private volatile int generation; - private boolean paused, failed, loadingMore; - private long photoElapsed; + private boolean paused, failed, loadingMore, parkedAtEnd; + private long photoElapsed, lastTick; @Override @OptIn(markerClass = UnstableApi.class) @@ -168,12 +172,14 @@ public final class ViewerActivity extends Activity { paused = false; failed = false; photoElapsed = 0; + lastTick = SystemClock.uptimeMillis(); + parkedAtEnd = false; message.setText(""); pauseIcon.setVisibility(View.GONE); heart.setVisibility(Likes.has(post().id) ? View.VISIBLE : View.GONE); - Glide.with(this).load(m.video ? m.thumbnailUrl : m.previewUrl).override(200).into(backdrop); + Glide.with(backdrop).load(m.video ? m.thumbnailUrl : m.previewUrl).override(200).into(backdrop); if (m.video) { - Glide.with(this).clear(photo); + Glide.with(photo).clear(photo); video.setVisibility(View.VISIBLE); player.setMediaItem(MediaItem.fromUri(m.previewUrl)); player.prepare(); @@ -182,7 +188,7 @@ public final class ViewerActivity extends Activity { } else { player.stop(); video.setVisibility(View.GONE); - Glide.with(this).load(m.previewUrl).listener(new RequestListener<Drawable>() { + Glide.with(photo).load(m.previewUrl).listener(new RequestListener<Drawable>() { @Override public boolean onLoadFailed(GlideException e, Object model, Target<Drawable> target, boolean first) { if (gen == generation) @@ -193,6 +199,8 @@ public final class ViewerActivity extends Activity { @Override public boolean onResourceReady(Drawable d, Object model, Target<Drawable> target, DataSource source, boolean first) { + if (gen == generation) + photoElapsed = 0; // count from when the photo is on screen return false; } }).into(photo); @@ -200,6 +208,7 @@ public final class ViewerActivity extends Activity { tick(); } + // ponytail: a grab already blocked in setDataSource is not interrupted, so a slow one delays the next post's frame; upgrade: a timeout or per-post cancellation. /** Swaps the video backdrop for a frame from 20-80 % of the clip; ends are often black. */ private void randomFrame(Post.Media m, int gen) { if (m.durationMs <= 0) @@ -214,7 +223,7 @@ public final class ViewerActivity extends Activity { } }, frame -> { if (frame != null && gen == generation) { - Glide.with(this).clear(backdrop); + Glide.with(backdrop).clear(backdrop); backdrop.setImageBitmap(frame); } }, e -> { @@ -229,8 +238,10 @@ public final class ViewerActivity extends Activity { long d = player.getDuration(); progress.setProgress(d > 0 ? (int) (1000 * player.getCurrentPosition() / d) : 0); } else { + long now = SystemClock.uptimeMillis(); if (!paused) - photoElapsed += TICK_MS; + photoElapsed += now - lastTick; + lastTick = now; long total = Prefs.photoSeconds() * 1000L; progress.setProgress((int) (1000 * Math.min(photoElapsed, total) / total)); if (photoElapsed >= total) { @@ -248,6 +259,7 @@ public final class ViewerActivity extends Activity { } else if (index < Playlist.posts.size() - 1) { show(index + 1); } else { + parkedAtEnd = true; setPaused(true); // end of the list } } @@ -262,6 +274,8 @@ public final class ViewerActivity extends Activity { private void setPaused(boolean p) { paused = p; + if (!p) + lastTick = SystemClock.uptimeMillis(); pauseIcon.setVisibility(p ? View.VISIBLE : View.GONE); if (media().video && !failed) { if (p) @@ -293,15 +307,18 @@ public final class ViewerActivity extends Activity { bigHeart.setVisibility(View.VISIBLE); bigHeart.animate().alpha(0f).setDuration(700).withEndAction(() -> bigHeart.setVisibility(View.GONE)).start(); } - Api.async(() -> { + Api.async(LIKES, () -> { Api.setLike(p.id, on); return true; }, ok -> { }, e -> { + if (Likes.has(p.id) != on) + return; // a newer toggle wins Likes.set(p.id, !on); if (!Playlist.posts.isEmpty() && post() == p) heart.setVisibility(on ? View.GONE : View.VISIBLE); - Toast.makeText(this, "Like failed: " + e.getMessage(), Toast.LENGTH_SHORT).show(); + String msg = e.getMessage(); + Toast.makeText(this, "Like failed" + (msg != null ? ": " + msg : ""), Toast.LENGTH_SHORT).show(); }); } @@ -311,16 +328,26 @@ public final class ViewerActivity extends Activity { return; loadingMore = true; int page = Playlist.reelsPage + 1; + List<Post> list = Playlist.posts; Api.async(() -> Api.reels(Playlist.reelsSeed, page), o -> { + if (isDestroyed() || Playlist.posts != list) + return; loadingMore = false; Playlist.reelsPage = page; - boolean first = Playlist.posts.isEmpty(); - Playlist.posts.addAll(Post.list(o, Prefs.server())); + boolean first = list.isEmpty(); + int old = list.size(); + list.addAll(Post.list(o, Prefs.server())); if (!o.optBoolean("hasMore")) Playlist.reelsSeed = 0; - if (first && !Playlist.posts.isEmpty()) + if (first && !list.isEmpty()) show(0); + else if (parkedAtEnd && index == old - 1 && list.size() > old) + show(index + 1); + else if (list.isEmpty() && Playlist.reelsSeed == 0) + message.setText("No reels"); }, e -> { + if (isDestroyed()) + return; loadingMore = false; if (Playlist.posts.isEmpty()) message.setText("Cannot load reels. Press Down to retry."); @@ -368,6 +395,22 @@ public final class ViewerActivity extends Activity { } @Override + protected void onStart() { + super.onStart(); + // The first show() already prepared (state is not IDLE), so this only revives a stopped decoder. + if (!Playlist.posts.isEmpty() && media().video && !failed && player.getPlaybackState() == Player.STATE_IDLE) { + player.setPlayWhenReady(!paused); + player.prepare(); + } + } + + @Override + protected void onStop() { + super.onStop(); + player.stop(); // free the video decoder while in the background + } + + @Override protected void onPause() { super.onPause(); if (!Playlist.posts.isEmpty() && !paused) @@ -379,6 +422,8 @@ public final class ViewerActivity extends Activity { super.onDestroy(); handler.removeCallbacksAndMessages(null); generation++; + Glide.with(photo).clear(photo); + Glide.with(backdrop).clear(backdrop); player.release(); } } |
