6.7.0 - Alpha14 - 修复 images.captureScreen 结果方向错误或黑边问题 (issue #337); 修复 ImageWrapper#recycle; 优化 device.rotation/rotation/width/height

This commit is contained in:
SuperMonster003
2026-01-12 00:03:50 +08:00
parent 6f97d73940
commit 27a43fbd62
10 changed files with 471 additions and 135 deletions

View File

@@ -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()
}
}

View File

@@ -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 {

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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;
}

View File

@@ -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();

View File

@@ -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
}
}
}

View File

@@ -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.