Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 80 additions & 18 deletions mobilewebview/android/src/org/mobilewebview/MobileWebView.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,15 +11,19 @@
import android.util.Log;
import android.app.Activity;
import android.net.Uri;
import android.content.ContextWrapper;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.PixelCopy;
import android.view.ViewParent;
import android.webkit.WebSettings;
import android.webkit.WebBackForwardList;
import android.webkit.WebHistoryItem;
import android.webkit.WebView;
import android.os.Handler;
import android.os.Looper;
import android.graphics.Rect;

import java.io.ByteArrayOutputStream;
import java.util.ArrayList;
Expand Down Expand Up @@ -422,35 +426,93 @@ public void captureSnapshotForFreeze(long requestId) {
return;
}
if (mWebView == null) {
nativeOnFreezeSnapshotReady(mNativePtr, requestId, 0, 0, null);
reportFreezeSnapshotFailure(requestId);
return;
}
final int w = mWebView.getWidth();
final int h = mWebView.getHeight();
if (w <= 0 || h <= 0) {
reportFreezeSnapshotFailure(requestId);
return;
}

try {
int w = mWebView.getWidth();
int h = mWebView.getHeight();
if (w <= 0 || h <= 0) {
nativeOnFreezeSnapshotReady(mNativePtr, requestId, 0, 0, null);
final Window window = resolveWindow(mWebView.getContext());
if (window == null) {
captureFreezeSoftware(requestId, w, h);
return;
}
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
bitmap.eraseColor(android.graphics.Color.WHITE);
Canvas canvas = new Canvas(bitmap);
mWebView.draw(canvas);
ByteBuffer buffer = ByteBuffer.allocateDirect(w * h * 4);
buffer.order(ByteOrder.nativeOrder());
bitmap.copyPixelsToBuffer(buffer);
byte[] pixels = new byte[w * h * 4];
buffer.rewind();
buffer.get(pixels);
bitmap.recycle();
nativeOnFreezeSnapshotReady(mNativePtr, requestId, w, h, pixels);

final Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
final int[] location = new int[2];
mWebView.getLocationInWindow(location);
final Rect srcRect = new Rect(location[0], location[1], location[0] + w, location[1] + h);

PixelCopy.request(window, srcRect, bitmap, result -> {
if (result == PixelCopy.SUCCESS) {
Comment thread
friofry marked this conversation as resolved.
deliverFreezeSnapshot(requestId, bitmap, w, h);
return;
}
bitmap.recycle();
Log.w(TAG, "PixelCopy freeze snapshot failed with code " + result
+ ", falling back to software draw");
captureFreezeSoftware(requestId, w, h);
}, new Handler(Looper.getMainLooper()));
} catch (RuntimeException e) {
Log.e(TAG, "captureSnapshotForFreeze failed", e);
nativeOnFreezeSnapshotReady(mNativePtr, requestId, 0, 0, null);
reportFreezeSnapshotFailure(requestId);
}
});
}

private void captureFreezeSoftware(long requestId, int w, int h) {
try {
Bitmap bitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
bitmap.eraseColor(android.graphics.Color.WHITE);
mWebView.draw(new Canvas(bitmap));
deliverFreezeSnapshot(requestId, bitmap, w, h);
} catch (RuntimeException e) {
Log.e(TAG, "software freeze snapshot failed", e);
reportFreezeSnapshotFailure(requestId);
}
}

private void deliverFreezeSnapshot(long requestId, Bitmap bitmap, int w, int h) {
final byte[] pixels;
try {
ByteBuffer buffer = ByteBuffer.allocate(w * h * 4);
buffer.order(ByteOrder.nativeOrder());
bitmap.copyPixelsToBuffer(buffer);
pixels = buffer.array();
} catch (RuntimeException e) {
Log.e(TAG, "deliverFreezeSnapshot failed", e);
reportFreezeSnapshotFailure(requestId);
return;
} finally {
bitmap.recycle();
}
withNativePtr(ptr -> nativeOnFreezeSnapshotReady(ptr, requestId, w, h, pixels));
}

