Recipes for using Codename One to develop cross-platform mobile apps in Java and Kotlin.

Themes

The following recipes include tips on customizing the look and feel of Codename one apps using themes, CSS, Styles, etc..

1. Platform-Specific Styling

Problem

You have used CSS to style your app, and it looks great on some devices but not on others. You want to change the font size of some styles, but only on specific devices.

Solution

Use CSS media queries to target styles at a specific device (e.g. desktop, tablet, or phone), platform (e.g. Android, iOS, Mac, Windows, etc…​), or device densities (e.g. low, medium, high, very high, etc..).

Example: A media query to override Label color on iOS only
@media platform-ios {
    Label {
        color: red;
    }
}

Media queries will allow you to target devices based on three axes: Platform, Device, and Density

Table 1. Platform Queries
Value Description

platform-ios

Apply only on iOS

platform-and

Apply only on Android

platform-mac

Apply Only on Mac desktop.

platform-win

Apply Only on Windows desktop.

Table 2. Device Queries
Value Description

device-desktop

Apply only desktop

device-tablet

Apply only on tablet

device-phone

Apply only on phone

Table 3. Density Queries
Value Description

density-very-low

Very Low Density 176x220 And Smaller

density-low

Low Density Up To 240x320

density-medium

Medium Density Up To 360x480

density-high

Hi Density Up To 480x854

density-very-high

Very Hi Density Up To 1440x720

density-hd

HD Up To 1920x1080

density-560

Intermediate density for screens that sit somewhere between HD to 2HD

density-2hd

Double the HD level density

density-4k

4K level density

You can combine media queries to increase the specificity.

Example: Targeting only 4k Android tablets
@media platform-and, device-tablet, density-4k {
    Label {
        font-size: 5mm;
    }
}

You can also combine more than one query of the same type to broaden the range of the query: .Example: Targeting only hd, 2hd, and 4k Android tablets

@media platform-and, device-tablet, density-4k, density-2hd, density-hd {
    Label {
        font-size: 5mm;
    }
}

Further Reading

2. Platform-Specific Font Scaling

Problem

Your app looks great except that on desktop, the fonts are a little too small. If you could only scale the fonts to be 25% larger on the desktop, your app would be perfect.

Solution

You can use font-scaling constants to scale all of the fonts in your stylesheet by a constant factor. You can use a "media-query-like" syntax to apply this scaling only on particular platforms, devices, or densities.

Example: Scaling Fonts to be 25% larger on desktop
#Constants {
    device-desktop-font-scale: "1.25";
}
Tip
In most cases it is better to use standard media queries to apply styles which target specific platforms in a more fine-grained manner.

Further Reading

Data Processing

The following recipes relate to data processing and conversion. This includes parsing data like JSON, HTML, and XML.

3. Parsing HTML

Problem

