HelloWorld
HelloWorld

Reputation: 2355

URLImage in InfiniteScrollAdapter in Simulator shows NPE (CodenameOne)

My app features an InfiniteScrollAdapter populated with images through URLImage and URLImage.ImageAdapter.

In the simulator (Iphone3GS or Xoom or GoogleNexus7), and NPE is shown the first time the InfiniteScrollAdapter appears, although the file does exist on the server.

Please note : In this test there was only one entry in the database. So on the image below what you should see is the same row (image + text) duplicated 3 times.

NPE in InfiniteScrollAdapter

Please note that the order in the undisplayed icon can differ

NPE with different order in the undisplayed icons

The code I used to download the image is :

Image tempPlaceholder = Image.createImage(
            ParametresGeneraux.SIZE_OF_REPORT_PIC_IN_PX,
            ParametresGeneraux.SIZE_OF_REPORT_PIC_IN_PX,
            ParametresGeneraux.accentColor);
    Graphics gr = tempPlaceholder.getGraphics();
    gr.setAntiAliased(true);
    gr.setColor(ParametresGeneraux.accentColor);
    gr.fillArc(0, 0, ParametresGeneraux.SIZE_OF_REPORT_PIC_IN_PX, ParametresGeneraux.SIZE_OF_REPORT_PIC_IN_PX, 0, 360);

    EncodedImage roundPlaceholder = EncodedImage.createFromImage(tempPlaceholder, true);

    final Image reportImage = URLImage.createToStorage(
                            roundPlaceholder,
                            photoFilenameInStorage,
                            currentReport.getPhotoPath(),
                            ParametresGeneraux.RESIZE_SCALE_WITH_ROUND_MASK
                    );

