RL Pebble <3

This commit is contained in:
meoware.exe committed 2026-09-17 23:20:29 +10:00
1 parent aaf2d9e99d
commit 1ab876ea59
25 files changed
+2202 -6

No files matched your search

+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Builds PebbleBridge*.smali into patches/extension/radiant
# Stubs only satisfy javac
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo="$(cd "$here/../.." && pwd)"
ANDROID_SDK="${ANDROID_SDK:-${ANDROID_HOME:-$HOME/Android/Sdk}}"
ANDROID_JAR="${ANDROID_JAR:-$(ls -d "$ANDROID_SDK"/platforms/android-3*/android.jar | sort -V | tail -1)}"
D8="${D8:-$(ls -d "$ANDROID_SDK"/build-tools/*/d8 | sort -V | tail -1)}"
# baksmali and its deps
SMALI_CP="${SMALI_CP:-$(ls "$HOME"/.cache/rlpb/tools/*.jar | tr '\n' :)}"
javac="${JAVA_HOME:+$JAVA_HOME/bin/}javac"
java="${JAVA_HOME:+$JAVA_HOME/bin/}java"
out="$here/build"
rm -rf "$out"
mkdir -p "$out/stubs" "$out/classes" "$out/dex" "$out/smali"
echo "==> javac stubs"
find "$here/stubs" -name '*.java' > "$out/stubs.list"
"$javac" -nowarn -g:none --release 8 -cp "$ANDROID_JAR" -d "$out/stubs" @"$out/stubs.list" 2>&1 |
grep -v 'bootstrap class path\|source value 8\|target value 8\|deprecat\|^[0-9] warning' || true
echo "==> javac sources"
find "$here/src" -name '*.java' > "$out/src.list"
"$javac" -nowarn -g:none --release 8 -cp "$ANDROID_JAR:$out/stubs" -d "$out/classes" @"$out/src.list" 2>&1 |
grep -v 'bootstrap class path\|source value 8\|target value 8\|deprecat\|^[0-9] warning' || true
echo "==> d8"
find "$out/classes" -name '*.class' > "$out/class.list"
bash "$D8" --min-api 24 --no-desugaring --lib "$ANDROID_JAR" --classpath "$out/stubs" \
--output "$out/dex" @"$out/class.list"
echo "==> baksmali"
"$java" -cp "$SMALI_CP" com.android.tools.smali.baksmali.Main d "$out/dex/classes.dex" -o "$out/smali"
echo "==> copy"
dest="$repo/patches/extension/radiant"
rm -f "$dest"/PebbleBridge*.smali
cp "$out"/smali/radiant/PebbleBridge*.smali "$dest/"
echo "==> wrote $dest"
@@ -0,0 +1,299 @@
package radiant;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSession;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Message;
import android.os.Messenger;
import android.os.SystemClock;
import android.support.v4.media.session.MediaSessionCompat;
import android.util.Log;
// Sends TIDAL playback and lyrics to the Manager relay
public final class PebbleBridge implements ServiceConnection {
private static final String TAG = "RLPebble";
private static final String MANAGER_PACKAGE = "com.meowarex.rlmobile";
private static final String RELAY_SERVICE = "com.meowarex.rlmobile.pebble.PebbleRelayService";
// Keep in sync with PebbleRelayService
private static final int MSG_SESSION = 1;
private static final int MSG_POSITION = 2;
private static final int MSG_LYRICS = 3;
// Resend when drift exceeds this
private static final long TOLERANCE_MS = 15L;
private static final long KEEPALIVE_MS = 5000L;
private static final Handler MAIN = new Handler(Looper.getMainLooper());
private static final Object LOCK = new Object();
private static PebbleBridge instance;
// Written by the player thread
private static long rawPosition = -1L;
private static long rawAt;
// Written by the lyrics worker
private static String lyricsKey;
private static String lyricsTitle;
private static String lyricsBody;
private static boolean lyricsFailed;
private final Context context;
private MediaSession.Token token;
private MediaController controller;
private Messenger relay;
private boolean bound;
private long retryBindAt;
private boolean playing;
private String mediaId;
private long sentPosition = -1L;
private long sentAt;
private boolean sentPlaying;
private long lastSendAt;
private PebbleBridge(Context context) {
this.context = context;
}
public static void onState(Context service, MediaSessionCompat session) {
try {
if (instance == null) {
Context app = service.getApplicationContext();
instance = new PebbleBridge(app != null ? app : service);
}
instance.update(session);
} catch (Throwable t) {
Log.w(TAG, "onState failed", t);
}
}
public static void onDestroy() {
try {
if (instance != null) {
instance.release();
instance = null;
}
} catch (Throwable t) {
Log.w(TAG, "onDestroy failed", t);
}
}
// Player thread, after getCurrentPosition
public static void onPosition(long position) {
final long at = SystemClock.elapsedRealtime();
synchronized (LOCK) {
rawPosition = position;
rawAt = at;
}
if (instance == null) return;
if (Looper.myLooper() == Looper.getMainLooper()) {
onSample(position, at);
} else {
MAIN.post(new Runnable() {
@Override
public void run() {
onSample(position, at);
}
});
}
}
// Lyrics worker thread, null body means failed
public static void onLyrics(String key, String title, String body) {
synchronized (LOCK) {
if (body == null && key != null && key.equals(lyricsKey) && lyricsBody != null) {
// Keep a body that had no lines
return;
}
lyricsKey = key;
lyricsTitle = title;
lyricsBody = body;
lyricsFailed = body == null;
}
MAIN.post(new Runnable() {
@Override
public void run() {
if (instance != null) instance.sendLyrics();
}
});
}
private static void onSample(long position, long at) {
PebbleBridge bridge = instance;
if (bridge != null) bridge.consider(position, at, false);
}
private void update(MediaSessionCompat session) {
if (session == null) return;
MediaSessionCompat.Token compat = session.getSessionToken();
Object raw = compat != null ? compat.getToken() : null;
if (!(raw instanceof MediaSession.Token)) return;
if (!raw.equals(token)) {
token = (MediaSession.Token) raw;
controller = new MediaController(context, token);
sendSession();
}
if (!bound && SystemClock.elapsedRealtime() >= retryBindAt) {
Intent intent = new Intent().setComponent(new ComponentName(MANAGER_PACKAGE, RELAY_SERVICE));
try {
bound = context.bindService(intent, this, Context.BIND_AUTO_CREATE);
} catch (Throwable t) {
bound = false;
}
if (!bound) {
// Manager missing, retry in a minute
retryBindAt = SystemClock.elapsedRealtime() + 60000L;
try {
context.unbindService(this);
} catch (Throwable ignored) {
}
Log.i(TAG, "RL Manager relay not available");
}
}
boolean wasPlaying = playing;
playing = readPlaying();
MediaMetadata metadata = controller != null ? controller.getMetadata() : null;
String id = metadata != null ? metadata.getString(MediaMetadata.METADATA_KEY_MEDIA_ID) : null;
boolean trackChanged = id != null && !id.equals(mediaId);
mediaId = id;
if (trackChanged) {
// Next poll sends the new track
sentPosition = -1L;
} else if (wasPlaying != playing) {
// Don't wait for the next poll
sendLatest(wasPlaying);
}
}
// Extrapolates the last reading to now
private void sendLatest(boolean advance) {
long position;
long at;
synchronized (LOCK) {
position = rawPosition;
at = rawAt;
}
if (position < 0) return;
long now = SystemClock.elapsedRealtime();
consider(advance ? position + (now - at) : position, now, true);
}
private boolean readPlaying() {
PlaybackState state = controller != null ? controller.getPlaybackState() : null;
return state != null && state.getState() == PlaybackState.STATE_PLAYING;
}
private void consider(long position, long at, boolean force) {
long predicted = sentPlaying ? sentPosition + (at - sentAt) : sentPosition;
boolean drifted = sentPosition < 0 || Math.abs(position - predicted) > TOLERANCE_MS;
if (!force && !drifted && sentPlaying == playing && at - lastSendAt < KEEPALIVE_MS) return;
if (sendPosition(position, at)) {
sentPosition = position;
sentAt = at;
sentPlaying = playing;
lastSendAt = at;
}
}
private void release() {
if (bound) {
try {
context.unbindService(this);
} catch (Throwable ignored) {
}
}
bound = false;
relay = null;
}
private void sendSession() {
if (relay == null || token == null) return;
Bundle data = new Bundle();
data.putParcelable("token", token);
data.putString("package", context.getPackageName());
send(MSG_SESSION, data);
}
private boolean sendPosition(long position, long at) {
if (relay == null) return false;
Bundle data = new Bundle();
data.putLong("position", position);
data.putLong("elapsedRealtime", at);
data.putBoolean("playing", playing);
if (mediaId != null) data.putString("mediaId", mediaId);
return send(MSG_POSITION, data);
}
private void sendLyrics() {
if (relay == null) return;
Bundle data = new Bundle();
synchronized (LOCK) {
if (lyricsKey == null) return;
data.putString("key", lyricsKey);
data.putString("title", lyricsTitle);
if (lyricsBody != null) data.putString("body", lyricsBody);
data.putBoolean("failed", lyricsFailed);
}
send(MSG_LYRICS, data);
}
private boolean send(int what, Bundle data) {
Messenger target = relay;
if (target == null) return false;
try {
Message message = Message.obtain(null, what);
message.setData(data);
target.send(message);
return true;
} catch (Throwable t) {
relay = null;
return false;
}
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
relay = new Messenger(service);
sendSession();
sentPosition = -1L;
sendLatest(playing);
sendLyrics();
}
@Override
public void onServiceDisconnected(ComponentName name) {
relay = null;
}
@Override
public void onBindingDied(ComponentName name) {
relay = null;
if (bound) {
try {
context.unbindService(this);
} catch (Throwable ignored) {
}
bound = false;
}
}
@Override
public void onNullBinding(ComponentName name) {
onBindingDied(name);
}
}
@@ -0,0 +1,9 @@
package android.support.v4.media.session;
public class MediaSessionCompat {
public Token getSessionToken() { return null; }
public static final class Token {
public Object getToken() { return null; }
}
}