Reputation: 2355
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.
Please note that the order in the undisplayed icon can differ
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 ?
Upvotes: 2
Views: 98
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();
});
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
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