mirror of
git://f0xx.org/android_cast
synced 2026-07-29 07:39:15 +03:00
snap
This commit is contained in:
@@ -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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,10 +71,20 @@ public final class CastTrayNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Intent intent = new Intent(app, TrayForegroundService.class);
|
Intent intent = new Intent(app, TrayForegroundService.class);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
try {
|
||||||
app.startForegroundService(intent);
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
} else {
|
app.startForegroundService(intent);
|
||||||
app.startService(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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
private static boolean isMainProcess(Context context) {
|
||||||
} catch (Exception e) {
|
return context.getPackageName().equals(Application.getProcessName());
|
||||||
Log.w(TAG, "wake watcher failed: " + e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
/** Alarm-only periodic upload; does not start the service now. */
|
||||||
}
|
public static void schedulePeriodicAlarm(Context context) {
|
||||||
CrashSettings settings = CrashSettingsStore.load(context);
|
Context app = context.getApplicationContext();
|
||||||
if (!settings.enabled) {
|
CrashSettings settings = CrashSettingsStore.load(app);
|
||||||
return;
|
AlarmManager alarm = (AlarmManager) app.getSystemService(Context.ALARM_SERVICE);
|
||||||
}
|
|
||||||
AlarmManager alarm = (AlarmManager) context.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();
|
||||||
|
|||||||
Reference in New Issue
Block a user