Skip to content

Commit 675f9f7

Browse files
Add manual push completion handler support for iOS (#4335)
Fixed #3541 * Add manual push completion handler support for iOS Added Display.notifyPushCompletion() to allow applications to manually signal when they have finished handling a push notification on iOS. This is useful for apps that need to perform background tasks (like playing audio) before the app is suspended. This feature is enabled by setting the build hint `ios.delayPushCompletion` to `true`. * Add manual push completion handler support for iOS Added Display.notifyPushCompletion() to allow applications to manually signal when they have finished handling a push notification on iOS. This is useful for apps that need to perform background tasks (like playing audio) before the app is suspended. This feature is enabled by setting the build hint `ios.delayPushCompletion` to `true`. * Implement cross-platform push completion handling for background tasks. Added Display.notifyPushCompletion() to manually signal the completion of a background push task (e.g. playing audio). On iOS, this delays the call to the system completion handler if the `ios.delayPushCompletion` or `delayPushCompletion` property is set. On Android, this releases a partial WakeLock acquired when the push is received, preventing the device from sleeping during the task if the `android.delayPushCompletion` or `delayPushCompletion` property is set. Updated `IOSImplementation`, `AndroidImplementation`, and `PushNotificationService` to support this logic. * Implement cross-platform push completion handling for background tasks (opt-in). Added Display.notifyPushCompletion() to manually signal the completion of a background push task. This mechanism is OPT-IN via the `delayPushCompletion` (or `android.delayPushCompletion`/`ios.delayPushCompletion`) build hint. If the hint is present and true: - Android: Acquires a `PARTIAL_WAKE_LOCK` upon receiving a push, preventing the device from sleeping until `notifyPushCompletion()` is called (or timeout). - iOS: Ensures the `remote-notification` background mode is enabled and delays firing the system completion handler until `notifyPushCompletion()` is called. If the hint is NOT present (default behavior): - Android: No wake lock is acquired. - iOS: The completion handler is fired immediately after the push callback. Updated `IOSImplementation`, `AndroidImplementation`, `PushNotificationService`, `AndroidGradleBuilder`, and `IPhoneBuilder`. * Implement cross-platform push completion handling for background tasks (opt-in). Added Display.notifyPushCompletion() to manually signal the completion of a background push task. This mechanism is OPT-IN via the `delayPushCompletion` (or `android.delayPushCompletion`/`ios.delayPushCompletion`) build hint. If the hint is present and true: - Android: Acquires a `PARTIAL_WAKE_LOCK` upon receiving a push, preventing the device from sleeping until `notifyPushCompletion()` is called (or timeout). - iOS: Ensures the `remote-notification` background mode is enabled and delays firing the system completion handler until `notifyPushCompletion()` is called. The builders (`IPhoneBuilder`, `AndroidGradleBuilder`) have been updated to check for this hint and automatically: 1. Inject the permission (Android) or capability (iOS) into the native project configuration. 2. Inject the `delayPushCompletion` property into the runtime environment so the logic in `PushNotificationService` and `IOSImplementation` activates. Updated the developer guide to document this new feature. * Fix compilation error in AndroidImplementation. Corrected the placement of `notifyPushCompletion()` in `AndroidImplementation.java` to resolve the "java.lang.Override is not a repeatable annotation type" error caused by improper nesting with `notifyCommandBehavior`. This completes the cross-platform push completion support feature, which is OPT-IN via the `delayPushCompletion` build hint. If the hint is present and true: - Android: Acquires a `PARTIAL_WAKE_LOCK` upon receiving a push, releasing it only when `notifyPushCompletion()` is called. - iOS: Delays firing the system completion handler until `notifyPushCompletion()` is called. Builders have been updated to inject the necessary permissions and runtime properties. Developer guide documentation has been updated. --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
1 parent c2a1706 commit 675f9f7

File tree

8 files changed

+119
-5
lines changed

8 files changed

+119
-5
lines changed

CodenameOne/src/com/codename1/impl/CodenameOneImplementation.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1263,6 +1263,12 @@ public void screenshot(SuccessCallback<Image> callback) {
12631263
callback.onSucess(img);
12641264
}
12651265

1266+
/**
1267+
* Notifies the platform that push notification processing is complete.
1268+
*/
1269+
public void notifyPushCompletion() {
1270+
}
1271+
12661272
/**
12671273
* Returns true if the platform supports a native image cache. The native image cache
12681274
* is different than just {@link FileSystemStorage#hasCachesDir()}. A native image cache

CodenameOne/src/com/codename1/ui/Display.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5152,6 +5152,22 @@ public void screenshot(SuccessCallback<Image> callback) {
51525152
impl.screenshot(callback);
51535153
}
51545154

5155+
/**
5156+
* Notifies the platform that push notification processing is complete.
5157+
* This is useful on iOS where the app is woken up in the background to handle
5158+
* a push notification and needs to signal completion to avoid being suspended
5159+
* prematurely.
5160+
* <p>
5161+
* If the {@code ios.delayPushCompletion} build hint (or property) is set to "true",
5162+
* Codename One will NOT automatically signal completion after the {@link com.codename1.push.PushCallback#push(String)}
5163+
* method returns. Instead, the application MUST invoke this method manually
5164+
* when it has finished its background work (e.g. playing audio, downloading content).
5165+
* </p>
5166+
*/
5167+
public void notifyPushCompletion() {
5168+
impl.notifyPushCompletion();
5169+
}
5170+
51555171
/**
51565172
* Convenience method to schedule a task to run on the EDT after {@literal timeout}ms.
51575173
*

Ports/Android/src/com/codename1/impl/android/AndroidImplementation.java

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@
5252
import android.graphics.drawable.Drawable;
5353
import android.media.AudioManager;
5454
import android.net.Uri;
55-
import android.os.Vibrator;
55+
import android.os.Vibrator;
56+
import android.os.PowerManager;
5657
import android.telephony.TelephonyManager;
5758
import android.util.DisplayMetrics;
5859
import android.util.Log;
@@ -280,6 +281,19 @@ public static void setActivity(CodenameOneActivity aActivity) {
280281
private int displayHeight;
281282
static CodenameOneActivity activity;
282283
static ComponentName activityComponentName;
284+
private static PowerManager.WakeLock pushWakeLock;
285+
public static void acquirePushWakeLock(long timeout) {
286+
if (getContext() == null) return;
287+
try {
288+
if (pushWakeLock == null) {
289+
PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
290+
pushWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "CN1:PushWakeLock");
291+
}
292+
pushWakeLock.acquire(timeout);
293+
} catch (Exception ex) {
294+
com.codename1.io.Log.e(ex);
295+
}
296+
}
283297

284298
private static Context context;
285299
RelativeLayout relativeLayout;
@@ -2783,7 +2797,18 @@ public void exitApplication() {
27832797
android.os.Process.killProcess(android.os.Process.myPid());
27842798
}
27852799

2786-
@Override
2800+
@Override
2801+
public void notifyPushCompletion() {
2802+
if (pushWakeLock != null && pushWakeLock.isHeld()) {
2803+
try {
2804+
pushWakeLock.release();
2805+
} catch (Exception ex) {
2806+
com.codename1.io.Log.e(ex);
2807+
}
2808+
}
2809+
}
2810+
2811+
@Override
27872812
public void notifyCommandBehavior(int commandBehavior) {
27882813
if (commandBehavior == Display.COMMAND_BEHAVIOR_NATIVE) {
27892814
if (getActivity() instanceof CodenameOneActivity) {

Ports/Android/src/com/codename1/impl/android/PushNotificationService.java

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,9 +109,20 @@ public void onDestroy() {
109109
public void push(final String value) {
110110
final PushCallback callback = getPushCallbackInstance();
111111
if(callback != null) {
112+
final boolean delayPushCompletion = "true".equals(Display.getInstance().getProperty("delayPushCompletion", "false")) ||
113+
"true".equals(Display.getInstance().getProperty("android.delayPushCompletion", "false"));
114+
if (delayPushCompletion) {
115+
AndroidImplementation.acquirePushWakeLock(30000);
116+
}
112117
Display.getInstance().callSerially(new Runnable() {
113118
public void run() {
114-
callback.push(value);
119+
try {
120+
callback.push(value);
121+
} finally {
122+
if (!delayPushCompletion) {
123+
Display.getInstance().notifyPushCompletion();
124+
}
125+
}
115126
}
116127
});
117128
} else {

Ports/iOSPort/src/com/codename1/impl/ios/IOSImplementation.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8115,7 +8115,10 @@ public void run() {
81158115

81168116
pushCallback.push(message);
81178117
} finally {
8118-
nativeInstance.firePushCompletionHandler();
8118+
if (!"true".equals(Display.getInstance().getProperty("delayPushCompletion", "false")) &&
8119+
!"true".equals(Display.getInstance().getProperty("ios.delayPushCompletion", "false"))) {
8120+
nativeInstance.firePushCompletionHandler();
8121+
}
81198122
}
81208123
}
81218124
});

docs/developer-guide/Push-Notifications.asciidoc

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,44 @@ NOTE: On iOS, hidden push messages (push type 2) will not be delivered when the
8787

8888
TIP: You can set the `android.background_push_handling` build hint to "true" to deliver push messages on Android when the app is minimized (running in the background). There is no equivalent setting on other platforms currently.
8989

90+
[[delay-push-completion]]
91+
=== Handling Long-Running Background Push Tasks
92+
93+
By default, the platform signals the completion of the push processing immediately after your `push(String)` callback returns. However, some tasks, such as playing audio (e.g. for Push-To-Talk apps), require more time to complete. If the app is in the background, the OS might suspend the app before the task completes if it thinks the push handling is finished.
94+
95+
To handle this, you can opt-in to manually signaling the completion of the push task using the `delayPushCompletion` build hint.
96+
97+
1. Add the `delayPushCompletion=true` build hint to your `codenameone_settings.properties`.
98+
2. In your `push(String)` callback, start your asynchronous task.
99+
3. When your task is complete, call `Display.getInstance().notifyPushCompletion()`.
100+
101+
Example:
102+
103+
[source,java]
104+
----
105+
public void push(String message) {
106+
if (isAudioMessage(message)) {
107+
// Play audio asynchronously
108+
playAudio(message, new Runnable() {
109+
public void run() {
110+
// Audio finished playing
111+
Display.getInstance().notifyPushCompletion();
112+
}
113+
});
114+
} else {
115+
// For standard messages, we can notify immediately or let it timeout (safest to notify)
116+
Display.getInstance().notifyPushCompletion();
117+
}
118+
}
119+
----
120+
121+
**How it works:**
122+
123+
* **Android:** The system acquires a `PARTIAL_WAKE_LOCK` when the push is received, keeping the CPU running even if the screen is off. Calling `notifyPushCompletion()` releases this lock. The lock has a safety timeout (e.g., 30 seconds) to prevent battery drain if you forget to call it.
124+
* **iOS:** The system delays calling the completion handler passed to the push delegate. This gives your app background execution time. Calling `notifyPushCompletion()` invokes the system completion handler.
125+
126+
NOTE: If you enable this feature, you **must** call `Display.getInstance().notifyPushCompletion()` in all code paths of your `push()` callback to ensure the device can sleep/suspend properly.
127+
90128

91129
=== Testing Push Support
92130

maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1163,6 +1163,10 @@ public boolean build(File sourceZip, final BuildRequest request) throws BuildExc
11631163
playFlag = "true";
11641164

11651165
gpsPermission = request.getArg("android.gpsPermission", "false").equals("true");
1166+
if (request.getArg("android.delayPushCompletion", "false").equals("true") ||
1167+
request.getArg("delayPushCompletion", "false").equals("true")) {
1168+
wakeLock = true;
1169+
}
11661170
mediaPlaybackPermission = false;
11671171
try {
11681172
scanClassesForPermissions(dummyClassesDir, new Executor.ClassScanner() {
@@ -3894,6 +3898,10 @@ private String createOnDestroyCode(BuildRequest request) {
38943898

38953899
private String createPostInitCode(BuildRequest request) {
38963900
String retVal = "";
3901+
if (request.getArg("android.delayPushCompletion", "false").equals("true") ||
3902+
request.getArg("delayPushCompletion", "false").equals("true")) {
3903+
retVal += "Display.getInstance().setProperty(\"android.delayPushCompletion\", \"true\");\n";
3904+
}
38973905
return retVal;
38983906
}
38993907

maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -956,8 +956,14 @@ public void usesClassMethod(String cls, String method) {
956956
+ " private boolean stopped = false;\n";
957957

958958
stubSourceCode += decodeFunction();
959+
String delayPushCompletion = "";
960+
if ("true".equals(request.getArg("ios.delayPushCompletion", "false")) ||
961+
"true".equals(request.getArg("delayPushCompletion", "false"))) {
962+
delayPushCompletion = " Display.getInstance().setProperty(\"ios.delayPushCompletion\", \"true\");\n";
963+
}
959964
stubSourceCode += " public void run() {\n"
960965
+ " Display.getInstance().setProperty(\"package_name\", PACKAGE_NAME);\n"
966+
+ delayPushCompletion
961967
+ " Display.getInstance().setProperty(\"AppVersion\", APPLICATION_VERSION);\n"
962968
+ " Display.getInstance().setProperty(\"AppName\", APPLICATION_NAME);\n"
963969
+ newStorage
@@ -2380,7 +2386,8 @@ public boolean accept(File file, String string) {
23802386
}
23812387
}
23822388
String backgroundModesStr = request.getArg("ios.background_modes", null);
2383-
if (includePush) {
2389+
if (includePush || "true".equals(request.getArg("ios.delayPushCompletion", "false")) ||
2390+
"true".equals(request.getArg("delayPushCompletion", "false"))) {
23842391
if (backgroundModesStr == null || !backgroundModesStr.contains("remote-notification")) {
23852392
if (backgroundModesStr == null) {
23862393
backgroundModesStr = "";

0 commit comments

Comments
 (0)