6.7.0 - Alpha14 - 修复 images.captureScreen 结果方向错误或黑边问题 (issue #337); 修复 ImageWrapper#recycle; 优化 device.rotation/rotation/width/height
This commit is contained in:
@@ -1,20 +0,0 @@
|
||||
package org.autojs.autojs.core.image
|
||||
|
||||
import android.media.Image
|
||||
import org.autojs.autojs.runtime.ScriptRuntime
|
||||
|
||||
/**
|
||||
* Created by SuperMonster003 on Dec 15, 2023.
|
||||
* Modified by SuperMonster003 as of May 20, 2025.
|
||||
*/
|
||||
// @Reference to Auto.js Pro 9.3.11 by SuperMonster003 on Dec 15, 2023.
|
||||
class CapturedImage(scriptRuntime: ScriptRuntime, image: Image) : ImageWrapper(scriptRuntime, image) {
|
||||
|
||||
override fun recycle() {
|
||||
/* Doing nothing to suppress default recycle method. */
|
||||
}
|
||||
|
||||
fun recycleInternal() {
|
||||
super.recycle()
|
||||
}
|
||||
}
|
||||
@@ -139,9 +139,21 @@ open class ImageWrapper : Shootable<ImageWrapper> {
|
||||
|
||||
constructor(scriptRuntime: ScriptRuntime, mediaImage: Image) {
|
||||
mScriptRuntime = scriptRuntime
|
||||
mMediaImage = mediaImage.also { addToList(it) }
|
||||
mWidth = mediaImage.width
|
||||
mHeight = mediaImage.height
|
||||
|
||||
// Detach from ImageReader lifecycle by copying pixels immediately.
|
||||
// zh-CN: 通过立即拷贝像素来与 ImageReader 的生命周期解耦.
|
||||
mBitmap = toBitmap(mediaImage).also { addToList(it) }
|
||||
|
||||
// Close the original Image ASAP to avoid holding unstable buffers.
|
||||
// zh-CN: 尽快关闭原始 Image, 避免持有不稳定的底层 buffer.
|
||||
mediaImage.close()
|
||||
|
||||
// Keep media references null after detaching.
|
||||
// zh-CN: 解耦后不再保留 media 引用.
|
||||
mMediaImage = null
|
||||
mPlane = null
|
||||
}
|
||||
|
||||
init {
|
||||
|
||||
@@ -4,20 +4,15 @@ import android.media.ImageReader;
|
||||
|
||||
/**
|
||||
* Created by SuperMonster003 on Dec 15, 2023.
|
||||
* Modified by SuperMonster003 as of Jan 11, 2026.
|
||||
*/
|
||||
// @Reference to Auto.js Pro 9.3.11 by SuperMonster003 on Dec 15, 2023.
|
||||
public class OnImageAvailableListenerSync implements ImageReader.OnImageAvailableListener {
|
||||
public record OnImageAvailableListenerSync(ScreenCapturer screenCapturer) implements ImageReader.OnImageAvailableListener {
|
||||
|
||||
public final ScreenCapturer mScreenCapturer;
|
||||
public final ImageReader mImageReader;
|
||||
|
||||
public OnImageAvailableListenerSync(ScreenCapturer screenCapturer, ImageReader imageReader) {
|
||||
this.mScreenCapturer = screenCapturer;
|
||||
this.mImageReader = imageReader;
|
||||
}
|
||||
|
||||
public final void onImageAvailable(ImageReader imageReader) {
|
||||
mScreenCapturer.setImageListenerSync(mImageReader);
|
||||
public void onImageAvailable(ImageReader imageReader) {
|
||||
// Use the actual callback parameter ImageReader.
|
||||
// zh-CN: 使用回调参数中的实际 ImageReader, 避免 refreshImageReader 后因引用过期导致无法唤醒等待线程.
|
||||
screenCapturer.setImageListenerSync(imageReader);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,10 @@ import android.media.projection.MediaProjection;
|
||||
import android.media.projection.MediaProjectionManager;
|
||||
import android.os.Build;
|
||||
import android.os.Handler;
|
||||
import android.os.HandlerThread;
|
||||
import android.os.SystemClock;
|
||||
import android.view.Display;
|
||||
import android.view.Surface;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import org.autojs.autojs.runtime.api.ScreenMetrics;
|
||||
@@ -27,13 +31,35 @@ import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* Created by Stardust on May 17, 2017.
|
||||
* Modified by SuperMonster003 as of May 19, 2022.
|
||||
* Modified by SuperMonster003 as of Jan 11, 2026.
|
||||
*/
|
||||
// @Reference to Auto.js Pro 9.3.11 by SuperMonster003 on Dec 19, 2023.
|
||||
public class ScreenCapturer {
|
||||
|
||||
private static final Pattern PATTERN_BUFFER_FORMAT_EXCEPTION = Pattern.compile("buffer format ([0-9a-zA-Z]+) doesn't match");
|
||||
|
||||
// Reduce total wait budget and use shorter waits to fail fast on abandoned BufferQueue.
|
||||
// zh-CN: 降低总等待预算并使用更短的等待粒度, 以在 BufferQueue 已被 abandoned 等场景下更快失败.
|
||||
private static final long CAPTURE_TOTAL_TIMEOUT_MS = 1200;
|
||||
|
||||
// Use smaller wait slices for faster convergence.
|
||||
// zh-CN: 使用更小的等待切片以更快收敛.
|
||||
private static final long IMAGE_AVAILABLE_WAIT_SLICE_MS = 60;
|
||||
|
||||
// Trigger self-healing earlier in the capture window.
|
||||
// zh-CN: 在 capture 窗口的更早阶段触发自愈.
|
||||
private static final long EARLY_HEALING_AT_MS = 600;
|
||||
|
||||
// Use a dedicated thread for ImageReader callbacks to avoid deadlock when capture() blocks.
|
||||
// zh-CN: 使用独立线程处理 ImageReader 回调, 避免 capture() 阻塞时与回调线程相同导致的 "自锁式等待".
|
||||
private final HandlerThread mImageCallbackThread;
|
||||
private final Handler mImageCallbackHandler;
|
||||
|
||||
// Listen to display changes even when AutoJs6 is in background (no foreground Activity).
|
||||
// zh-CN: 即使 AutoJs6 在后台 (无前台 Activity), 也通过 DisplayListener 监听显示变化, 避免配置事件丢失.
|
||||
private final DisplayManager mDisplayManager;
|
||||
private final DisplayManager.DisplayListener mDisplayListener;
|
||||
|
||||
public static final int ORIENTATION_AUTO = Configuration.ORIENTATION_UNDEFINED; // 0
|
||||
public static final int ORIENTATION_LANDSCAPE = Configuration.ORIENTATION_LANDSCAPE; // 1
|
||||
public static final int ORIENTATION_PORTRAIT = Configuration.ORIENTATION_PORTRAIT; // 2
|
||||
@@ -42,7 +68,6 @@ public class ScreenCapturer {
|
||||
private volatile Image mUnderUsingImage;
|
||||
private final int mScreenDensity;
|
||||
private final Handler mHandler;
|
||||
private final Context mContext;
|
||||
private final int mOrientation;
|
||||
private final Object mImageAvailableLock = new Object();
|
||||
private final Options mOptions;
|
||||
@@ -51,24 +76,83 @@ public class ScreenCapturer {
|
||||
private VirtualDisplay mVirtualDisplay;
|
||||
private OnScreenCaptureAvailableListener mOnScreenCaptureAvailableListener;
|
||||
private int mDetectedOrientation;
|
||||
private int mAppliedOrientation = ORIENTATION_AUTO;
|
||||
private int mPixelFormat = PixelFormat.RGBA_8888;
|
||||
private volatile boolean mImageAvailable = false;
|
||||
private boolean mShouldRefreshVirtualDisplayOnNextCapture = false;
|
||||
|
||||
public ScreenCapturer(Context context, Intent data, Options options, Handler handler) {
|
||||
mContext = context;
|
||||
mOptions = options;
|
||||
mHandler = handler;
|
||||
mMediaProjection = ((MediaProjectionManager) mContext.getSystemService(Context.MEDIA_PROJECTION_SERVICE)).getMediaProjection(Activity.RESULT_OK, (Intent) data.clone());
|
||||
|
||||
mImageCallbackThread = new HandlerThread("ScreenCapturer-ImageReader");
|
||||
mImageCallbackThread.start();
|
||||
mImageCallbackHandler = new Handler(mImageCallbackThread.getLooper());
|
||||
|
||||
mDisplayManager = (DisplayManager) context.getSystemService(Context.DISPLAY_SERVICE);
|
||||
|
||||
mDisplayListener = new DisplayManager.DisplayListener() {
|
||||
@Override
|
||||
public void onDisplayAdded(int displayId) {
|
||||
/* Ignored. */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayRemoved(int displayId) {
|
||||
/* Ignored. */
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisplayChanged(int displayId) {
|
||||
// Only care about DEFAULT_DISPLAY.
|
||||
// zh-CN: 只关心 DEFAULT_DISPLAY.
|
||||
if (displayId != Display.DEFAULT_DISPLAY) return;
|
||||
|
||||
// Mark refresh for next capture to survive background state.
|
||||
// zh-CN: 标记下一次 capture 刷新, 用于后台状态下避免错过配置变化事件.
|
||||
refreshDetectedOrientation();
|
||||
if (mOptions.isAsync) {
|
||||
mHandler.post(() -> refreshVirtualDisplay(mDetectedOrientation, false));
|
||||
} else {
|
||||
mShouldRefreshVirtualDisplayOnNextCapture = true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Register on a non-blocking handler.
|
||||
// zh-CN: 使用不易被阻塞的 handler 注册监听.
|
||||
mDisplayManager.registerDisplayListener(mDisplayListener, mImageCallbackHandler);
|
||||
|
||||
mMediaProjection = ((MediaProjectionManager) context.getSystemService(Context.MEDIA_PROJECTION_SERVICE))
|
||||
.getMediaProjection(Activity.RESULT_OK, (Intent) data.clone());
|
||||
mScreenDensity = options.density;
|
||||
mOrientation = options.orientation;
|
||||
refreshVirtualDisplay(mOrientation == ORIENTATION_AUTO ? mDetectedOrientation : mOrientation, true);
|
||||
|
||||
refreshDetectedOrientation();
|
||||
refreshVirtualDisplay(mOrientation == ORIENTATION_AUTO ? mDetectedOrientation : mOrientation, true);
|
||||
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
private void refreshDetectedOrientation() {
|
||||
mDetectedOrientation = mContext.getResources().getConfiguration().orientation;
|
||||
// Prefer rotation over (w/h) for orientation detection.
|
||||
// zh-CN: 使用 rotation 而不是 (w/h) 判断方向.
|
||||
int rotation = ScreenMetrics.getRotation();
|
||||
mDetectedOrientation = (rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270)
|
||||
? ORIENTATION_LANDSCAPE
|
||||
: ORIENTATION_PORTRAIT;
|
||||
}
|
||||
|
||||
private int getExpectedWidthByDetectedOrientation() {
|
||||
// Keep consistent with refreshVirtualDisplay() sizing logic.
|
||||
// zh-CN: 与 refreshVirtualDisplay() 的尺寸计算保持一致.
|
||||
return ScreenMetrics.getOrientationAwareScreenWidth(mDetectedOrientation);
|
||||
}
|
||||
|
||||
private int getExpectedHeightByDetectedOrientation() {
|
||||
// Keep consistent with refreshVirtualDisplay() sizing logic.
|
||||
// zh-CN: 与 refreshVirtualDisplay() 的尺寸计算保持一致.
|
||||
return ScreenMetrics.getOrientationAwareScreenHeight(mDetectedOrientation);
|
||||
}
|
||||
|
||||
public record Options(int width, int height, int orientation, int density, boolean isAsync) {
|
||||
@@ -85,18 +169,64 @@ public class ScreenCapturer {
|
||||
void onCaptureAvailable(Image image);
|
||||
}
|
||||
|
||||
private Image acquireLatestImage() {
|
||||
waitForImageAvailable();
|
||||
try {
|
||||
return mImageReader.acquireLatestImage();
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
Integer pixelFormat = getPixelFormat(ex);
|
||||
if (pixelFormat != null) {
|
||||
setPixelFormat(pixelFormat);
|
||||
waitForImageAvailable();
|
||||
return mImageReader.acquireLatestImage();
|
||||
private Image acquireLatestImage(long deadlineUptimeMillis) {
|
||||
// Always wait for a fresh frame in sync mode.
|
||||
// zh-CN: 同步模式下每次都等待一帧新图.
|
||||
if (!mOptions.isAsync) {
|
||||
synchronized (mImageAvailableLock) {
|
||||
mImageAvailable = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Try several times but never exceed the given deadline.
|
||||
// zh-CN: 做有限次数重试, 但绝不超过 deadline 指定的总时间预算.
|
||||
for (int i = 0; i < 20; i++) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
if (now >= deadlineUptimeMillis) return null;
|
||||
|
||||
long remain = deadlineUptimeMillis - now;
|
||||
waitForImageAvailable(Math.min(IMAGE_AVAILABLE_WAIT_SLICE_MS, remain));
|
||||
|
||||
try {
|
||||
Image img = mImageReader.acquireLatestImage();
|
||||
if (img != null) return img;
|
||||
} catch (UnsupportedOperationException ex) {
|
||||
Integer pixelFormat = getPixelFormat(ex);
|
||||
if (pixelFormat != null) {
|
||||
setPixelFormat(pixelFormat);
|
||||
if (!mOptions.isAsync) {
|
||||
synchronized (mImageAvailableLock) {
|
||||
mImageAvailable = false;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
|
||||
// Reset and wait again.
|
||||
// zh-CN: 重置标记并继续等待下一帧.
|
||||
if (!mOptions.isAsync) {
|
||||
synchronized (mImageAvailableLock) {
|
||||
mImageAvailable = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void waitForImageAvailable(long timeoutMillis) {
|
||||
if (!mImageAvailable) {
|
||||
synchronized (mImageAvailableLock) {
|
||||
if (!mImageAvailable) {
|
||||
try {
|
||||
mImageAvailableLock.wait(timeoutMillis);
|
||||
} catch (InterruptedException ex) {
|
||||
throw new ScriptInterruptedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,7 +254,16 @@ public class ScreenCapturer {
|
||||
}, mHandler);
|
||||
}
|
||||
|
||||
mVirtualDisplay = mMediaProjection.createVirtualDisplay(ScreenCapturer.class.getSimpleName(), width, height, screenDensity, DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), null, null);
|
||||
mVirtualDisplay = mMediaProjection.createVirtualDisplay(
|
||||
ScreenCapturer.class.getSimpleName(),
|
||||
width,
|
||||
height,
|
||||
screenDensity,
|
||||
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
|
||||
mImageReader.getSurface(),
|
||||
null,
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
private void refreshImageReader(int width, int height) {
|
||||
@@ -136,7 +275,10 @@ public class ScreenCapturer {
|
||||
}
|
||||
int maxImages = mOptions.isAsync ? 1 : 3;
|
||||
mImageReader = ImageReader.newInstance(width, height, mPixelFormat, maxImages);
|
||||
setImageListener(mHandler);
|
||||
|
||||
// Always dispatch ImageReader callbacks on the dedicated thread.
|
||||
// zh-CN: 始终在独立线程分发 ImageReader 回调.
|
||||
setImageListener(mImageCallbackHandler);
|
||||
}
|
||||
|
||||
private void refreshVirtualDisplay(int orientation, boolean isInit) {
|
||||
@@ -149,21 +291,44 @@ public class ScreenCapturer {
|
||||
width.set(ScreenMetrics.getOrientationAwareScreenWidth(orientation));
|
||||
height.set(ScreenMetrics.getOrientationAwareScreenHeight(orientation));
|
||||
}
|
||||
|
||||
// Recreate VirtualDisplay when orientation changes in AUTO mode.
|
||||
// VirtualDisplay.resize(...) may not reliably switch output orientation on some devices,
|
||||
// causing canvas size to mismatch content orientation.
|
||||
// zh-CN:
|
||||
// AUTO 模式下只要方向发生变化, 就直接重建 VirtualDisplay.
|
||||
// 部分设备上 VirtualDisplay.resize(...) 可能无法可靠切换输出方向, 从而导致画布尺寸与内容方向错配.
|
||||
boolean shouldRecreate = !isInit
|
||||
&& mVirtualDisplay != null
|
||||
&& mOrientation == ORIENTATION_AUTO
|
||||
&& orientation != mAppliedOrientation;
|
||||
|
||||
if (mVirtualDisplay == null) {
|
||||
if (isInit) {
|
||||
initVirtualDisplay(width.get(), height.get(), mScreenDensity);
|
||||
mAppliedOrientation = orientation;
|
||||
}
|
||||
} else {
|
||||
refreshImageReader(width.get(), height.get());
|
||||
mVirtualDisplay.setSurface(mImageReader.getSurface());
|
||||
mVirtualDisplay.resize(width.get(), height.get(), mScreenDensity);
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldRecreate) {
|
||||
mVirtualDisplay.release();
|
||||
mVirtualDisplay = null;
|
||||
initVirtualDisplay(width.get(), height.get(), mScreenDensity);
|
||||
mAppliedOrientation = orientation;
|
||||
return;
|
||||
}
|
||||
|
||||
refreshImageReader(width.get(), height.get());
|
||||
mVirtualDisplay.setSurface(mImageReader.getSurface());
|
||||
mVirtualDisplay.resize(width.get(), height.get(), mScreenDensity);
|
||||
mAppliedOrientation = orientation;
|
||||
}
|
||||
|
||||
private void setImageListener(Handler handler) {
|
||||
ImageReader.OnImageAvailableListener o = mOptions.isAsync
|
||||
? new OnImageAvailableListenerAsync(this)
|
||||
: new OnImageAvailableListenerSync(this, mImageReader);
|
||||
: new OnImageAvailableListenerSync(this);
|
||||
mImageReader.setOnImageAvailableListener(o, handler);
|
||||
}
|
||||
|
||||
@@ -182,12 +347,12 @@ public class ScreenCapturer {
|
||||
}
|
||||
|
||||
public void setImageListenerSync(ImageReader imageReader) {
|
||||
imageReader.setOnImageAvailableListener(null, null);
|
||||
if (!mImageAvailable && imageReader == mImageReader) {
|
||||
synchronized (mImageAvailableLock) {
|
||||
mImageAvailable = true;
|
||||
mImageAvailableLock.notifyAll();
|
||||
}
|
||||
// Always notify for the current ImageReader.
|
||||
// zh-CN: 只要回调来自当前 ImageReader, 就直接唤醒等待线程, 避免信号丢失.
|
||||
if (imageReader != mImageReader) return;
|
||||
synchronized (mImageAvailableLock) {
|
||||
mImageAvailable = true;
|
||||
mImageAvailableLock.notifyAll();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,37 +362,88 @@ public class ScreenCapturer {
|
||||
mVirtualDisplay.setSurface(mImageReader.getSurface());
|
||||
}
|
||||
|
||||
private void waitForImageAvailable() {
|
||||
if (!mImageAvailable) {
|
||||
synchronized (mImageAvailableLock) {
|
||||
if (!mImageAvailable) {
|
||||
try {
|
||||
mImageAvailableLock.wait();
|
||||
} catch (InterruptedException ex) {
|
||||
throw new ScriptInterruptedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Image capture() {
|
||||
if (mOptions.isAsync) {
|
||||
throw new IllegalStateException("capture() is not available in async mode");
|
||||
}
|
||||
|
||||
final long start = SystemClock.uptimeMillis();
|
||||
final long deadline = start + CAPTURE_TOTAL_TIMEOUT_MS;
|
||||
|
||||
// For AUTO mode, do a best-effort self-check before acquiring the frame.
|
||||
// zh-CN: AUTO 模式下, 在取帧之前做一次尽力自检, 发现画布尺寸不匹配则主动刷新 VirtualDisplay.
|
||||
if (mOrientation == ORIENTATION_AUTO) {
|
||||
refreshDetectedOrientation();
|
||||
|
||||
int expectedWidth = getExpectedWidthByDetectedOrientation();
|
||||
int expectedHeight = getExpectedHeightByDetectedOrientation();
|
||||
|
||||
// ImageReader size represents the "canvas" size of VirtualDisplay.
|
||||
// zh-CN: ImageReader 尺寸代表 VirtualDisplay 的 "画布" 尺寸.
|
||||
if (mImageReader != null
|
||||
&& (mImageReader.getWidth() != expectedWidth || mImageReader.getHeight() != expectedHeight)) {
|
||||
refreshVirtualDisplay(mDetectedOrientation, false);
|
||||
}
|
||||
}
|
||||
|
||||
if (mShouldRefreshVirtualDisplayOnNextCapture) {
|
||||
mShouldRefreshVirtualDisplayOnNextCapture = false;
|
||||
refreshVirtualDisplay(mDetectedOrientation, false);
|
||||
}
|
||||
Image acquireLatestImage = acquireLatestImage();
|
||||
if (acquireLatestImage != null) {
|
||||
|
||||
// Retry a few times to skip transitional frames after resizing/switching,
|
||||
// but respect the total timeout budget.
|
||||
// zh-CN: 在 resize/切换后重试少量次数以跳过过渡帧, 但必须遵守总超时预算.
|
||||
for (int i = 0; i < 5; i++) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
if (now >= deadline) break;
|
||||
|
||||
// Early self-healing in AUTO mode to avoid spending the whole budget waiting on a bad pipeline.
|
||||
// zh-CN: AUTO 模式下尽早自愈, 避免把整个预算都耗在一个已失效的管线 (如 BufferQueue abandoned) 上.
|
||||
if (mOrientation == ORIENTATION_AUTO && (now - start) >= EARLY_HEALING_AT_MS) {
|
||||
refreshDetectedOrientation();
|
||||
refreshVirtualDisplay(mDetectedOrientation, false);
|
||||
}
|
||||
|
||||
Image acquireLatestImage = acquireLatestImage(deadline);
|
||||
if (acquireLatestImage == null) continue;
|
||||
|
||||
if (mOrientation == ORIENTATION_AUTO) {
|
||||
int expectedWidth = getExpectedWidthByDetectedOrientation();
|
||||
int expectedHeight = getExpectedHeightByDetectedOrientation();
|
||||
if (acquireLatestImage.getWidth() != expectedWidth || acquireLatestImage.getHeight() != expectedHeight) {
|
||||
// Drop mismatched frame and refresh display once more.
|
||||
// zh-CN: 丢弃尺寸不匹配的帧, 并再次刷新 display.
|
||||
acquireLatestImage.close();
|
||||
refreshVirtualDisplay(mDetectedOrientation, false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (mUnderUsingImage != null) {
|
||||
mUnderUsingImage.close();
|
||||
}
|
||||
mUnderUsingImage = acquireLatestImage;
|
||||
return mUnderUsingImage;
|
||||
}
|
||||
return mUnderUsingImage;
|
||||
|
||||
// If timed out, force a best-effort rebuild once to recover from "no-frame" bad state.
|
||||
// zh-CN: 若超时, 尝试强制重建一次以从 "无帧" 坏状态中自愈.
|
||||
if (mOrientation == ORIENTATION_AUTO) {
|
||||
refreshDetectedOrientation();
|
||||
if (mVirtualDisplay != null) {
|
||||
mVirtualDisplay.release();
|
||||
mVirtualDisplay = null;
|
||||
}
|
||||
initVirtualDisplay(getExpectedWidthByDetectedOrientation(), getExpectedHeightByDetectedOrientation(), mScreenDensity);
|
||||
mAppliedOrientation = mDetectedOrientation;
|
||||
mShouldRefreshVirtualDisplayOnNextCapture = false;
|
||||
}
|
||||
|
||||
// Do not return stale cached image when no valid frame is available.
|
||||
// zh-CN: 当无法获取到有效帧时, 不要返回旧缓存帧.
|
||||
return null;
|
||||
}
|
||||
|
||||
public Options getOptions() {
|
||||
@@ -236,13 +452,15 @@ public class ScreenCapturer {
|
||||
|
||||
@Subscribe
|
||||
public void onConfigurationChanged(Configuration configuration) {
|
||||
if (mOrientation == ORIENTATION_AUTO && mDetectedOrientation != configuration.orientation) {
|
||||
refreshDetectedOrientation();
|
||||
if (mOptions.isAsync) {
|
||||
mHandler.post(() -> refreshVirtualDisplay(mDetectedOrientation, false));
|
||||
} else {
|
||||
mShouldRefreshVirtualDisplayOnNextCapture = true;
|
||||
}
|
||||
if (mOrientation != ORIENTATION_AUTO) return;
|
||||
|
||||
// Always schedule refresh for AUTO mode.
|
||||
// zh-CN: AUTO 模式下收到配置变化事件时总是安排下一次 capture 刷新.
|
||||
refreshDetectedOrientation();
|
||||
if (mOptions.isAsync) {
|
||||
mHandler.post(() -> refreshVirtualDisplay(mDetectedOrientation, false));
|
||||
} else {
|
||||
mShouldRefreshVirtualDisplayOnNextCapture = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,6 +478,15 @@ public class ScreenCapturer {
|
||||
if (mUnderUsingImage != null) {
|
||||
mUnderUsingImage.close();
|
||||
}
|
||||
|
||||
// Unregister display listener to avoid leaks.
|
||||
// zh-CN: 反注册 DisplayListener 以避免泄漏.
|
||||
mDisplayManager.unregisterDisplayListener(mDisplayListener);
|
||||
|
||||
// Quit callback thread to avoid leaks.
|
||||
// zh-CN: 退出回调线程以避免泄漏.
|
||||
mImageCallbackThread.quitSafely();
|
||||
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.util.Log;
|
||||
import org.autojs.autojs.AbstractAutoJs;
|
||||
import org.autojs.autojs.core.looper.LooperHelper;
|
||||
import org.autojs.autojs.runtime.ScriptRuntime;
|
||||
import org.autojs.autojs.runtime.exception.ScriptInterruptedException;
|
||||
import org.autojs.autojs.script.JavaScriptSource;
|
||||
import org.autojs.autojs.script.ScriptSource;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
@@ -74,8 +75,10 @@ public class LoopBasedJavaScriptEngine extends RhinoJavaScriptEngine {
|
||||
continue;
|
||||
} catch (Throwable t) {
|
||||
mLooping = false;
|
||||
if (AbstractAutoJs.isInrt() && t.getMessage() != null) {
|
||||
ScriptRuntime.popException(t.getMessage());
|
||||
if (AbstractAutoJs.isInrt() && !ScriptInterruptedException.causedByInterrupt(t)) {
|
||||
if (t.getMessage() != null) {
|
||||
ScriptRuntime.popException(t.getMessage());
|
||||
}
|
||||
}
|
||||
throw t;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,6 @@ import org.autojs.autojs.AutoJs;
|
||||
import org.autojs.autojs.annotation.ScriptInterface;
|
||||
import org.autojs.autojs.annotation.ScriptVariable;
|
||||
import org.autojs.autojs.concurrent.VolatileDispose;
|
||||
import org.autojs.autojs.core.image.CapturedImage;
|
||||
import org.autojs.autojs.core.image.ImageWrapper;
|
||||
import org.autojs.autojs.core.image.RhinoColorFinder;
|
||||
import org.autojs.autojs.core.image.Shootable;
|
||||
@@ -110,7 +109,7 @@ public class Images {
|
||||
private final ScreenMetrics mScreenMetrics;
|
||||
private volatile ScreenCapturer.OnScreenCaptureAvailableListener mOnScreenCaptureAvailableListener;
|
||||
private Image mPreCapture;
|
||||
private CapturedImage mPreCaptureImage;
|
||||
private ImageWrapper mPreCaptureImage;
|
||||
private ScreenCapturer mScreenCapturer;
|
||||
private ScreenCaptureRequester mScreenCaptureRequester;
|
||||
|
||||
@@ -271,15 +270,60 @@ public class Images {
|
||||
if (mScreenCapturer == null) {
|
||||
throw new SecurityException(mContext.getString(R.string.error_no_screen_capture_permission));
|
||||
}
|
||||
Image capture = mScreenCapturer.capture();
|
||||
if (capture != mPreCapture || mPreCaptureImage == null) {
|
||||
|
||||
// Retry in Java side to avoid leaking transient null frames to JS.
|
||||
// zh-CN: 在 Java 层做重试, 避免把短暂的 null 帧暴露给 JS 层.
|
||||
Image capture = null;
|
||||
for (int i = 0; i < 6; i++) {
|
||||
capture = mScreenCapturer.capture();
|
||||
if (capture != null) break;
|
||||
try {
|
||||
Thread.sleep(40);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Optional extra small backoff for switching moments.
|
||||
// zh-CN: 可选的额外小退避, 用于方向/应用切换瞬间.
|
||||
if (capture == null) {
|
||||
try {
|
||||
Thread.sleep(60);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
for (int i = 0; i < 2; i++) {
|
||||
capture = mScreenCapturer.capture();
|
||||
if (capture != null) break;
|
||||
try {
|
||||
Thread.sleep(40);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If still no valid frame, fallback to previous cached image when possible.
|
||||
// zh-CN: 若仍无有效帧, 尽可能回退到上一帧缓存图像.
|
||||
if (capture == null) {
|
||||
if (mPreCaptureImage != null && !mPreCaptureImage.isRecycled()) {
|
||||
// Return a clone to avoid user recycling the internal cache.
|
||||
// zh-CN: 返回 clone, 避免用户 recycle() 影响内部缓存.
|
||||
return mPreCaptureImage.clone();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Recreate wrapper when image instance changed OR cached wrapper is missing OR cached wrapper was recycled.
|
||||
// zh-CN: 当 Image 实例发生变化/缓存包装对象为空/缓存包装对象已被回收时, 重新创建包装对象.
|
||||
if (capture != mPreCapture || mPreCaptureImage == null || mPreCaptureImage.isRecycled()) {
|
||||
mPreCapture = capture;
|
||||
if (mPreCaptureImage != null) {
|
||||
mPreCaptureImage.recycleInternal();
|
||||
}
|
||||
if (capture != null) {
|
||||
mPreCaptureImage = new CapturedImage(mScriptRuntime, capture);
|
||||
mPreCaptureImage.recycle();
|
||||
}
|
||||
mPreCaptureImage = new ImageWrapper(mScriptRuntime, capture);
|
||||
}
|
||||
return mPreCaptureImage;
|
||||
}
|
||||
@@ -584,7 +628,7 @@ public class Images {
|
||||
mPreCapture = null;
|
||||
}
|
||||
if (mPreCaptureImage != null) {
|
||||
mPreCaptureImage.recycleInternal();
|
||||
mPreCaptureImage.recycle();
|
||||
mPreCaptureImage = null;
|
||||
}
|
||||
releaseScreenCaptureRequester();
|
||||
|
||||
@@ -6,14 +6,20 @@ import android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||
import android.content.res.Configuration.ORIENTATION_PORTRAIT
|
||||
import android.content.res.Resources
|
||||
import android.graphics.Point
|
||||
import android.hardware.display.DisplayManager
|
||||
import android.os.Build
|
||||
import android.util.DisplayMetrics
|
||||
import android.view.Display
|
||||
import android.view.Surface
|
||||
import android.view.Surface.ROTATION_0
|
||||
import android.view.WindowManager
|
||||
import org.autojs.autojs.app.GlobalAppContext
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
/**
|
||||
* Created by Stardust on Apr 26, 2017.
|
||||
* Modified by SuperMonster003 as of Jan 11, 2026.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class ScreenMetrics {
|
||||
@@ -59,13 +65,86 @@ class ScreenMetrics {
|
||||
get() = mActivityRef.get()?.windowManager
|
||||
?: GlobalAppContext.get().getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
||||
|
||||
// Prefer DisplayManager for rotation to reduce Activity/ROM inconsistencies.
|
||||
// zh-CN: rotation 优先使用 DisplayManager, 以降低 Activity 引用/ROM 行为差异导致的方向读数不一致.
|
||||
private val mDisplayManager: DisplayManager?
|
||||
get() = GlobalAppContext.get().getSystemService(Context.DISPLAY_SERVICE) as? DisplayManager
|
||||
|
||||
private var mIsInitialized = false
|
||||
private var mActivityRef = WeakReference<Activity?>(null)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
@JvmStatic
|
||||
val rotation: Int
|
||||
get() = mWindowManager.defaultDisplay?.rotation ?: ROTATION_0
|
||||
get() {
|
||||
// Prefer system Display rotation when available.
|
||||
// zh-CN: 尽可能使用系统 Display 的 rotation.
|
||||
val dm = mDisplayManager
|
||||
val display: Display? = dm?.getDisplay(Display.DEFAULT_DISPLAY)
|
||||
val rot = display?.rotation
|
||||
if (rot != null) return rot
|
||||
|
||||
// Fallback to legacy WindowManager/defaultDisplay.
|
||||
// zh-CN: 回退到旧实现 WindowManager/defaultDisplay.
|
||||
return mWindowManager.defaultDisplay?.rotation ?: ROTATION_0
|
||||
}
|
||||
|
||||
// Cache stable long/short sides to avoid inconsistent width/height on some ROMs.
|
||||
// zh-CN: 缓存稳定的长边/短边, 避免部分 ROM 在多次运行脚本/跨应用切换后出现宽高读数不一致.
|
||||
@Volatile
|
||||
private var sStableLongSide: Int = 0
|
||||
|
||||
@Volatile
|
||||
private var sStableShortSide: Int = 0
|
||||
|
||||
private fun ensureStableSides() {
|
||||
if (sStableLongSide > 0 && sStableShortSide > 0) return
|
||||
|
||||
// Prefer WindowMetrics on Android R+.
|
||||
// zh-CN: 在 Android R+ 优先使用 WindowMetrics.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
val bounds = mWindowManager.maximumWindowMetrics.bounds
|
||||
val w = bounds.width().absoluteValue
|
||||
val h = bounds.height().absoluteValue
|
||||
if (w > 0 && h > 0) {
|
||||
sStableLongSide = maxOf(w, h)
|
||||
sStableShortSide = minOf(w, h)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to getRealMetrics().
|
||||
// zh-CN: 回退到 getRealMetrics().
|
||||
@Suppress("DEPRECATION")
|
||||
run {
|
||||
val metricsLegacy = DisplayMetrics()
|
||||
mWindowManager.defaultDisplay.apply { getRealMetrics(metricsLegacy) }
|
||||
val w = metricsLegacy.widthPixels
|
||||
val h = metricsLegacy.heightPixels
|
||||
if (w > 0 && h > 0) {
|
||||
sStableLongSide = maxOf(w, h)
|
||||
sStableShortSide = minOf(w, h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val deviceScreenWidth: Int
|
||||
get() {
|
||||
ensureStableSides()
|
||||
val rot = rotation
|
||||
val isLandscape = rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270
|
||||
return if (isLandscape) sStableLongSide else sStableShortSide
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val deviceScreenHeight: Int
|
||||
get() {
|
||||
ensureStableSides()
|
||||
val rot = rotation
|
||||
val isLandscape = rot == Surface.ROTATION_90 || rot == Surface.ROTATION_270
|
||||
return if (isLandscape) sStableShortSide else sStableLongSide
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
val orientation: Int
|
||||
@@ -79,32 +158,6 @@ class ScreenMetrics {
|
||||
val isScreenLandscape: Boolean
|
||||
get() = orientation == ORIENTATION_LANDSCAPE
|
||||
|
||||
@JvmStatic
|
||||
val deviceScreenWidth: Int
|
||||
get() = getScreenWidthCompat()
|
||||
|
||||
@JvmStatic
|
||||
val deviceScreenHeight: Int
|
||||
get() = getScreenHeightCompat()
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getScreenWidthCompat(): Int {
|
||||
// resources?.displayMetrics?.widthPixels?.takeIf { it > 0 }?.let { return it }
|
||||
val metricsLegacy = DisplayMetrics()
|
||||
return mWindowManager.defaultDisplay.apply { getRealMetrics(metricsLegacy) }.let { display ->
|
||||
maxOf(metricsLegacy.widthPixels, display.width, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getScreenHeightCompat(): Int {
|
||||
// resources?.displayMetrics?.heightPixels?.takeIf { it > 0 }?.let { return it }
|
||||
val metricsLegacy = DisplayMetrics()
|
||||
return mWindowManager.defaultDisplay.apply { getRealMetrics(metricsLegacy) }.let { display ->
|
||||
maxOf(metricsLegacy.heightPixels, display.height, 0)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
@Suppress("DEPRECATION")
|
||||
val deviceScreenDensity: Int
|
||||
@@ -118,6 +171,11 @@ class ScreenMetrics {
|
||||
fun init(activity: Activity) {
|
||||
mActivityRef = WeakReference(activity)
|
||||
mIsInitialized = true
|
||||
|
||||
// Reset stable cache when Activity is (re)initialized.
|
||||
// zh-CN: Activity 初始化/重建时重置稳定缓存, 以适配可能的显示模式变化.
|
||||
sStableLongSide = 0
|
||||
sStableShortSide = 0
|
||||
}
|
||||
|
||||
private fun toOriAwarePoint(a: Int, b: Int) = arrayOf(minOf(a, b), maxOf(a, b))
|
||||
@@ -125,15 +183,29 @@ class ScreenMetrics {
|
||||
.let { Point(it[0], it[1]) }
|
||||
|
||||
@JvmStatic
|
||||
fun getOrientationAwareScreenWidth(orientation: Int) = when (orientation) {
|
||||
ORIENTATION_LANDSCAPE -> deviceScreenHeight
|
||||
else -> deviceScreenWidth
|
||||
fun getOrientationAwareScreenWidth(orientation: Int): Int {
|
||||
// Use stable sides instead of querying deviceScreenWidth/Height multiple times.
|
||||
// zh-CN: 使用稳定长边/短边, 避免重复查询导致的不一致.
|
||||
ensureStableSides()
|
||||
val longSide = sStableLongSide
|
||||
val shortSide = sStableShortSide
|
||||
return when (orientation) {
|
||||
ORIENTATION_LANDSCAPE -> longSide
|
||||
else -> shortSide
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getOrientationAwareScreenHeight(orientation: Int) = when (orientation) {
|
||||
ORIENTATION_LANDSCAPE -> deviceScreenWidth
|
||||
else -> deviceScreenHeight
|
||||
fun getOrientationAwareScreenHeight(orientation: Int): Int {
|
||||
// Use stable sides instead of querying deviceScreenWidth/Height multiple times.
|
||||
// zh-CN: 使用稳定长边/短边, 避免重复查询导致的不一致.
|
||||
ensureStableSides()
|
||||
val longSide = sStableLongSide
|
||||
val shortSide = sStableShortSide
|
||||
return when (orientation) {
|
||||
ORIENTATION_LANDSCAPE -> shortSide
|
||||
else -> longSide
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -282,9 +282,9 @@ class Images(scriptRuntime: ScriptRuntime) : Augmentable(scriptRuntime), AsEmitt
|
||||
requestScreenCapture(scriptRuntime, arrayOf())
|
||||
}
|
||||
when {
|
||||
path.isJsNullish() -> rtImages.captureScreen() as ImageWrapper
|
||||
path.isJsNullish() -> rtImages.captureScreen()
|
||||
else -> rtImages.captureScreen(scriptRuntime.files.nonNullPath(coerceString(path)))
|
||||
}
|
||||
} ?: throw WrappedRuntimeException("Failed to capture screen image")
|
||||
}
|
||||
|
||||
// @Reference to module __images__.js from Auto.js Pro 9.3.11 by SuperMonster003 on Dec 19, 2023.
|
||||
|
||||
Reference in New Issue
Block a user