Skip to content

Commit 0bdd6c6

Browse files
committed
fix(android): use gpu rendered image for freeze snapshots
* fallback to software rendering * size overlay from captured image dimensions
1 parent 32a7017 commit 0bdd6c6

4 files changed

Lines changed: 121 additions & 24 deletions

File tree

mobilewebview/android/src/org/mobilewebview/MobileWebView.java

Lines changed: 80 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,19 @@
1111
import android.util.Log;
1212
import android.app.Activity;
1313
import android.net.Uri;
14+
import android.content.ContextWrapper;
1415
import android.view.View;
1516
import android.view.ViewGroup;
17+
import android.view.Window;
18+
import android.view.PixelCopy;
1619
import android.view.ViewParent;
1720
import android.webkit.WebSettings;
1821
import android.webkit.WebBackForwardList;
1922
import android.webkit.WebHistoryItem;
2023
import android.webkit.WebView;
2124
import android.os.Handler;
2225
import android.os.Looper;
26+
import android.graphics.Rect;
2327

2428
import java.io.ByteArrayOutputStream;
2529
import java.util.ArrayList;
@@ -422,35 +426,93 @@ public void captureSnapshotForFreeze(long requestId) {
422426
return;
423427
}
424428
if (mWebView == null) {
425-
nativeOnFreezeSnapshotReady(mNativePtr, requestId, 0, 0, null);
429+
reportFreezeSnapshotFailure(requestId);
426430
return;
427431
}
432+
final int w = mWebView.getWidth();
433+
final int h = mWebView.getHeight();
434+
if (w <= 0 || h <= 0) {
435+
reportFreezeSnapshotFailure(requestId);
436+
return;
437+
}
438+
428439
try {
429-
int w = mWebView.getWidth();
430-
int h = mWebView.getHeight();
431-
if (w <= 0 || h <= 0) {
432-
nativeOnFreezeSnapshotReady(mNativePtr, requestId, 0, 0, null);
440+
final Window window = resolveWindow(mWebView.getContext());
441+
if (window == null) {
442+
captureFreezeSoftware(requestId, w, h);
433443
return;
434444
}
435-
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
436-
bitmap.eraseColor(android.graphics.Color.WHITE);
437-
Canvas canvas = new Canvas(bitmap);
438-
mWebView.draw(canvas);
439-
ByteBuffer buffer = ByteBuffer.allocateDirect(w * h * 4);
440-
buffer.order(ByteOrder.nativeOrder());
441-
bitmap.copyPixelsToBuffer(buffer);
442-
byte[] pixels = new byte[w * h * 4];
443-
buffer.rewind();
444-
buffer.get(pixels);
445-
bitmap.recycle();
446-
nativeOnFreezeSnapshotReady(mNativePtr, requestId, w, h, pixels);
445+
446+
final Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
447+
final int[] location = new int[2];
448+
mWebView.getLocationInWindow(location);
449+
final Rect srcRect = new Rect(location[0], location[1], location[0] + w, location[1] + h);
450+
451+
PixelCopy.request(window, srcRect, bitmap, result -> {
452+
if (result == PixelCopy.SUCCESS) {
453+
deliverFreezeSnapshot(requestId, bitmap, w, h);
454+
return;
455+
}
456+
bitmap.recycle();
457+
Log.w(TAG, "PixelCopy freeze snapshot failed with code " + result
458+
+ ", falling back to software draw");
459+
captureFreezeSoftware(requestId, w, h);
460+
}, new Handler(Looper.getMainLooper()));
447461
} catch (RuntimeException e) {
448462
Log.e(TAG, "captureSnapshotForFreeze failed", e);
449-
nativeOnFreezeSnapshotReady(mNativePtr, requestId, 0, 0, null);
463+
reportFreezeSnapshotFailure(requestId);
450464
}
451465
});
452466
}
453467

