v0.9.4: a turn keeps running when the app is not on screen
A phone freezes an app it cannot see, and the front end had nothing holding it: a turn that took minutes -- a long shell command, a provider that thinks, a build -- stopped quietly the moment the user pressed home, and the answer was only there if the window happened to come back. The engine's own /attach flow made this visible: an APK turn survived, a pocket did not. - app/android/src/xyz/whcself/cstagent/CstService.java: a foreground service started with an explicit component (nothing outside the app can reach it, no intent filter) for as long as a turn runs. Being a foreground service makes the process one Android neither freezes nor kills; a partial wake lock (30 min ceiling) keeps the CPU awake with the screen off. Its ongoing notification says what is running -- "CST-agent is working" plus the first line of the prompt -- on the same cst-agent channel the notification_send tool uses, and taps back into the app. A phone that refuses the foreground start (Android 12+ does, from the background) loses the protection, not the turn: the failure is caught and the service stops itself. - Manifest: FOREGROUND_SERVICE, FOREGROUND_SERVICE_DATA_SYNC (API 34+ wants the typed one) and WAKE_LOCK, plus the <service> with foregroundServiceType="dataSync". The first build of this failed with "Expected '>', but got ' '" -- a manifest comment cannot contain "--". - Front end: app::holdForeground(text) / app::releaseForeground() in ToolProvider.h, one call for every platform -- Android starts and stops the service through JNI, a desktop has nothing to hold (its window is the app). AgentCore::send() calls it when the turn starts (firstLine() of the prompt is what the notification says) and finishTurn() releases it, so the notification exists exactly as long as the work does. CST_APP_NO_SERVICE=1 keeps a test run from touching the phone. - On the NX789J, with the app sent home and the screen off: the service read back isForeground=true with its notification, the wake lock cst-agent:turn was taken at the start of the turn and dropped at its end, run_shell "sleep 60" finished with the screen dark, and the service was gone after. Desktop: build clean, --uitest all green, --selftest and cst-app-selftest OK (the hold is a no-op there, so nothing about the Qt side changed). Version 0.9.4.
This commit is contained in:
@@ -374,6 +374,10 @@ androiddeployqt copies `app/android/` with the rest of the package source and
|
|||||||
Qt's Gradle template puts its `src/` on `java.srcDirs`, so no build-script
|
Qt's Gradle template puts its `src/` on `java.srcDirs`, so no build-script
|
||||||
change is needed (`Notifier.java` is the first one; a Java helper is the only
|
change is needed (`Notifier.java` is the first one; a Java helper is the only
|
||||||
way to catch a throwable where the platform throws instead of returning).
|
way to catch a throwable where the platform throws instead of returning).
|
||||||
|
`CstService.java` (the foreground service that holds a phone awake while a
|
||||||
|
turn runs) went in the same way; it is started by `app::holdForeground()`
|
||||||
|
when a turn begins and stopped by `app::releaseForeground()` when it ends,
|
||||||
|
so nothing about it is visible when the app is idle.
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
@@ -398,6 +402,8 @@ way to catch a throwable where the platform throws instead of returning).
|
|||||||
transcript draws a file card with Open and Share (see the Files section)
|
transcript draws a file card with Open and Share (see the Files section)
|
||||||
- [x] Android platform tools (JNI: notification, clipboard, share sheet) and the
|
- [x] Android platform tools (JNI: notification, clipboard, share sheet) and the
|
||||||
file card's open/share through the app's FileProvider
|
file card's open/share through the app's FileProvider
|
||||||
|
- [x] a turn keeps running with the app off screen (Android foreground service
|
||||||
|
+ wake lock; nothing to do on a desktop)
|
||||||
- [ ] a real turn driven through the window (needs a provider key)
|
- [ ] a real turn driven through the window (needs a provider key)
|
||||||
- [ ] SAF file picker, the back gesture, the icon glyphs on the phone
|
- [ ] SAF file picker, the back gesture, the icon glyphs on the phone
|
||||||
- [ ] Kirigami (the rest of the Breeze look: colors, controls), tray,
|
- [ ] Kirigami (the rest of the Breeze look: colors, controls), tray,
|
||||||
|
|||||||
@@ -18,10 +18,12 @@
|
|||||||
|
|
||||||
Permissions: INTERNET for the providers the user configures,
|
Permissions: INTERNET for the providers the user configures,
|
||||||
POST_NOTIFICATIONS for the app's notification_send tool (Android 13+
|
POST_NOTIFICATIONS for the app's notification_send tool (Android 13+
|
||||||
asks at runtime; the tool stays hidden until it is granted). No storage
|
asks at runtime; the tool stays hidden until it is granted), and the
|
||||||
permission: the file picker is the system one (SAF) and its input is
|
foreground-service permissions with WAKE_LOCK for the service that keeps
|
||||||
copied into the app's directory. No cleartext traffic: every provider URL
|
a turn going while the phone is in a pocket (CstService.java). No
|
||||||
is https. -->
|
storage permission: the file picker is the system one (SAF) and its
|
||||||
|
input is copied into the app's directory. No cleartext traffic: every
|
||||||
|
provider URL is https. -->
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="xyz.whcself.cstagent"
|
package="xyz.whcself.cstagent"
|
||||||
android:installLocation="auto"
|
android:installLocation="auto"
|
||||||
@@ -33,6 +35,9 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||||
|
|
||||||
<supports-screens
|
<supports-screens
|
||||||
android:anyDensity="true"
|
android:anyDensity="true"
|
||||||
@@ -84,5 +89,16 @@
|
|||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/qtprovider_paths" />
|
android:resource="@xml/qtprovider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
|
||||||
|
<!-- A turn can outlive the screen: while one is running the front end
|
||||||
|
starts this service (AgentCore::send -> app::holdForeground), which
|
||||||
|
makes the process a foreground one, so Android neither freezes nor
|
||||||
|
kills it in the background, and takes a partial wake lock so the
|
||||||
|
CPU keeps going with the phone in a pocket. dataSync is the type
|
||||||
|
that fits "the app is finishing some work". -->
|
||||||
|
<service
|
||||||
|
android:name="xyz.whcself.cstagent.CstService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="dataSync" />
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
package xyz.whcself.cstagent;
|
||||||
|
|
||||||
|
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.ServiceInfo;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.os.PowerManager;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The service that keeps a turn running while the app is not on screen.
|
||||||
|
*
|
||||||
|
* A phone freezes an app it cannot see (and may kill it), so a turn that takes
|
||||||
|
* minutes -- a build, a long web fetch, a provider that thinks -- dies in the
|
||||||
|
* pocket. While one is going, AgentCore::send() starts this service through
|
||||||
|
* app::holdForeground(); being a foreground service makes the process one
|
||||||
|
* Android does not touch, and the partial wake lock keeps the CPU awake with
|
||||||
|
* the screen off. turnFinished() stops it again (app::releaseForeground()), so
|
||||||
|
* the permanent notification exists exactly as long as the work does.
|
||||||
|
*
|
||||||
|
* Started with an explicit component (AndroidTools.cpp builds the Intent), no
|
||||||
|
* intent filter: nothing outside the app can start it.
|
||||||
|
*/
|
||||||
|
public class CstService extends Service {
|
||||||
|
/** Same channel the notification_send tool uses: one place in settings. */
|
||||||
|
private static final String CHANNEL_ID = "cst-agent";
|
||||||
|
private static final int NOTE_ID = 2;
|
||||||
|
private static final String ACTION_STOP = "xyz.whcself.cstagent.STOP";
|
||||||
|
/** A turn that runs longer than this is stuck; the lock must not outlive it. */
|
||||||
|
private static final long WAKE_LOCK_TIMEOUT_MS = 30 * 60 * 1000L;
|
||||||
|
|
||||||
|
private PowerManager.WakeLock wake;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
if (intent != null && ACTION_STOP.equals(intent.getAction())) {
|
||||||
|
stopEverything();
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
String text = intent == null ? null : intent.getStringExtra("text");
|
||||||
|
try {
|
||||||
|
startForeground(NOTE_ID, buildNote(text));
|
||||||
|
} catch (Throwable t) {
|
||||||
|
/* Android 12+ refuses a foreground start from the background: the
|
||||||
|
* turn then simply runs without the protection rather than not at
|
||||||
|
* all. Nothing to show the user here -- the turn's own result is
|
||||||
|
* what matters. */
|
||||||
|
stopSelf();
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
acquireWake();
|
||||||
|
return START_NOT_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
releaseWake();
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopEverything() {
|
||||||
|
releaseWake();
|
||||||
|
if (Build.VERSION.SDK_INT >= 24)
|
||||||
|
stopForeground(STOP_FOREGROUND_REMOVE);
|
||||||
|
else
|
||||||
|
stopForeground(true);
|
||||||
|
stopSelf();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acquireWake() {
|
||||||
|
try {
|
||||||
|
if (wake == null) {
|
||||||
|
PowerManager pm =
|
||||||
|
(PowerManager) getSystemService(Context.POWER_SERVICE);
|
||||||
|
if (pm == null)
|
||||||
|
return;
|
||||||
|
wake = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK,
|
||||||
|
"cst-agent:turn");
|
||||||
|
wake.setReferenceCounted(false);
|
||||||
|
}
|
||||||
|
wake.acquire(WAKE_LOCK_TIMEOUT_MS);
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
/* no wake lock: the turn still runs while the screen is on */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void releaseWake() {
|
||||||
|
try {
|
||||||
|
if (wake != null && wake.isHeld())
|
||||||
|
wake.release();
|
||||||
|
} catch (Throwable ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "the agent is working: <first line of the prompt>", tapping opens it. */
|
||||||
|
private Notification buildNote(String text) {
|
||||||
|
Context ctx = getApplicationContext();
|
||||||
|
NotificationManager manager =
|
||||||
|
(NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
if (manager != null && Build.VERSION.SDK_INT >= 26) {
|
||||||
|
NotificationChannel channel = new NotificationChannel(
|
||||||
|
CHANNEL_ID, "CST-agent", NotificationManager.IMPORTANCE_LOW);
|
||||||
|
channel.setDescription("A turn is running");
|
||||||
|
manager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
Notification.Builder builder = Build.VERSION.SDK_INT >= 26
|
||||||
|
? new Notification.Builder(ctx, CHANNEL_ID)
|
||||||
|
: new Notification.Builder(ctx);
|
||||||
|
builder.setContentTitle("CST-agent is working")
|
||||||
|
.setSmallIcon(smallIcon())
|
||||||
|
.setOngoing(true)
|
||||||
|
.setShowWhen(false);
|
||||||
|
if (text != null && !text.isEmpty())
|
||||||
|
builder.setContentText(text);
|
||||||
|
Intent open = ctx.getPackageManager().getLaunchIntentForPackage(ctx.getPackageName());
|
||||||
|
if (open != null) {
|
||||||
|
open.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
builder.setContentIntent(PendingIntent.getActivity(ctx, 0, open,
|
||||||
|
PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT));
|
||||||
|
}
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A framework drawable first (the same order the tool uses), then the app's. */
|
||||||
|
private int smallIcon() {
|
||||||
|
int icon = getResources().getIdentifier("ic_dialog_info", "drawable", "android");
|
||||||
|
if (icon == 0)
|
||||||
|
icon = getResources().getIdentifier("stat_notify_chat", "drawable", "android");
|
||||||
|
if (icon == 0)
|
||||||
|
icon = getApplicationInfo().icon;
|
||||||
|
return icon;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -188,6 +188,18 @@ void AgentCore::close() {
|
|||||||
|
|
||||||
/* --- turns --------------------------------------------------------------- */
|
/* --- turns --------------------------------------------------------------- */
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
/* The first line of a prompt, cut short: the line the phone's "working"
|
||||||
|
* notification carries while the turn runs (app::holdForeground). */
|
||||||
|
QString firstLine(const QString &s) {
|
||||||
|
const int nl = s.indexOf(QLatin1Char('\n'));
|
||||||
|
QString line = (nl < 0 ? s : s.left(nl)).trimmed();
|
||||||
|
if (line.size() > 120)
|
||||||
|
line = line.left(119) + QStringLiteral("…");
|
||||||
|
return line;
|
||||||
|
}
|
||||||
|
} // namespace
|
||||||
|
|
||||||
void AgentCore::send(const QString &prompt) {
|
void AgentCore::send(const QString &prompt) {
|
||||||
if (!m_agent || m_running || prompt.trimmed().isEmpty())
|
if (!m_agent || m_running || prompt.trimmed().isEmpty())
|
||||||
return;
|
return;
|
||||||
@@ -195,6 +207,12 @@ void AgentCore::send(const QString &prompt) {
|
|||||||
* message the composer just appended, before the turn can say anything. */
|
* message the composer just appended, before the turn can say anything. */
|
||||||
const QString sending = pendingImages();
|
const QString sending = pendingImages();
|
||||||
setRunning(true);
|
setRunning(true);
|
||||||
|
/* A turn can take minutes: hold the process while it runs, or a phone
|
||||||
|
* freezes the app the moment it leaves the screen. "" on a desktop and
|
||||||
|
* whenever the hold is in place. */
|
||||||
|
const QString held = app::holdForeground(firstLine(prompt));
|
||||||
|
if (!held.isEmpty())
|
||||||
|
emit statusChanged(held);
|
||||||
if (!sending.isEmpty())
|
if (!sending.isEmpty())
|
||||||
emit promptImages(sending);
|
emit promptImages(sending);
|
||||||
m_turn = new Turn(this, prompt);
|
m_turn = new Turn(this, prompt);
|
||||||
@@ -210,6 +228,7 @@ void AgentCore::finishTurn(int rc, const QString &err) {
|
|||||||
Turn *t = static_cast<Turn *>(sender());
|
Turn *t = static_cast<Turn *>(sender());
|
||||||
post([this, rc, err] {
|
post([this, rc, err] {
|
||||||
setRunning(false);
|
setRunning(false);
|
||||||
|
app::releaseForeground(); /* the turn is over: let the phone sleep again */
|
||||||
/* The prompt carried whatever was pending; the engine dropped it. */
|
/* The prompt carried whatever was pending; the engine dropped it. */
|
||||||
m_attachNames.clear();
|
m_attachNames.clear();
|
||||||
emit pendingChanged();
|
emit pendingChanged();
|
||||||
|
|||||||
@@ -67,6 +67,16 @@ QString openLocalFile(const QString &path, const QString &title, QString *status
|
|||||||
* desktop has no share sheet, so the path goes to the clipboard. */
|
* desktop has no share sheet, so the path goes to the clipboard. */
|
||||||
QString shareLocalFile(const QString &path, const QString &title,
|
QString shareLocalFile(const QString &path, const QString &title,
|
||||||
QString *status);
|
QString *status);
|
||||||
|
/* Hold the machine while a turn runs, so it survives the app leaving the
|
||||||
|
* screen: a phone starts a foreground service with a partial wake lock (the
|
||||||
|
* process is one Android will not freeze or kill, and the CPU keeps going
|
||||||
|
* with the screen off) and shows a notification naming the work; a desktop
|
||||||
|
* does nothing -- the window *is* the app there. Returns "" when the hold is
|
||||||
|
* in place (or when the platform has nothing to do), a reason otherwise; the
|
||||||
|
* turn runs either way. `text` is what the phone's notification says. */
|
||||||
|
QString holdForeground(const QString &text);
|
||||||
|
/* Drop it again, at the end of the turn (a phone: the notification goes). */
|
||||||
|
void releaseForeground();
|
||||||
|
|
||||||
} // namespace app
|
} // namespace app
|
||||||
|
|
||||||
|
|||||||
@@ -577,6 +577,91 @@ QString shareLocalFile(const QString &path, const QString &title,
|
|||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* An Intent aimed at one of our own classes, by name: the Java side is
|
||||||
|
* reached through an explicit component, so no other app can call it (and no
|
||||||
|
* intent filter has to exist). Empty action means "start". */
|
||||||
|
QJniObject serviceIntent(const QJniObject &ctx, const QString &action) {
|
||||||
|
QJniObject pkg = ctx.callObjectMethod("getPackageName",
|
||||||
|
"()Ljava/lang/String;");
|
||||||
|
if (!pkg.isValid())
|
||||||
|
return QJniObject();
|
||||||
|
QJniObject comp(
|
||||||
|
"android/content/ComponentName", "(Ljava/lang/String;Ljava/lang/String;)V",
|
||||||
|
pkg.object<jstring>(),
|
||||||
|
QJniObject::fromString(
|
||||||
|
QStringLiteral("xyz.whcself.cstagent.CstService"))
|
||||||
|
.object<jstring>());
|
||||||
|
if (!comp.isValid())
|
||||||
|
return QJniObject();
|
||||||
|
QJniObject intent("android/content/Intent");
|
||||||
|
intent.callObjectMethod(
|
||||||
|
"setComponent",
|
||||||
|
"(Landroid/content/ComponentName;)Landroid/content/Intent;",
|
||||||
|
comp.object<jobject>());
|
||||||
|
if (!action.isEmpty())
|
||||||
|
intent.callObjectMethod("setAction",
|
||||||
|
"(Ljava/lang/String;)Landroid/content/Intent;",
|
||||||
|
QJniObject::fromString(action).object<jstring>());
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* A turn can outlive the screen, and a phone freezes or kills an app it
|
||||||
|
* cannot see: CstService (app/android/src/xyz/whcself/cstagent/CstService.java)
|
||||||
|
* is started as a foreground service for as long as one runs, and it takes the
|
||||||
|
* partial wake lock that keeps the CPU awake with the screen off. The explicit
|
||||||
|
* component means nothing outside the app can start it. */
|
||||||
|
QString holdForeground(const QString &text) {
|
||||||
|
if (qEnvironmentVariableIsSet("CST_APP_NO_SERVICE"))
|
||||||
|
return QString(); /* a test run leaves the phone alone */
|
||||||
|
QString err;
|
||||||
|
onGuiThread([&] {
|
||||||
|
QJniObject ctx = appContext();
|
||||||
|
if (!ctx.isValid()) {
|
||||||
|
err = QStringLiteral("no Android context");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QJniObject intent = serviceIntent(ctx, QString());
|
||||||
|
if (!intent.isValid()) {
|
||||||
|
err = QStringLiteral("no ComponentName for CstService");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
intent.callObjectMethod(
|
||||||
|
"putExtra",
|
||||||
|
"(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;",
|
||||||
|
QJniObject::fromString(QStringLiteral("text")).object<jstring>(),
|
||||||
|
QJniObject::fromString(text).object<jstring>());
|
||||||
|
QJniObject started =
|
||||||
|
sdkInt() >= 26
|
||||||
|
? ctx.callObjectMethod(
|
||||||
|
"startForegroundService",
|
||||||
|
"(Landroid/content/Intent;)Landroid/content/ComponentName;",
|
||||||
|
intent.object<jobject>())
|
||||||
|
: ctx.callObjectMethod(
|
||||||
|
"startService",
|
||||||
|
"(Landroid/content/Intent;)Landroid/content/ComponentName;",
|
||||||
|
intent.object<jobject>());
|
||||||
|
if (!started.isValid())
|
||||||
|
err = QStringLiteral("the phone refused the foreground service");
|
||||||
|
});
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
void releaseForeground() {
|
||||||
|
if (qEnvironmentVariableIsSet("CST_APP_NO_SERVICE"))
|
||||||
|
return;
|
||||||
|
onGuiThread([&] {
|
||||||
|
QJniObject ctx = appContext();
|
||||||
|
if (!ctx.isValid())
|
||||||
|
return;
|
||||||
|
QJniObject intent = serviceIntent(
|
||||||
|
ctx, QStringLiteral("xyz.whcself.cstagent.STOP"));
|
||||||
|
if (intent.isValid())
|
||||||
|
ctx.callMethod<jboolean>("stopService",
|
||||||
|
"(Landroid/content/Intent;)Z",
|
||||||
|
intent.object<jobject>());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
int platformRegisterAndroid(AgentCore *core) {
|
int platformRegisterAndroid(AgentCore *core) {
|
||||||
if (!core)
|
if (!core)
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -127,6 +127,16 @@ QString shareLocalFile(const QString &path, const QString &title,
|
|||||||
return QString();
|
return QString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A desktop has nothing to hold: the window is the app, and a turn runs as
|
||||||
|
* long as it is open -- the machine does not freeze a process it can see.
|
||||||
|
* Kept here so the front end calls one API on every platform. */
|
||||||
|
QString holdForeground(const QString &text) {
|
||||||
|
(void)text;
|
||||||
|
return QString();
|
||||||
|
}
|
||||||
|
|
||||||
|
void releaseForeground() {}
|
||||||
|
|
||||||
int platformRegisterLinux(AgentCore *core) {
|
int platformRegisterLinux(AgentCore *core) {
|
||||||
static const cst_tool_param_t params[CST_TOOL_MAX_PARAMS] = {
|
static const cst_tool_param_t params[CST_TOOL_MAX_PARAMS] = {
|
||||||
{"message", "string", 1, "text of the notification"},
|
{"message", "string", 1, "text of the notification"},
|
||||||
|
|||||||
@@ -187,6 +187,16 @@ have, so the script rewrites the shebang of the build-tools wrappers.
|
|||||||
with Open and Share -- the copy lives next to the session, marked `for_user`
|
with Open and Share -- the copy lives next to the session, marked `for_user`
|
||||||
so no request ever contains it (see `app/README.md`, "Files the agent
|
so no request ever contains it (see `app/README.md`, "Files the agent
|
||||||
delivers").
|
delivers").
|
||||||
|
* **A turn survives the app leaving the screen**: `AgentCore::send()` calls
|
||||||
|
`app::holdForeground()`, which starts `CstService`
|
||||||
|
(`app/android/src/xyz/whcself/cstagent/CstService.java`) as a foreground
|
||||||
|
service and takes a partial wake lock; `finishTurn()` stops it again. On the
|
||||||
|
NX789J: with the app sent home and the screen off, the service showed
|
||||||
|
`isForeground=true` with its `CST-agent is working` notification (id 2, the
|
||||||
|
same `cst-agent` channel), the lock `cst-agent:turn` was acquired at the
|
||||||
|
start of the turn and released at its end, `run_shell sleep 60` finished with
|
||||||
|
the screen dark, and the service was gone once the turn was over.
|
||||||
|
`CST_APP_NO_SERVICE=1` keeps a test run from touching the phone.
|
||||||
* Still to come: the SAF picker for attaching a picture from another app, the
|
* Still to come: the SAF picker for attaching a picture from another app, the
|
||||||
back gesture, and the icon glyphs -- `ThemeIcon` finds no icon files on the
|
back gesture, and the icon glyphs -- `ThemeIcon` finds no icon files on the
|
||||||
phone and draws empty boxes where the desktop shows Breeze icons.
|
phone and draws empty boxes where the desktop shows Breeze icons.
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
#define CST_VERSION_MAJOR 0
|
#define CST_VERSION_MAJOR 0
|
||||||
#define CST_VERSION_MINOR 9
|
#define CST_VERSION_MINOR 9
|
||||||
#define CST_VERSION_PATCH 3
|
#define CST_VERSION_PATCH 4
|
||||||
|
|
||||||
#define CST_VERSION_STR_(x) #x
|
#define CST_VERSION_STR_(x) #x
|
||||||
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
|
#define CST_VERSION_STR(x) CST_VERSION_STR_(x)
|
||||||
|
|||||||
Reference in New Issue
Block a user