You want to parse some HTML content into a data structure. You can’t simply use the XMLParser class because the content is not well-formed XML, but you would like to be able to work with the parsed document using the same tools (e.g. Result and Element.

Solution

Use the HTMLParser class from the CN1HTMLParser cn1lib. It contains a simple API for parsing an HTML string into an Element, the same type of element that XMLParser returns.

Usage example:

HTMLParser parser = new HTMLParser();

Element root = parser.parse(htmlString).get(); (1)
Result r = Result.fromContent(root);

// Now modify the document
// In this example we're going to replace image src with placeholders
// so we can load them separately.
List<Element> images = r.getAsArray("//img");
int index = 0;
List<String> toLoad = new ArrayList<>();
if (images != null) {
    for (Element img : images) {
        String src = img.getAttribute("src");
        if (src.startsWith("http://*/") || (!src.startsWith("http://") && !src.startsWith("data:") && !src.startsWith("https"))) {
            img.setAttribute("id", "nt-image-"+index);
            toLoad.add(src);
            img.setAttribute("src", "");
            index++;
        }
    }
}

// Now write the document as well-formed XML.
XMLWriter writer = new XMLWriter(true);
String pageContent = writer.toXML(root);
  1. The parse() method returns an Async promise. If you want to use it synchronously, you can call get(), which will wait until parsing is done.

Alternate Async Usage

The above example uses the get() method to wait until the result is ready, but you can use the parser asynchronously as well:

parser.parse(htmlString).ready(root->{
     // root is the root Element of the document.
});

Discussion

The HTMLParser class wraps an off-screen BrowserComponent to use the platform’s native webview to actually parse the HTML. It then serializes the DOM as XML, which is then re-parsed using the Codename One XML parser. There are pitfalls to this approach, including performance (it takes time to pass data back-and forth between a webview, after all), and possibly different results on different platforms.

Note
The Codename One core library also includes an HTMLParser class at com.codename1.ui.html.HTMLParser. This parser is meant to be used as part of the deprecated HTMLComponent class, which is a light-weight web view component that used to be used on platforms that didn’t have a native webview, e.g. J2ME. Now all modern platforms have a native webview, so this component isn’t used much. Additionally the HTMLParser class in that package doesn’t support all HTML, and will fail in strange ways if you try to use it headlessly.

Further Reading

  1. XMLParser Javadocs - Since the output of HTMLParser is the same as XMLParser, you can find some useful examples in the XMLParser javadocs.

.

4. Using the Clipboard

Problem

You want to copy and paste to and from the system clipboard.

Solution

Use the Display.copyToClipboard() and Display.getPasteDataFromClipboard() to copy andn paste to/from the system clipboard respectively.

Example: Copying to the Clipboard
Display.getInstance().copyToClipboard("Some text to copy");
Example: Copying text from clipboard into Label
Object pasteData = Display.getInstance().getPasteDataFromClipboard();
Label text = new Label();
if (pasteData instanceof String) {
    text.setText((String)pasteData);
} else {
    ToastBar.showInfoMessage("Paste data is not text");
}
Important

In the Javascript port we are restricted by the browser’s sandbox. We can’t just access the system clipboard data for security reasons. However, if the user initiates a paste via Ctrl-V, Command-V, EditPaste, etc.., the system clipboard contents will be loaded into the Codename One clipboard, so that the next time you call getPasteDataFromClipboard(), it will include those contents.

You can use Form.addPasteListener(ActionListener) to be notified when the clipboard contents are updated via this method so that you can respond appropriately - usually by calling getPasteDataFromClipboard() and doing something with the data.

Full Example allowing copy and paste using the Clipboard API
package com.codename1.samples;


import com.codename1.components.ToastBar;
import static com.codename1.ui.CN.*;
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.ui.Dialog;
import com.codename1.ui.Label;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.util.Resources;
import com.codename1.io.Log;
import com.codename1.ui.Toolbar;
import java.io.IOException;
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.io.NetworkEvent;
import com.codename1.ui.Button;
import com.codename1.ui.CN;
import com.codename1.ui.TextArea;
import com.codename1.ui.TextField;
import com.codename1.ui.layouts.GridLayout;


public class ClipboardSample {

    private Form current;
    private Resources theme;

    public void init(Object context) {
        // use two network threads instead of one
        updateNetworkThreadCount(2);

        theme = UIManager.initFirstTheme("/theme");

        // Enable Toolbar on all Forms by default
        Toolbar.setGlobalToolbar(true);

        // Pro only feature
        Log.bindCrashProtection(true);

        addNetworkErrorListener(err -> {
            // prevent the event from propagating
            err.consume();
            if(err.getError() != null) {
                Log.e(err.getError());
            }
            Log.sendLogAsync();
            Dialog.show("Connection Error", "There was a networking error in the connection to " + err.getConnectionRequest().getUrl(), "OK", null);
        });
    }

    public void start() {
        if(current != null){
            current.show();
            return;
        }
        Form hi = new Form("Hi World", BoxLayout.y());
        TextField text = new TextField();
        Button copyBtn = new Button("Copy");
        copyBtn.addActionListener(evt->{
            Display.getInstance().copyToClipboard(text.getText());
        });
        Button pasteBtn = new Button("Paste");
        pasteBtn.addActionListener(evt->{
            if ("html5".equalsIgnoreCase(CN.getPlatformName())) {
                // In the browser, we don't have permission, in general, to read from the clipboard
                // but the user can initiate a paste using Ctrl-V or Cmd-V, or Edit > Paste,
                // and the data will be received in the paste listener.
                Dialog.show("Help", "Please key-codes or Edit > Paste to paste content.", "OK", null);
                return;
            }
            handlePaste(text);
        });

        // The paste listener is informed when the user initiates a paste using
        // key-codes or browser menu items (Edit > Paste).  This is currently only
        // used by the Javascript port.
        hi.addPasteListener(evt->{
            handlePaste(text);
        });

        hi.add(text)
                .add(GridLayout.encloseIn(2, copyBtn, pasteBtn));

        hi.show();
    }

    /**
     * Pastes the current clipboard data as text into the given TextArea.
     * @param text The textarea to paste into
     */
    private void handlePaste(TextArea text) {
        Object pasteData = Display.getInstance().getPasteDataFromClipboard();
        if (pasteData instanceof String) {
            text.setText((String)pasteData);
        } else {
            ToastBar.showInfoMessage("Paste data is not text");
        }
    }

    public void stop() {
        current = getCurrentForm();
        if(current instanceof Dialog) {
            ((Dialog)current).dispose();
            current = getCurrentForm();
        }
    }

    public void destroy() {
    }

}

Security

The following recipes relate to application security.

5. Detecting Jailbroken Device

Problem

You want to detect whether the device your app is running on is Jailbroken or rooted.

Solution

While there is no way to know whether the device is rooted with 100% certainty, you can use the CN1JailbreakDetect cn1lib to to make a good guess. This cn1lib acts as a thin wrapper around the RootBeer Android library, and DTTJailbreakDetection iOS library, which employ heuristics to determine whether the device has likely been jailbroken.

Example

package com.codename1.samples;


import com.codename1.ext.jailbreak.JailbreakDetect;
import static com.codename1.ui.CN.*;
import com.codename1.ui.Display;
import com.codename1.ui.Form;
import com.codename1.ui.Dialog;
import com.codename1.ui.Label;
import com.codename1.ui.plaf.UIManager;
import com.codename1.ui.util.Resources;
import com.codename1.io.Log;
import com.codename1.ui.Toolbar;
import java.io.IOException;
import com.codename1.ui.layouts.BoxLayout;
import com.codename1.io.NetworkEvent;
import com.codename1.ui.Button;
import com.codename1.ui.Command;


public class JailbreakDetectionSample {

    private Form current;
    private Resources theme;

    public void init(Object context) {
        // use two network threads instead of one
        updateNetworkThreadCount(2);

        theme = UIManager.initFirstTheme("/theme");

        // Enable Toolbar on all Forms by default
        Toolbar.setGlobalToolbar(true);

        // Pro only feature
        Log.bindCrashProtection(true);

        addNetworkErrorListener(err -> {
            // prevent the event from propagating
            err.consume();
            if(err.getError() != null) {
                Log.e(err.getError());
            }
            Log.sendLogAsync();
            Dialog.show("Connection Error", "There was a networking error in the connection to " + err.getConnectionRequest().getUrl(), "OK", null);
        });
    }

    public void start() {
        if(current != null){
            current.show();
            return;
        }
        Form hi = new Form("Jailbreak Detection", BoxLayout.y());
        Button detect = new Button("Detect Jailbreak");
        detect.addActionListener(evt->{
            if (JailbreakDetect.isJailbreakDetectionSupported()) {
                if (JailbreakDetect.isJailbroken()) {
                    Dialog.show("Jailbroken","This device is jailbroken", new Command("OK") );
                } else {
                    Dialog.show("Not Jailbroken", "Probably not jailbroken.  But can't be 100% sure.", new Command("OK"));
                }
            } else {
                Dialog.show("No Idea", "No support for jailbreak detection on this device.", new Command("OK"));
            }
        });
        hi.add(detect);
        hi.show();
    }

    public void stop() {
        current = getCurrentForm();
        if(current instanceof Dialog) {
            ((Dialog)current).dispose();
            current = getCurrentForm();
        }
    }

    public void destroy() {
    }

}
Tip
This sample is part of the Codename One samples project, and can be run directly from the Codename One SampleRunner.

Discussion

The CN1JailbreakDetect provides two useful static methods for jailbreak detection:

  1. isJailbreakDetectionSupported() - This checks if the jailbreak detection is even supported on this platform.

  2. isJailBroken() - This checks if the device is jailbroken. If detection is not supported, then this will always return false.

Currently jailbreak detection is only supported on Android and iOS.

Important
There is NO way to know with 100% certainty whether or not a device has been jailbroken.

Further Reading

6. Hiding Sensitive Data When Entering Background

Problem

iOS will take a screenshot of your app when it enters the background that it uses for various previews of the app state. You want to hide sensitive data in your app’s UI to prevent this information from leaking out via these screenshots.

Solution

You can use the ios.blockScreenshotsOnEnterBackground=true build hint to prevent iOS from taking screenshots app goes into the background. This will cause the canvas on which the Codename One UI is drawn to be hidden in the didEnterBackground hook and unhidden in the willEnterForeground hook.

Warning

This will cause your app to appear as a blank white rectangle when the user is browsing through opened apps.

Image 270420 124718.733
Figure 1. Notice the app in the middle is blank white because it has been set to block iOS screenshots.

Discussion

You might have been tempted to try to modify the UI inside the stop() lifecycle method of your app, since it is called itself by the didEnterBackground hook. This strategy will work in some platforms, but not on iOS because the screenshot call is made immediately upon the didEnterBackground method returning - and the stop() method runs on the EDT (a different thread), so this is not possible.

Javascript

The following sections contain tips for working with the Javascript port.

7. Sending Messages to Outside Webpage

Problem

You want to send a message from your Codename One app to the webpage that contains it.

Solution

You can use CN.postMessage(), in Codename One to send the message. The message will be dispatched to Javascript event listeners in the outside webpage that register to receive cn1outbox events.

Sending a message from Codename One to the outside webpage.
// The Java side
MessageEvent message = new MessageEvent(
    null,        // event source... we'll leave it null
    "Hello",     // The message to deliver
    0            // Optional message code.
);
//Dispatch the message
CN.postMessage(message);
Receiving messages from Codename One in outside webpage.
// The javascript side
window.addEventListener('cn1outbox', function(evt) {
    var message = evt.detail;
    var code = evt.code;
    ...
});

Discussion

The CN.postMessage() method allows you to send a message to the native platform. When deploying as a Javascript app, these messages are converted to custom DOM events and dispatched on the window object. The event name is "cn1outbox", so you can receive events like this from the "javascript" side by registering an event listener for these types of events on the window object.

8. Receiving Messages from the Outside Webpage

Problem

You want to send messages from Javascript (i.e. the page containing the app) to the Codename One app.

Solution

From Javascript, you can dispatch a custom event named 'cn1inbox' on the window object. You can receive these events in Codename One using the CN.addMessageListener() method.

Sending Message from Javascript that can be received inside Codename One app.
// The javascript side
var message = new CustomEvent('cn1inbox', {detail: 'Hello', code: 0});
window.dispatchEvent(message);
Receiving Event in Codename One
// The Java side
CN.addMessageListener(evt->{
    String message = evt.getMessage();
    int code = evt.getCode();
    ...
});

Discussion

The CN.addMessageListener() and CN.removeMessageListener() methods allow you to register listeners to receive messages from the native platform. When the app is deployed as a Javascript app, the webpage can target these listeners using a custom DOM event named 'cn1inbox'. The Codename One app will receive all events of this type, and dispatch them to the listeners that were registered using CN.addMessageListener().

9. Notify Webpage When App is Started

Problem

You want to notify the outside page when the app is finished loading. If the webpage needs to communicate with the app, it is very helpful to know when the app is ready.

Solution

Register a DOM event listener on the window object for the aftercn1start event.

window.addEventListener('aftercn1start', function(evt) {
   console.log("The Codename One app has started...");
   ...
});

Discussion

Codename One broadcasts its lifecycle events as DOM events so that the webpage can stay synchronized with its status. The following events are currently supported:

Table 4. Supported DOM events
Event Description

beforecn1init

Fired before the init() method is run.

aftercn1init

Fired after the init() method is run.

beforecn1start

Fired before the start() method is run.

aftercn1start

Fired after the start() method is run.

Note
Currently The stop() and destroy() lifecycle methods are not used in the Javascript port, as there doesn’t seem to be a logical place to fire them. This may change in the future.

In addition to these DOM events, you can also check window.cn1Initialized and window.cn1Started for true to see if the init() and start() methods have already run.

10. Deploying as a "Headless" Javascript App

Problem

You want to deploy your app inside a webpage "headlessly". I.e. You don’t want the user to see the app. This might be useful if you just want to use your app as a javascript library.

Solution

Embed the app inside a 1-pixel iframe.

<!doctype html>
<html>
  <head> ... </head>
  <body>
    <iframe
      id='myapp'
      style='position:fixed; width: 1px; height:1px; border:none; bottom:0'
      src='https://example.com/MyApp/index.html'
    />
    .. Rest of webpage..
  </body>
</html>
Note
By trial and error, we have determined that displaying the iframe with 1px width and height is the best solution. Using display:none causes the browser to not load the iframe at all. Positioning the iframe outside the viewport, causes some APIs to be turned off (e.g. microphone).

Discussion

If you are deploying your app as a headless app, then you are likely expecting to be able to communicate between the webpage and your app. You will also need to be notified of lifecycle events in your app so you know when it has finished loading. Be aware of CORS (cross-origin-resource-sharing) browser policies if the page containing the <iframe> is loaded from a different domain than your app.

CORS Checklist

If the app (inside the iframe) is hosted at a different domain than the parent page (the page with the <iframe> tag), then you need to jump through some hoops to get things working.

  1. Make sure that you are not sending the X-Frame-Options response header with your app. This header prevents your page from being displayed inside an iframe. Many web hosts add this header automatically.

  2. If you want to use features like "camera" and "microphone", you’ll need to add the "allow" attribute to your iframe tag. E.g. <iframe allow="camera;microphone" …​/>. For more information about this attribute, see This article.

  3. If you need to communicate between the parent window and the iframe document (i.e. the window with your app, you’ll need to use Window.postMessage(). You can access the iframe’s "window" object using myIframe.contentWindow.

11. Playing Audio in a Headless App

Problem

In some cases Codename One apps may be deployed as "headless" apps. This can be achieved by simply embedding the app inside an iframe and positioning the iframe outside the main view port (e.g. x=-1000, y=-1000). If you are deploying the app this way, you may run into cases where the app requires user interaction. For example, if you try to play audio in the app, and you are running on iOS, then the app may require some user interaction in order for Safari to allow the audio. Codename One apps deal with this situation by prompting the user to play the audio. However, if the app is off screen, the user won’t see this prompt, so the audio will just not play.

Note
The user will only be prompted for the first audio clip that the app tries to play. Subsequent clips can be played unimpeded.

Solution

Codename One broadcasts a custom DOM event named "cn1userprompt" when a prompt is displayed that the user needs to interact with. You can register an event listener in the outside webpage to listen for this event, and display the iframe in such cases.

The "cn1userpromptresponse" custom DOM event will be dispatched after the user has finished the interaction.

myIframe.contentWindow.addEventListener('cn1userprompt', function(evt) {
    // The app requires user interaction..  display the iframe
});

myIframe.contentWindow.addEventListener('cn1userpromptresponse', function(evt) {
    // The user has finished their interaction... you can hide the iframe
});

12. Displaying Custom Prompt to Play Audio

Background

On some browsers (e.g. Safari), your app can only play audio as a direct response to user interaction. E.g. The user needs to actually click on the screen to initiate audio play. This is only required for the first audio clip that your app plays. If the app is ever denied permission to play an audio clip by the browser, it will display a prompt to the user saying "Audio Ready", with a "Play Now" button. When the user presses that button, the audio will begin to play.

Problem

You want to customize the dialog prompt that is displayed to ask the user for permission to play audio.

Solution

Register a message listener using CN.addMessageListener(), and call isPromptForAudioPlayer() on the received MessageEvent object to see if it is a prompt to play audio. If isPromptForAudioPlayer() returns true, then you can consume() the event to signal that you’ll be displaying a custom dialog, and then you can display your own dialog as shown in the example below. When the user has accepted or rejected the permission prompt, you must call the complete() method on the promise that you obtain using the getPromptPromise() method. complete(true) indicates that the user decided to play the audio. complete(false) indicates that the user decided not to play the audio.

E.g.

CN.addMessageListener(evt->{
    if (evt.isPromptForAudioPlayer()) { (1)
        System.out.println("Received a prompt for the audio player... audio is ready");
        // This is a prompt that is shown when there is audio ready to play
        // but the user needs to interact.  This is javascript-only to get around
        // restrictions that only allow audio in direct response to user interaction

        // We should display some kind of UI to let the user know that the audio is ready
        // and they need to press a button to play it.
        evt.consume(); (2)
        CN.callSerially(()-> { (3)
            MessageEvent.PromptPromise res = evt.getPromptPromise(); (4)
            if (Dialog.show("Audio Ready", "The audio is ready.", "Play", "Cancel")) {
                res.complete(true); (5)
            } else {
                res.complete(false); (6)
            }
            return;
        });
        return;

    }
});
  1. isPromptForAudioPlayer() tells us that this event is a prompt to play audio.

  2. Important You must call evt.consume() to let Codename One that you are going to handle this prompt. Otherwise, the default permission prompt will still be shown.

  3. Because we are using a modal dialog which will block the event dispatch, we wrap the dialog in callSerially() so this event dispatch won’t be blocked. This is not absolutely necessary, but it will make it easier to follow the app’s logic, as these prompts are designed to by asynchronous.

  4. Obtain the PromptPromise from the event which we will use to convey the user’s response back to the app. YOU MUST call the complete() on this promise no matter what, or the app will lock up.

  5. If the user elected to "Play" the audio, then call res.complete(true) on the promise.

  6. If the user elected not to play the audio, then call res.complete(false) on the promise.

Tip
You can also use the isPromptForAudioRecorder() method to detect a request for the audio recorder prompt.

Discussion

In this example we used a modal dialog to prompt the user, but you can use any UI mechanism you like for prompting the user. A Sheet, an interaction dialog, or a separate Form. You just need to remember to call complete() on the promise after the user has made their choice. If you forget to call complete() it could lock up the app.

Important
Calling complete(true) directly without actually displaying a dialog to the user won’t work. It is the "click" that satisfies the browsers "media engagement index" restrictions so that it will allow the app to play audio. The user can click anywhere in the app; but they need to click. If you call complete(true) without the user clicking, then the app will try to play the audio and just fail.

Further Reading