From 176988a62d10a916fbf0479a18ddcba0e2b7ee6d Mon Sep 17 00:00:00 2001 From: Anton Afanasyeu Date: Thu, 21 May 2026 12:49:19 +0200 Subject: [PATCH] snap --- .../androidcast/AndroidCastApplication.java | 3 - .../foxx/androidcast/CastActivityTracker.java | 1 + .../foxx/androidcast/CastTrayNotifier.java | 32 +++++-- .../foxx/androidcast/crash/CrashReporter.java | 17 ++-- .../crash/CrashWatcherService.java | 89 ++++++++++++++----- 5 files changed, 104 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/com/foxx/androidcast/AndroidCastApplication.java b/app/src/main/java/com/foxx/androidcast/AndroidCastApplication.java index 2cc057e..f61693f 100644 --- a/app/src/main/java/com/foxx/androidcast/AndroidCastApplication.java +++ b/app/src/main/java/com/foxx/androidcast/AndroidCastApplication.java @@ -15,7 +15,6 @@ import android.app.Application; import android.content.Context; import com.foxx.androidcast.crash.CrashReporter; -import com.foxx.androidcast.crash.CrashWatcherService; import com.foxx.androidcast.media.CodecPriorityCatalog; /** Applies saved theme and locale at process start. */ @@ -31,8 +30,6 @@ public class AndroidCastApplication extends Application { CodecPriorityCatalog.init(this); CastLocaleHelper.applyStoredLocale(this); CastThemeHelper.applyNightMode(this); - CastTrayNotifier.syncOnAppStart(this); CrashReporter.install(this); - CrashWatcherService.scheduleUploadSweep(this); } } diff --git a/app/src/main/java/com/foxx/androidcast/CastActivityTracker.java b/app/src/main/java/com/foxx/androidcast/CastActivityTracker.java index 6c1a626..4e33480 100644 --- a/app/src/main/java/com/foxx/androidcast/CastActivityTracker.java +++ b/app/src/main/java/com/foxx/androidcast/CastActivityTracker.java @@ -24,6 +24,7 @@ public final class CastActivityTracker { public static void onResume(Activity activity) { if (activity != null) { lastActivityClass = activity.getClass(); + CastTrayNotifier.syncWhenForeground(activity); } } diff --git a/app/src/main/java/com/foxx/androidcast/CastTrayNotifier.java b/app/src/main/java/com/foxx/androidcast/CastTrayNotifier.java index 82a0e36..f399171 100644 --- a/app/src/main/java/com/foxx/androidcast/CastTrayNotifier.java +++ b/app/src/main/java/com/foxx/androidcast/CastTrayNotifier.java @@ -11,6 +11,7 @@ package com.foxx.androidcast; * - Cursor Agent (project assistant) * Digest: SHA256 292d7f278b9726120e113e803105641b313de93e2efa129dd3af428d1aa68033 **********************************************************************/ +import android.app.ForegroundServiceStartNotAllowedException; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -19,6 +20,7 @@ import android.app.Service; import android.content.Context; import android.content.Intent; import android.os.Build; +import android.util.Log; import androidx.core.app.NotificationCompat; @@ -26,6 +28,7 @@ import androidx.core.app.NotificationCompat; * Persistent status-bar icon while cast services run (when enabled in settings). */ public final class CastTrayNotifier { + private static final String TAG = "CastTrayNotifier"; public static final String CHANNEL_TRAY = "cast_tray"; public static final int ID_TRAY = 3; @@ -41,9 +44,14 @@ public final class CastTrayNotifier { ensureTrayService(app); } - /** Apply saved preference at process start (idle tray when enabled). */ - public static void syncOnAppStart(Context context) { - onPreferenceChanged(context); + /** + * Start idle tray when an activity is foreground (API 31+ blocks FGS from + * {@link android.app.Application#onCreate()}). + */ + public static void syncWhenForeground(Context context) { + if (AppPreferences.isShowTrayIcon(context)) { + ensureTrayService(context); + } } /** User toggled the preference — show or hide immediately. */ @@ -63,10 +71,20 @@ public final class CastTrayNotifier { return; } Intent intent = new Intent(app, TrayForegroundService.class); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - app.startForegroundService(intent); - } else { - app.startService(intent); + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + app.startForegroundService(intent); + } else { + app.startService(intent); + } + } catch (ForegroundServiceStartNotAllowedException e) { + Log.w(TAG, "tray FGS deferred (not foreground): " + e.getMessage()); + } catch (IllegalStateException e) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Log.w(TAG, "tray FGS deferred: " + e.getMessage()); + } else { + throw e; + } } } diff --git a/app/src/main/java/com/foxx/androidcast/crash/CrashReporter.java b/app/src/main/java/com/foxx/androidcast/crash/CrashReporter.java index 8f6aa35..afc8087 100644 --- a/app/src/main/java/com/foxx/androidcast/crash/CrashReporter.java +++ b/app/src/main/java/com/foxx/androidcast/crash/CrashReporter.java @@ -11,8 +11,8 @@ package com.foxx.androidcast.crash; * - Cursor Agent (project assistant) * Digest: SHA256 5254fee83476d4a80db718169cbc13b61b18697148a8625baab8df0833b1d78f **********************************************************************/ +import android.app.Application; import android.content.Context; -import android.content.Intent; import android.util.Log; import com.foxx.androidcast.stats.SessionStatsContext; @@ -29,6 +29,9 @@ public final class CrashReporter { public static void install(Context context) { Context app = context.getApplicationContext(); + if (!isMainProcess(app)) { + return; + } if (app instanceof android.app.Application) { CrashRuntimeContext.init((android.app.Application) app); } @@ -89,12 +92,10 @@ public final class CrashReporter { } private static void wakeWatcher(Context context) { - try { - Intent i = new Intent(context, CrashWatcherService.class); - i.setAction(CrashWatcherService.ACTION_UPLOAD_PENDING); - context.startService(i); - } catch (Exception e) { - Log.w(TAG, "wake watcher failed: " + e.getMessage()); - } + CrashWatcherService.requestImmediateSweep(context, true); + } + + private static boolean isMainProcess(Context context) { + return context.getPackageName().equals(Application.getProcessName()); } } diff --git a/app/src/main/java/com/foxx/androidcast/crash/CrashWatcherService.java b/app/src/main/java/com/foxx/androidcast/crash/CrashWatcherService.java index 20175e4..6949c9a 100644 --- a/app/src/main/java/com/foxx/androidcast/crash/CrashWatcherService.java +++ b/app/src/main/java/com/foxx/androidcast/crash/CrashWatcherService.java @@ -18,6 +18,7 @@ import android.content.Context; import android.content.Intent; import android.os.Build; import android.os.IBinder; +import android.os.SystemClock; import android.util.Log; import com.foxx.androidcast.util.IoUtils; @@ -28,6 +29,7 @@ import java.io.File; import java.io.FileInputStream; import java.nio.charset.StandardCharsets; import java.util.List; +import java.util.concurrent.atomic.AtomicBoolean; /** * Standalone process ({@code :crashwatcher}) — uploads pending crash JSON even if the main @@ -37,30 +39,31 @@ public final class CrashWatcherService extends Service { private static final String TAG = "CrashWatcher"; public static final String ACTION_UPLOAD_PENDING = "com.foxx.androidcast.crash.UPLOAD_PENDING"; private static final int ALARM_REQUEST = 4102; + private static final long IMMEDIATE_DEBOUNCE_MS = 30_000L; + private static final AtomicBoolean SWEEP_RUNNING = new AtomicBoolean(false); + private static volatile long lastImmediateRequestElapsedMs; + + /** One immediate sweep (debounced) plus the next periodic alarm. */ public static void scheduleUploadSweep(Context context) { - try { - Intent i = new Intent(context, CrashWatcherService.class); - i.setAction(ACTION_UPLOAD_PENDING); - context.startService(i); - } catch (Exception e) { - Log.w(TAG, "scheduleUploadSweep: " + e.getMessage()); - } - CrashSettings settings = CrashSettingsStore.load(context); - if (!settings.enabled) { - return; - } - AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + Context app = context.getApplicationContext(); + schedulePeriodicAlarm(app); + requestImmediateSweep(app); + } + + /** Alarm-only periodic upload; does not start the service now. */ + public static void schedulePeriodicAlarm(Context context) { + Context app = context.getApplicationContext(); + CrashSettings settings = CrashSettingsStore.load(app); + AlarmManager alarm = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE); if (alarm == null) { return; } - Intent periodic = new Intent(context, CrashWatcherService.class); - periodic.setAction(ACTION_UPLOAD_PENDING); - PendingIntent pi = PendingIntent.getService( - context, - ALARM_REQUEST, - periodic, - PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + PendingIntent pi = pendingAlarmIntent(app); + if (!settings.enabled) { + alarm.cancel(pi); + return; + } long trigger = System.currentTimeMillis() + settings.uploadIntervalMs; if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { alarm.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pi); @@ -69,13 +72,59 @@ public final class CrashWatcherService extends Service { } } + public static void requestImmediateSweep(Context context) { + requestImmediateSweep(context, false); + } + + /** Start a sweep now if one is not already running (coalesces duplicate requests). */ + public static void requestImmediateSweep(Context context, boolean bypassDebounce) { + Context app = context.getApplicationContext(); + if (!CrashSettingsStore.load(app).enabled) { + return; + } + if (!bypassDebounce) { + long now = SystemClock.elapsedRealtime(); + synchronized (CrashWatcherService.class) { + if (now - lastImmediateRequestElapsedMs < IMMEDIATE_DEBOUNCE_MS) { + return; + } + lastImmediateRequestElapsedMs = now; + } + } + if (SWEEP_RUNNING.get()) { + return; + } + try { + Intent i = new Intent(app, CrashWatcherService.class); + i.setAction(ACTION_UPLOAD_PENDING); + app.startService(i); + } catch (Exception e) { + Log.w(TAG, "requestImmediateSweep: " + e.getMessage()); + } + } + + private static PendingIntent pendingAlarmIntent(Context app) { + Intent periodic = new Intent(app, CrashWatcherService.class); + periodic.setAction(ACTION_UPLOAD_PENDING); + return PendingIntent.getService( + app, + ALARM_REQUEST, + periodic, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE); + } + @Override public int onStartCommand(Intent intent, int flags, int startId) { + if (!SWEEP_RUNNING.compareAndSet(false, true)) { + stopSelf(startId); + return START_NOT_STICKY; + } new Thread(() -> { try { runSweep(); } finally { - scheduleUploadSweep(getApplicationContext()); + SWEEP_RUNNING.set(false); + schedulePeriodicAlarm(getApplicationContext()); stopSelf(startId); } }, "CrashWatcherSweep").start();