aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDanilo M. <danix@danix.xyz>2026-10-22 11:25:54 +0200
committerDanilo M. <danix@danix.xyz>2026-10-22 11:25:54 +0200
commit2e7672cef12dddfa3c2511904b58d86baf52f6b8 (patch)
treed37d9ffe2ca734a83b74e2e42eb046e084fb6ca0
parentb86d85b16399827bc64583bee3d0283259020a14 (diff)
downloadmygram-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.java4
-rw-r--r--app/src/main/java/mygram/droid/ViewerActivity.java69
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();
}
}