Skip to content

Commit 0db61df

Browse files
author
farfromrefug
committed
fix(android): preventPreClearDrawable and silence some okhttp errors
1 parent a4579e3 commit 0db61df

File tree

4 files changed

+161
-14
lines changed

4 files changed

+161
-14
lines changed

packages/image/platforms/android/java/com/nativescript/image/CustomDataFetcher.java

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@
1313
import java.io.InputStream;
1414
import java.util.Map;
1515
import android.util.Log;
16+
import java.io.FilterInputStream;
17+
import java.io.InterruptedIOException;
18+
import java.net.SocketException;
19+
import java.io.PrintStream;
20+
import java.io.PrintWriter;
1621

1722
public class CustomDataFetcher implements DataFetcher<InputStream> {
1823
private final Call.Factory client;
@@ -26,6 +31,73 @@ public CustomDataFetcher(Call.Factory client, CustomGlideUrl url) {
2631
this.url = url;
2732
}
2833

34+
// Buffer-less wrapper that masks cancel/reset IOExceptions so they don't yield large stack traces
35+
private static class MaskingInputStream extends FilterInputStream {
36+
protected MaskingInputStream(InputStream in) { super(in); }
37+
38+
@Override
39+
public int read() throws IOException {
40+
try {
41+
return super.read();
42+
} catch (IOException e) {
43+
if (isCancelException(e)) {
44+
throw SilentCancelException.INSTANCE;
45+
}
46+
throw e;
47+
}
48+
}
49+
50+
@Override
51+
public int read(byte[] b, int off, int len) throws IOException {
52+
try {
53+
return super.read(b, off, len);
54+
} catch (IOException e) {
55+
if (isCancelException(e)) {
56+
throw SilentCancelException.INSTANCE;
57+
}
58+
throw e;
59+
}
60+
}
61+
}
62+
63+
// A throwable that avoids printing stack traces or messages when logged.
64+
private static final class SilentCancelException extends IOException {
65+
static final SilentCancelException INSTANCE = new SilentCancelException();
66+
private SilentCancelException() {
67+
super((String) null);
68+
setStackTrace(new StackTraceElement[0]);
69+
}
70+
@Override
71+
public String toString() { return ""; }
72+
@Override
73+
public void printStackTrace(PrintStream s) { /* no-op */ }
74+
@Override
75+
public void printStackTrace(PrintWriter s) { /* no-op */ }
76+
@Override
77+
public synchronized Throwable fillInStackTrace() { return this; }
78+
}
79+
80+
// Heuristic detection for request-cancellation exceptions (stream reset, socket closed, etc.)
81+
private static boolean isCancelException(Throwable e) {
82+
if (e == null) return false;
83+
Throwable t = e;
84+
while (t != null) {
85+
String cls = t.getClass().getName();
86+
if (cls != null && (cls.endsWith("StreamResetException") || cls.contains("StreamReset"))) {
87+
return true;
88+
}
89+
if (t instanceof InterruptedIOException) return true;
90+
if (t instanceof SocketException) return true;
91+
String msg = t.getMessage();
92+
if (msg != null) {
93+
String lower = msg.toLowerCase();
94+
if (lower.contains("cancel") || lower.contains("canceled") || lower.contains("stream was reset")) return true;
95+
}
96+
t = t.getCause();
97+
}
98+
return false;
99+
}
100+
29101
@Override
30102
public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super InputStream> callback) {
31103
// Build request with headers
@@ -82,9 +154,16 @@ public void loadData(@NonNull Priority priority, @NonNull DataCallback<? super I
82154
}
83155

84156
stream = responseBody.byteStream();
157+
// Wrap to mask noisy "StreamResetException: stream was reset: CANCEL" traces
158+
stream = new MaskingInputStream(stream);
85159
callback.onDataReady(stream);
86160

87161
} catch (IOException e) {
162+
// If request was canceled, call onLoadFailed with a trimmed exception (no stack trace) to avoid large logs
163+
if (isCancelException(e)) {
164+
callback.onLoadFailed(SilentCancelException.INSTANCE);
165+
return;
166+
}
88167
callback.onLoadFailed(e);
89168
}
90169
}

packages/image/platforms/android/java/com/nativescript/image/MatrixDrawableImageViewTarget.java

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ public class MatrixDrawableImageViewTarget extends ViewTarget<ImageView, Drawabl
2020
@Nullable
2121
private Animatable animatable;
2222