468+
private void captureFreezeSoftware(long requestId, int w, int h) {
469+
try {
470+
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
471+
bitmap.eraseColor(android.graphics.Color.WHITE);
472+
mWebView.draw(new Canvas(bitmap));
473+
deliverFreezeSnapshot(requestId, bitmap, w, h);
474+
} catch (RuntimeException e) {
475+
Log.e(TAG, "software freeze snapshot failed", e);
476+
reportFreezeSnapshotFailure(requestId);
477+
}
478+
}
479+
480+
private void deliverFreezeSnapshot(long requestId, Bitmap bitmap, int w, int h) {
481+
final byte[] pixels;
482+
try {
483+
ByteBuffer buffer = ByteBuffer.allocate(w * h * 4);
484+
buffer.order(ByteOrder.nativeOrder());
485+
bitmap.copyPixelsToBuffer(buffer);
486+
pixels = buffer.array();
487+
} catch (RuntimeException e) {
488+
Log.e(TAG, "deliverFreezeSnapshot failed", e);
489+
reportFreezeSnapshotFailure(requestId);
490+
return;
491+
} finally {
492+
bitmap.recycle();
493+
}
494+
withNativePtr(ptr -> nativeOnFreezeSnapshotReady(ptr, requestId, w, h, pixels));
495+
}
496+
497+
private void reportFreezeSnapshotFailure(long requestId) {
498+
withNativePtr(ptr -> nativeOnFreezeSnapshotReady(ptr, requestId, 0, 0, null));
499+
}
500+
501+
private Window resolveWindow(Context context) {
502+
Context current = context;
503+
while (current instanceof ContextWrapper) {
504+
if (current instanceof Activity) {
505+
return ((Activity) current).getWindow();
506+
}
507+
Context base = ((ContextWrapper) current).getBaseContext();
508+
if (base == current) {
509+
break;
510+
}
511+
current = base;
512+
}
513+
return null;
514+
}
515+
454516
public void setInteractionEnabled(boolean enabled) {
455517
runOnMainThread(() -> {
456518
if (mWebView == null) return;

mobilewebview/src/common/mobilewebviewbackend.cpp

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@ void MobileWebViewBackendPrivate::notifySnapshotReady(quint64 requestId, const Q
244244
q_ptr->setClip(true);
245245
m_snapshotItem->setImage(image);
246246
m_snapshotItem->setVisible(true);
247-
updateFreezeOverlayGeometry();
247+
applyFreezeOverlaySizeFromImage(image);
248248

249249
const quint64 captureToken = requestId;
250250
QPointer<MobileWebViewBackend> guard(q_ptr);
@@ -271,14 +271,23 @@ void MobileWebViewBackendPrivate::clearFreezeState()
271271
updateNativeVisibility(q_ptr->isVisible());
272272
}
273273

274-
void MobileWebViewBackendPrivate::updateFreezeOverlayGeometry()
274+
void MobileWebViewBackendPrivate::applyFreezeOverlaySizeFromImage(const QImage &image)
275275
{
276-
if (!m_snapshotItem || !q_ptr) {
276+
if (!m_snapshotItem || !q_ptr || image.isNull()) {
277277
return;
278278
}
279+
280+
qreal dpr = 1.0;
281+
if (QQuickWindow *window = q_ptr->window()) {
282+
dpr = window->devicePixelRatio();
283+
}
284+
if (dpr <= 0) {
285+
dpr = 1.0;
286+
}
287+
279288
m_snapshotItem->setPosition(QPointF(0, 0));
280-
m_snapshotItem->setWidth(q_ptr->width());
281-
m_snapshotItem->setHeight(q_ptr->height());
289+
m_snapshotItem->setWidth(image.width() / dpr);
290+
m_snapshotItem->setHeight(image.height() / dpr);
282291
}
283292

284293
void MobileWebViewBackendPrivate::restoreClipState()

mobilewebview/src/common/mobilewebviewbackend_p.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class MobileWebViewBackendPrivate
6262
void notifySnapshotReady(quint64 requestId, const QImage &image);
6363

6464
void clearFreezeState();
65-
void updateFreezeOverlayGeometry();
65+
void applyFreezeOverlaySizeFromImage(const QImage &image);
6666
void restoreClipState();
6767

6868
/// Native WebView is hidden only in Frozen state (overlay replaces it).

mobilewebview/tests/tst_mobilewebviewbackend_common.mm

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ void captureSnapshotImpl(quint64 requestId) override
166166
void forwardsCallsAndStateChanges();
167167
void freezeIntentIsSynchronousAndCaptureCompletes();
168168
void freezeOverlayKeepsCaptureSizeOnResize();
169+
void freezeOverlayUsesCapturedImageSizeNotCurrentBackend();
169170
void freezeCancelledBeforeNotifyIgnoresStaleCallback();
170171
void freezeEmptySnapshotAbortsAndEmits();
171172
void freezeDoubleSetTrueOnlyCapturesOnce();
@@ -348,6 +349,31 @@ void captureSnapshotImpl(quint64 requestId) override
348349
QCOMPARE(d->m_snapshotItem->height(), 100.0);
349350
}
350351

352+
void MobileWebViewBackendCommonTest::freezeOverlayUsesCapturedImageSizeNotCurrentBackend()
353+
{
354+
g_lastCreatedPrivate = nullptr;
355+
MobileWebViewBackend backend;
356+
auto *d = g_lastCreatedPrivate;
357+
QVERIFY(d != nullptr);
358+
359+
backend.setWidth(200);
360+
backend.setHeight(100);
361+
backend.setFreeze(true);
362+
const quint64 rid = d->m_freezeRequestId;
363+
364+
// Simulate a resize while async snapshot capture is still in flight.
365+
backend.setWidth(50);
366+
backend.setHeight(25);
367+
368+
QImage img(200, 100, QImage::Format_ARGB32);
369+
img.fill(QColor(Qt::red));
370+
d->notifySnapshotReady(rid, img);
371+
372+
QVERIFY(d->m_snapshotItem != nullptr);
373+
QCOMPARE(d->m_snapshotItem->width(), 200.0);
374+
QCOMPARE(d->m_snapshotItem->height(), 100.0);
375+
}
376+
351377
void MobileWebViewBackendCommonTest::freezeCancelledBeforeNotifyIgnoresStaleCallback()
352378
{
353379
g_lastCreatedPrivate = nullptr;

0 commit comments

Comments
 (0)