mirror of
https://github.com/meowarex/rl-mobile.git
synced 2026-08-26 22:17:44 +10:00
Fix offline Shader + Lyric Seeking
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package radiant;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.drawable.BitmapDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.Choreographer;
|
||||
|
||||
import androidx.compose.foundation.layout.SizeKt;
|
||||
@@ -18,15 +20,17 @@ import androidx.compose.ui.draw.DrawModifierKt;
|
||||
import androidx.compose.ui.geometry.Size;
|
||||
import androidx.compose.ui.graphics.AndroidCanvas_androidKt;
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope;
|
||||
import androidx.compose.ui.platform.AndroidCompositionLocals_androidKt;
|
||||
|
||||
import coil.request.CachePolicy;
|
||||
|
||||
import com.tidal.android.feature.playerscreen.ui.composables.p3;
|
||||
import com.tidal.android.feature.playerscreen.ui.model.PlayerBackgroundStyle;
|
||||
import com.tidal.android.image.coil.base.CoilImageLoader;
|
||||
import com.tidal.android.image.core.b;
|
||||
|
||||
import dev.kawarp.KawarpEngine;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@@ -35,17 +39,10 @@ import java.util.concurrent.Executors;
|
||||
*
|
||||
* The engine (shader, blur pipeline, settings, crossfade) lives in the Kawarp-AGSL library
|
||||
* (cloned at tools/kawarp-agsl) and is compiled into this bundle by tools/kawarp-build/build.sh;
|
||||
* this class only holds what is TIDAL/Compose-specific:
|
||||
* the patched-in composable entry points, cover fetching by TIDAL uuid, the Choreographer ->
|
||||
* snapshot-state frame clock, the Manager's option tokens, and the fallback to TIDAL's own
|
||||
* blurred backdrop.
|
||||
*
|
||||
* One class on purpose (its own draw lambda, frame callback and loader) - see the engine's
|
||||
* class doc for the dex-injection constraints.
|
||||
* this class only holds what is TIDAL/Compose-specific
|
||||
*/
|
||||
public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallback {
|
||||
public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallback, b0.c {
|
||||
|
||||
private static final String TAG = "RLKawarp";
|
||||
/** Stop the frame loop once nothing has drawn for this long (player left / backgrounded). */
|
||||
private static final long IDLE_STOP_MS = 500L;
|
||||
|
||||
@@ -69,26 +66,31 @@ public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallbac
|
||||
SnapshotStateKt.mutableStateOf$default(Boolean.FALSE, null, 2, null);
|
||||
|
||||
/** The shared instance render passes to drawBehind (also serves as the frame callback). */
|
||||
private static final Kawarp DRAW = new Kawarp(null, 0);
|
||||
private static final Kawarp DRAW = new Kawarp(0, null, 0);
|
||||
|
||||
private static KawarpEngine engine;
|
||||
/** TIDAL's application-scoped Coil loader and the context its requests are built with. */
|
||||
private static volatile coil.f loader;
|
||||
private static volatile Context appContext;
|
||||
private static volatile String requestedUuid;
|
||||
private static volatile int loadToken;
|
||||
|
||||
private static long lastDrawUptime;
|
||||
private static boolean frameScheduled, loggedDraw;
|
||||
private static boolean frameScheduled;
|
||||
private static int frameTick;
|
||||
|
||||
/** Set when this instance is a queued cover load; null/0 on the shared DRAW instance. */
|
||||
/** Set when this instance is a queued cover load; 0/null/0 on the shared DRAW instance. */
|
||||
private final int albumId;
|
||||
private final String uuid;
|
||||
private final int token;
|
||||
|
||||
private Kawarp(String uuid, int token) {
|
||||
private Kawarp(int albumId, String uuid, int token) {
|
||||
this.albumId = albumId;
|
||||
this.uuid = uuid;
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Composition
|
||||
|
||||
|
||||
@@ -106,19 +108,39 @@ public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallbac
|
||||
public static void render(int albumId, String coverUuid, boolean isPlaying,
|
||||
Modifier modifier, Composer composer) {
|
||||
composer.startReplaceGroup(0x52415750);
|
||||
attach(composer);
|
||||
boolean ok = !((Boolean) fallbackState.getValue()).booleanValue() && ensureEngine();
|
||||
if (!ok) {
|
||||
// No (working) AGSL here - fall back to TIDAL's own blurred backdrop.
|
||||
p3.a(albumId, coverUuid, isPlaying, false, null, modifier, composer, 0);
|
||||
} else {
|
||||
engine.setPlaying(isPlaying);
|
||||
request(coverUuid);
|
||||
request(albumId, coverUuid);
|
||||
Modifier m = DrawModifierKt.drawBehind(SizeKt.fillMaxSize(modifier, 1.0f), DRAW);
|
||||
SpacerKt.Spacer(m, composer, 0);
|
||||
}
|
||||
composer.endReplaceGroup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab the app's image loader the same way TIDAL's composables do (yd0.c)
|
||||
*/
|
||||
private static void attach(Composer composer) {
|
||||
if (loader != null) return;
|
||||
try {
|
||||
Object local = composer.consume(AndroidCompositionLocals_androidKt.getLocalContext());
|
||||
if (!(local instanceof Context)) return;
|
||||
Context app = ((Context) local).getApplicationContext();
|
||||
if (!(app instanceof ce0.b.a)) return;
|
||||
Object tidalLoader = ((ce0.b.a) app).a().a();
|
||||
if (!(tidalLoader instanceof CoilImageLoader)) return;
|
||||
appContext = app;
|
||||
loader = ((CoilImageLoader) tidalLoader).a;
|
||||
} catch (Throwable t) {
|
||||
// No loader -> request() never runs and the backdrop simply stays on the last cover.
|
||||
}
|
||||
}
|
||||
|
||||
/** Engine construction compiles the AGSL so this doubles as the support probe. */
|
||||
private static boolean ensureEngine() {
|
||||
if (engine != null) return true;
|
||||
@@ -138,52 +160,69 @@ public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallbac
|
||||
engine = e;
|
||||
return true;
|
||||
} catch (Throwable t) {
|
||||
Log.i(TAG, "AGSL rejected, staying on the native backdrop: " + t);
|
||||
fallbackState.setValue(Boolean.TRUE);
|
||||
fallbackState.setValue(Boolean.TRUE); // AGSL rejected: stay on the native backdrop
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Cover loading (Runnable on LOADER)
|
||||
|
||||
|
||||
private static void request(String coverUuid) {
|
||||
// Cover loading: a queued Kawarp is both the LOADER task and the request's Coil Target
|
||||
|
||||
|
||||
private static void request(int albumId, String coverUuid) {
|
||||
if (coverUuid == null || coverUuid.length() == 0 || coverUuid.equals(requestedUuid)) return;
|
||||
if (loader == null) return; // attach() failed; retried next composition
|
||||
requestedUuid = coverUuid;
|
||||
LOADER.execute(new Kawarp(coverUuid, ++loadToken));
|
||||
LOADER.execute(new Kawarp(albumId, coverUuid, ++loadToken));
|
||||
}
|
||||
|
||||
/**
|
||||
* TIDAL's own backdrop request (PlayerBackgroundKt.rememberBlurredArtwork)
|
||||
*/
|
||||
public void run() {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
// Same URL shape TIDAL itself builds in he0.a; 640 is plenty ahead of a 128 downscale.
|
||||
URL url = new URL("https://resources.tidal.com/images/"
|
||||
+ uuid.replace('-', '/') + "/640x640.jpg");
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.setConnectTimeout(10000);
|
||||
connection.setReadTimeout(15000);
|
||||
InputStream stream = connection.getInputStream();
|
||||
Bitmap source;
|
||||
try {
|
||||
source = BitmapFactory.decodeStream(stream);
|
||||
} finally {
|
||||
stream.close();
|
||||
}
|
||||
if (source == null || token != loadToken) return;
|
||||
engine.setCover(source);
|
||||
source.recycle();
|
||||
Log.i(TAG, "cover submitted: " + uuid);
|
||||
xd0.g tidal = new xd0.g(appContext, new b.a(String.valueOf(albumId), uuid),
|
||||
null, null, false, null, null, false);
|
||||
coil.request.h.a req = coil.request.h.a(com.tidal.android.image.coil.c.a(tidal, null));
|
||||
coil.request.b d = req.b;
|
||||
req.b = new coil.request.b(d.a, d.b, d.c, d.d, d.e, d.f, d.g, d.h,
|
||||
d.i, d.j, CachePolicy.DISABLED);
|
||||
req.d = this;
|
||||
loader.b(req.a());
|
||||
} catch (Throwable t) {
|
||||
Log.i(TAG, "cover load failed: " + t);
|
||||
// Let the next track (or a re-entry into the player) retry this cover.
|
||||
if (token == loadToken) requestedUuid = null;
|
||||
} finally {
|
||||
if (connection != null) connection.disconnect();
|
||||
fail();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Target.onSuccess */
|
||||
public void a(Drawable drawable) {
|
||||
if (token != loadToken) return; // user skipped on
|
||||
Bitmap cover = toBitmap(drawable);
|
||||
if (cover == null) { fail(); return; }
|
||||
// setCover copies, the bitmap stays Coil's (memory cache)
|
||||
engine.setCover(cover);
|
||||
}
|
||||
|
||||
/** Target.onStart - nothing to show while loading. */
|
||||
public void b(Drawable placeholder) {}
|
||||
|
||||
/** Target.onError - not in any of TIDAL's stores. */
|
||||
public void d(Drawable error) { fail(); }
|
||||
|
||||
private void fail() {
|
||||
if (token == loadToken) requestedUuid = null;
|
||||
}
|
||||
|
||||
private static Bitmap toBitmap(Drawable d) {
|
||||
if (d == null) return null;
|
||||
if (d instanceof BitmapDrawable) return ((BitmapDrawable) d).getBitmap();
|
||||
Bitmap out = Bitmap.createBitmap(256, 256, Bitmap.Config.ARGB_8888);
|
||||
d.setBounds(0, 0, 256, 256);
|
||||
d.draw(new Canvas(out));
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
// Frame loop
|
||||
|
||||
|
||||
@@ -215,16 +254,11 @@ public final class Kawarp implements am0.l, Runnable, Choreographer.FrameCallbac
|
||||
long packed = scope.getSizeNHjbRc();
|
||||
float width = Size.getWidthImpl(packed);
|
||||
float height = Size.getHeightImpl(packed);
|
||||
if (!loggedDraw) {
|
||||
loggedDraw = true;
|
||||
Log.i(TAG, "first draw: " + width + "x" + height + " ready=" + engine.isReady());
|
||||
}
|
||||
try {
|
||||
engine.draw(AndroidCanvas_androidKt.getNativeCanvas(scope.getDrawContext().getCanvas()),
|
||||
width, height);
|
||||
} catch (Throwable t) {
|
||||
Log.i(TAG, "AGSL draw failed, falling back: " + t);
|
||||
fallbackState.setValue(Boolean.TRUE);
|
||||
fallbackState.setValue(Boolean.TRUE); // AGSL draw failed: recompose onto p3.a
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,5 @@ package androidx.compose.runtime;
|
||||
public interface Composer {
|
||||
void startReplaceGroup(int key);
|
||||
void endReplaceGroup();
|
||||
Object consume(CompositionLocal key);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.runtime;
|
||||
public abstract class CompositionLocal {}
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.runtime;
|
||||
public abstract class ProvidableCompositionLocal extends CompositionLocal {}
|
||||
@@ -0,0 +1,2 @@
|
||||
package androidx.compose.ui.layout;
|
||||
public interface ContentScale {}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
package androidx.compose.ui.platform;
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal;
|
||||
public final class AndroidCompositionLocals_androidKt {
|
||||
public static ProvidableCompositionLocal getLocalContext() { return null; }
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package b0;
|
||||
import android.graphics.drawable.Drawable;
|
||||
/** coil.target.Target (R8 name in TIDAL 2.200): a=onSuccess, b=onStart, d=onError. */
|
||||
public interface c {
|
||||
void a(Drawable success);
|
||||
void b(Drawable start);
|
||||
void d(Drawable error);
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package ce0;
|
||||
/** Dagger image component; the Application implements b.a and hands out the loader. */
|
||||
public interface b {
|
||||
xd0.e a();
|
||||
interface a { b a(); }
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package coil;
|
||||
/** coil.ImageLoader; b() is enqueue(). */
|
||||
public interface f {
|
||||
coil.request.d b(coil.request.h request);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package coil.request;
|
||||
public enum CachePolicy { ENABLED, READ_ONLY, WRITE_ONLY, DISABLED }
|
||||
@@ -0,0 +1,19 @@
|
||||
package coil.request;
|
||||
import kotlinx.coroutines.CoroutineDispatcher;
|
||||
/**
|
||||
* coil.request.DefaultRequestOptions; i/j/k are the memory/disk/network cache policies (proven
|
||||
* via h$a.a() -> ImageRequest.o/p/q -> Options.m/n/o, the last of which HttpUriFetcher gates on).
|
||||
* Fields are deliberately non-final: a `final boolean h = false` would be a compile-time constant
|
||||
* and javac would fold the read away instead of emitting iget-boolean. (claude)
|
||||
*/
|
||||
public final class b {
|
||||
public CoroutineDispatcher a, b, c, d;
|
||||
public d0.c.a e;
|
||||
public coil.size.Precision f;
|
||||
public android.graphics.Bitmap.Config g;
|
||||
public boolean h;
|
||||
public CachePolicy i, j, k;
|
||||
public b(CoroutineDispatcher a, CoroutineDispatcher b, CoroutineDispatcher c, CoroutineDispatcher d,
|
||||
d0.c.a transition, coil.size.Precision precision, android.graphics.Bitmap.Config config,
|
||||
boolean h, CachePolicy memory, CachePolicy disk, CachePolicy network) {}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package coil.request;
|
||||
/** coil.request.Disposable */
|
||||
public interface d {}
|
||||
@@ -0,0 +1,12 @@
|
||||
package coil.request;
|
||||
/** coil.request.ImageRequest; a(h) is newBuilder(), h.a is the Builder. */
|
||||
public final class h {
|
||||
public static a a(h request) { return null; }
|
||||
public static final class a {
|
||||
/** defaults block the built request copies its cache policies from */
|
||||
public b b;
|
||||
/** target */
|
||||
public b0.c d;
|
||||
public h a() { return null; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
package coil.size;
|
||||
public enum Precision { EXACT, INEXACT, AUTOMATIC }
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.tidal.android.image.coil.base;
|
||||
/** TIDAL's xd0.e implementation; a is the underlying Coil ImageLoader. */
|
||||
public final class CoilImageLoader implements xd0.e {
|
||||
public final coil.f a = null;
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.tidal.android.image.coil;
|
||||
/** TIDAL request -> Coil request; installs the AlbumCover data the interceptor resolves. */
|
||||
public final class c {
|
||||
public static coil.request.h a(xd0.g request, androidx.compose.ui.layout.ContentScale scale) { return null; }
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package com.tidal.android.image.core;
|
||||
/** TIDAL's sealed image-source type; b$a is AlbumCover(albumId, coverUuid). */
|
||||
public abstract class b {
|
||||
public static final class a extends b { public a(String albumId, String cover) {} }
|
||||
/** b$h = a resolved location; b$h$a is the placeholder/error drawable spec. */
|
||||
public interface h { interface a {} }
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package d0;
|
||||
/** coil.transition.Transition; c.a is Transition.Factory. */
|
||||
public interface c { interface a {} }
|
||||
@@ -0,0 +1,2 @@
|
||||
package kotlinx.coroutines;
|
||||
public abstract class CoroutineDispatcher {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package xd0;
|
||||
/** TIDAL's ImageLoader interface (implemented by CoilImageLoader). */
|
||||
public interface e {}
|
||||
@@ -0,0 +1,3 @@
|
||||
package xd0;
|
||||
/** Size spec wrapper accepted by xd0.g's constructor; we always pass null. */
|
||||
public final class f {}
|
||||
@@ -0,0 +1,8 @@
|
||||
package xd0;
|
||||
import android.content.Context;
|
||||
import com.tidal.android.image.core.b;
|
||||
/** TIDAL's ImageRequest. */
|
||||
public final class g {
|
||||
public g(Context context, b source, b.h.a placeholder, b.h.a error, boolean crossfade,
|
||||
java.util.List transformations, f size, boolean allowHardware) {}
|
||||
}
|
||||
Reference in New Issue
Block a user