Skip to content

Prompt for POST_NOTIFICATIONS so Android live activities can start (discussion #5444) - #5505

Merged
shai-almog merged 17 commits into
masterfrom
android-live-activity-notification-permission
Aug 2, 2026
Merged

Prompt for POST_NOTIFICATIONS so Android live activities can start (discussion #5444)#5505
shai-almog merged 17 commits into
masterfrom
android-live-activity-notification-permission

Conversation

@shai-almog

Copy link
Copy Markdown
Collaborator

Reported in discussion #5444: "Why the Surfaces don't work in android side? any build hint necessary to work?"

No build hint is needed. Live activities were unreachable on Android 13+.

The bug

The ongoing notification a live activity lowers to needs the POST_NOTIFICATIONS runtime permission on API 33+. The build declares it in the manifest whenever surfaces.json sets "liveActivities": true (AndroidGradleBuilder.java:3021), but nothing ever asked the user for it — the only runtime requests in the port are on the push path, the local-notification path and the explicit requestNotificationPermission API, none of which run for a surfaces-only app. The default targetSdkVersion is well past 32, so there is no legacy auto-prompt either.

areNotificationsEnabled() reads false while the permission is ungranted, so on a fresh install:

  • Surfaces.isLiveActivitySupported()false
  • LiveActivity.start() bailed to an inert handle before ever reaching the bridge (LiveActivity.java:90)
  • update() / end() were silent no-ops

No exception, no log. The simulator can't catch it either — notifications are enabled there, so SurfaceDiagnostics.inertActivity never fires.

The developer guide meanwhile promised the opposite (External-Surfaces.asciidoc:126):

On Android it lowers to an ongoing notification that renders the same content; Android 13 and newer prompts for the notification permission, which the build declares for you.

The fix

isSupported() stops conflating "the user turned notifications off" with "nobody has asked yet". From API 33, when areNotificationsEnabled() is false it checks why: granted-but-disabled is a settled choice and stays unsupported; merely-ungranted is a pending prompt and counts as supported. Below API 33 behaviour is unchanged. This distinction is what makes the fix reachable at all — reporting false there would make the core skip the very call that prompts.

start() raises the prompt through the existing checkForPermission(..., forceAsk=true) path — the same one requestNotificationPermission uses, so it blocks the caller while keeping the EDT pumping, and skips the CN1 rationale dialog (safe off the EDT, where the guide tells apps to publish from). update() and end() never prompt: they act on an activity that was already granted at start.

Three refusals are distinguished and logged to CN1Surfaces instead of failing silently:

Case Behaviour
No foreground activity to prompt from (background service / push) Refused, flag left alone so the next foreground start still asks
POST_NOTIFICATIONS absent from the manifest (surfaces.json lacks "liveActivities": true) Refused with a message naming the fix — an undeclared permission is auto-denied with no UI, which would otherwise be remembered as a user refusal
The user declines Remembered, so isSupported() reports false from then on rather than nagging on every start

A decline can't trap an app: enabling notifications later in the system settings makes areNotificationsEnabled() true, which is checked first, so the flag is never consulted.

Docs follow the behaviour — the guide sentence is now true and gained the specifics, as did the Android per-platform notes and the LiveActivity.start / isSupported javadoc.

Verification

  • mvn -Pcompile-android -pl android verify — compiles, SpotBugs 0 findings
  • core-unittests -Dtest='Surface*'51/51 (SurfaceTest 29, SurfaceRasterizerTest 22)

Not exercised on hardware. The port has no unit-test harness (maven/android has no src/test), so the permission state machine is reasoned rather than run. The grant / decline / background-start paths want a manual pass on an Android 13+ device before release.

Scope note: this fixes the live-activity half. The widget half looked correct under review — receivers, per-kind provider codegen, @xml/cn1_widget_* metadata and layout copies are all in place, and exported="false" is correct for AppWidget providers.

🤖 Generated with Claude Code

…iscussion #5444)

On Android 13+ the ongoing notification a live activity lowers to needs the
POST_NOTIFICATIONS runtime permission. The build declares it in the manifest
whenever surfaces.json sets "liveActivities": true, but nothing ever asked the
user for it: the only runtime requests in the port are on the push path, the
local-notification path and the explicit requestNotificationPermission API,
none of which run for a surfaces-only app, and the default targetSdkVersion is
well past 32 so there is no legacy auto-prompt either.

The result on a fresh install was a silent dead end. areNotificationsEnabled()
reads false while the permission is ungranted, so isLiveActivitySupported()
returned false, LiveActivity.start() bailed to an inert handle before ever
reaching the bridge, and update()/end() were no-ops. No exception, no log. The
simulator can't catch it -- notifications are enabled there, so
SurfaceDiagnostics.inertActivity never fires. The developer guide meanwhile
promised "Android 13 and newer prompts for the notification permission, which
the build declares for you", which was simply untrue.

isSupported() stops conflating "the user turned notifications off" with "nobody
has asked yet". From API 33, when areNotificationsEnabled() is false it now
checks why: granted-but-disabled is a settled choice and stays unsupported,
merely-ungranted is a pending prompt and counts as supported. That distinction
is what makes the fix reachable at all -- reporting false there would make the
core skip the very call that prompts.

start() then raises the prompt through the existing checkForPermission(...,
forceAsk=true) path, the same one requestNotificationPermission uses, which
blocks the caller while keeping the EDT pumping and skips the CN1 rationale
dialog (so it is safe off the EDT, where the guide tells apps to publish from).
update() and end() never prompt: they act on an activity that was already
granted at start.

Three refusals are now distinguished and logged to CN1Surfaces instead of
failing silently -- no foreground activity to prompt from (the refusal flag is
left alone so the next foreground start still asks), POST_NOTIFICATIONS absent
from the manifest because surfaces.json lacks "liveActivities": true (an
undeclared permission is auto-denied with no UI, which would otherwise be
remembered as a user refusal), and an actual decline. A decline is persisted in
the surface store so isSupported() reports false from then on rather than
nagging on every start; it can't trap an app, because enabling notifications
later in the system settings makes areNotificationsEnabled() true and the flag
is never consulted.

The guide sentence is now true and gained the specifics, as did the Android
per-platform notes and the LiveActivity.start/isSupported javadoc.

android module compiles with SpotBugs at zero findings; core-unittests
Surface* 51/51. Not exercised on hardware -- the port has no unit-test harness
-- so the grant/decline/background-start paths still want a manual pass on an
Android 13+ device.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 1, 2026 15:19

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses Android 13+ behavior where live activities (lowered to ongoing notifications) were effectively unreachable because POST_NOTIFICATIONS was declared but never requested at runtime. It adds a first-start prompt path, tracks user refusal to avoid repeated prompts, and updates developer documentation/Javadoc to match the new behavior.

Changes:

  • Persist a “notifications permission refused” flag for live activities on Android (CN1SurfaceStore).
  • Update Android live-activity support and start logic to distinguish “pending prompt” vs “disabled”, and to raise POST_NOTIFICATIONS permission prompt on first start (CN1LiveActivityManager).
  • Update surfaces developer guide and LiveActivity Javadoc to describe Android 13+ prompting/refusal behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
Ports/Android/src/com/codename1/impl/android/surfaces/CN1SurfaceStore.java Adds persisted flag for remembered notification permission refusal.
Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java Adds permission prompting and refusal handling for POST_NOTIFICATIONS; adjusts support detection.
docs/developer-guide/External-Surfaces.asciidoc Updates Android live-activity documentation to reflect prompt/refusal and foreground requirement.
CodenameOne/src/com/codename1/surfaces/LiveActivity.java Updates public API docs to describe Android 13+ permission prompt/refusal behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java Outdated

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4b5075bceb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java Outdated
Comment thread Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java Outdated
Comment thread Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java Outdated
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Developer Guide build artifacts are available for download from this workflow run:

Developer Guide quality checks:

  • AsciiDoc linter: No issues found (report)
  • Vale: No alerts found (report)
  • Paragraph capitalization: No paragraph capitalization issues (report)
  • LanguageTool: No grammar matches (report)
  • Image references: No unused images detected (report)

… precisely

Three review findings, all correct.

Codex P1: AndroidNativeUtil.getActivity() stays non-null after onStop, so a
null check does not prove there is UI to prompt from -- a background fetch or
push handler running with the app stopped would have tried to raise the dialog
anyway. Check CodenameOneActivity.isBackground() instead. (The comment's other
scenario, getActivity() returning a CodenameOneBackgroundFetchActivity, cannot
happen: setActivity is typed to CodenameOneActivity. The instanceof still earns
its keep by making the cast total.)

Codex P2: a dismissed dialog was latched as a permanent refusal. The platform
hands back one bare boolean for an explicit "Don't allow", a dialog swiped away
without choosing, and a request auto-denied with no UI at all, so the first
false cannot be read as a decision. Count prompts instead of latching a flag,
bounded at two -- Android auto-denies after two refusals, so a third attempt
would never reach the user. Only attempts that actually reached the user count:
no foreground activity and a missing manifest entry both return without
spending one.

Codex P2: denials made outside surfaces (push registration,
Display.requestNotificationPermission, local notifications) left the old
surfaces-private flag unset, so isSupported() claimed true. Nothing is now
consulted before the live permission state, so a grant from any of those paths
takes effect immediately and clears the count, and a denial from them costs at
most the two bounded attempts -- which show no UI once the system is
auto-denying -- before isSupported() settles on false.

Copilot: isPermissionDeclared() could not tell "no permissions declared" from
"lookup failed" because getRequestedPermissions() flattens both into an empty
list, and it can throw NPE of its own on a null requestedPermissions array. Read
PackageInfo directly instead and answer the narrower question the caller
actually has -- isPermissionMissing() is true only when the manifest was read
and POST_NOTIFICATIONS is definitely absent, so an unreadable manifest proceeds
to the prompt rather than being mistaken for a broken build.

SpotBugs rejected the first cut of that helper (NP_BOOLEAN_RETURN_NULL on a
three-state Boolean); the boolean phrasing above is both cleaner and clean under
the gate.

Docs follow: the guide, the Android per-platform notes and the LiveActivity
javadoc now describe two bounded attempts rather than a single latched refusal.

android module compiles with SpotBugs at zero findings; core-unittests Surface*
51/51.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 1, 2026 15:32
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

✅ Continuous Quality Report

Test & Coverage

Static Analysis

  • SpotBugs [Report archive]
    • ByteCodeTranslator: 0 findings (no issues)
    • android: 0 findings (no issues)
    • codenameone-maven-plugin: 0 findings (no issues)
    • core-unittests: 0 findings (no issues)
    • ios: 0 findings (no issues)
  • PMD: 0 findings (no issues) [Report archive]
  • Checkstyle: 0 findings (no issues) [Report archive]

Generated automatically by the PR CI workflow.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2717c61356

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Cloudflare Preview

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java:229

  • recordNotificationPrompt(ctx) is executed before the permission request. This means a successful grant can still briefly persist an incremented prompt count (it is later cleared), and in crash/race/exception scenarios it can leave a stale count even though the user granted (or the dialog never showed). This can incorrectly exhaust the 2-attempt budget after a later revoke. Recording should happen only after the request returns ungranted (i.e., only for outcomes that “didn’t end up granted”, per the method’s doc).
        CN1SurfaceStore.recordNotificationPrompt(ctx);
        boolean granted;
        try {
            granted = AndroidImplementation.checkForPermission(
                    "android.permission.POST_NOTIFICATIONS",
                    "This is required to show live activities", true);
        } catch (Throwable t) {

… only answers

Codex P2: isSupported() returns early the moment areNotificationsEnabled() is
true, so an app that observes a grant only through that call never cleared the
stored prompt count. A later revoke then left one attempt instead of the two
CN1SurfaceStore.clearNotificationPrompts documents. Clear it there too --
observing the grant is what retires earlier refusals, not the path it arrived
by.

Copilot: recordNotificationPrompt ran before the request, so a granted outcome
transiently incremented the count, and an app killed while the dialog was up
spent an attempt on a question the user never answered. Record only once the
request comes back ungranted; the throw path does not record either, for the
same reason. This also matches what the counter's own javadoc claims to count.

Vale (the failing `build` job): Microsoft.Auto rejects the hyphen in
"auto-denies". Reworded to "Android stops showing the dialog after two refusals
anyway", which reads better than the unhyphenated spelling would.

android module compiles with SpotBugs at zero findings; core-unittests Surface*
51/51; vale clean on the developer guide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 1, 2026 15:44

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9597059ae3

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java Outdated
Codex P2: LiveActivity.start is documented as callable from any thread, and
checkForPermission drives the activity's one shared requestForPermission flag
and request code. Two concurrent starts on a fresh install therefore both passed
canPromptAgain, both entered the request, and a single dialog outcome released
both callers -- which then each recorded an attempt, spending the whole
two-prompt budget on one user answer.

Guard the request with a static lock and re-read the permission under it, so a
caller that waited sees whatever the winner produced: granted returns true
without a second dialog, and an outcome is counted once. The re-check has to be
inside the lock rather than only at the top, otherwise the waiter still acts on
the state it sampled before blocking.

Holding the lock across a user-visible dialog is intended -- start already
documents that it blocks the calling thread until the user answers -- and it is
the only lock on this path, so there is no ordering hazard.

android module compiles with SpotBugs at zero findings; core-unittests Surface*
51/51.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java:112

  • In start(), the short-circuit !isSupported(ctx) || ... prevents ensureNotificationPermission() from running when the prompt budget is exhausted (the exact state where isSupported() returns false on API 33+). This makes ensureNotificationPermission()'s "refused twice" logging unreachable and leaves the budget check duplicated in two places. Consider running ensureNotificationPermission() before isSupported() (with explicit ctx/API guards) so the refusal reason can still be logged and the logic stays consistent.
    /// Raises the `POST_NOTIFICATIONS` prompt first when Android 13+ needs it, blocking the
    /// calling thread until the user answers.
    public static String start(Context ctx, String descriptorJson, Map<String, byte[]> images) {

docs/developer-guide/External-Surfaces.asciidoc:180

  • This sentence says the prompt attempt is only counted when it "reached the user", but the implementation can’t reliably distinguish a visible dialog from an OS auto-deny; what it actually avoids counting is cases where no request can be issued (no foreground activity / permission missing) or where the request didn’t complete (exception). Suggest rephrasing to match the behavior.
Widgets are rendered through `RemoteViews` by generated per-kind providers; no Android-specific build hints are needed, and the per-kind sizing metadata comes from `surfaces.json`. Timeline entry flips are scheduled with inexact alarms (a 30-second window) to avoid the exact-alarm permission by default; apps that need to-the-second flips can opt in with the `android.surfaces.exactAlarms` build hint. Second-precision countdowns still tick natively through `Chronometer`. Live activities lower to ongoing notifications, which on Android 13 and newer require the `POST_NOTIFICATIONS` runtime permission: the build declares it for you when `surfaces.json` sets `"liveActivities": true`, and the first `LiveActivity.start(...)` raises the system prompt, blocking the calling thread until the user answers. Codename One raises it at most twice across an install -- Android stops showing the dialog after two refusals anyway -- and only counts an attempt that reached the user, so a dismissed dialog costs one attempt rather than disabling live activities outright. Start the first activity while your app is in the foreground: there is no UI to prompt from in a background service or push handler, so a start from one before the permission is granted is refused without spending an attempt (`adb logcat -s CN1Surfaces` says which case you hit). A grant from anywhere counts -- these prompts, push registration, `Display.requestNotificationPermission(...)` or the system settings -- because the live permission state is always checked first. The approximations listed in the node catalog table apply: font weights collapse to regular/bold, circular progress falls back to linear, relative dates refresh only on entry flips, and vector nodes render as bitmaps.

Copilot AI review requested due to automatic review settings August 1, 2026 15:54

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4d74fc51e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java:115

  • start() short-circuits on isSupported(ctx) before calling ensureNotificationPermission(ctx). When API 33+ returns false from isSupported() due solely to the prompt budget being exhausted (canPromptAgain()==false), this prevents ensureNotificationPermission() from emitting its warning log about the two refusals, reintroducing a silent failure mode.
    public static String start(Context ctx, String descriptorJson, Map<String, byte[]> images) {
        if (!isSupported(ctx) || !ensureNotificationPermission(ctx)) {
            return null;
        }

docs/developer-guide/External-Surfaces.asciidoc:180

  • This paragraph says the permission prompt logic "only counts an attempt that reached the user", but the implementation counts any completed request that returns ungranted (including cases where Android auto-denies without showing UI). The code only avoids counting cases where it cannot make a request at all (e.g. no foreground activity, missing manifest permission, or an exception).
Widgets are rendered through `RemoteViews` by generated per-kind providers; no Android-specific build hints are needed, and the per-kind sizing metadata comes from `surfaces.json`. Timeline entry flips are scheduled with inexact alarms (a 30-second window) to avoid the exact-alarm permission by default; apps that need to-the-second flips can opt in with the `android.surfaces.exactAlarms` build hint. Second-precision countdowns still tick natively through `Chronometer`. Live activities lower to ongoing notifications, which on Android 13 and newer require the `POST_NOTIFICATIONS` runtime permission: the build declares it for you when `surfaces.json` sets `"liveActivities": true`, and the first `LiveActivity.start(...)` raises the system prompt, blocking the calling thread until the user answers. Codename One raises it at most twice across an install -- Android stops showing the dialog after two refusals anyway -- and only counts an attempt that reached the user, so a dismissed dialog costs one attempt rather than disabling live activities outright. Start the first activity while your app is in the foreground: there is no UI to prompt from in a background service or push handler, so a start from one before the permission is granted is refused without spending an attempt (`adb logcat -s CN1Surfaces` says which case you hit). A grant from anywhere counts -- these prompts, push registration, `Display.requestNotificationPermission(...)` or the system settings -- because the live permission state is always checked first. The approximations listed in the node catalog table apply: font weights collapse to regular/bold, circular progress falls back to linear, relative dates refresh only on entry flips, and vector nodes render as bitmaps.

… claims

Copilot: start() tested isSupported() first, which reports false exactly when the
prompt budget is spent, so it short-circuited past the one branch that logs why
the start was refused -- the case a developer most needs to see, in a PR whose
whole point is that this failure used to be silent. Ask for the permission
first. Nothing gets prompted that isSupported() would have rejected on
capability grounds, because POST_NOTIFICATIONS only exists from API 33 and live
activities need API 24.

Copilot: the guide claimed attempts are only counted when the prompt "reached
the user", which the implementation cannot tell -- an OS auto-deny is reported
exactly like a refusal. What it actually skips counting is a request it never
issued (no foreground activity, permission missing from the manifest) or one
that threw. Reworded to say that, and to admit a dismissed dialog does cost an
attempt.

Codex: PERMISSION_LOCK serializes surfaces against itself, not against a camera
or location request in flight elsewhere -- those share the same activity-wide
requestForPermission flag and request code 1, and their callback can release
this caller early. That is a pre-existing defect in the shared permission
machinery, and fixing it properly means per-request completion state in
AndroidImplementation, which touches every permission in the port and wants its
own change. Documented at the lock instead, with why the damage is bounded: a
spuriously counted attempt costs one of two prompts, and any later grant from
any source clears the count.

android module compiles with SpotBugs at zero findings; core-unittests Surface*
51/51; vale clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 1, 2026 16:01
@shai-almog

Copy link
Copy Markdown
Collaborator Author

Review status through 4c10300f3 — recording the two Copilot findings that arrived as suppressed comments, since those do not create resolvable threads:

  • start() short-circuit hid the exhausted-budget log — real, and the worst kind of regression for this PR specifically: isSupported() reports false exactly when the prompt budget is spent, so testing it first skipped the one branch that logs why the start was refused, reintroducing the silent failure this change exists to remove. Fixed by asking for the permission before the capability check; nothing gets prompted that isSupported() would have rejected anyway, since POST_NOTIFICATIONS only exists from API 33 and live activities need API 24.
  • The guide overclaimed what is counted — it said an attempt is only spent when the prompt "reached the user", which the implementation cannot know: Android reports an auto-deny exactly like a refusal. What it actually skips counting is a request it never issued or one that threw. Reworded, and it now admits a dismissed dialog does cost an attempt.

Copilot re-reported both against 4d74fc51e; they were already fixed in 4c10300f3.

All six inline threads are resolved. One was closed with a partial decline rather than a fix — codex correctly noted that PERMISSION_LOCK does not serialize against permission requests raised elsewhere in the app, because AndroidImplementation.checkForPermission shares one activity-wide flag and request code across all ~40 of its call sites. That is a pre-existing defect in the shared machinery, not in this path, and fixing it properly (per-request completion state, distinct request codes) touches every permission in the Android port. It is documented at the lock with its bounded blast radius; happy to open a separate issue to track it.

Still outstanding and not something I can close from here: none of the permission state machine has run on hardware. The two-attempt budget, the foreground gate, the manifest check and the lock are all reasoned. A pass on an Android 13+ device — grant, decline twice, dismiss, background start — is worth doing before release.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c10300f38

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java Outdated
Comment thread Ports/Android/src/com/codename1/impl/android/surfaces/CN1SurfaceStore.java Outdated
…pe the budget to the install

Codex: an app that publishes widgets but never sets "liveActivities": true gets
no POST_NOTIFICATIONS declaration from the build, and isSupported() then
returned true forever on API 33+ -- ungranted, count zero -- while every start()
stopped at the manifest check without spending an attempt, so the count could
never settle it. Deliberately not counting those attempts is what made the lie
permanent. isSupported() now consults the manifest too. The diagnostic still
reaches the log because round 4 moved ensureNotificationPermission ahead of
isSupported in start().

The manifest lookup is now cached: isSupported is called per screen or per
frame by real apps, and a binder round trip per call is not free. Only a
definite verdict is cached -- a failed lookup is retried rather than frozen --
and a manifest cannot change under a live process, since an app update kills it.

Codex: Codename One builds allow backup by default, so the cn1surfaces
preferences ride along to a reinstall or a new device. A restored count of 2
would suppress the dialog forever on what the API documents as a fresh install:
the same permanent silent failure this whole path exists to remove. The count is
now stamped with the installation that earned it, using firstInstallTime --
preserved across app updates, which must not hand back a spent budget, and
changed by a genuine reinstall or a restore onto another device. A count whose
stamp does not match reads as zero. That keeps it self-contained; excluding the
file from backup would have meant build-side backup rules for one integer.

android module compiles with SpotBugs at zero findings; core-unittests Surface*
51/51.

Unrelated CI flake on the previous head: HealthEdtDeliveryTest
.aFacadeActionDeliversOnTheEdt failed once in build-test (8). It is an EDT
timing test in com.codename1.health, nothing this branch touches, it passes
locally 7/7, and master is green. This push re-runs it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ba24fe660e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java Outdated
Comment thread Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (1)

Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java:380

  • The comment in this finally block says that a thrown permission request "leaves waiters free to ask rather than adopting an outcome that never happened", but answeredWhileWaiting also treats permissionRequestInFlight as an adopt condition. Because permissionRequestInFlight is intentionally cleared only in the outer finally (after PERMISSION_LOCK is released), a waiting caller can still "adopt" a thrown/failed request in the handoff window. Either adjust the comment to match the behavior, or adjust the state machine so thrown requests don’t get adopted.
                    // The generation moves only for an answer, so a throw leaves waiters free to
                    // ask rather than adopting an outcome that never happened. The in-flight flag
                    // is deliberately NOT cleared here -- see the outer finally.

@shai-almog

shai-almog commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 181 screenshots: 181 matched.
✅ JavaScript-port screenshot tests passed.

@shai-almog

shai-almog commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 148 screenshots: 148 matched.
✅ Native Mac screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 231 seconds

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 61ms / native 3ms = 20.3x speedup
SIMD float-mul (64K x300) java 64ms / native 3ms = 21.3x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 160.000 ms
Base64 CN1 decode 124.000 ms
Base64 native encode 617.000 ms
Base64 encode ratio (CN1/native) 0.259x (74.1% faster)
Base64 native decode 301.000 ms
Base64 decode ratio (CN1/native) 0.412x (58.8% faster)
Base64 SIMD encode 58.000 ms
Base64 encode ratio (SIMD/CN1) 0.363x (63.7% faster)
Base64 SIMD decode 43.000 ms
Base64 decode ratio (SIMD/CN1) 0.347x (65.3% faster)
Base64 encode ratio (SIMD/native) 0.094x (90.6% faster)
Base64 decode ratio (SIMD/native) 0.143x (85.7% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 11.000 ms
Image createMask (SIMD on) 5.000 ms
Image createMask ratio (SIMD on/off) 0.455x (54.5% faster)
Image applyMask (SIMD off) 62.000 ms
Image applyMask (SIMD on) 44.000 ms
Image applyMask ratio (SIMD on/off) 0.710x (29.0% faster)
Image modifyAlpha (SIMD off) 50.000 ms
Image modifyAlpha (SIMD on) 60.000 ms
Image modifyAlpha ratio (SIMD on/off) 1.200x (20.0% slower)
Image modifyAlpha removeColor (SIMD off) 72.000 ms
Image modifyAlpha removeColor (SIMD on) 60.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 0.833x (16.7% faster)

@shai-almog

shai-almog commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 149 screenshots: 149 matched.
✅ Native iOS Metal screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 485 seconds

Build and Run Timing

Metric Duration
Simulator Boot 88000 ms
Simulator Boot (Run) 1000 ms
App Install 14000 ms
App Launch 2000 ms
Test Execution 466000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 110ms / native 3ms = 36.6x speedup
SIMD float-mul (64K x300) java 69ms / native 27ms = 2.5x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 219.000 ms
Base64 CN1 decode 148.000 ms
Base64 native encode 952.000 ms
Base64 encode ratio (CN1/native) 0.230x (77.0% faster)
Base64 native decode 390.000 ms
Base64 decode ratio (CN1/native) 0.379x (62.1% faster)
Base64 SIMD encode 62.000 ms
Base64 encode ratio (SIMD/CN1) 0.283x (71.7% faster)
Base64 SIMD decode 52.000 ms
Base64 decode ratio (SIMD/CN1) 0.351x (64.9% faster)
Base64 encode ratio (SIMD/native) 0.065x (93.5% faster)
Base64 decode ratio (SIMD/native) 0.133x (86.7% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 9.000 ms
Image createMask (SIMD on) 4.000 ms
Image createMask ratio (SIMD on/off) 0.444x (55.6% faster)
Image applyMask (SIMD off) 174.000 ms
Image applyMask (SIMD on) 190.000 ms
Image applyMask ratio (SIMD on/off) 1.092x (9.2% slower)
Image modifyAlpha (SIMD off) 210.000 ms
Image modifyAlpha (SIMD on) 184.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.876x (12.4% faster)
Image modifyAlpha removeColor (SIMD off) 186.000 ms
Image modifyAlpha removeColor (SIMD on) 247.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.328x (32.8% slower)

The Android legs of this PR failed deterministically -- three jobs, twice each --
with DesktopMode, Media360Panorama, VRStereoScene and VideoIODecodedFrames
producing no screenshot, sixteen tests never running, and no completion marker.
The same four pass on master.

Root cause is this branch. SurfacesPublishTest does:

    if (LiveActivity.isSupported()) { LiveActivity.start(...); }

Before this branch isSupported() returned false on an emulator, because
POST_NOTIFICATIONS is ungranted and areNotificationsEnabled() reads false, so
the start was skipped. Making that case report supported is the entire point of
the fix -- and it means the suite now reaches start(), which asks for the
permission. On an unattended emulator nobody answers: the requesting thread
waits on the dialog indefinitely and the modal sits on top of every screenshot
that follows, so the suite stops producing output partway through.

Grant the permission from the instrumentation entry point before the app is
launched, which is what an unattended suite has to do and what a device user
does once by hand. The suite then exercises the granted live-activity path
rather than the prompt. The grant is best-effort: below API 33 the permission
does not exist and pm grant rejects it, which must not fail the run.

Product behaviour is deliberately unchanged. Blocking the caller until the user
answers is what every other Codename One permission does through
AndroidImplementation.checkForPermission -- camera, location, contacts -- and
what LiveActivity.start already documents.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 1, 2026 18:26
check-copyright-headers validates added and modified files, and this one
predates the gate without a header, so touching it in the previous commit
tripped the check. Add the Codename One GPLv2 + Classpath Exception header the
script expects; verified locally with scripts/check-copyright-headers.sh.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (1)

scripts/device-runner-app/androidTest/DeviceRunnerInstrumentationTest.java:70

  • The catch-all warning message is misleading on API 33+ failures (it always says the failure is "expected below API 33"), and concatenating the Throwable drops the stack trace from logcat. This makes it harder to diagnose real failures (e.g. permission missing from manifest) where the suite may later hang on a system dialog.
    /// thread waits on it indefinitely and the modal sits on top of every screenshot that follows,
    /// so the suite stops emitting output partway through and never reaches its completion marker.
    /// Granting up front is what a device user does once by hand, and it lets the suite exercise

Copilot AI review requested due to automatic review settings August 1, 2026 18:29

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (1)

scripts/device-runner-app/androidTest/DeviceRunnerInstrumentationTest.java:91

  • The catch block logs the exception via string concatenation, which drops the stack trace. Using the Log.w overload with a Throwable will preserve useful diagnostics in cases where this fails for reasons other than "below API 33".
            Log.w(TAG, "Could not grant POST_NOTIFICATIONS (expected below API 33): " + t);

@shai-almog

shai-almog commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 217 screenshots: 217 matched.
✅ Native Apple Watch (watchOS, Core Graphics) screenshot tests passed.

@shai-almog

shai-almog commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 144 screenshots: 144 matched.
✅ Native Apple TV (tvOS, Metal) screenshot tests passed.

@shai-almog

shai-almog commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 151 screenshots: 151 matched.

Native Android coverage

  • 📊 Line coverage: 7.85% (7605/96841 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 7.72% (39610/513127), branch 2.80% (1363/48627), complexity 3.15% (1646/52177), method 4.87% (1345/27595), class 9.98% (367/3679)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

✅ Native Android screenshot tests passed.

Native Android coverage

  • 📊 Line coverage: 7.85% (7605/96841 lines covered) [HTML preview] (artifact android-coverage-report, jacocoAndroidReport/html/index.html)
    • Other counters: instruction 7.72% (39610/513127), branch 2.80% (1363/48627), complexity 3.15% (1646/52177), method 4.87% (1345/27595), class 9.98% (367/3679)
    • Lowest covered classes
      • kotlin.collections.kotlin.collections.ArraysKt___ArraysKt – 0.00% (0/6367 lines covered)
      • kotlin.collections.unsigned.kotlin.collections.unsigned.UArraysKt___UArraysKt – 0.00% (0/2384 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.ClassReader – 0.00% (0/1524 lines covered)
      • kotlin.collections.kotlin.collections.CollectionsKt___CollectionsKt – 0.00% (0/1187 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.MethodWriter – 0.00% (0/922 lines covered)
      • kotlin.sequences.kotlin.sequences.SequencesKt___SequencesKt – 0.00% (0/736 lines covered)
      • com.google.common.cache.com.google.common.cache.LocalCache$Segment – 0.00% (0/726 lines covered)
      • okio.okio.Buffer – 0.00% (0/687 lines covered)
      • kotlin.text.kotlin.text.StringsKt___StringsKt – 0.00% (0/625 lines covered)
      • org.jacoco.agent.rt.internal_0e20598.asm.org.jacoco.agent.rt.internal_0e20598.asm.Frame – 0.00% (0/570 lines covered)

Benchmark Results

Detailed Performance Metrics

Metric Duration
SIMD kernel backend scalar fallback (no native SIMD)
SIMD int-add (64K x300) java 314ms / native 211ms = 1.4x speedup
SIMD float-mul (64K x300) java 136ms / native 152ms = 0.8x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path gated to scalar (CPU autovectorizes scalar; explicit SIMD not beneficial here)
Base64 CN1 encode 116.000 ms
Base64 CN1 decode 77.000 ms
Base64 native encode 527.000 ms
Base64 encode ratio (CN1/native) 0.220x (78.0% faster)
Base64 native decode 354.000 ms
Base64 decode ratio (CN1/native) 0.218x (78.2% faster)
Image encode benchmark status skipped (SIMD unsupported)

@shai-almog

shai-almog commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

Compared 143 screenshots: 143 matched.
✅ Native iOS screenshot tests passed.

Benchmark Results

  • VM Translation Time: 0 seconds
  • Compilation Time: 258 seconds

Build and Run Timing

Metric Duration
Simulator Boot 63000 ms
Simulator Boot (Run) 0 ms
App Install 14000 ms
App Launch 6000 ms
Test Execution 414000 ms

Detailed Performance Metrics

Metric Duration
SIMD kernel backend SSE2 (x64) / NEON (arm64) native kernels
SIMD int-add (64K x300) java 72ms / native 5ms = 14.4x speedup
SIMD float-mul (64K x300) java 144ms / native 5ms = 28.8x speedup
SIMD kernel correctness PASS (native result == scalar reference)
Base64 payload size 8192 bytes
Base64 benchmark iterations 6000
Base64 SIMD byte path active (NEON-accelerated)
Base64 CN1 encode 183.000 ms
Base64 CN1 decode 233.000 ms
Base64 native encode 1396.000 ms
Base64 encode ratio (CN1/native) 0.131x (86.9% faster)
Base64 native decode 382.000 ms
Base64 decode ratio (CN1/native) 0.610x (39.0% faster)
Base64 SIMD encode 95.000 ms
Base64 encode ratio (SIMD/CN1) 0.519x (48.1% faster)
Base64 SIMD decode 69.000 ms
Base64 decode ratio (SIMD/CN1) 0.296x (70.4% faster)
Base64 encode ratio (SIMD/native) 0.068x (93.2% faster)
Base64 decode ratio (SIMD/native) 0.181x (81.9% faster)
Image encode benchmark iterations 100
Image createMask (SIMD off) 8.000 ms
Image createMask (SIMD on) 2.000 ms
Image createMask ratio (SIMD on/off) 0.250x (75.0% faster)
Image applyMask (SIMD off) 84.000 ms
Image applyMask (SIMD on) 47.000 ms
Image applyMask ratio (SIMD on/off) 0.560x (44.0% faster)
Image modifyAlpha (SIMD off) 110.000 ms
Image modifyAlpha (SIMD on) 35.000 ms
Image modifyAlpha ratio (SIMD on/off) 0.318x (68.2% faster)
Image modifyAlpha removeColor (SIMD off) 71.000 ms
Image modifyAlpha removeColor (SIMD on) 108.000 ms
Image modifyAlpha removeColor ratio (SIMD on/off) 1.521x (52.1% slower)

… stop guessing at the prompt

Two findings on the legacy-target branch from round 11, both correct.

Codex: the bypass returned true before isPermissionMissing ran, so an app that
uses widgets but never set "liveActivities": true got an active handle for a
notification Android would not post, and isSupported then settled on false down
its own legacy path without ever emitting the missing-manifest diagnosis. The
declaration check now comes first.

Codex: returning true also meant notifyActivity posted immediately. The
platform's prompt is asynchronous -- it is tied to the next activity start, not
to channel creation returning -- so a fresh install was still ungranted, nothing
appeared, and start nevertheless handed back a handle reporting isActive() with
no repost once the user allowed. Create the channel so the platform has its
trigger, re-check, and refuse this start when the permission has not arrived;
the next start posts for real.

isSupported's legacy branch stops guessing too. It inferred "the user was asked"
from a channel existing, which does not follow, and Android exposes nothing that
separates "not asked yet" from "asked and declined" -- areNotificationsEnabled
is false for both. It now reports the one knowable thing, whether the permission
was declared, so a legacy app keeps retrying rather than being locked out by a
wrong guess. hasNotificationChannels was the only caller of that inference and
is deleted rather than left as dead code.

This branch has now been wrong in three consecutive rounds, in three different
directions, and it cannot be exercised here: it needs an Android 13+ device
running a deliberately down-targeted build. It is reasoned from documented
behaviour, not observed, and deserves a device before release more than anything
else in this change.

android module compiles with SpotBugs at zero findings; core-unittests Surface*
51/51.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 1, 2026 21:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (1)

scripts/device-runner-app/androidTest/DeviceRunnerInstrumentationTest.java:93

  • grantNotificationPermission() assumes pm grant succeeded and always logs "Granted" once the pipe is drained. UiAutomation.executeShellCommand() generally won't throw on command failure (e.g., permission not in manifest / not allowed), so this can silently leave POST_NOTIFICATIONS ungranted and the suite can still hang on the runtime prompt. Capture the command output and only log success when it is empty (or otherwise indicates success); also skip running the command entirely below API 33 to avoid expected failures.
    private void grantNotificationPermission(String packageName) {
        String command = "pm grant " + packageName + " android.permission.POST_NOTIFICATIONS";
        try {
            UiAutomation automation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
            ParcelFileDescriptor pfd = automation.executeShellCommand(command);

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a64c5afceb

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

…ground too

Codex: the legacy-target branch created the notification channel without the
foreground check the modern path puts in front of its request. Creating that
channel is precisely what arms the platform-owned prompt, and Android defers the
prompt to the next activity launch -- so a background push or service reaching
this path would ambush the user with a notification dialog the next time they
opened the app, for a live activity they never saw asked for, and the start was
refused anyway.

Apply the same foreground gate before creating the channel.

android module compiles with SpotBugs at zero findings; core-unittests Surface*
51/51.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings August 1, 2026 22:50

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Ports/Android/src/com/codename1/impl/android/surfaces/CN1LiveActivityManager.java:421

  • AndroidImplementation.checkForPermission() can return false without ever showing/issuing a permission request (e.g. if getActivity() becomes null). In that case this code still treats the result as “answered”, increments the generation, and records a prompt attempt, which can exhaust the 2-attempt budget without the user ever seeing a prompt.
            boolean granted = false;
            boolean answered = false;
            try {
                granted = AndroidImplementation.checkForPermission(
                        "android.permission.POST_NOTIFICATIONS",

scripts/device-runner-app/androidTest/DeviceRunnerInstrumentationTest.java:93

  • pm grant failures won’t throw here; they are printed to the command’s stdout/stderr stream. The current implementation drains and discards the output, then logs “Granted …” unconditionally, which can both mislead debugging and mask why the permission wasn’t actually granted (e.g. below API 33, or if the app didn’t declare the permission). Also, logging the Throwable via string concatenation drops the stack trace.
        String command = "pm grant " + packageName + " android.permission.POST_NOTIFICATIONS";
        try {
            UiAutomation automation = InstrumentationRegistry.getInstrumentation().getUiAutomation();
            ParcelFileDescriptor pfd = automation.executeShellCommand(command);
            try (FileInputStream fis = new FileInputStream(pfd.getFileDescriptor())) {

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7fc392caba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@shai-almog
shai-almog merged commit 3898e77 into master Aug 2, 2026
33 checks passed
@shai-almog
shai-almog deleted the android-live-activity-notification-permission branch August 2, 2026 01:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants