1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 05:17:39 +03:00
This commit is contained in:
Anton Afanasyeu
2026-05-21 12:49:19 +02:00
parent 55d588afa4
commit 176988a62d
5 changed files with 104 additions and 38 deletions

View File

@@ -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);
}
}

View File

@@ -24,6 +24,7 @@ public final class CastActivityTracker {
public static void onResume(Activity activity) {
if (activity != null) {
lastActivityClass = activity.getClass();
CastTrayNotifier.syncWhenForeground(activity);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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());
}
}

View File

@@ -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();