And here is the overridden imageAdapter method :

    public final static URLImage.ImageAdapter RESIZE_SCALE_WITH_ROUND_MASK = new URLImage.ImageAdapter() {
    @Override
    public EncodedImage adaptImage(EncodedImage downloadedImage, EncodedImage placeholderImage) {
        final Image[] tmp = new Image[1];

        if (!Display.getInstance().isEdt()) {
            // The image scaling has to be called from EDT
            Display.getInstance().callSeriallyAndWait(() -> {

                tmp[0] = downloadedImage.scaledLargerRatio(placeholderImage.getWidth(), placeholderImage.getHeight());
                if (tmp[0].getWidth() > placeholderImage.getWidth()) {
                    int diff = tmp[0].getWidth() - placeholderImage.getWidth();
                    int x = diff / 2;
                    tmp[0] = tmp[0].subImage(x, 0, placeholderImage.getWidth(), placeholderImage.getHeight(), true);
                } else if (tmp[0].getHeight() > placeholderImage.getHeight()) {
                    int diff = tmp[0].getHeight() - placeholderImage.getHeight();
                    int y = diff / 2;
                    tmp[0] = tmp[0].subImage(0, y, Math.min(placeholderImage.getWidth(), tmp[0].getWidth()),
                            Math.min(placeholderImage.getHeight(), tmp[0].getHeight()), true);
                }
            });
        } else {
            tmp[0] = downloadedImage.scaledLargerRatio(placeholderImage.getWidth(), placeholderImage.getHeight());
            if (tmp[0].getWidth() > placeholderImage.getWidth()) {
                int diff = tmp[0].getWidth() - placeholderImage.getWidth();
                int x = diff / 2;
                tmp[0] = tmp[0].subImage(x, 0, placeholderImage.getWidth(), placeholderImage.getHeight(), true);
            } else if (tmp[0].getHeight() > placeholderImage.getHeight()) {
                int diff = tmp[0].getHeight() - placeholderImage.getHeight();
                int y = diff / 2;
                tmp[0] = tmp[0].subImage(0, y, Math.min(placeholderImage.getWidth(), tmp[0].getWidth()),
                        Math.min(placeholderImage.getHeight(), tmp[0].getHeight()), true);
            }
        }

        EncodedImage[] image2Return = new EncodedImage[1];
        if (!Display.getInstance().isEdt()) {
            // The image scaling has to be called from EDT
            Display.getInstance().callSeriallyAndWait(() -> {
                Image roundMask = Image.createImage(tmp[0].getWidth(), tmp[0].getHeight(), 0xff000000);
                Graphics gr = roundMask.getGraphics();
                gr.setColor(0xffffff);

                gr.fillArc(0, 0, tmp[0].getWidth(), tmp[0].getHeight(), 0, 360);
                Object mask = roundMask.createMask();
                tmp[0] = tmp[0].applyMask(mask);
                image2Return[0] = EncodedImage.createFromImage(tmp[0], false);
            });
        } else {
            Image roundMask = Image.createImage(tmp[0].getWidth(), tmp[0].getHeight(), 0xff000000);
            Graphics gr = roundMask.getGraphics();
            gr.setColor(0xffffff);

            gr.fillArc(0, 0, tmp[0].getWidth(), tmp[0].getHeight(), 0, 360);
            Object mask = roundMask.createMask();
            tmp[0] = tmp[0].applyMask(mask);
            image2Return[0] = EncodedImage.createFromImage(tmp[0], false);
        }

        return image2Return[0];

    } 

In the stacktrace, the NPE seems to stem from the overridden URLImage.ImageAdapter :

java.lang.IllegalArgumentException: create image failed for the given image data of length: 0 at com.codename1.ui.Image.createImage(Image.java:654) at com.codename1.ui.EncodedImage.getInternal(EncodedImage.java:365) at com.codename1.ui.EncodedImage.getInternalImpl(EncodedImage.java:340) at com.codename1.ui.EncodedImage.getHeight(EncodedImage.java:522) at com.codename1.ui.Image.scaledLargerRatio(Image.java:899) at com.my.application.ParametresGeneraux$1.lambda$adaptImage$0(ParametresGeneraux.java:564) at com.codename1.ui.RunnableWrapper.run(RunnableWrapper.java:95) at com.codename1.ui.Display.processSerialCalls(Display.java:1154) at com.codename1.ui.Display.edtLoopImpl(Display.java:1098) at com.codename1.ui.Display.invokeAndBlock(Display.java:1207) at com.codename1.ui.Display.invokeAndBlock(Display.java:1244) at com.codename1.ui.URLImage$DownloadCompleted.actionPerformed(URLImage.java:233) at com.codename1.ui.URLImage$4.onSucess(URLImage.java:301) at com.codename1.ui.URLImage$4.onSucess(URLImage.java:297) at com.codename1.util.CallbackDispatcher.run(CallbackDispatcher.java:53) at com.codename1.ui.Display.processSerialCalls(Display.java:1154) at com.codename1.ui.Display.edtLoopImpl(Display.java:1098) at com.codename1.ui.Display.mainEDTLoop(Display.java:999) at com.codename1.ui.RunnableWrapper.run(RunnableWrapper.java:120) at com.codename1.impl.CodenameOneThread.run(CodenameOneThread.java:176) [EDT] 0:0:0,1 - Codename One revisions: e5c43877074c18b4b5c7748d000e5cfac75ab749 2318

[EDT] 0:0:0,1 - Exception: java.lang.NullPointerException - null java.lang.NullPointerException at com.codename1.impl.javase.JavaSEPort.scale(JavaSEPort.java:3996) at com.codename1.ui.Image.scale(Image.java:1007) at com.codename1.ui.Image.scaledImpl(Image.java:953) at com.codename1.ui.Image.scaled(Image.java:918) at com.codename1.impl.javase.JavaSEPort$71.save(JavaSEPort.java:7659) at com.codename1.ui.EncodedImage.scaledEncoded(EncodedImage.java:626) at com.codename1.ui.EncodedImage.scaled(EncodedImage.java:653) at com.codename1.ui.Image.scaledLargerRatio(Image.java:904) at com.my.application.ParametresGeneraux$1.lambda$adaptImage$0(ParametresGeneraux.java:564) at com.codename1.ui.RunnableWrapper.run(RunnableWrapper.java:95) at com.codename1.ui.Display.processSerialCalls(Display.java:1154) at com.codename1.ui.Display.edtLoopImpl(Display.java:1098) at com.codename1.ui.Display.invokeAndBlock(Display.java:1207) at com.codename1.ui.Display.invokeAndBlock(Display.java:1244) at com.codename1.ui.URLImage$DownloadCompleted.actionPerformed(URLImage.java:233) at com.codename1.ui.URLImage$4.onSucess(URLImage.java:301) at com.codename1.ui.URLImage$4.onSucess(URLImage.java:297) at com.codename1.util.CallbackDispatcher.run(CallbackDispatcher.java:53) at com.codename1.ui.Display.processSerialCalls(Display.java:1154) at com.codename1.ui.Display.edtLoopImpl(Display.java:1098) at com.codename1.ui.Display.mainEDTLoop(Display.java:999) at com.codename1.ui.RunnableWrapper.run(RunnableWrapper.java:120) at com.codename1.impl.CodenameOneThread.run(CodenameOneThread.java:176)

Moreover, a glance in the .cn1 directory shows the URLImage storage file name with the suffix "ImageURLTMP" which does not appear when everything works without NPE.

Finally, if I come back to this form later, everything works as expected (images were shown, no NPE). I tried to test for downloadedImage nullness in imageAdapter but the EncodedImage is not null.

How can I avoid this NPE?

Edit March 1st 2017

Following the answers from @Diamond and @Shai, I believe the NPE occurs because the InfiniteScrollAdapter wants to fill in the screen with rows and consequently launches the download of the same image simultaneously (because it is not in cache). So a solution could be to prevent the InfiniteScrollAdapter to loop (so it becomes finite). How can I do that ?

Please also note that there is not 404 error, the Network monitor shows response code 200 as depicted below. However the image should not be downloaded 3 times, should it ?

No 404

Upvotes: 2

Views: 98

Answers (2)

Diamond
Diamond

Reputation: 7483

Change your ImageAdapter to the following:

public static final URLImage.ImageAdapter RESIZE_SCALE_WITH_ROUND_MASK = new URLImage.ImageAdapter() {
    @Override
    public EncodedImage adaptImage(EncodedImage downloadedImage, EncodedImage placeholderImage) {
        Image tmp = downloadedImage.scaledLargerRatio(placeholderImage.getWidth(), placeholderImage.getHeight());
        if (tmp.getWidth() > placeholderImage.getWidth()) {
            int diff = tmp.getWidth() - placeholderImage.getWidth();
            int x = diff / 2;
            tmp = tmp.subImage(x, 0, placeholderImage.getWidth(), placeholderImage.getHeight(), true);
        } else if (tmp.getHeight() > placeholderImage.getHeight()) {
            int diff = tmp.getHeight() - placeholderImage.getHeight();
            int y = diff / 2;
            tmp = tmp.subImage(0, y, Math.min(placeholderImage.getWidth(), tmp.getWidth()),
                    Math.min(placeholderImage.getHeight(), tmp.getHeight()), true);
        }
        Image roundMask = Image.createImage(tmp.getWidth(), tmp.getHeight(), 0xff000000);
        Graphics gr = roundMask.getGraphics();
        gr.setColor(0xffffff);
        gr.fillArc(0, 0, tmp.getWidth(), tmp.getHeight(), 0, 360);
        Object mask = roundMask.createMask();
        tmp = tmp.applyMask(mask);
        return EncodedImage.createFromImage(tmp, false);
    }

    @Override
    public boolean isAsyncAdapter() {
        return true;
    }
};

No need to check EDT.

Make sure your tempPlaceholder image is applied to your component first and at the end of your logic, call your URLImage in a callSerially() method:

Image tempPlaceholder = Image.createImage(
        ParametresGeneraux.SIZE_OF_REPORT_PIC_IN_PX,
        ParametresGeneraux.SIZE_OF_REPORT_PIC_IN_PX,
        ParametresGeneraux.accentColor);
Graphics gr = tempPlaceholder.getGraphics();
gr.setAntiAliased(true);
gr.setColor(ParametresGeneraux.accentColor);
gr.fillArc(0, 0, ParametresGeneraux.SIZE_OF_REPORT_PIC_IN_PX, ParametresGeneraux.SIZE_OF_REPORT_PIC_IN_PX, 0, 360);

myComponent.setIcon(tempPlaceholder);


...


//Then call this at the end of everything
Display.getInstance().callSerially(() -> {
    EncodedImage roundPlaceholder = EncodedImage.createFromImage(tempPlaceholder, true);

    final Image reportImage = URLImage.createToStorage(
                        roundPlaceholder,
                        photoFilenameInStorage,
                        currentReport.getPhotoPath(),
                        ParametresGeneraux.RESIZE_SCALE_WITH_ROUND_MASK
                );
    myComponent.setIcon(reportImage);
    myComponent.getComponentForm().repaint();
});

Edit:

Based on @Shai's answer, you could check if you are currently downloading the same image and prevent another one from being pulled. Because this usually causes a conflict:

//Declare this at the top of your class
final static private Map<String, Image> LOADED_URLS = new HashMap<>(); 

//Then change the URLImage image method to this
Display.getInstance().callSerially(() -> {
    EncodedImage roundPlaceholder = EncodedImage.createFromImage(tempPlaceholder, true);

    final Image reportImage = LOADED_URLS.containsKey(photoFilenameInStorage) ? LOADED_URLS.get(photoFilenameInStorage)
                        : URLImage.createToStorage(
                        roundPlaceholder,
                        photoFilenameInStorage,
                        currentReport.getPhotoPath(),
                        ParametresGeneraux.RESIZE_SCALE_WITH_ROUND_MASK
                );
    LOADED_URLS.put(photoFilenameInStorage, reportImage);
    myComponent.setIcon(reportImage);
    myComponent.getComponentForm().repaint();
});

Upvotes: 1

Shai Almog
Shai Almog

Reputation: 52760

In your adapter check if downloadedImage.getData() is null. I assume it's not and it's a 404 error page or something similar to that.

In that case your adapter can catch the exception and just return a fallback that matches what you expect to see when no image exists.

This works the second time around since the system sees the tmp file and assumes a download is in progress so it doesn't invoke the download code again. The tmp file is later renamed to the final downloadable file.

Upvotes: 1

Related Questions