mirror of
git://f0xx.org/android_cast
synced 2026-07-29 08:37:37 +03:00
tray bar rework
This commit is contained in:
@@ -14,6 +14,7 @@ package com.foxx.androidcast;
|
||||
import android.app.Application;
|
||||
import android.content.Context;
|
||||
|
||||
import com.foxx.androidcast.CastTrayNotifier;
|
||||
import com.foxx.androidcast.commercial.CommercialFeatures;
|
||||
import com.foxx.androidcast.crash.CrashReporter;
|
||||
import com.foxx.androidcast.dev.DevAdbWifiKeeper;
|
||||
@@ -46,5 +47,6 @@ public class AndroidCastApplication extends Application {
|
||||
CommercialFeatures.refreshFromPreferences(this);
|
||||
RemoteAccessCoordinator.onBootIfNeeded(this);
|
||||
DevAdbWifiKeeper.ensureStarted(this);
|
||||
CastTrayNotifier.ensureTrayService(this);
|
||||
}
|
||||
}
|
||||
|
||||
92
app/src/main/java/com/foxx/androidcast/CastTrayContent.java
Normal file
92
app/src/main/java/com/foxx/androidcast/CastTrayContent.java
Normal file
@@ -0,0 +1,92 @@
|
||||
package com.foxx.androidcast;
|
||||
|
||||
import com.foxx.androidcast.remoteaccess.RemoteAccessMode;
|
||||
import com.foxx.androidcast.remoteaccess.RemoteAccessStatusStore;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Pure text assembly for the single persistent tray notification. */
|
||||
public final class CastTrayContent {
|
||||
public static final class Body {
|
||||
public final String summary;
|
||||
public final String bigText;
|
||||
|
||||
public Body(String summary, String bigText) {
|
||||
this.summary = summary != null ? summary : "";
|
||||
this.bigText = bigText != null ? bigText : "";
|
||||
}
|
||||
}
|
||||
|
||||
private CastTrayContent() {}
|
||||
|
||||
public static Body build(
|
||||
String idleTapToOpen,
|
||||
String castActive,
|
||||
String adbWaiting,
|
||||
String adbLineFormat,
|
||||
String raPollFormat,
|
||||
String raConnected,
|
||||
String raSessionFormat,
|
||||
boolean casting,
|
||||
boolean adbEnabled,
|
||||
String adbConnectLine,
|
||||
RemoteAccessMode raMode,
|
||||
RemoteAccessStatusStore.Snapshot raSnap) {
|
||||
if (casting) {
|
||||
return new Body(castActive, castActive);
|
||||
}
|
||||
List<String> lines = new ArrayList<>();
|
||||
String raLine = remoteAccessLine(raPollFormat, raConnected, raSessionFormat, raMode, raSnap);
|
||||
if (raLine != null && !raLine.isEmpty()) {
|
||||
lines.add(raLine);
|
||||
}
|
||||
if (adbEnabled) {
|
||||
String adbLine = adbConnectLine == null || adbConnectLine.isEmpty()
|
||||
? adbWaiting
|
||||
: String.format(adbLineFormat, adbConnectLine);
|
||||
lines.add(adbLine);
|
||||
}
|
||||
if (!lines.isEmpty()) {
|
||||
String joined = joinLines(lines);
|
||||
return new Body(lines.get(0), joined);
|
||||
}
|
||||
return new Body(idleTapToOpen, idleTapToOpen);
|
||||
}
|
||||
|
||||
static String remoteAccessLine(
|
||||
String pollFormat,
|
||||
String connected,
|
||||
String sessionFormat,
|
||||
RemoteAccessMode mode,
|
||||
RemoteAccessStatusStore.Snapshot snap) {
|
||||
if (mode == null || !mode.isActive()) {
|
||||
return null;
|
||||
}
|
||||
if (snap != null && RemoteAccessStatusStore.STATE_SUCCESS.equals(snap.state)) {
|
||||
if (snap.sessionId != null && !snap.sessionId.isEmpty()) {
|
||||
String line = String.format(sessionFormat, snap.sessionId);
|
||||
if (mode == RemoteAccessMode.RSSH) {
|
||||
return line + " (RSSH)";
|
||||
}
|
||||
return line;
|
||||
}
|
||||
if (mode == RemoteAccessMode.RSSH) {
|
||||
return connected + " (RSSH)";
|
||||
}
|
||||
return connected;
|
||||
}
|
||||
return String.format(pollFormat, mode.name());
|
||||
}
|
||||
|
||||
private static String joinLines(List<String> lines) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (int i = 0; i < lines.size(); i++) {
|
||||
if (i > 0) {
|
||||
sb.append('\n');
|
||||
}
|
||||
sb.append(lines.get(i));
|
||||
}
|
||||
return sb.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,5 @@
|
||||
package com.foxx.androidcast;
|
||||
|
||||
/*********************************************************************
|
||||
* CastTrayNotifier.java
|
||||
* Created at: Sun 17 May 2026 18:17:14 +0200
|
||||
* Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
|
||||
* Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
|
||||
* class CastTrayNotifier
|
||||
* Contributors:
|
||||
* - Anton Afanasyeu <a.afanasieff@gmail.com> (3 commits, 156 lines)
|
||||
* - Cursor Agent (project assistant)
|
||||
* Digest: SHA256 292d7f278b9726120e113e803105641b313de93e2efa129dd3af428d1aa68033
|
||||
**********************************************************************/
|
||||
import android.app.ForegroundServiceStartNotAllowedException;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
@@ -24,50 +13,69 @@ import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.foxx.androidcast.dev.DevAdbConnectInfo;
|
||||
import com.foxx.androidcast.dev.DevAdbWifiKeeper;
|
||||
import com.foxx.androidcast.remoteaccess.RemoteAccessMode;
|
||||
import com.foxx.androidcast.remoteaccess.RemoteAccessStatusStore;
|
||||
|
||||
/**
|
||||
* Persistent status-bar icon while cast services run (when enabled in settings).
|
||||
* Single persistent status-bar notification for Android Cast (prod + dev modes).
|
||||
*/
|
||||
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;
|
||||
|
||||
/** Legacy per-feature notification ids — cancelled on every refresh. */
|
||||
public static final int LEGACY_DEV_ADB_ID = 50391;
|
||||
public static final int LEGACY_RA_ID = 0x7a00;
|
||||
public static final int LEGACY_RA_VPN_ID = 0x7a01;
|
||||
|
||||
private CastTrayNotifier() {}
|
||||
|
||||
/** Sync tray with cast service lifecycle (call from foreground services). */
|
||||
/** Sync tray content with app / dev service state. */
|
||||
public static void refresh(Context context) {
|
||||
Context app = context.getApplicationContext();
|
||||
if (!AppPreferences.isShowTrayIcon(app)) {
|
||||
cancelLegacyNotifications(app);
|
||||
if (!shouldMaintainTray(app)) {
|
||||
stopTrayService(app);
|
||||
hide(app);
|
||||
return;
|
||||
}
|
||||
ensureTrayService(app);
|
||||
publish(app);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)) {
|
||||
if (shouldMaintainTray(context)) {
|
||||
ensureTrayService(context);
|
||||
}
|
||||
}
|
||||
|
||||
/** User toggled the preference — show or hide immediately. */
|
||||
public static void onPreferenceChanged(Context context) {
|
||||
Context app = context.getApplicationContext();
|
||||
if (AppPreferences.isShowTrayIcon(app)) {
|
||||
ensureTrayService(app);
|
||||
} else {
|
||||
stopTrayService(app);
|
||||
hide(app);
|
||||
refresh(context);
|
||||
}
|
||||
|
||||
public static boolean shouldMaintainTray(Context context) {
|
||||
if (AppPreferences.isShowTrayIcon(context)) {
|
||||
return true;
|
||||
}
|
||||
return hasDevTrayContent(context);
|
||||
}
|
||||
|
||||
static boolean hasDevTrayContent(Context context) {
|
||||
if (!BuildConfig.DEBUG) {
|
||||
return false;
|
||||
}
|
||||
if (AppPreferences.isDevAdbWifiKeeperEnabled(context)) {
|
||||
return true;
|
||||
}
|
||||
return AppPreferences.getDevRemoteAccessMode(context).isActive();
|
||||
}
|
||||
|
||||
public static void ensureTrayService(Context context) {
|
||||
Context app = context.getApplicationContext();
|
||||
if (!AppPreferences.isShowTrayIcon(app)) {
|
||||
if (!shouldMaintainTray(app)) {
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(app, TrayForegroundService.class);
|
||||
@@ -92,34 +100,60 @@ public final class CastTrayNotifier {
|
||||
context.getApplicationContext().stopService(new Intent(context, TrayForegroundService.class));
|
||||
}
|
||||
|
||||
/** Foreground tray — not swipe-dismissible on stock Android. */
|
||||
/** Foreground tray owned by {@link TrayForegroundService}. */
|
||||
public static void startForegroundTray(Service service) {
|
||||
if (!AppPreferences.isShowTrayIcon(service)) {
|
||||
if (!shouldMaintainTray(service)) {
|
||||
service.stopSelf();
|
||||
return;
|
||||
}
|
||||
createChannel(service);
|
||||
boolean idle = !CastActiveState.isSenderCasting() && !CastActiveState.isReceiverListening();
|
||||
Notification n = buildNotification(service, idle);
|
||||
Notification n = buildNotification(service);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
service.startForeground(ID_TRAY, n, android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
|
||||
service.startForeground(
|
||||
ID_TRAY,
|
||||
n,
|
||||
android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE);
|
||||
} else {
|
||||
service.startForeground(ID_TRAY, n);
|
||||
}
|
||||
}
|
||||
|
||||
private static void show(Context context, boolean idle) {
|
||||
if (AppPreferences.isShowTrayIcon(context)) {
|
||||
ensureTrayService(context);
|
||||
return;
|
||||
/** Other foreground services reuse the same tray notification id. */
|
||||
public static void startForeground(Service service, int fgsType) {
|
||||
createChannel(service);
|
||||
Notification n = buildNotification(service);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && fgsType != 0) {
|
||||
service.startForeground(ID_TRAY, n, fgsType);
|
||||
} else {
|
||||
service.startForeground(ID_TRAY, n);
|
||||
}
|
||||
hide(context);
|
||||
}
|
||||
|
||||
private static Notification buildNotification(Context context, boolean idle) {
|
||||
String body = idle
|
||||
? context.getString(R.string.tray_cast_idle)
|
||||
: context.getString(R.string.tray_cast_active);
|
||||
public static Notification buildNotification(Context context) {
|
||||
createChannel(context);
|
||||
boolean casting = CastActiveState.isSenderCasting() || CastActiveState.isReceiverListening();
|
||||
boolean adbEnabled = BuildConfig.DEBUG && AppPreferences.isDevAdbWifiKeeperEnabled(context);
|
||||
RemoteAccessMode raMode = BuildConfig.DEBUG
|
||||
? AppPreferences.getDevRemoteAccessMode(context)
|
||||
: RemoteAccessMode.DISABLED;
|
||||
RemoteAccessStatusStore.Snapshot raSnap = RemoteAccessStatusStore.load(context);
|
||||
DevAdbConnectInfo adbInfo = adbEnabled ? DevAdbWifiKeeper.getLastInfo(context) : null;
|
||||
String adbLine = adbInfo != null ? adbInfo.primaryConnectLine() : "";
|
||||
|
||||
CastTrayContent.Body body = CastTrayContent.build(
|
||||
context.getString(R.string.tray_cast_idle),
|
||||
context.getString(R.string.tray_cast_active),
|
||||
context.getString(R.string.dev_adb_wifi_notification_waiting),
|
||||
context.getString(R.string.dev_adb_wifi_notification_line),
|
||||
context.getString(R.string.dev_remote_access_notification_poll),
|
||||
context.getString(R.string.dev_remote_access_notification_connected),
|
||||
context.getString(R.string.dev_remote_access_notification_session),
|
||||
casting,
|
||||
adbEnabled,
|
||||
adbLine,
|
||||
raMode,
|
||||
raSnap);
|
||||
|
||||
PendingIntent pi = PendingIntent.getActivity(
|
||||
context, 0, CastActivityTracker.resumeIntent(context),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
@@ -128,9 +162,10 @@ public final class CastTrayNotifier {
|
||||
PendingIntent deletePi = PendingIntent.getBroadcast(
|
||||
context, 1, deleted,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
return new NotificationCompat.Builder(context, CHANNEL_TRAY)
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_TRAY)
|
||||
.setContentTitle(context.getString(R.string.app_name))
|
||||
.setContentText(body)
|
||||
.setContentText(body.summary)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentIntent(pi)
|
||||
.setDeleteIntent(deletePi)
|
||||
@@ -142,8 +177,31 @@ public final class CastTrayNotifier {
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE)
|
||||
.build();
|
||||
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE);
|
||||
if (!body.bigText.equals(body.summary)) {
|
||||
builder.setStyle(new NotificationCompat.BigTextStyle().bigText(body.bigText));
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
static void publish(Context context) {
|
||||
if (!shouldMaintainTray(context)) {
|
||||
return;
|
||||
}
|
||||
NotificationManager nm = context.getSystemService(NotificationManager.class);
|
||||
if (nm != null) {
|
||||
nm.notify(ID_TRAY, buildNotification(context));
|
||||
}
|
||||
}
|
||||
|
||||
static void cancelLegacyNotifications(Context context) {
|
||||
NotificationManager nm = context.getSystemService(NotificationManager.class);
|
||||
if (nm == null) {
|
||||
return;
|
||||
}
|
||||
nm.cancel(LEGACY_DEV_ADB_ID);
|
||||
nm.cancel(LEGACY_RA_ID);
|
||||
nm.cancel(LEGACY_RA_VPN_ID);
|
||||
}
|
||||
|
||||
private static void hide(Context context) {
|
||||
|
||||
@@ -1,19 +1,10 @@
|
||||
package com.foxx.androidcast;
|
||||
|
||||
/*********************************************************************
|
||||
* TrayForegroundService.java
|
||||
* Created at: Sun 17 May 2026 22:49:49 +0200
|
||||
* Updated at: Wed 20 May 2026 15:17:13 +0200 by Anton Afanasyeu <a.afanasieff@gmail.com>
|
||||
* Commit: 5d8e82d2e60a21fff3138d2a394ee4e8b4c6dcb8
|
||||
* class TrayForegroundService
|
||||
* Contributors:
|
||||
* - Anton Afanasyeu <a.afanasieff@gmail.com> (2 commits, 48 lines)
|
||||
* - Cursor Agent (project assistant)
|
||||
* Digest: SHA256 5697518674c876a08eb78c08ca151686a079c96d2d3dc238c32666d83b5e88b8
|
||||
**********************************************************************/
|
||||
import android.app.Service;
|
||||
import android.content.Intent;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.os.Looper;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
@@ -22,15 +13,31 @@ import androidx.annotation.Nullable;
|
||||
* (non-FGS status notifications are dismissible on Android 13+).
|
||||
*/
|
||||
public class TrayForegroundService extends Service {
|
||||
private static final long HEARTBEAT_MS = 90_000L;
|
||||
|
||||
private final Handler handler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable heartbeat = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
if (!CastTrayNotifier.shouldMaintainTray(TrayForegroundService.this)) {
|
||||
stopSelf();
|
||||
return;
|
||||
}
|
||||
CastTrayNotifier.startForegroundTray(TrayForegroundService.this);
|
||||
handler.postDelayed(this, HEARTBEAT_MS);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
CastTrayNotifier.startForegroundTray(this);
|
||||
handler.postDelayed(heartbeat, HEARTBEAT_MS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
if (!AppPreferences.isShowTrayIcon(this)) {
|
||||
if (!CastTrayNotifier.shouldMaintainTray(this)) {
|
||||
stopSelf();
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
@@ -40,6 +47,7 @@ public class TrayForegroundService extends Service {
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
handler.removeCallbacks(heartbeat);
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@@ -25,6 +25,6 @@ public class TrayNotificationReceiver extends BroadcastReceiver {
|
||||
if (!AppPreferences.isShowTrayIcon(context)) {
|
||||
return;
|
||||
}
|
||||
CastTrayNotifier.ensureTrayService(context);
|
||||
CastTrayNotifier.refresh(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,28 +1,19 @@
|
||||
package com.foxx.androidcast.dev;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.Network;
|
||||
import android.net.NetworkCapabilities;
|
||||
import android.net.NetworkRequest;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.foxx.androidcast.AppPreferences;
|
||||
import com.foxx.androidcast.BuildConfig;
|
||||
import com.foxx.androidcast.DeveloperSettingsActivity;
|
||||
import com.foxx.androidcast.R;
|
||||
import com.foxx.androidcast.CastTrayNotifier;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
@@ -33,9 +24,6 @@ import java.util.concurrent.Executors;
|
||||
|
||||
/** Debug-only: re-enable adb wifi on network changes and publish connect info. */
|
||||
public final class DevAdbWifiKeeper {
|
||||
private static final String CHANNEL_ID = "dev_adb_wifi";
|
||||
private static final int NOTIFICATION_ID = 50391;
|
||||
|
||||
private static final ExecutorService WORKER = Executors.newSingleThreadExecutor(r -> {
|
||||
Thread t = new Thread(r, "DevAdbWifiKeeper");
|
||||
t.setDaemon(true);
|
||||
@@ -146,7 +134,6 @@ public final class DevAdbWifiKeeper {
|
||||
}
|
||||
|
||||
private void start() {
|
||||
ensureNotificationChannel();
|
||||
loadCachedJsonIntoHttpServer();
|
||||
httpServer.start();
|
||||
registerNetworkCallback();
|
||||
@@ -172,11 +159,7 @@ public final class DevAdbWifiKeeper {
|
||||
private void shutdown() {
|
||||
unregisterNetworkCallback();
|
||||
httpServer.stop();
|
||||
NotificationManager nm =
|
||||
(NotificationManager) appContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (nm != null) {
|
||||
nm.cancel(NOTIFICATION_ID);
|
||||
}
|
||||
CastTrayNotifier.refresh(appContext);
|
||||
}
|
||||
|
||||
private void registerNetworkCallback() {
|
||||
@@ -224,7 +207,7 @@ public final class DevAdbWifiKeeper {
|
||||
Log.d(DevAdbConnectInfo.LOG_TAG, "refresh reason=" + reason
|
||||
+ " secure=" + info.secureSettingsGranted
|
||||
+ " connect=" + info.connect);
|
||||
MAIN.post(() -> updateNotification(info));
|
||||
MAIN.post(() -> CastTrayNotifier.refresh(appContext));
|
||||
}
|
||||
|
||||
private void writeJsonFile(String json) {
|
||||
@@ -239,53 +222,4 @@ public final class DevAdbWifiKeeper {
|
||||
Log.w(DevAdbConnectInfo.LOG_TAG, "write adb_connect.json failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void ensureNotificationChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
NotificationManager nm =
|
||||
(NotificationManager) appContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (nm == null) {
|
||||
return;
|
||||
}
|
||||
NotificationChannel channel = new NotificationChannel(
|
||||
CHANNEL_ID,
|
||||
appContext.getString(R.string.dev_adb_wifi_notification_channel),
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
channel.setDescription(appContext.getString(R.string.dev_adb_wifi_notification_channel_desc));
|
||||
nm.createNotificationChannel(channel);
|
||||
}
|
||||
|
||||
private void updateNotification(DevAdbConnectInfo info) {
|
||||
if (!AppPreferences.isDevAdbWifiKeeperEnabled(appContext)) {
|
||||
return;
|
||||
}
|
||||
NotificationManager nm =
|
||||
(NotificationManager) appContext.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
if (nm == null) {
|
||||
return;
|
||||
}
|
||||
String line = info.primaryConnectLine();
|
||||
String text = line.isEmpty()
|
||||
? appContext.getString(R.string.dev_adb_wifi_notification_waiting)
|
||||
: appContext.getString(R.string.dev_adb_wifi_notification_line, line);
|
||||
Intent openDev = new Intent(appContext, DeveloperSettingsActivity.class);
|
||||
openDev.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
PendingIntent content = PendingIntent.getActivity(
|
||||
appContext,
|
||||
0,
|
||||
openDev,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
Notification notification = new NotificationCompat.Builder(appContext, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_launcher_foreground)
|
||||
.setContentTitle(appContext.getString(R.string.dev_adb_wifi_notification_title))
|
||||
.setContentText(text)
|
||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(text))
|
||||
.setContentIntent(content)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.build();
|
||||
nm.notify(NOTIFICATION_ID, notification);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
package com.foxx.androidcast.remoteaccess;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
@@ -13,12 +10,9 @@ import android.os.IBinder;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.foxx.androidcast.AppPreferences;
|
||||
import com.foxx.androidcast.DeveloperSettingsActivity;
|
||||
import com.foxx.androidcast.CastTrayNotifier;
|
||||
import com.foxx.androidcast.DeviceInfo;
|
||||
import com.foxx.androidcast.R;
|
||||
import com.foxx.androidcast.remoteaccess.control.RemoteAccessControlClient;
|
||||
import com.foxx.androidcast.vpn.VpnServiceClient;
|
||||
|
||||
@@ -44,8 +38,6 @@ public final class RemoteAccessService extends Service {
|
||||
public static final String ACTION_DISABLE_ONCE = "com.foxx.androidcast.remoteaccess.DISABLE_ONCE";
|
||||
public static final String EXTRA_MODE = "mode";
|
||||
|
||||
private static final int NOTIF_ID = 0x7a00;
|
||||
private static final String CHANNEL_ID = "remote_access";
|
||||
private static final int MIN_POLL_MS = 60_000;
|
||||
private static final int MAX_POLL_MS = 420_000;
|
||||
|
||||
@@ -102,8 +94,9 @@ public final class RemoteAccessService extends Service {
|
||||
if (ACTION_STOP.equals(action)) {
|
||||
cancelPoll(this);
|
||||
RemoteAccessTunnelHelper.tearDownAll(this);
|
||||
stopForeground(true);
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
stopSelf();
|
||||
CastTrayNotifier.refresh(this);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
if (ACTION_DISABLE_ONCE.equals(action)) {
|
||||
@@ -118,8 +111,11 @@ public final class RemoteAccessService extends Service {
|
||||
stop(this);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
ensureChannel();
|
||||
startForeground(NOTIF_ID, buildNotification(mode));
|
||||
int fgsType = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
? android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
|
||||
: 0;
|
||||
CastTrayNotifier.startForeground(this, fgsType);
|
||||
CastTrayNotifier.ensureTrayService(this);
|
||||
loadOrCreateSessionRandom();
|
||||
checkSessionExpiry();
|
||||
final RemoteAccessMode pollMode = mode;
|
||||
@@ -309,22 +305,7 @@ public final class RemoteAccessService extends Service {
|
||||
}
|
||||
|
||||
private void updateNotificationConnected(String sessionId, String tunnel) {
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm == null) {
|
||||
return;
|
||||
}
|
||||
String text = sessionId.isEmpty()
|
||||
? getString(R.string.dev_remote_access_notification_connected)
|
||||
: getString(R.string.dev_remote_access_notification_session, sessionId);
|
||||
if ("ssh_reverse".equals(tunnel)) {
|
||||
text = text + " (RSSH)";
|
||||
}
|
||||
nm.notify(NOTIF_ID, new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(getString(R.string.dev_remote_access_notification_title))
|
||||
.setContentText(text)
|
||||
.setOngoing(true)
|
||||
.build());
|
||||
CastTrayNotifier.refresh(this);
|
||||
}
|
||||
|
||||
private void scheduleNextPoll() {
|
||||
@@ -355,33 +336,6 @@ public final class RemoteAccessService extends Service {
|
||||
am.cancel(pi);
|
||||
}
|
||||
|
||||
private Notification buildNotification(RemoteAccessMode mode) {
|
||||
Intent open = new Intent(this, DeveloperSettingsActivity.class);
|
||||
PendingIntent pi = PendingIntent.getActivity(this, 0, open,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(getString(R.string.dev_remote_access_notification_title))
|
||||
.setContentText(getString(R.string.dev_remote_access_notification_poll, mode.name()))
|
||||
.setContentIntent(pi)
|
||||
.setOngoing(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void ensureChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm == null) {
|
||||
return;
|
||||
}
|
||||
NotificationChannel ch = new NotificationChannel(CHANNEL_ID,
|
||||
getString(R.string.dev_remote_access_notification_channel),
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
nm.createNotificationChannel(ch);
|
||||
}
|
||||
|
||||
private static JSONObject buildDeviceMeta(Context context) throws Exception {
|
||||
JSONObject device = new JSONObject();
|
||||
device.put("name", AppPreferences.getUsername(context));
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.foxx.androidcast.remoteaccess;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.foxx.androidcast.CastTrayNotifier;
|
||||
|
||||
/**
|
||||
* Last remote-access / VPN tunnel event for developer network self-test.
|
||||
*/
|
||||
@@ -63,6 +65,7 @@ public final class RemoteAccessStatusStore {
|
||||
.putString(KEY_SESSION_ID, sessionId != null ? sessionId : "")
|
||||
.putLong(KEY_UPDATED_MS, System.currentTimeMillis())
|
||||
.apply();
|
||||
CastTrayNotifier.refresh(context.getApplicationContext());
|
||||
}
|
||||
|
||||
public static Snapshot load(Context context) {
|
||||
@@ -92,7 +95,7 @@ public final class RemoteAccessStatusStore {
|
||||
public final String sessionId;
|
||||
public final long updatedMs;
|
||||
|
||||
Snapshot(
|
||||
public Snapshot(
|
||||
String type,
|
||||
String state,
|
||||
String method,
|
||||
|
||||
@@ -1,22 +1,15 @@
|
||||
package com.foxx.androidcast.vpn;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.pm.ServiceInfo;
|
||||
import android.net.VpnService;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.util.Log;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
|
||||
import com.foxx.androidcast.DeveloperSettingsActivity;
|
||||
import com.foxx.androidcast.R;
|
||||
import com.foxx.androidcast.CastTrayNotifier;
|
||||
import com.foxx.androidcast.remoteaccess.WireGuardQuickConfig;
|
||||
|
||||
import java.net.InetAddress;
|
||||
@@ -28,9 +21,6 @@ public final class RemoteAccessVpnService extends VpnService {
|
||||
public static final String ACTION_DOWN = "com.foxx.androidcast.vpn.VPN_DOWN";
|
||||
public static final String EXTRA_WG_CONFIG = "wg_config";
|
||||
|
||||
private static final int NOTIF_ID = 0x7a01;
|
||||
private static final String CHANNEL_ID = "remote_access_vpn";
|
||||
|
||||
private ParcelFileDescriptor tun;
|
||||
|
||||
public static boolean startWithConfig(Context context, String wgConfig) {
|
||||
@@ -61,14 +51,18 @@ public final class RemoteAccessVpnService extends VpnService {
|
||||
String action = intent != null ? intent.getAction() : null;
|
||||
if (ACTION_DOWN.equals(action)) {
|
||||
tearDown();
|
||||
stopForeground(true);
|
||||
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||
stopSelf();
|
||||
CastTrayNotifier.refresh(this);
|
||||
return START_NOT_STICKY;
|
||||
}
|
||||
if (ACTION_UP.equals(action)) {
|
||||
String cfg = intent.getStringExtra(EXTRA_WG_CONFIG);
|
||||
ensureChannel();
|
||||
startForeground(NOTIF_ID, buildNotification("WireGuard remote access"));
|
||||
int fgsType = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q
|
||||
? ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
|
||||
: 0;
|
||||
CastTrayNotifier.startForeground(this, fgsType);
|
||||
CastTrayNotifier.ensureTrayService(this);
|
||||
establishTun(cfg);
|
||||
return START_STICKY;
|
||||
}
|
||||
@@ -102,6 +96,7 @@ public final class RemoteAccessVpnService extends VpnService {
|
||||
b.setMtu(1280);
|
||||
tun = b.establish();
|
||||
Log.i(TAG, "VPN interface established");
|
||||
CastTrayNotifier.refresh(this);
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "VPN establish failed", e);
|
||||
}
|
||||
@@ -117,33 +112,6 @@ public final class RemoteAccessVpnService extends VpnService {
|
||||
}
|
||||
}
|
||||
|
||||
private Notification buildNotification(String text) {
|
||||
Intent open = new Intent(this, DeveloperSettingsActivity.class);
|
||||
PendingIntent pi = PendingIntent.getActivity(this, 0, open,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
return new NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
.setSmallIcon(R.drawable.ic_notification)
|
||||
.setContentTitle(getString(R.string.dev_remote_access_notification_title))
|
||||
.setContentText(text)
|
||||
.setContentIntent(pi)
|
||||
.setOngoing(true)
|
||||
.build();
|
||||
}
|
||||
|
||||
private void ensureChannel() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
return;
|
||||
}
|
||||
NotificationManager nm = getSystemService(NotificationManager.class);
|
||||
if (nm == null) {
|
||||
return;
|
||||
}
|
||||
NotificationChannel ch = new NotificationChannel(CHANNEL_ID,
|
||||
getString(R.string.dev_remote_access_notification_channel),
|
||||
NotificationManager.IMPORTANCE_LOW);
|
||||
nm.createNotificationChannel(ch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
tearDown();
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.foxx.androidcast;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import com.foxx.androidcast.remoteaccess.RemoteAccessMode;
|
||||
import com.foxx.androidcast.remoteaccess.RemoteAccessStatusStore;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
public class CastTrayContentTest {
|
||||
|
||||
@Test
|
||||
public void prodIdle_showsTapToOpen() {
|
||||
CastTrayContent.Body body = CastTrayContent.build(
|
||||
"Tap to open",
|
||||
"Cast active",
|
||||
"Waiting for Wi‑Fi",
|
||||
"adb connect %1$s",
|
||||
"Polling BE (%1$s)",
|
||||
"Connected",
|
||||
"Session %1$s",
|
||||
false,
|
||||
false,
|
||||
"",
|
||||
RemoteAccessMode.DISABLED,
|
||||
null);
|
||||
assertEquals("Tap to open", body.summary);
|
||||
assertEquals("Tap to open", body.bigText);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void casting_overridesDevLines() {
|
||||
CastTrayContent.Body body = CastTrayContent.build(
|
||||
"Tap to open",
|
||||
"Cast active",
|
||||
"Waiting",
|
||||
"adb %1$s",
|
||||
"Poll %1$s",
|
||||
"Connected",
|
||||
"Session %1$s",
|
||||
true,
|
||||
true,
|
||||
"192.168.1.1:5555",
|
||||
RemoteAccessMode.RSSH,
|
||||
snapshot(RemoteAccessStatusStore.STATE_WAIT, ""));
|
||||
assertEquals("Cast active", body.summary);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void devMode_combinesRaAndAdbLines() {
|
||||
RemoteAccessStatusStore.Snapshot snap =
|
||||
snapshot(RemoteAccessStatusStore.STATE_WAIT, "");
|
||||
CastTrayContent.Body body = CastTrayContent.build(
|
||||
"Tap to open",
|
||||
"Cast active",
|
||||
"Waiting for Wi‑Fi",
|
||||
"adb connect %1$s",
|
||||
"Polling BE (%1$s)",
|
||||
"Connected",
|
||||
"Session %1$s",
|
||||
false,
|
||||
true,
|
||||
"10.0.0.5:5555",
|
||||
RemoteAccessMode.RSSH,
|
||||
snap);
|
||||
assertEquals("Polling BE (RSSH)", body.summary);
|
||||
assertTrue(body.bigText.contains("Polling BE (RSSH)"));
|
||||
assertTrue(body.bigText.contains("adb connect 10.0.0.5:5555"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void remoteAccess_connectedSession_appendsRsshTag() {
|
||||
RemoteAccessStatusStore.Snapshot snap =
|
||||
snapshot(RemoteAccessStatusStore.STATE_SUCCESS, "abc-123");
|
||||
String line = CastTrayContent.remoteAccessLine(
|
||||
"Polling BE (%1$s)",
|
||||
"Connected",
|
||||
"Connected · session %1$s",
|
||||
RemoteAccessMode.RSSH,
|
||||
snap);
|
||||
assertEquals("Connected · session abc-123 (RSSH)", line);
|
||||
}
|
||||
|
||||
private static RemoteAccessStatusStore.Snapshot snapshot(String state, String sessionId) {
|
||||
return new RemoteAccessStatusStore.Snapshot(
|
||||
"rssh", state, "", "", "", "", sessionId, 0L);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user