1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 07:20:00 +03:00
This commit is contained in:
Anton Afanasyeu
2026-06-08 22:37:05 +02:00
parent 1e08efb9a4
commit df79962dee
12 changed files with 653 additions and 36 deletions

View File

@@ -5,8 +5,11 @@ public final class BackendEndpoints {
public static final String APPS_HOST = "apps.f0xx.org";
public static final String CRASHES_BASE =
"https://" + APPS_HOST + "/app/androidcast_project/crashes";
public static final String GRAPHS_BASE =
"https://" + APPS_HOST + "/app/androidcast_project/graphs";
public static final String HEARTBEAT_URL = CRASHES_BASE + "/api/heartbeat.php";
public static final String UPLOAD_URL = CRASHES_BASE + "/api/upload.php";
public static final String GRAPH_UPLOAD_URL = GRAPHS_BASE + "/api/graph_upload.php";
private BackendEndpoints() {}
@@ -26,4 +29,16 @@ public final class BackendEndpoints {
}
return trimmed;
}
/** Maps a crashes upload URL to the graphs session ingest endpoint. */
public static String graphUploadUrlFromCrashesUrl(String crashesUrl) {
if (crashesUrl == null || crashesUrl.isEmpty()) {
return GRAPH_UPLOAD_URL;
}
String trimmed = normalizeCrashesUrl(crashesUrl.trim());
if (trimmed.contains("/crashes/")) {
return trimmed.replace("/crashes/", "/graphs/").replace("/api/upload.php", "/api/graph_upload.php");
}
return GRAPH_UPLOAD_URL;
}
}

View File

@@ -0,0 +1,106 @@
package com.foxx.androidcast.stats;
import android.content.Context;
import android.os.Build;
import com.foxx.androidcast.AppPreferences;
import com.foxx.androidcast.BuildConfig;
import com.foxx.androidcast.DeviceInfo;
import com.foxx.androidcast.crash.CrashSettingsStore;
import com.foxx.androidcast.crash.CrashUploadClient;
import com.foxx.androidcast.network.BackendEndpoints;
import org.json.JSONObject;
import java.util.Locale;
import java.util.UUID;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/** POST anonymous session aggregates to the graphs ingest API ({@code graph_upload.php}). */
public final class GraphSessionUploader {
private static final ExecutorService EXEC = Executors.newSingleThreadExecutor(r -> {
Thread t = new Thread(r, "graph-session-upload");
t.setDaemon(true);
return t;
});
private GraphSessionUploader() {}
public static void uploadAsync(Context context, JSONObject sessionRoot) {
if (context == null || sessionRoot == null || !AppPreferences.isGrabSessionStats(context)) {
return;
}
final Context app = context.getApplicationContext();
EXEC.execute(() -> upload(app, sessionRoot));
}
static boolean upload(Context context, JSONObject sessionRoot) {
try {
JSONObject payload = toGraphPayload(context, sessionRoot);
if (payload == null) {
return false;
}
String url = BackendEndpoints.graphUploadUrlFromCrashesUrl(
CrashSettingsStore.load(context).uploadUrl);
return CrashUploadClient.upload(url, payload);
} catch (Exception ignored) {
return false;
}
}
static JSONObject toGraphPayload(Context context, JSONObject root) throws Exception {
long started = root.optLong("started_at_epoch_ms", 0L);
long ended = root.optLong("ended_at_epoch_ms", 0L);
if (started <= 0 || ended <= 0 || ended < started) {
return null;
}
String direction = root.optString("direction", "send");
String ingestDir = "recv".equalsIgnoreCase(direction) ? "receiver" : "sender";
String transport = root.optString("transport", "wifi").toLowerCase(Locale.US);
if (transport.isEmpty()) {
transport = "wifi";
}
JSONObject payload = new JSONObject();
payload.put("schema_version", 1);
payload.put("ingest_kind", "graph_session");
payload.put("session_id", sessionId(started, direction));
payload.put("device_id", DeviceInfo.getDeviceUuid(context));
payload.put("app_version", BuildConfig.VERSION_NAME);
payload.put("sdk_int", Build.VERSION.SDK_INT);
payload.put("started_at_epoch_ms", started);
payload.put("ended_at_epoch_ms", ended);
payload.put("duration_s", Math.max(0, (int) ((ended - started) / 1000L)));
payload.put("direction", ingestDir);
payload.put("transport", transport);
payload.put("install_source", "direct");
payload.put("ntp_source", AppPreferences.isDevBackendNtpSyncEnabled(context) ? "backend" : "device");
if (root.has("avg_video_bitrate_kbps")) {
payload.put("avg_kbps", Math.max(0, (int) Math.round(root.optDouble("avg_video_bitrate_kbps", 0))));
}
if (root.has("avg_loss_ratio")) {
payload.put("packet_loss_pct", Math.max(0.0, root.optDouble("avg_loss_ratio", 0) * 100.0));
}
JSONObject globals = root.optJSONObject("globals");
String reason = globals != null ? globals.optString("reason", "") : "";
payload.put("completed", reason.isEmpty() || !"crash".equalsIgnoreCase(reason));
JSONObject meta = new JSONObject();
meta.put("capture_mode", root.optString("capture_mode", ""));
meta.put("video_codec", root.optString("video_codec", ""));
meta.put("role", root.optString("role", ""));
if (root.has("avg_encode_fps")) {
meta.put("avg_encode_fps", root.optDouble("avg_encode_fps", 0));
}
payload.put("meta", meta);
return payload;
}
private static String sessionId(long startedMs, String direction) {
String seed = startedMs + ":" + direction + ":" + UUID.randomUUID();
return "mob-" + Integer.toHexString(seed.hashCode()) + "-" + startedMs;
}
}

View File

@@ -274,6 +274,7 @@ public final class SessionStatsRecorder {
try {
JSONObject root = buildSessionJson(endedMs, reason);
SessionStatsStore.saveSession(context, startedMs, direction, root);
GraphSessionUploader.uploadAsync(context, root);
} catch (Exception ignored) {
}
}

View File

@@ -17,4 +17,10 @@ public class BackendEndpointsTest {
String legacy = "https://f0xx.org/app/androidcast_project/crashes/api/upload.php";
assertEquals(BackendEndpoints.UPLOAD_URL, BackendEndpoints.normalizeCrashesUrl(legacy));
}
@Test
public void graphUploadUrlFromCrashesUrl() {
assertEquals(BackendEndpoints.GRAPH_UPLOAD_URL,
BackendEndpoints.graphUploadUrlFromCrashesUrl(BackendEndpoints.UPLOAD_URL));
}
}