mirror of
https://github.com/razzant/ouroboros.git
synced 2026-10-03 04:07:04 +00:00
fix(android): abandon rejected installer sessions
Expose owner-keyed cleanup for rejected or unknown PackageInstaller outcomes, so a failed confirmation cannot leave a sealed session behind. Preserve the terminal receipt and notification state without retrying the APK.
This commit is contained in:
parent
627ac43541
commit
cd1b50d2ef
5 changed files with 36 additions and 4 deletions
|
|
@ -13,6 +13,9 @@ To pass a Linux download, use content.write to transfer bytes to a writable
|
|||
provider URI (an app-owned MediaStore entry or a granted document), then install
|
||||
that URI. mode='wt' replaces contents; mode='wa' appends a subsequent chunk.
|
||||
Root android-exec remains a separate native installation path.
|
||||
After a rejected or unknown PackageInstaller result, use packages.install.abandon
|
||||
with the same idempotency_key to close that installer-owned session; it is a
|
||||
cleanup operation and never starts a replacement installation.
|
||||
|
||||
location.state reports grants/providers. location.get accepts provider and
|
||||
timeout_ms (1000..30000); its result includes age_ms and mock when a fix exists.
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ final class AndroidBridge implements Closeable {
|
|||
static final String SOCKET = "ai.ouroboros.android.rpc";
|
||||
private static final int MAX_REQUEST_BYTES = 4 * 1024 * 1024;
|
||||
private static final String[] METHODS = {"capabilities", "packages.list", "packages.inspect", "packages.sessions",
|
||||
"packages.install", "packages.install.status",
|
||||
"packages.install", "packages.install.status", "packages.install.abandon",
|
||||
"providers.list", "intent.resolve", "intent.start", "content.query", "content.call",
|
||||
"content.insert", "content.update", "content.delete", "content.read", "content.write",
|
||||
"location.state", "location.get", "accessibility.state", "accessibility.windows",
|
||||
|
|
@ -159,6 +159,7 @@ final class AndroidBridge implements Closeable {
|
|||
case "packages.sessions": return packageSessions(pm, p);
|
||||
case "packages.install": return installPackage(pm, p);
|
||||
case "packages.install.status": return installStatus(p);
|
||||
case "packages.install.abandon": return abandonInstall(pm, p);
|
||||
case "providers.list": {
|
||||
JSONArray rows = new JSONArray();
|
||||
List<ProviderInfo> providers = pm.queryContentProviders(null, 0, PackageManager.MATCH_DISABLED_COMPONENTS);
|
||||
|
|
@ -286,6 +287,26 @@ final class AndroidBridge implements Closeable {
|
|||
return new JSONObject(value).put("known", true);
|
||||
}
|
||||
|
||||
/** Abandon one installer-owned session after an unknown or rejected outcome. */
|
||||
private synchronized JSONObject abandonInstall(PackageManager pm, JSONObject p) throws Exception {
|
||||
String key = p.getString("idempotency_key");
|
||||
String value = receipts().getString(key, null);
|
||||
if (value == null) return new JSONObject().put("known", false).put("idempotency_key", key);
|
||||
JSONObject receipt = new JSONObject(value);
|
||||
int sessionId = receipt.optInt("session_id", -1);
|
||||
if (sessionId < 0) return receipt.put("known", true).put("outcome", "unknown")
|
||||
.put("retry_automatically", false);
|
||||
if (pm.getPackageInstaller().getSessionInfo(sessionId) == null)
|
||||
return receipt.put("known", true).put("outcome", "unknown")
|
||||
.put("retry_automatically", false);
|
||||
pm.getPackageInstaller().abandonSession(sessionId);
|
||||
receipt.put("status", "abandoned").put("outcome", "abandoned")
|
||||
.put("completion_observed", true).put("status_message", "Session abandoned by owner");
|
||||
receipts().edit().putString(key, receipt.toString()).commit();
|
||||
PackageInstallReceiver.cancelNotification(context, sessionId);
|
||||
return receipt.put("known", true);
|
||||
}
|
||||
|
||||
private android.content.SharedPreferences receipts() {
|
||||
return context.getSharedPreferences("package_install_receipts", Context.MODE_PRIVATE);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ public final class PackageInstallReceiver extends BroadcastReceiver {
|
|||
static final String SESSION = "session_id";
|
||||
private static final String CHANNEL = "ouroboros_package_installs";
|
||||
|
||||
static void cancelNotification(Context context, int sessionId) {
|
||||
NotificationManager manager = context.getSystemService(NotificationManager.class);
|
||||
if (manager != null) manager.cancel(CHANNEL, sessionId);
|
||||
}
|
||||
|
||||
@Override public void onReceive(Context context, Intent intent) {
|
||||
if (!ACTION.equals(intent.getAction())) return;
|
||||
String key = intent.getStringExtra(KEY);
|
||||
|
|
@ -67,7 +72,7 @@ public final class PackageInstallReceiver extends BroadcastReceiver {
|
|||
receipt.put("confirmation_delivery", "notification_failed");
|
||||
}
|
||||
} else receipt.put("confirmation_delivery", "unavailable");
|
||||
} else if (manager != null) manager.cancel(CHANNEL, sessionId);
|
||||
} else cancelNotification(context, sessionId);
|
||||
prefs.edit().putString(key, receipt.toString()).apply();
|
||||
} catch (Exception error) {
|
||||
android.util.Log.e("OuroborosHost", "Package install result could not be recorded", error);
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ class AndroidHostTest(unittest.TestCase):
|
|||
receiver = manifest.find("application/receiver[@android:name='.PackageInstallReceiver']", {"android": "http://schemas.android.com/apk/res/android"})
|
||||
self.assertIsNotNone(receiver)
|
||||
source = (HOST / "src/ai/ouroboros/android/AndroidBridge.java").read_text()
|
||||
for marker in ("\"packages.sessions\"", "\"packages.install\"",
|
||||
for marker in ("\"packages.sessions\"", "\"packages.install\"", "\"packages.install.abandon\"",
|
||||
"idempotency_key", "source_sha256", "completion_observed",
|
||||
"retry_automatically", "rollback"):
|
||||
self.assertIn(marker, source)
|
||||
|
|
|
|||
|
|
@ -244,7 +244,10 @@ package before deciding whether to recover. A pending install posts an Ouroboros
|
|||
notification that opens Android's original confirmation screen when tapped;
|
||||
it remains incomplete until the system reports success or failure. Keep
|
||||
Ouroboros notifications enabled for this handoff. A disabled or failed notification
|
||||
is reported in `confirmation_delivery`; it is not successful consent.
|
||||
is reported in `confirmation_delivery`; it is not successful consent. Use
|
||||
`packages.install.abandon` with the same idempotency key to close a rejected or
|
||||
unknown installer session after inspecting it; it records an abandoned terminal
|
||||
outcome and never starts a replacement installation.
|
||||
For this path, enable **Install unknown apps** for Ouroboros in Android settings;
|
||||
`capabilities.can_request_package_installs` reads that special access separately
|
||||
from ordinary runtime permissions. The host can read only source URIs available
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue