1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 04:57:40 +03:00
Signed-off-by: Anton Afanasyeu <a.afanasieff@gmail.com>
This commit is contained in:
Anton Afanasyeu
2026-05-18 18:26:28 +02:00
parent 8f991ad79b
commit 6d02a9ee82
12 changed files with 503 additions and 40 deletions

View File

@@ -27,7 +27,8 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"> android:exported="true"
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden|density">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
@@ -37,7 +38,7 @@
<activity <activity
android:name=".sender.SenderActivity" android:name=".sender.SenderActivity"
android:exported="false" android:exported="false"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden" /> android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize|keyboardHidden|density" />
<activity <activity
android:name=".receiver.ReceiverActivity" android:name=".receiver.ReceiverActivity"

View File

@@ -7,7 +7,7 @@ import android.content.Context;
public class AndroidCastApplication extends Application { public class AndroidCastApplication extends Application {
@Override @Override
protected void attachBaseContext(Context base) { protected void attachBaseContext(Context base) {
super.attachBaseContext(CastLocaleHelper.attach(base)); super.attachBaseContext(base);
} }
@Override @Override

View File

@@ -49,6 +49,8 @@ public class ReceiverCastService extends Service {
private static final String TAG = "ReceiverCastService"; private static final String TAG = "ReceiverCastService";
public static final String ACTION_START = "com.foxx.androidcast.RECEIVER_START"; public static final String ACTION_START = "com.foxx.androidcast.RECEIVER_START";
public static final String ACTION_STOP = "com.foxx.androidcast.STOP_RECV"; public static final String ACTION_STOP = "com.foxx.androidcast.STOP_RECV";
public static final String ACTION_PLAYBACK_UI = "com.foxx.androidcast.RECV_PLAYBACK_UI";
public static final String EXTRA_PLAYBACK_UI_VISIBLE = "playback_ui_visible";
public static final String EXTRA_PIN = "pin"; public static final String EXTRA_PIN = "pin";
public static final String EXTRA_ALLOW_AUDIO = "allow_audio"; public static final String EXTRA_ALLOW_AUDIO = "allow_audio";
@@ -82,6 +84,8 @@ public class ReceiverCastService extends Service {
private volatile boolean reconfiguring; private volatile boolean reconfiguring;
private volatile boolean awaitingKeyframe = true; private volatile boolean awaitingKeyframe = true;
private boolean playbackLaunched; private boolean playbackLaunched;
/** True while {@link ReceiverPlaybackActivity} is in the foreground (started/stopped). */
private volatile boolean playbackUiVisible;
private int decoderWidth; private int decoderWidth;
private int decoderHeight; private int decoderHeight;
private boolean decoderActive; private boolean decoderActive;
@@ -174,6 +178,13 @@ public class ReceiverCastService extends Service {
stopSelfSafely(); stopSelfSafely();
return START_NOT_STICKY; return START_NOT_STICKY;
} }
if (intent != null && ACTION_PLAYBACK_UI.equals(intent.getAction())) {
playbackUiVisible = intent.getBooleanExtra(EXTRA_PLAYBACK_UI_VISIBLE, false);
if (!playbackUiVisible) {
playbackLaunched = false;
}
return START_STICKY;
}
if (intent != null && ACTION_START.equals(intent.getAction())) { if (intent != null && ACTION_START.equals(intent.getAction())) {
String pin = intent.getStringExtra(EXTRA_PIN); String pin = intent.getStringExtra(EXTRA_PIN);
if (pin == null) { if (pin == null) {
@@ -575,6 +586,9 @@ public class ReceiverCastService extends Service {
/** Opens or refreshes fullscreen UI with robot overlay (listening / waiting for stream). */ /** Opens or refreshes fullscreen UI with robot overlay (listening / waiting for stream). */
private void openPlaybackAwaiting(int messageResId) { private void openPlaybackAwaiting(int messageResId) {
if (!playbackUiVisible) {
return;
}
playbackLaunched = true; playbackLaunched = true;
Intent intent = new Intent(this, ReceiverPlaybackActivity.class); Intent intent = new Intent(this, ReceiverPlaybackActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
@@ -584,12 +598,15 @@ public class ReceiverCastService extends Service {
} }
private void notifyPlaybackDimensions(int width, int height) { private void notifyPlaybackDimensions(int width, int height) {
broadcastStreamStarted(width, height);
if (!playbackUiVisible) {
return;
}
Intent intent = new Intent(this, ReceiverPlaybackActivity.class); Intent intent = new Intent(this, ReceiverPlaybackActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_SINGLE_TOP);
intent.putExtra(ReceiverPlaybackActivity.EXTRA_WIDTH, width); intent.putExtra(ReceiverPlaybackActivity.EXTRA_WIDTH, width);
intent.putExtra(ReceiverPlaybackActivity.EXTRA_HEIGHT, height); intent.putExtra(ReceiverPlaybackActivity.EXTRA_HEIGHT, height);
startActivity(intent); startActivity(intent);
broadcastStreamStarted(width, height);
} }
private void closePlaybackUi() { private void closePlaybackUi() {
@@ -842,70 +859,78 @@ public class ReceiverCastService extends Service {
} }
private void broadcastStatus(String s) { private void broadcastStatus(String s) {
int n = callbacks.beginBroadcast(); broadcastToCallbacks(cb -> {
for (int i = 0; i < n; i++) {
try { try {
callbacks.getBroadcastItem(i).onStatus(s); cb.onStatus(s);
} catch (RemoteException ignored) { } catch (RemoteException ignored) {
} }
} });
callbacks.finishBroadcast();
} }
private void broadcastAuthenticated(String name) { private void broadcastAuthenticated(String name) {
int n = callbacks.beginBroadcast(); broadcastToCallbacks(cb -> {
for (int i = 0; i < n; i++) {
try { try {
callbacks.getBroadcastItem(i).onAuthenticated(name); cb.onAuthenticated(name);
} catch (RemoteException ignored) { } catch (RemoteException ignored) {
} }
} });
callbacks.finishBroadcast();
} }
private void broadcastStreamStarted(int w, int h) { private void broadcastStreamStarted(int w, int h) {
int n = callbacks.beginBroadcast(); broadcastToCallbacks(cb -> {
for (int i = 0; i < n; i++) {
try { try {
callbacks.getBroadcastItem(i).onStreamStarted(w, h); cb.onStreamStarted(w, h);
} catch (RemoteException ignored) { } catch (RemoteException ignored) {
} }
} });
callbacks.finishBroadcast();
} }
private void broadcastVideoRendering() { private void broadcastVideoRendering() {
int n = callbacks.beginBroadcast(); broadcastToCallbacks(cb -> {
for (int i = 0; i < n; i++) {
try { try {
callbacks.getBroadcastItem(i).onVideoRendering(); cb.onVideoRendering();
} catch (RemoteException ignored) { } catch (RemoteException ignored) {
} }
} });
callbacks.finishBroadcast();
} }
private void broadcastStreamIdle() { private void broadcastStreamIdle() {
int n = callbacks.beginBroadcast(); broadcastToCallbacks(cb -> {
for (int i = 0; i < n; i++) {
try { try {
callbacks.getBroadcastItem(i).onStreamIdle(); cb.onStreamIdle();
} catch (RemoteException ignored) { } catch (RemoteException ignored) {
} }
} });
callbacks.finishBroadcast();
} }
private void broadcastDisconnected() { private void broadcastDisconnected() {
broadcastToCallbacks(cb -> {
try {
cb.onDisconnected();
} catch (RemoteException ignored) {
}
});
}
private interface CallbackAction {
void dispatch(ICastStatusCallback callback) throws RemoteException;
}
private void broadcastToCallbacks(CallbackAction action) {
synchronized (callbacks) {
int n = callbacks.beginBroadcast(); int n = callbacks.beginBroadcast();
try {
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
try { try {
callbacks.getBroadcastItem(i).onDisconnected(); action.dispatch(callbacks.getBroadcastItem(i));
} catch (RemoteException ignored) { } catch (RemoteException ignored) {
} }
} }
} finally {
callbacks.finishBroadcast(); callbacks.finishBroadcast();
} }
}
}
private void releaseAll() { private void releaseAll() {
finishSessionStatsAsync(); finishSessionStatsAsync();

View File

@@ -71,6 +71,8 @@ public class ReceiverPlaybackActivity extends DrawerHostActivity {
private ICastReceiverService receiverService; private ICastReceiverService receiverService;
private boolean bound; private boolean bound;
private boolean autoStartListening;
private boolean listeningCancelled;
private final ICastStatusCallback.Stub statusCallback = new ICastStatusCallback.Stub() { private final ICastStatusCallback.Stub statusCallback = new ICastStatusCallback.Stub() {
@Override @Override
@@ -186,7 +188,8 @@ public class ReceiverPlaybackActivity extends DrawerHostActivity {
applyAspectRatio(); applyAspectRatio();
updatePipUi(); updatePipUi();
if (getIntent().getBooleanExtra(EXTRA_AUTO_START_LISTENING, false)) { autoStartListening = getIntent().getBooleanExtra(EXTRA_AUTO_START_LISTENING, false);
if (autoStartListening) {
startListeningService(); startListeningService();
showAwaitingOverlay(R.string.playback_listening); showAwaitingOverlay(R.string.playback_listening);
} else if (getIntent().getBooleanExtra(EXTRA_SHOW_AWAITING, true)) { } else if (getIntent().getBooleanExtra(EXTRA_SHOW_AWAITING, true)) {
@@ -291,11 +294,21 @@ public class ReceiverPlaybackActivity extends DrawerHostActivity {
@Override @Override
protected void onStart() { protected void onStart() {
super.onStart(); super.onStart();
setPlaybackUiVisible(true);
bindService(new Intent(this, ReceiverCastService.class), connection, Context.BIND_AUTO_CREATE); bindService(new Intent(this, ReceiverCastService.class), connection, Context.BIND_AUTO_CREATE);
} }
@Override @Override
protected void onStop() { protected void onStop() {
setPlaybackUiVisible(false);
if (autoStartListening && !listeningCancelled && !castSessionActive && !streamRendering
&& bound && receiverService != null) {
try {
receiverService.stopReceiving();
listeningCancelled = true;
} catch (RemoteException ignored) {
}
}
if (bound && receiverService != null) { if (bound && receiverService != null) {
try { try {
receiverService.unregisterCallback(statusCallback); receiverService.unregisterCallback(statusCallback);
@@ -311,6 +324,13 @@ public class ReceiverPlaybackActivity extends DrawerHostActivity {
super.onStop(); super.onStop();
} }
private void setPlaybackUiVisible(boolean visible) {
Intent intent = new Intent(this, ReceiverCastService.class);
intent.setAction(ReceiverCastService.ACTION_PLAYBACK_UI);
intent.putExtra(ReceiverCastService.EXTRA_PLAYBACK_UI_VISIBLE, visible);
startService(intent);
}
private void onStreamLost() { private void onStreamLost() {
streamRendering = false; streamRendering = false;
showAwaitingOverlay(R.string.playback_stream_lost); showAwaitingOverlay(R.string.playback_stream_lost);

View File

@@ -955,15 +955,20 @@ public class ScreenCastService extends Service implements
} }
private void broadcastStatus(String s) { private void broadcastStatus(String s) {
synchronized (callbacks) {
int n = callbacks.beginBroadcast(); int n = callbacks.beginBroadcast();
try {
for (int i = 0; i < n; i++) { for (int i = 0; i < n; i++) {
try { try {
callbacks.getBroadcastItem(i).onStatus(s); callbacks.getBroadcastItem(i).onStatus(s);
} catch (RemoteException ignored) { } catch (RemoteException ignored) {
} }
} }
} finally {
callbacks.finishBroadcast(); callbacks.finishBroadcast();
} }
}
}
private VideoEncoderSink.Callback buildVideoEncoderCallback(String mime) { private VideoEncoderSink.Callback buildVideoEncoderCallback(String mime) {
return new VideoEncoderSink.Callback() { return new VideoEncoderSink.Callback() {

View File

@@ -44,7 +44,14 @@ public class SettingsStringArrayAdapter extends ArrayAdapter<String> {
@Override @Override
@NonNull @NonNull
public View getView(int position, View convertView, @NonNull ViewGroup parent) { public View getView(int position, View convertView, @NonNull ViewGroup parent) {
TextView tv = (TextView) super.getView(position, convertView, parent); // Spinner may recycle dropdown rows (FrameLayout) into the closed row — never cast blindly.
TextView tv;
if (convertView instanceof TextView) {
tv = (TextView) convertView;
} else {
tv = (TextView) LayoutInflater.from(getContext())
.inflate(R.layout.spinner_item_settings, parent, false);
}
tv.setText(getItem(position)); tv.setText(getItem(position));
tv.setAlpha(isEnabled(position) ? 1f : 0.45f); tv.setAlpha(isEnabled(position) ? 1f : 0.45f);
return tv; return tv;

21
desktop/README.md Normal file
View File

@@ -0,0 +1,21 @@
# Android Cast desktop
## Session Studio (`session-studio/`)
JavaFX desktop app for **session log analysis** (Linux `.jar` or native image later).
```bash
./gradlew :session-studio:run
```
Open `session_*.json` from the device (`files/session_stats/`). New captures include a `samples[]` time series (~2 s) for charts.
### Roadmap (full mobile parity + analytics)
| Phase | Scope | Estimate |
|-------|--------|----------|
| 1 | Session Studio charts (overlay, multi-session, export) | 23 months |
| 2 | Desktop sender (screen capture) + receiver window | 46 months |
| 3 | Transport/codecs/FEC parity with mobile | 34 months |
Packaging: prefer **jpackage AppImage** over raw JAR for Linux users; JAR remains fine for developers.

View File

@@ -0,0 +1,23 @@
plugins {
id 'application'
id 'org.openjfx.javafxplugin' version '0.1.0'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
javafx {
version = '21'
modules = ['javafx.controls']
}
application {
mainClass = 'com.foxx.androidcast.studio.SessionStudioApp'
}
dependencies {
implementation 'org.json:json:20240303'
}

View File

@@ -0,0 +1,181 @@
package com.foxx.androidcast.studio;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import java.util.ArrayList;
import java.util.List;
/** Multi-series line chart with hover tooltip for session samples. */
public final class SessionChartPane extends StackPane {
private static final Color[] SERIES_COLORS = {
Color.CORNFLOWERBLUE, Color.ORANGERED, Color.MEDIUMSEAGREEN, Color.GOLD, Color.VIOLET
};
private final Canvas canvas = new Canvas(800, 480);
private final List<SessionDocument> sessions = new ArrayList<>();
private boolean absoluteTime = true;
private boolean showBitrate = true;
private boolean showRtt;
private boolean showLoss;
private String hoverText = "";
public SessionChartPane() {
getChildren().add(canvas);
canvas.widthProperty().bind(widthProperty());
canvas.heightProperty().bind(heightProperty().subtract(0));
canvas.addEventHandler(MouseEvent.MOUSE_MOVED, this::onMouseMoved);
canvas.addEventHandler(MouseEvent.MOUSE_EXITED, e -> {
hoverText = "";
draw();
});
}
public void setAbsoluteTime(boolean absoluteTime) {
this.absoluteTime = absoluteTime;
}
public void render(List<SessionDocument> docs, boolean bitrate, boolean rtt, boolean loss) {
sessions.clear();
if (docs != null) {
sessions.addAll(docs);
}
showBitrate = bitrate;
showRtt = rtt;
showLoss = loss;
draw();
}
private void onMouseMoved(MouseEvent e) {
if (sessions.isEmpty()) {
return;
}
double pad = 48;
double w = canvas.getWidth() - pad * 2;
double h = canvas.getHeight() - pad * 2;
if (w <= 0 || h <= 0) {
return;
}
long tMax = maxTimeMs();
if (tMax <= 0) {
return;
}
double relX = (e.getX() - pad) / w;
if (relX < 0 || relX > 1) {
hoverText = "";
draw();
return;
}
long tQuery = (long) (relX * tMax);
SessionDocument doc = sessions.get(0);
SessionDocument.SamplePoint nearest = null;
long best = Long.MAX_VALUE;
for (SessionDocument.SamplePoint p : doc.samples) {
long d = Math.abs(p.tMs - tQuery);
if (d < best) {
best = d;
nearest = p;
}
}
if (nearest != null) {
hoverText = String.format("t=%d ms bitrate=%.0f kbps rtt=%d ms loss=%.3f",
nearest.tMs, nearest.videoBitrateKbps, nearest.rttMs, nearest.lossRatio);
}
draw();
}
private long maxTimeMs() {
long max = 1;
for (SessionDocument doc : sessions) {
for (SessionDocument.SamplePoint p : doc.samples) {
max = Math.max(max, p.tMs);
}
if (!absoluteTime && doc.durationMs > max) {
max = doc.durationMs;
}
}
return max;
}
private void draw() {
GraphicsContext g = canvas.getGraphicsContext2D();
double w = canvas.getWidth();
double h = canvas.getHeight();
g.setFill(Color.web("#1a1a1a"));
g.fillRect(0, 0, w, h);
if (sessions.isEmpty()) {
g.setFill(Color.LIGHTGRAY);
g.setFont(Font.font(14));
g.fillText("Open session_*.json (enable Grab session stats on device)", 24, 40);
return;
}
double pad = 48;
double plotW = w - pad * 2;
double plotH = h - pad * 2;
long tMax = maxTimeMs();
g.setStroke(Color.GRAY);
g.strokeRect(pad, pad, plotW, plotH);
int series = 0;
for (int si = 0; si < sessions.size(); si++) {
SessionDocument doc = sessions.get(si);
if (doc.samples.isEmpty()) {
continue;
}
Color color = SERIES_COLORS[si % SERIES_COLORS.length];
if (showBitrate) {
drawSeries(g, doc, pad, plotW, plotH, tMax, color,
p -> p.videoBitrateKbps, 0, 20_000);
series++;
}
if (showRtt) {
drawSeries(g, doc, pad, plotW, plotH, tMax, color.deriveColor(0, 1, 1, 0.6),
p -> p.rttMs, 0, 500);
}
if (showLoss) {
drawSeries(g, doc, pad, plotW, plotH, tMax, color.deriveColor(30, 1, 1, 0.6),
p -> p.lossRatio * 1000, 0, 1000);
}
}
if (!hoverText.isEmpty()) {
g.setFill(Color.rgb(0, 0, 0, 0.75));
g.fillRoundRect(56, 12, Math.min(plotW, 420), 28, 8, 8);
g.setFill(Color.WHITE);
g.setFont(Font.font(12));
g.fillText(hoverText, 64, 30);
}
g.setFill(Color.LIGHTGRAY);
g.setFont(Font.font(11));
g.fillText(absoluteTime ? "X: ms from session start" : "X: relative (multi-session)", pad, h - 12);
}
private interface ValueFn {
double value(SessionDocument.SamplePoint p);
}
private void drawSeries(GraphicsContext g, SessionDocument doc, double pad, double plotW, double plotH,
long tMax, Color color, ValueFn fn, double minY, double maxY) {
if (doc.samples.size() < 2) {
return;
}
g.setStroke(color);
g.beginPath();
boolean first = true;
for (SessionDocument.SamplePoint p : doc.samples) {
double x = pad + (p.tMs / (double) tMax) * plotW;
double v = fn.value(p);
double y = pad + plotH - ((v - minY) / (maxY - minY)) * plotH;
if (first) {
g.moveTo(x, y);
first = false;
} else {
g.lineTo(x, y);
}
}
g.stroke();
}
}

View File

@@ -0,0 +1,69 @@
package com.foxx.androidcast.studio;
import org.json.JSONArray;
import org.json.JSONObject;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/** Parsed session_*.json (aggregate fields + optional time series). */
public final class SessionDocument {
public final String path;
public final String role;
public final String transport;
public final long startedMs;
public final long durationMs;
public final List<SamplePoint> samples = new ArrayList<>();
private SessionDocument(String path, JSONObject root) {
this.path = path;
role = root.optString("role", "?");
transport = root.optString("transport", "?");
startedMs = root.optLong("started_at_epoch_ms", 0L);
durationMs = root.optLong("duration_ms", 0L);
JSONArray arr = root.optJSONArray("samples");
if (arr != null) {
for (int i = 0; i < arr.length(); i++) {
JSONObject o = arr.optJSONObject(i);
if (o != null) {
samples.add(SamplePoint.fromJson(o));
}
}
}
}
public static SessionDocument load(Path file) throws Exception {
String text = Files.readString(file, StandardCharsets.UTF_8);
JSONObject root = new JSONObject(text);
return new SessionDocument(file.getFileName().toString(), root);
}
public String title() {
return path + " (" + role + ", " + transport + ")";
}
public static final class SamplePoint {
public final long tMs;
public final double videoBitrateKbps;
public final int rttMs;
public final double lossRatio;
SamplePoint(long tMs, double videoBitrateKbps, int rttMs, double lossRatio) {
this.tMs = tMs;
this.videoBitrateKbps = videoBitrateKbps;
this.rttMs = rttMs;
this.lossRatio = lossRatio;
}
static SamplePoint fromJson(JSONObject o) {
return new SamplePoint(
o.optLong("t_ms", 0L),
o.optDouble("video_bitrate_kbps", 0.0),
o.optInt("rtt_ms", 0),
o.optDouble("loss_ratio", 0.0));
}
}
}

View File

@@ -0,0 +1,110 @@
package com.foxx.androidcast.studio;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.ListView;
import javafx.scene.control.ToggleGroup;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import java.io.File;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
/**
* Linux desktop session log viewer (foundation for full Android Cast parity on desktop).
* Open one or more session_*.json files; charts use {@code samples[]} when present (schema v1+).
*/
public class SessionStudioApp extends Application {
private final List<SessionDocument> sessions = new ArrayList<>();
private final SessionChartPane chartPane = new SessionChartPane();
private final Label statusLabel = new Label("Open session_*.json files from the device.");
@Override
public void start(Stage stage) {
Button openBtn = new Button("Open sessions…");
CheckBox absTime = new CheckBox("Absolute time (single session)");
absTime.setSelected(true);
absTime.setOnAction(e -> chartPane.setAbsoluteTime(absTime.isSelected()));
ToggleGroup metrics = new ToggleGroup();
CheckBox metricBitrate = metricToggle(metrics, "Video bitrate", true);
CheckBox metricRtt = metricToggle(metrics, "RTT", false);
CheckBox metricLoss = metricToggle(metrics, "Loss ratio", false);
Runnable refresh = () -> chartPane.render(sessions,
metricBitrate.isSelected(), metricRtt.isSelected(), metricLoss.isSelected());
metricBitrate.setOnAction(e -> refresh.run());
metricRtt.setOnAction(e -> refresh.run());
metricLoss.setOnAction(e -> refresh.run());
absTime.setOnAction(e -> {
chartPane.setAbsoluteTime(absTime.isSelected());
refresh.run();
});
ListView<String> sessionList = new ListView<>();
sessionList.setPrefHeight(120);
sessionList.getSelectionModel().selectedIndexProperty().addListener((o, a, b) -> refresh.run());
HBox toolbar = new HBox(12, openBtn, absTime, metricBitrate, metricRtt, metricLoss);
toolbar.setStyle("-fx-padding: 8;");
VBox top = new VBox(8, toolbar, sessionList, statusLabel);
top.setStyle("-fx-padding: 8;");
BorderPane root = new BorderPane();
root.setTop(top);
root.setCenter(chartPane);
openBtn.setOnAction(e -> {
openFiles(stage);
sessionList.getItems().clear();
for (SessionDocument doc : sessions) {
sessionList.getItems().add(doc.title());
}
refresh.run();
});
stage.setTitle("Android Cast Session Studio");
stage.setScene(new Scene(root, 960, 640));
stage.show();
}
private static CheckBox metricToggle(ToggleGroup group, String text, boolean selected) {
CheckBox box = new CheckBox(text);
box.setSelected(selected);
return box;
}
private void openFiles(Stage stage) {
FileChooser chooser = new FileChooser();
chooser.setTitle("Session JSON files");
chooser.getExtensionFilters().add(
new FileChooser.ExtensionFilter("Session JSON", "*.json"));
List<File> files = chooser.showOpenMultipleDialog(stage);
if (files == null || files.isEmpty()) {
return;
}
sessions.clear();
for (File f : files) {
try {
sessions.add(SessionDocument.load(Path.of(f.getAbsolutePath())));
} catch (Exception ex) {
statusLabel.setText("Failed: " + f.getName() + "" + ex.getMessage());
return;
}
}
statusLabel.setText("Loaded " + sessions.size() + " session file(s).");
}
public static void main(String[] args) {
launch(args);
}
}

View File

@@ -124,6 +124,7 @@ for ARCH_ABI in ${ARCH_ABIS}; do
done done
echo "==> Building debug APK (JAVA_HOME=$JAVA_HOME)" echo "==> Building debug APK (JAVA_HOME=$JAVA_HOME)"
./gradlew --stop
./gradlew clean assembleDebug ./gradlew clean assembleDebug
if [[ "$RUN_UNIT_TESTS" == "1" ]]; then if [[ "$RUN_UNIT_TESTS" == "1" ]]; then