23+
// If true, clear drawable first on load, error. Disabling will prevent flickering
24+
// but wont look good with recycled views
25+
private boolean mClearFirst = true;
26+
2327
public MatrixDrawableImageViewTarget(ImageView view) {
2428
super(view);
2529
}
@@ -33,6 +37,15 @@ public MatrixDrawableImageViewTarget(ImageView view, boolean waitForLayout) {
3337
super(view, waitForLayout);
3438
}
3539

40+
/**
41+
* Control whether to preserve current drawable on load start/clear.
42+
* Default is true (clear). Call this immediately after creating the target if you want
43+
* to disable this with false.
44+
*/
45+
public void setClearFirst(boolean preserve) {
46+
this.mClearFirst = preserve;
47+
}
48+
3649
/**
3750
* Returns the current {@link android.graphics.drawable.Drawable} being
3851
* displayed in the view
@@ -66,8 +79,9 @@ public void setDrawable(Drawable drawable) {
6679
@Override
6780
public void onLoadStarted(@Nullable Drawable placeholder) {
6881
super.onLoadStarted(placeholder);
69-
70-
// setResourceInternal(null);
82+
if (this.mClearFirst) {
83+
setResourceInternal(null);
84+
}
7185
if (placeholder != null) {
7286
setDrawable(placeholder);
7387
}
@@ -83,7 +97,9 @@ public void onLoadStarted(@Nullable Drawable placeholder) {
8397
@Override
8498
public void onLoadFailed(@Nullable Drawable errorDrawable) {
8599
super.onLoadFailed(errorDrawable);
86-
setResourceInternal(null);
100+
if (this.mClearFirst) {
101+
setResourceInternal(null);
102+
}
87103
if (errorDrawable != null) {
88104
setDrawable(errorDrawable);
89105
}
@@ -102,7 +118,9 @@ public void onLoadCleared(@Nullable Drawable placeholder) {
102118
if (animatable != null) {
103119
animatable.stop();
104120
}
105-
// setResourceInternal(null);
121+
if (this.mClearFirst) {
122+
setResourceInternal(null);
123+
}
106124
if (placeholder != null) {
107125
setDrawable(placeholder);
108126
}

src/image/index.android.ts

Lines changed: 57 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -407,6 +407,8 @@ export class Img extends ImageBase {
407407
//TODO: remove as it needs to be added after TS 5.7 change https://github.com/microsoft/TypeScript/pull/59860
408408
[key: symbol]: (...args: any[]) => any | void;
409409

410+
public preventPreClearDrawable = false;
411+
410412
nativeViewProtected: com.nativescript.image.MatrixImageView;
411413
//@ts-expect-error just for declaration
412414
nativeImageViewProtected: com.nativescript.image.MatrixImageView;
@@ -442,6 +444,8 @@ export class Img extends ImageBase {
442444
}
443445

444446
public disposeNativeView() {
447+
// Cancel any running Glide requests before dispose
448+
this.cancelCurrentRequest();
445449
this.progressCallback = null;
446450
this.loadSourceCallback = null;
447451
}
@@ -583,9 +587,35 @@ export class Img extends ImageBase {
583587
}
584588
}
585589

590+
// Clear any active Glide target/request attached to this view.
591+
private cancelCurrentRequest(): void {
592+
const ctx = this._context;
593+
if (!ctx) {
594+
return;
595+
}
596+
if (this.currentTarget) {
597+
// cancel and drop reference; Any callbacks from the old target will be swallowed.
598+
try {
599+
com.bumptech.glide.Glide.with(ctx).clear(this.currentTarget);
600+
} catch (err) {
601+
// ignore
602+
}
603+
this.currentTarget = null;
604+
}
605+
// if (this.nativeViewProtected) {
606+
// try {
607+
// com.bumptech.glide.Glide.with(ctx).clear(this.nativeViewProtected);
608+
// } catch (err) {
609+
// // ignore
610+
// }
611+
// }
612+
}
613+
586614
private loadImageWithGlide(uri: string) {
587615
const view = this.nativeViewProtected;
588616
const context = this._context;
617+
// Cancel any prior Glide request/target for this view before starting a new one.
618+
this.cancelCurrentRequest();
589619
// Determine if this is a network request
590620
this.isNetworkRequest = typeof uri === 'string' && (uri.startsWith('http://') || uri.startsWith('https://'));
591621

@@ -747,6 +777,15 @@ export class Img extends ImageBase {
747777
onLoadFailed(e: any, model: any, target: any, isFirstResource: boolean): boolean {
748778
const instance = owner.get();
749779
if (instance) {
780+
// If this callback is for a previously canceled request, swallow it to avoid
781+
// emitting/logging errors for obsolete requests.
782+
if (instance.currentTarget && target !== instance.currentTarget) {
783+
instance.progressCallback = null;
784+
instance.loadSourceCallback = null;
785+
// Swallow: we handled it (don't let Glide default log/clear)
786+
return true;
787+
}
788+
instance.currentTarget = null;
750789
instance.progressCallback = null; // Clean up
751790
instance.loadSourceCallback = null;
752791
instance.notifyFailure(e);
@@ -756,6 +795,13 @@ export class Img extends ImageBase {
756795
onResourceReady(resource: android.graphics.drawable.Drawable, model: any, target: any, dataSource: any, isFirstResource: boolean): boolean {
757796
const instance = owner.get();
758797
if (instance) {
798+
// Ignore if the callback is from a previous request (stale).
799+
if (instance.currentTarget && target !== instance.currentTarget) {
800+
instance.progressCallback = null;
801+
instance.loadSourceCallback = null;
802+
return true;
803+
}
804+
instance.currentTarget = null;
759805
instance.progressCallback = null; // Clean up
760806
instance.loadSourceCallback = null;
761807

@@ -802,8 +848,14 @@ export class Img extends ImageBase {
802848
}
803849
});
804850

805-
this.currentTarget = new com.nativescript.image.MatrixDrawableImageViewTarget(view)
806-
requestBuilder.signature(signature).listener(new com.nativescript.image.CompositeRequestListener(objectArr)).into(this.currentTarget);
851+
// Always create and track our custom target so we can detect stale callbacks later.
852+
const target = new com.nativescript.image.MatrixDrawableImageViewTarget(view);
853+
this.currentTarget = target;
854+
if (this.preventPreClearDrawable) {
855+
target.setClearFirst(false);
856+
}
857+
858+
requestBuilder.signature(signature).listener(new com.nativescript.image.CompositeRequestListener(objectArr)).into(target);
807859
}
808860

809861
private notifyLoadSource(source: string) {
@@ -942,12 +994,7 @@ export class Img extends ImageBase {
942994
this.loadImageWithGlide(uri);
943995
} else {
944996
// Clear existing request before removing the drawable
945-
if (this.currentTarget) {
946-
com.bumptech.glide.Glide.with(this._context).clear(this.currentTarget);
947-
this.currentTarget = null;
948-
} else {
949-
com.bumptech.glide.Glide.with(this._context).clear(view);
950-
}
997+
this.cancelCurrentRequest();
951998
view.setImageDrawable(null);
952999
}
9531000
}
@@ -1010,14 +1057,14 @@ export class Img extends ImageBase {
10101057
}
10111058

10121059
startAnimating() {
1013-
const drawable = this.nativeViewProtected.getDrawable()
1060+
const drawable = this.nativeViewProtected.getDrawable();
10141061
if (drawable && drawable instanceof android.graphics.drawable.Animatable) {
10151062
drawable.start();
10161063
}
10171064
}
10181065

10191066
stopAnimating() {
1020-
const drawable = this.nativeViewProtected.getDrawable()
1067+
const drawable = this.nativeViewProtected.getDrawable();
10211068
if (drawable && drawable instanceof android.graphics.drawable.Animatable) {
10221069
drawable.stop();
10231070
}

src/image/typings/ui_image.android.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,9 @@ declare namespace com {
419419
public getOpacity(): number;
420420
}
421421
export class MatrixDrawableImageViewTarget extends com.bumptech.glide.request.target.ViewTarget<globalAndroid.widget.ImageView,globalAndroid.graphics.drawable.Drawable> implements com.bumptech.glide.request.transition.Transition.ViewAdapter {
422+
setClearFirst(arg0: boolean) {
423+
throw new Error('Method not implemented.');
424+
}
422425
public static class: java.lang.Class<com.nativescript.image.MatrixDrawableImageViewTarget>;
423426
public constructor(view: globalAndroid.widget.ImageView);
424427
public getCurrentDrawable(): globalAndroid.graphics.drawable.Drawable;

0 commit comments

Comments
 (0)