mirror of
git://f0xx.org/android_cast
synced 2026-07-29 07:20:00 +03:00
be sync
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user