private void reportFreezeSnapshotFailure(long requestId) {
withNativePtr(ptr -> nativeOnFreezeSnapshotReady(ptr, requestId, 0, 0, null));
}

private Window resolveWindow(Context context) {
Context current = context;
while (current instanceof ContextWrapper) {
if (current instanceof Activity) {
return ((Activity) current).getWindow();
}
Context base = ((ContextWrapper) current).getBaseContext();
if (base == current) {
break;
}
current = base;
}
return null;
}

public void setInteractionEnabled(boolean enabled) {
runOnMainThread(() -> {
if (mWebView == null) return;
Expand Down
36 changes: 30 additions & 6 deletions mobilewebview/src/common/mobilewebviewbackend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -237,9 +237,14 @@ void MobileWebViewBackendPrivate::notifySnapshotReady(quint64 requestId, const Q
if (!m_snapshotItem) {
m_snapshotItem = new MobileWebViewSnapshotItem(q_ptr);
}
if (!m_freezeClipStateStored) {
m_clipStateBeforeFreeze = q_ptr->clip();
m_freezeClipStateStored = true;
}
q_ptr->setClip(true);
m_snapshotItem->setImage(image);
m_snapshotItem->setVisible(true);
updateFreezeOverlayGeometry();
applyFreezeOverlaySizeFromImage(image);

const quint64 captureToken = requestId;
QPointer<MobileWebViewBackend> guard(q_ptr);
Expand All @@ -262,17 +267,36 @@ void MobileWebViewBackendPrivate::clearFreezeState()
m_snapshotItem->deleteLater();
m_snapshotItem = nullptr;
}
restoreClipState();
updateNativeVisibility(q_ptr->isVisible());
}

void MobileWebViewBackendPrivate::updateFreezeOverlayGeometry()
void MobileWebViewBackendPrivate::applyFreezeOverlaySizeFromImage(const QImage &image)
{
if (!m_snapshotItem || !q_ptr) {
if (!m_snapshotItem || !q_ptr || image.isNull()) {
return;
}

qreal dpr = 1.0;
if (QQuickWindow *window = q_ptr->window()) {
dpr = window->devicePixelRatio();
}
if (dpr <= 0) {
dpr = 1.0;
}

m_snapshotItem->setPosition(QPointF(0, 0));
m_snapshotItem->setWidth(q_ptr->width());
m_snapshotItem->setHeight(q_ptr->height());
m_snapshotItem->setWidth(image.width() / dpr);
m_snapshotItem->setHeight(image.height() / dpr);
}

void MobileWebViewBackendPrivate::restoreClipState()
{
if (!q_ptr || !m_freezeClipStateStored) {
return;
}
q_ptr->setClip(m_clipStateBeforeFreeze);
m_freezeClipStateStored = false;
}

void MobileWebViewBackendPrivate::setupTransport()
Expand Down Expand Up @@ -633,6 +657,7 @@ void MobileWebViewBackend::setFreeze(bool freeze)
MobileWebViewSnapshotItem *overlay = d->m_snapshotItem;
d->m_snapshotItem = nullptr;
d->m_freezeState = FS::Idle;
d->restoreClipState();
d->updateNativeVisibility(d->q_ptr->isVisible());
emit freezeChanged();
QTimer::singleShot(kFreezeOverlayFrameDelayMs, this, [overlay]() {
Expand Down Expand Up @@ -795,7 +820,6 @@ void MobileWebViewBackend::geometryChange(const QRectF &newGeometry, const QRect
if (newGeometry != oldGeometry) {
Q_D(MobileWebViewBackend);
d->updateNativeGeometry(newGeometry);
Comment thread
friofry marked this conversation as resolved.
d->updateFreezeOverlayGeometry();
}
}

Expand Down
5 changes: 4 additions & 1 deletion mobilewebview/src/common/mobilewebviewbackend_p.h
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ class MobileWebViewBackendPrivate
void notifySnapshotReady(quint64 requestId, const QImage &image);

void clearFreezeState();
void updateFreezeOverlayGeometry();
void applyFreezeOverlaySizeFromImage(const QImage &image);
void restoreClipState();

/// Native WebView is hidden only in Frozen state (overlay replaces it).
bool shouldShowNativeWebView(bool qmlItemVisible) const
Expand Down Expand Up @@ -104,6 +105,8 @@ class MobileWebViewBackendPrivate
/// DPR from QQuickWindow at requestSnapshot time; used to convert logical targetSize to pixels.
qreal m_publicSnapshotDpr = 1.0;
MobileWebViewSnapshotItem *m_snapshotItem = nullptr;
bool m_freezeClipStateStored = false;
bool m_clipStateBeforeFreeze = false;

// Common methods (implemented in mobilewebviewbackend.cpp)
void setLoading(bool loading);
Expand Down
57 changes: 57 additions & 0 deletions mobilewebview/tests/tst_mobilewebviewbackend_common.mm
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,8 @@ void captureSnapshotImpl(quint64 requestId) override
private slots:
void forwardsCallsAndStateChanges();
void freezeIntentIsSynchronousAndCaptureCompletes();
void freezeOverlayKeepsCaptureSizeOnResize();
void freezeOverlayUsesCapturedImageSizeNotCurrentBackend();
void freezeCancelledBeforeNotifyIgnoresStaleCallback();
void freezeEmptySnapshotAbortsAndEmits();
void freezeDoubleSetTrueOnlyCapturesOnce();
Expand Down Expand Up @@ -317,6 +319,61 @@ void captureSnapshotImpl(quint64 requestId) override
QCOMPARE(freezeSpy.count(), 1);
}

void MobileWebViewBackendCommonTest::freezeOverlayKeepsCaptureSizeOnResize()
{
g_lastCreatedPrivate = nullptr;
MobileWebViewBackend backend;
auto *d = g_lastCreatedPrivate;
QVERIFY(d != nullptr);

backend.setWidth(200);
backend.setHeight(100);
backend.setFreeze(true);

QImage img(200, 100, QImage::Format_ARGB32);
img.fill(QColor(Qt::red));
d->notifySnapshotReady(d->m_freezeRequestId, img);

QVERIFY(d->m_snapshotItem != nullptr);
QCOMPARE(d->m_snapshotItem->width(), 200.0);
QCOMPARE(d->m_snapshotItem->height(), 100.0);

backend.setWidth(100);
backend.setHeight(50);
QCOMPARE(d->m_snapshotItem->width(), 200.0);
QCOMPARE(d->m_snapshotItem->height(), 100.0);

backend.setWidth(400);
backend.setHeight(200);
QCOMPARE(d->m_snapshotItem->width(), 200.0);
QCOMPARE(d->m_snapshotItem->height(), 100.0);
}

void MobileWebViewBackendCommonTest::freezeOverlayUsesCapturedImageSizeNotCurrentBackend()
{
g_lastCreatedPrivate = nullptr;
MobileWebViewBackend backend;
auto *d = g_lastCreatedPrivate;
QVERIFY(d != nullptr);

backend.setWidth(200);
backend.setHeight(100);
backend.setFreeze(true);
const quint64 rid = d->m_freezeRequestId;

// Simulate a resize while async snapshot capture is still in flight.
backend.setWidth(50);
backend.setHeight(25);

QImage img(200, 100, QImage::Format_ARGB32);
img.fill(QColor(Qt::red));
d->notifySnapshotReady(rid, img);

QVERIFY(d->m_snapshotItem != nullptr);
QCOMPARE(d->m_snapshotItem->width(), 200.0);
QCOMPARE(d->m_snapshotItem->height(), 100.0);
}

void MobileWebViewBackendCommonTest::freezeCancelledBeforeNotifyIgnoresStaleCallback()
{
g_lastCreatedPrivate = nullptr;
Expand Down
Loading