Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,12 @@ public void screenshot(SuccessCallback<Image> callback) {
callback.onSucess(img);
}

/**
* Notifies the platform that push notification processing is complete.
*/
public void notifyPushCompletion() {
}

/**
* Returns true if the platform supports a native image cache. The native image cache
* is different than just {@link FileSystemStorage#hasCachesDir()}. A native image cache
Expand Down
16 changes: 16 additions & 0 deletions CodenameOne/src/com/codename1/ui/Display.java
Original file line number Diff line number Diff line change
Expand Up @@ -5154,6 +5154,22 @@ public void screenshot(SuccessCallback<Image> callback) {
impl.screenshot(callback);
}

/**
* Notifies the platform that push notification processing is complete.
* This is useful on iOS where the app is woken up in the background to handle
* a push notification and needs to signal completion to avoid being suspended
* prematurely.
* <p>
* If the {@code ios.delayPushCompletion} build hint (or property) is set to "true",
* Codename One will NOT automatically signal completion after the {@link com.codename1.push.PushCallback#push(String)}
* method returns. Instead, the application MUST invoke this method manually
* when it has finished its background work (e.g. playing audio, downloading content).
* </p>
*/
public void notifyPushCompletion() {
impl.notifyPushCompletion();
}

/**
* Convenience method to schedule a task to run on the EDT after {@literal timeout}ms.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,8 @@
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.net.Uri;
import android.os.Vibrator;
import android.os.Vibrator;
import android.os.PowerManager;
import android.telephony.TelephonyManager;
import android.util.DisplayMetrics;
import android.util.Log;
Expand Down Expand Up @@ -280,6 +281,19 @@ public static void setActivity(CodenameOneActivity aActivity) {
private int displayHeight;
static CodenameOneActivity activity;
static ComponentName activityComponentName;
private static PowerManager.WakeLock pushWakeLock;
public static void acquirePushWakeLock(long timeout) {
if (getContext() == null) return;
try {
if (pushWakeLock == null) {
PowerManager pm = (PowerManager) getContext().getSystemService(Context.POWER_SERVICE);
pushWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "CN1:PushWakeLock");
}
pushWakeLock.acquire(timeout);
} catch (Exception ex) {
com.codename1.io.Log.e(ex);
}
}

private static Context context;
RelativeLayout relativeLayout;
Expand Down Expand Up @@ -2783,7 +2797,18 @@ public void exitApplication() {
android.os.Process.killProcess(android.os.Process.myPid());
}

@Override
@Override
public void notifyPushCompletion() {
if (pushWakeLock != null && pushWakeLock.isHeld()) {
try {
pushWakeLock.release();
} catch (Exception ex) {
com.codename1.io.Log.e(ex);
}
}
}

@Override
public void notifyCommandBehavior(int commandBehavior) {
if (commandBehavior == Display.COMMAND_BEHAVIOR_NATIVE) {
if (getActivity() instanceof CodenameOneActivity) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,20 @@ public void onDestroy() {
public void push(final String value) {
final PushCallback callback = getPushCallbackInstance();
if(callback != null) {
final boolean delayPushCompletion = "true".equals(Display.getInstance().getProperty("delayPushCompletion", "false")) ||
"true".equals(Display.getInstance().getProperty("android.delayPushCompletion", "false"));
if (delayPushCompletion) {
AndroidImplementation.acquirePushWakeLock(30000);
}
Display.getInstance().callSerially(new Runnable() {
public void run() {
callback.push(value);
try {
callback.push(value);
} finally {
if (!delayPushCompletion) {
Display.getInstance().notifyPushCompletion();
}
}
}
});
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8115,7 +8115,10 @@ public void run() {

pushCallback.push(message);
} finally {
nativeInstance.firePushCompletionHandler();
if (!"true".equals(Display.getInstance().getProperty("delayPushCompletion", "false")) &&
!"true".equals(Display.getInstance().getProperty("ios.delayPushCompletion", "false"))) {
nativeInstance.firePushCompletionHandler();
}
}
}
});
Expand Down
38 changes: 38 additions & 0 deletions docs/developer-guide/Push-Notifications.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,44 @@ NOTE: On iOS, hidden push messages (push type 2) will not be delivered when the

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.

[[delay-push-completion]]
=== Handling Long-Running Background Push Tasks

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.

To handle this, you can opt-in to manually signaling the completion of the push task using the `delayPushCompletion` build hint.

1. Add the `delayPushCompletion=true` build hint to your `codenameone_settings.properties`.
2. In your `push(String)` callback, start your asynchronous task.
3. When your task is complete, call `Display.getInstance().notifyPushCompletion()`.

Example:

[source,java]
----
public void push(String message) {
if (isAudioMessage(message)) {
// Play audio asynchronously
playAudio(message, new Runnable() {
public void run() {
// Audio finished playing
Display.getInstance().notifyPushCompletion();
}
});
} else {
// For standard messages, we can notify immediately or let it timeout (safest to notify)
Display.getInstance().notifyPushCompletion();
}
}
----

**How it works:**

* **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.
* **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.

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.


=== Testing Push Support

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1163,6 +1163,10 @@ public boolean build(File sourceZip, final BuildRequest request) throws BuildExc
playFlag = "true";

gpsPermission = request.getArg("android.gpsPermission", "false").equals("true");
if (request.getArg("android.delayPushCompletion", "false").equals("true") ||
request.getArg("delayPushCompletion", "false").equals("true")) {
wakeLock = true;
}
mediaPlaybackPermission = false;
try {
scanClassesForPermissions(dummyClassesDir, new Executor.ClassScanner() {
Expand Down Expand Up @@ -3894,6 +3898,10 @@ private String createOnDestroyCode(BuildRequest request) {

private String createPostInitCode(BuildRequest request) {
String retVal = "";
if (request.getArg("android.delayPushCompletion", "false").equals("true") ||
request.getArg("delayPushCompletion", "false").equals("true")) {
retVal += "Display.getInstance().setProperty(\"android.delayPushCompletion\", \"true\");\n";
}
return retVal;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -956,8 +956,14 @@ public void usesClassMethod(String cls, String method) {
+ " private boolean stopped = false;\n";

stubSourceCode += decodeFunction();
String delayPushCompletion = "";
if ("true".equals(request.getArg("ios.delayPushCompletion", "false")) ||
"true".equals(request.getArg("delayPushCompletion", "false"))) {
delayPushCompletion = " Display.getInstance().setProperty(\"ios.delayPushCompletion\", \"true\");\n";
}
stubSourceCode += " public void run() {\n"
+ " Display.getInstance().setProperty(\"package_name\", PACKAGE_NAME);\n"
+ delayPushCompletion
+ " Display.getInstance().setProperty(\"AppVersion\", APPLICATION_VERSION);\n"
+ " Display.getInstance().setProperty(\"AppName\", APPLICATION_NAME);\n"
+ newStorage
Expand Down Expand Up @@ -2380,7 +2386,8 @@ public boolean accept(File file, String string) {
}
}
String backgroundModesStr = request.getArg("ios.background_modes", null);
if (includePush) {
if (includePush || "true".equals(request.getArg("ios.delayPushCompletion", "false")) ||
"true".equals(request.getArg("delayPushCompletion", "false"))) {
if (backgroundModesStr == null || !backgroundModesStr.contains("remote-notification")) {
if (backgroundModesStr == null) {
backgroundModesStr = "";
Expand Down
Loading