1
0
mirror of git://f0xx.org/android_cast synced 2026-07-29 06:39:09 +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 android.content.Context;
import com.foxx.androidcast.crash.CrashReporter; import com.foxx.androidcast.crash.CrashReporter;
import com.foxx.androidcast.crash.CrashWatcherService;
import com.foxx.androidcast.media.CodecPriorityCatalog; import com.foxx.androidcast.media.CodecPriorityCatalog;
/** Applies saved theme and locale at process start. */ /** Applies saved theme and locale at process start. */
@@ -31,8 +30,6 @@ public class AndroidCastApplication extends Application {
CodecPriorityCatalog.init(this); CodecPriorityCatalog.init(this);
CastLocaleHelper.applyStoredLocale(this); CastLocaleHelper.applyStoredLocale(this);
CastThemeHelper.applyNightMode(this); CastThemeHelper.applyNightMode(this);
CastTrayNotifier.syncOnAppStart(this);
CrashReporter.install(this); CrashReporter.install(this);
CrashWatcherService.scheduleUploadSweep(this);
} }
} }

View File

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

View File

@@ -11,6 +11,7 @@ package com.foxx.androidcast;
* - Cursor Agent (project assistant) * - Cursor Agent (project assistant)
* Digest: SHA256 292d7f278b9726120e113e803105641b313de93e2efa129dd3af428d1aa68033 * Digest: SHA256 292d7f278b9726120e113e803105641b313de93e2efa129dd3af428d1aa68033
**********************************************************************/ **********************************************************************/
import android.app.ForegroundServiceStartNotAllowedException;
import android.app.Notification; import android.app.Notification;
import android.app.NotificationChannel; import android.app.NotificationChannel;
import android.app.NotificationManager; import android.app.NotificationManager;
@@ -19,6 +20,7 @@ import android.app.Service;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Build; import android.os.Build;
import android.util.Log;
import androidx.core.app.NotificationCompat; 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). * Persistent status-bar icon while cast services run (when enabled in settings).
*/ */
public final class CastTrayNotifier { public final class CastTrayNotifier {
private static final String TAG = "CastTrayNotifier";
public static final String CHANNEL_TRAY = "cast_tray"; public static final String CHANNEL_TRAY = "cast_tray";
public static final int ID_TRAY = 3; public static final int ID_TRAY = 3;
@@ -41,9 +44,14 @@ public final class CastTrayNotifier {
ensureTrayService(app); ensureTrayService(app);
} }
/** Apply saved preference at process start (idle tray when enabled). */ /**
public static void syncOnAppStart(Context context) { * Start idle tray when an activity is foreground (API 31+ blocks FGS from
onPreferenceChanged(context); * {@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. */ /** User toggled the preference — show or hide immediately. */
@@ -63,11 +71,21 @@ public final class CastTrayNotifier {
return; return;
} }
Intent intent = new Intent(app, TrayForegroundService.class); Intent intent = new Intent(app, TrayForegroundService.class);
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
app.startForegroundService(intent); app.startForegroundService(intent);
} else { } else {
app.startService(intent); 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;
}
}
} }
public static void stopTrayService(Context context) { public static void stopTrayService(Context context) {

View File

@@ -11,8 +11,8 @@ package com.foxx.androidcast.crash;
* - Cursor Agent (project assistant) * - Cursor Agent (project assistant)
* Digest: SHA256 5254fee83476d4a80db718169cbc13b61b18697148a8625baab8df0833b1d78f * Digest: SHA256 5254fee83476d4a80db718169cbc13b61b18697148a8625baab8df0833b1d78f
**********************************************************************/ **********************************************************************/
import android.app.Application;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.util.Log; import android.util.Log;
import com.foxx.androidcast.stats.SessionStatsContext; import com.foxx.androidcast.stats.SessionStatsContext;
@@ -29,6 +29,9 @@ public final class CrashReporter {
public static void install(Context context) { public static void install(Context context) {
Context app = context.getApplicationContext(); Context app = context.getApplicationContext();
if (!isMainProcess(app)) {
return;
}
if (app instanceof android.app.Application) { if (app instanceof android.app.Application) {
CrashRuntimeContext.init((android.app.Application) app); CrashRuntimeContext.init((android.app.Application) app);
} }
@@ -89,12 +92,10 @@ public final class CrashReporter {
} }
private static void wakeWatcher(Context context) { private static void wakeWatcher(Context context) {
try { CrashWatcherService.requestImmediateSweep(context, true);
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());
} }
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.content.Intent;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import com.foxx.androidcast.util.IoUtils; import com.foxx.androidcast.util.IoUtils;
@@ -28,6 +29,7 @@ import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
/** /**
* Standalone process ({@code :crashwatcher}) — uploads pending crash JSON even if the main * 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"; private static final String TAG = "CrashWatcher";
public static final String ACTION_UPLOAD_PENDING = "com.foxx.androidcast.crash.UPLOAD_PENDING"; public static final String ACTION_UPLOAD_PENDING = "com.foxx.androidcast.crash.UPLOAD_PENDING";
private static final int ALARM_REQUEST = 4102; 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) { public static void scheduleUploadSweep(Context context) {
try { Context app = context.getApplicationContext();
Intent i = new Intent(context, CrashWatcherService.class); schedulePeriodicAlarm(app);
i.setAction(ACTION_UPLOAD_PENDING); requestImmediateSweep(app);
context.startService(i);
} catch (Exception e) {
Log.w(TAG, "scheduleUploadSweep: " + e.getMessage());
} }
CrashSettings settings = CrashSettingsStore.load(context);
if (!settings.enabled) { /** Alarm-only periodic upload; does not start the service now. */
return; public static void schedulePeriodicAlarm(Context context) {
} Context app = context.getApplicationContext();
AlarmManager alarm = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); CrashSettings settings = CrashSettingsStore.load(app);
AlarmManager alarm = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE);
if (alarm == null) { if (alarm == null) {
return; return;
} }
Intent periodic = new Intent(context, CrashWatcherService.class); PendingIntent pi = pendingAlarmIntent(app);
periodic.setAction(ACTION_UPLOAD_PENDING); if (!settings.enabled) {
PendingIntent pi = PendingIntent.getService( alarm.cancel(pi);
context, return;
ALARM_REQUEST, }
periodic,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
long trigger = System.currentTimeMillis() + settings.uploadIntervalMs; long trigger = System.currentTimeMillis() + settings.uploadIntervalMs;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
alarm.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, trigger, pi); 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 @Override
public int onStartCommand(Intent intent, int flags, int startId) { public int onStartCommand(Intent intent, int flags, int startId) {
if (!SWEEP_RUNNING.compareAndSet(false, true)) {
stopSelf(startId);
return START_NOT_STICKY;
}
new Thread(() -> { new Thread(() -> {
try { try {
runSweep(); runSweep();
} finally { } finally {
scheduleUploadSweep(getApplicationContext()); SWEEP_RUNNING.set(false);
schedulePeriodicAlarm(getApplicationContext());
stopSelf(startId); stopSelf(startId);
} }
}, "CrashWatcherSweep").start(); }, "CrashWatcherSweep").start();