mirror of
git://f0xx.org/android_cast
synced 2026-07-29 06:39:09 +03:00
snap
This commit is contained in:
@@ -25,7 +25,6 @@ import org.json.JSONObject;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Locale;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Builds anonymous crash report JSON (schema v1). */
|
||||
public final class CrashReportBuilder {
|
||||
@@ -85,19 +84,34 @@ public final class CrashReportBuilder {
|
||||
JSONObject root = new JSONObject();
|
||||
try {
|
||||
long now = System.currentTimeMillis();
|
||||
String stamp = CrashReportStore.formatTimestamp(now);
|
||||
root.put("schema_version", 1);
|
||||
root.put("report_id", UUID.randomUUID().toString());
|
||||
root.put("report_id", stamp);
|
||||
root.put("generated_at_epoch_ms", now);
|
||||
root.put("crash_type", crashType);
|
||||
root.put("process", "main");
|
||||
root.put("scenario", CrashRuntimeContext.scenarioLabel());
|
||||
root.put("process", currentProcessName());
|
||||
root.put("device", deviceJson());
|
||||
root.put("app", appJson(context));
|
||||
root.put("build", buildJson());
|
||||
root.put("runtime_context", CrashRuntimeContext.snapshot(context));
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return root;
|
||||
}
|
||||
|
||||
private static String currentProcessName() {
|
||||
try {
|
||||
Class<?> at = Class.forName("android.app.ActivityThread");
|
||||
Object proc = at.getMethod("currentProcessName").invoke(null);
|
||||
if (proc instanceof String && !((String) proc).isEmpty()) {
|
||||
return (String) proc;
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return "main";
|
||||
}
|
||||
|
||||
private static JSONObject deviceJson() throws Exception {
|
||||
JSONObject d = new JSONObject();
|
||||
d.put("manufacturer", Build.MANUFACTURER);
|
||||
|
||||
@@ -17,16 +17,22 @@ import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Pending/uploaded crash JSON in app-private storage (shared across processes). */
|
||||
public final class CrashReportStore {
|
||||
private static final String ROOT = "crash_reports";
|
||||
private static final String PENDING = "pending";
|
||||
private static final String UPLOADED = "uploaded";
|
||||
/** Same stamp as {@code session_send_yyyyMMdd_HHmmss.json} / {@code session_recv_*.json}. */
|
||||
public static final String PREFIX = "crash_";
|
||||
private static final String SUFFIX = ".json";
|
||||
|
||||
private CrashReportStore() {}
|
||||
|
||||
@@ -46,8 +52,21 @@ public final class CrashReportStore {
|
||||
return dir;
|
||||
}
|
||||
|
||||
public static File newPendingFile(File filesDir, String reportId) {
|
||||
return new File(pendingDir(filesDir), "crash_" + reportId + ".json");
|
||||
public static String formatTimestamp(long epochMs) {
|
||||
return new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.US).format(new Date(epochMs));
|
||||
}
|
||||
|
||||
/** {@code crash_yyyyMMdd_HHmmss.json} (suffix {@code _N} if the second collides). */
|
||||
public static File newPendingFile(File filesDir, long generatedAtMs) {
|
||||
String base = PREFIX + formatTimestamp(generatedAtMs);
|
||||
File dir = pendingDir(filesDir);
|
||||
File candidate = new File(dir, base + SUFFIX);
|
||||
int n = 1;
|
||||
while (candidate.exists()) {
|
||||
candidate = new File(dir, base + "_" + n + SUFFIX);
|
||||
n++;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
public static void writeJson(File file, JSONObject json) throws Exception {
|
||||
@@ -71,7 +90,7 @@ public final class CrashReportStore {
|
||||
|
||||
public static List<File> listPending(File filesDir) {
|
||||
File dir = pendingDir(filesDir);
|
||||
File[] files = dir.listFiles((d, n) -> n.endsWith(".json"));
|
||||
File[] files = dir.listFiles((d, n) -> n.startsWith(PREFIX) && n.endsWith(SUFFIX));
|
||||
if (files == null || files.length == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ public final class CrashReporter {
|
||||
|
||||
public static void install(Context context) {
|
||||
Context app = context.getApplicationContext();
|
||||
if (app instanceof android.app.Application) {
|
||||
CrashRuntimeContext.init((android.app.Application) app);
|
||||
}
|
||||
CrashNativeBridge.init(app);
|
||||
final Thread.UncaughtExceptionHandler previous =
|
||||
Thread.getDefaultUncaughtExceptionHandler();
|
||||
@@ -70,8 +73,12 @@ public final class CrashReporter {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
String id = report.optString("report_id", String.valueOf(System.currentTimeMillis()));
|
||||
File file = CrashReportStore.newPendingFile(context.getFilesDir(), id);
|
||||
long generatedAt = report.optLong("generated_at_epoch_ms", System.currentTimeMillis());
|
||||
File file = CrashReportStore.newPendingFile(context.getFilesDir(), generatedAt);
|
||||
report.put("report_id", file.getName().substring(
|
||||
CrashReportStore.PREFIX.length(),
|
||||
file.getName().length() - ".json".length()));
|
||||
report.put("report_file", file.getName());
|
||||
CrashReportStore.writeJson(file, report);
|
||||
CrashReportStore.pruneOldestPending(context.getFilesDir(),
|
||||
CrashSettingsStore.load(context).maxPendingFiles);
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
package com.foxx.androidcast.crash;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.foxx.androidcast.CastActiveState;
|
||||
import com.foxx.androidcast.stats.SessionStatsContext;
|
||||
import com.foxx.androidcast.stats.SessionStatsRecorder;
|
||||
import com.foxx.androidcast.stats.SessionStatsStore;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
/**
|
||||
* App-wide runtime snapshot for crash reports (cast idle, UI foreground, last session file).
|
||||
* Updated from {@link AndroidCastApplication} lifecycle and cast services.
|
||||
*/
|
||||
public final class CrashRuntimeContext {
|
||||
private static final AtomicLong APP_STARTED_MS = new AtomicLong(0L);
|
||||
private static final AtomicReference<String> LAST_ACTIVITY = new AtomicReference<>("");
|
||||
private CrashRuntimeContext() {}
|
||||
|
||||
public static void init(Application app) {
|
||||
APP_STARTED_MS.compareAndSet(0L, System.currentTimeMillis());
|
||||
app.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
|
||||
@Override
|
||||
public void onActivityResumed(Activity activity) {
|
||||
LAST_ACTIVITY.set(activity.getClass().getSimpleName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityCreated(Activity a, Bundle b) {}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(Activity a) {}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(Activity a) {}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(Activity a) {}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(Activity a, Bundle b) {}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(Activity a) {}
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isCastSessionActive() {
|
||||
return CastActiveState.isSenderCasting() || CastActiveState.isReceiverListening();
|
||||
}
|
||||
|
||||
/** JSON attached to every crash report (with or without an active cast stats recorder). */
|
||||
public static JSONObject snapshot(Context context) {
|
||||
JSONObject runtime = new JSONObject();
|
||||
try {
|
||||
long started = APP_STARTED_MS.get();
|
||||
if (started <= 0L) {
|
||||
started = System.currentTimeMillis();
|
||||
}
|
||||
runtime.put("app_started_epoch_ms", started);
|
||||
runtime.put("last_activity", LAST_ACTIVITY.get());
|
||||
runtime.put("sender_cast_active", CastActiveState.isSenderCasting());
|
||||
runtime.put("receiver_cast_active", CastActiveState.isReceiverListening());
|
||||
runtime.put("cast_active", isCastSessionActive());
|
||||
|
||||
SessionStatsRecorder active = SessionStatsContext.get();
|
||||
runtime.put("session_stats_active", active != null);
|
||||
if (active != null) {
|
||||
runtime.put("session_direction", active.getDirection());
|
||||
}
|
||||
|
||||
String lastSession = lastSessionFileName(context);
|
||||
if (!lastSession.isEmpty()) {
|
||||
runtime.put("last_session_file", lastSession);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return runtime;
|
||||
}
|
||||
|
||||
public static String scenarioLabel() {
|
||||
if (SessionStatsContext.get() != null || isCastSessionActive()) {
|
||||
return "cast_session";
|
||||
}
|
||||
return "app_runtime";
|
||||
}
|
||||
|
||||
private static String lastSessionFileName(Context context) {
|
||||
try {
|
||||
List<File> files = SessionStatsStore.listSessionFiles(context);
|
||||
if (files.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
return files.get(0).getName();
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,8 +127,16 @@ public final class CrashWatcherService extends Service {
|
||||
signal = text.substring(sigIdx + 7, end > sigIdx ? end : text.length()).trim();
|
||||
}
|
||||
JSONObject report = CrashReportBuilder.buildNativeCrash(this, signal, text);
|
||||
String id = report.optString("report_id", stub.getName());
|
||||
CrashReportStore.writeJson(CrashReportStore.newPendingFile(filesDir, id), report);
|
||||
long generatedAt = report.optLong("generated_at_epoch_ms", stub.lastModified());
|
||||
if (generatedAt <= 0L) {
|
||||
generatedAt = System.currentTimeMillis();
|
||||
}
|
||||
File pending = CrashReportStore.newPendingFile(filesDir, generatedAt);
|
||||
report.put("report_id", pending.getName().substring(
|
||||
CrashReportStore.PREFIX.length(),
|
||||
pending.getName().length() - ".json".length()));
|
||||
report.put("report_file", pending.getName());
|
||||
CrashReportStore.writeJson(pending, report);
|
||||
stub.delete();
|
||||
} catch (Exception e) {
|
||||
Log.w(TAG, "ingest native stub failed: " + stub.getName());
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.foxx.androidcast.crash;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.rules.TemporaryFolder;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class CrashReportStoreTest {
|
||||
@org.junit.Rule
|
||||
public TemporaryFolder tmp = new TemporaryFolder();
|
||||
|
||||
@Test
|
||||
public void pendingFile_usesSessionStyleTimestamp() throws Exception {
|
||||
File files = tmp.newFolder("files");
|
||||
long ms = 1_704_067_200_000L; // 2024-01-01 00:00:00 UTC — pattern only
|
||||
File f = CrashReportStore.newPendingFile(files, ms);
|
||||
assertTrue(f.getName().matches("crash_\\d{8}_\\d{6}\\.json"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void formatTimestamp_matchesSessionStatsPattern() {
|
||||
String stamp = CrashReportStore.formatTimestamp(171_000_000_0000L);
|
||||
assertEquals(15, stamp.length());
|
||||
assertTrue(stamp.contains("_"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user