6.3.3 - PR Review; 新增代码注释功能; 修复 ClassLoader 栈溢出 / VSCode 插件问题
This commit is contained in:
@@ -11,16 +11,13 @@ import android.view.accessibility.AccessibilityNodeInfo
|
||||
import org.autojs.autojs.core.accessibility.SimpleActionAutomator.Companion.AccessibilityEventCallback
|
||||
import org.autojs.autojs.core.automator.AccessibilityEventWrapper
|
||||
import org.autojs.autojs.event.EventDispatcher
|
||||
import org.autojs.autojs.pref.Language
|
||||
import org.autojs.autojs.pref.Pref
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
interface AccessibilityServiceCallback {
|
||||
fun onConnected()
|
||||
fun onDisconnected()
|
||||
}
|
||||
/**
|
||||
* Created by Stardust on 2017/5/2.
|
||||
*/
|
||||
@@ -35,15 +32,13 @@ open class AccessibilityService : android.accessibilityservice.AccessibilityServ
|
||||
|
||||
private val eventExecutor by lazy { Executors.newSingleThreadExecutor() }
|
||||
|
||||
private fun eventNameToType(str: String): Int {
|
||||
private fun eventNameToType(event: String): Int {
|
||||
return try {
|
||||
val sb = StringBuilder()
|
||||
sb.append("TYPE_")
|
||||
val upperCase = str.uppercase(Locale.getDefault())
|
||||
sb.append(upperCase)
|
||||
AccessibilityEvent::class.java.getField(sb.toString()).get(null) as Int
|
||||
AccessibilityEvent::class.java.getField(
|
||||
"TYPE_${event.uppercase(Language.getPrefLanguage().locale)}"
|
||||
).get(null) as Int
|
||||
} catch (unused: NoSuchFieldException) {
|
||||
throw IllegalArgumentException("unknown event type: $str")
|
||||
throw IllegalArgumentException("Unknown event: $event")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,7 +161,8 @@ open class AccessibilityService : android.accessibilityservice.AccessibilityServ
|
||||
fun isNotRunning() = !isRunning()
|
||||
|
||||
fun addDelegate(uniquePriority: Int, delegate: AccessibilityDelegate) {
|
||||
// 用于记录eventTypes中的事件id
|
||||
// @Hint by 抠脚本人 on Jul 10, 2023.
|
||||
// ! 用于记录 eventTypes 中的事件 id.
|
||||
delegates[uniquePriority] = delegate
|
||||
val set = delegate.eventTypes
|
||||
if (set == null) {
|
||||
@@ -204,10 +200,12 @@ open class AccessibilityService : android.accessibilityservice.AccessibilityServ
|
||||
LOCK.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun clearAccessibilityEventCallback() {
|
||||
instance?.eventBox?.clear()
|
||||
}
|
||||
|
||||
fun setCallback(listener: AccessibilityServiceCallback) {
|
||||
callback = listener
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package org.autojs.autojs.core.accessibility
|
||||
|
||||
/**
|
||||
* Created by 抠脚本人 on Jul 10, 2023.
|
||||
*/
|
||||
interface AccessibilityServiceCallback {
|
||||
fun onConnected()
|
||||
fun onDisconnected()
|
||||
}
|
||||
@@ -43,6 +43,10 @@ class AccessibilityTool(val context: Context) {
|
||||
enableIfNeeded().also { if (!it) launchSettings() }
|
||||
}
|
||||
|
||||
fun launchSettings() {
|
||||
this@AccessibilityTool.launchSettings()
|
||||
}
|
||||
|
||||
fun enableIfNeeded() = when {
|
||||
enableWithRootIfNeeded() -> true
|
||||
enableWithSecureIfNeeded() -> true
|
||||
|
||||
@@ -182,7 +182,9 @@ class SimpleActionAutomator(private val accessibilityBridge: AccessibilityBridge
|
||||
@ScriptInterface
|
||||
fun ensureService() = accessibilityBridge.ensureServiceEnabled()
|
||||
|
||||
//todo:优化实现方式
|
||||
// @Created by 抠脚本人 on Jul 10, 2023.
|
||||
// TODO by 抠脚本人 on Jul 10, 2023.
|
||||
// ! 优化实现方式
|
||||
// TODO by SuperMonster003 on Jul 12, 2023.
|
||||
// ! Ref to Auto.js Pro
|
||||
fun registerEvent(eventName: String, callback: AccessibilityEventCallback) {
|
||||
@@ -190,6 +192,7 @@ class SimpleActionAutomator(private val accessibilityBridge: AccessibilityBridge
|
||||
AccessibilityService.instance?.addAccessibilityEventCallback(eventName, callback)
|
||||
}
|
||||
|
||||
// @Created by 抠脚本人 on Jul 10, 2023.
|
||||
fun removeEvent(eventName: String) {
|
||||
AccessibilityService.instance?.removeAccessibilityEventCallback(eventName)
|
||||
}
|
||||
@@ -251,12 +254,17 @@ class SimpleActionAutomator(private val accessibilityBridge: AccessibilityBridge
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val accessibilityDelegateCounter = AtomicInteger(1000)
|
||||
val TAG: String = SimpleActionAutomator::class.java.name
|
||||
|
||||
/**
|
||||
* Created by 抠脚本人 on Jul 10, 2023.
|
||||
*/
|
||||
interface AccessibilityEventCallback {
|
||||
fun onAccessibilityEvent(event: AccessibilityEventWrapper)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import android.view.accessibility.AccessibilityEvent
|
||||
import android.view.accessibility.AccessibilityNodeInfo
|
||||
|
||||
/**
|
||||
* Created by 抠脚本人 on Jul 10, 2023.
|
||||
* Created by 抠脚本人 (https://github.com/little-alei) on Jul 10, 2023.
|
||||
*/
|
||||
class AccessibilityEventWrapper(event: AccessibilityEvent) {
|
||||
val raw = event
|
||||
|
||||
@@ -40,15 +40,18 @@ open class UiObject(
|
||||
|
||||
private val bounds by lazy { AccessibilityNodeInfoHelper.getBoundsInScreen(this) }
|
||||
|
||||
constructor(info: Any?, allocator: AccessibilityNodeInfoAllocator, indexInParent: Int) : this(
|
||||
info,
|
||||
allocator,
|
||||
0,
|
||||
indexInParent
|
||||
)
|
||||
constructor(
|
||||
info: Any?,
|
||||
allocator: AccessibilityNodeInfoAllocator,
|
||||
indexInParent: Int,
|
||||
) : this(info, allocator, 0, indexInParent)
|
||||
|
||||
@JvmOverloads
|
||||
constructor(info: Any?, depth: Int = 0, indexInParent: Int = -1) : this(info, null, depth, indexInParent)
|
||||
constructor(
|
||||
info: Any?,
|
||||
depth: Int = 0,
|
||||
indexInParent: Int = -1,
|
||||
) : this(info, null, depth, indexInParent)
|
||||
|
||||
open fun parent(): UiObject? = try {
|
||||
super.getParent()?.let { node ->
|
||||
@@ -63,37 +66,63 @@ open class UiObject(
|
||||
null.also { e.printStackTrace() }
|
||||
}
|
||||
|
||||
open fun child(i: Int): UiObject? = try {
|
||||
super.getChild(i)?.run { UiObject(unwrap(), depth + 1, i) }
|
||||
} catch (e: IllegalStateException) {
|
||||
// FIXME: 2017/5/5
|
||||
null.also { e.printStackTrace() }
|
||||
fun parent(i: Int): UiObject? = if (i < 0) throw Exception("i < 0") else compass("p$i")
|
||||
|
||||
open fun child(i: Int): UiObject? {
|
||||
if (i < 0) {
|
||||
return (i + childCount).takeIf { it >= 0 }?.let { child(it) }
|
||||
}
|
||||
return try {
|
||||
super.getChild(i)?.run { UiObject(unwrap(), depth + 1, i) }
|
||||
} catch (e: IllegalStateException) {
|
||||
// FIXME: 2017/5/5
|
||||
null.also { e.printStackTrace() }
|
||||
}
|
||||
}
|
||||
|
||||
// @Deprecated by SuperMonster003 on Jul 20, 2023.
|
||||
// ! Author: 抠脚本人
|
||||
// ! Reason: Replaced with offset(i).
|
||||
@Deprecated("Deprecated in Java", ReplaceWith("offset(i)"))
|
||||
open fun brother(i: Int): UiObject? = offset(i)
|
||||
|
||||
open fun offset(i: Int): UiObject? = try {
|
||||
parent()?.child(indexInParent + i)
|
||||
if (i == 0) this
|
||||
else parent()?.child(indexInParent + i)
|
||||
} catch (e: ArrayIndexOutOfBoundsException) {
|
||||
null.also { e.printStackTrace() }
|
||||
}
|
||||
|
||||
open fun sibling(i: Int): UiObject? = try {
|
||||
parent()?.child(i)
|
||||
if (i == indexInParent) this
|
||||
else parent()?.child(i)
|
||||
} catch (e: ArrayIndexOutOfBoundsException) {
|
||||
null.also { e.printStackTrace() }
|
||||
}
|
||||
|
||||
open fun nextSibling() = sibling(1)
|
||||
fun siblingCount() = parent()?.childCount ?: 1
|
||||
|
||||
open fun previousSibling() = sibling(-1)
|
||||
fun isSingleton() = siblingCount() == 1
|
||||
|
||||
fun firstSibling() = sibling(0)
|
||||
|
||||
fun lastSibling() = sibling(-1)
|
||||
|
||||
open fun nextSibling() = offset(1)
|
||||
|
||||
open fun previousSibling() = offset(-1)
|
||||
|
||||
open fun childCount() = childCount
|
||||
|
||||
fun hasChildren() = childCount > 0
|
||||
|
||||
fun children(): UiObjectCollection = List(childCount) { child(it) }.let { UiObjectCollection.of(it) }
|
||||
fun firstChild() = child(0)
|
||||
|
||||
fun lastChild() = child(childCount - 1)
|
||||
|
||||
fun children() = List(childCount) { child(it) }.let { UiObjectCollection.of(it) }
|
||||
|
||||
fun siblings() = List(siblingCount()) { sibling(it) }.let { UiObjectCollection.of(it) }
|
||||
|
||||
fun indexInParent() = indexInParent
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@ import javax.crypto.spec.SecretKeySpec
|
||||
/**
|
||||
* Created by SuperMonster003 on Jun 15, 2023.
|
||||
*/
|
||||
// @Reference to com.stardust.autojs.core.cypto.Crypto.class on Jun 15, 2023.
|
||||
// @Reference to com.stardust.autojs.core.cypto.Crypto.class from Auto.js Pro 9.3.11 on Jun 15, 2023.
|
||||
// ! There is a strong possibility that "cypto" is a typo.
|
||||
// @Reference to Auto.js Pro 9.3.11 module __$crypto__.js on Jun 15, 2023.
|
||||
// @Reference to module __$crypto__.js from Auto.js Pro 9.3.11 on Jun 15, 2023.
|
||||
object Crypto {
|
||||
|
||||
private val scriptRuntime by lazy { AutoJs.instance.runtime }
|
||||
|
||||
@@ -44,7 +44,7 @@ public class ScreenCapturer {
|
||||
public static final int ORIENTATION_LANDSCAPE = Configuration.ORIENTATION_LANDSCAPE;
|
||||
public static final int ORIENTATION_PORTRAIT = Configuration.ORIENTATION_PORTRAIT;
|
||||
|
||||
// @Reference to TonyJiangWJ/Auto.js on May 19, 2022.
|
||||
// @Reference to TonyJiangWJ/Auto.js (https://github.com/TonyJiangWJ/Auto.js) on May 19, 2022.
|
||||
// ! Snippet:
|
||||
// ! private final ConcurrentHashMap<ScriptRuntime, Boolean> registeredRuntimes = new ConcurrentHashMap<>();
|
||||
private static final List<ScriptRuntime> mScriptRuntimes = Collections.synchronizedList(new ArrayList<>());
|
||||
|
||||
@@ -44,7 +44,7 @@ public class ScreenCapturerForegroundService extends Service {
|
||||
ForegroundServiceUtils.startForeground(new ForegroundServiceCreator.Builder(this)
|
||||
.setClassName(sClassName)
|
||||
.setIntent(
|
||||
// @Reference to TonyJiangWJ/Auto.js on Apr 10, 2022
|
||||
// @Reference to TonyJiangWJ/Auto.js (https://github.com/TonyJiangWJ/Auto.js) on Apr 10, 2022
|
||||
ScreenCaptureRequestActivity.getIntent(this)
|
||||
)
|
||||
.setNotificationId(NOTIFICATION_ID)
|
||||
|
||||
@@ -1,114 +1,112 @@
|
||||
package org.autojs.autojs.core.looper
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.MessageQueue
|
||||
import android.os.MessageQueue.IdleHandler
|
||||
import android.util.Log
|
||||
import org.autojs.autojs.lang.ThreadCompat
|
||||
import org.autojs.autojs.rhino.AutoJsContext
|
||||
import org.autojs.autojs.runtime.ScriptRuntime
|
||||
import org.autojs.autojs.runtime.api.Threads
|
||||
import org.autojs.autojs.runtime.api.Timers
|
||||
import org.autojs.autojs.runtime.exception.ScriptInterruptedException
|
||||
import org.mozilla.javascript.Context
|
||||
import java.util.concurrent.ConcurrentLinkedQueue
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/7/29.
|
||||
* Transformed by aiselp on Jul 4, 2023.
|
||||
* Modified by SuperMonster003 as of Jul 12, 2023.
|
||||
* Transformed by SuperMonster003 on Jul 12, 2023.
|
||||
*/
|
||||
/**
|
||||
* update by aiselp on 2023/6/4
|
||||
* 调整内容:
|
||||
* 使此类只负责单loop线程生命周期管理,移除繁琐的调用链
|
||||
* 调整timer由此类创建
|
||||
* 通过向此类添加AsyncTask以监听线程退出事件
|
||||
*/
|
||||
class Loopers(val runtime: ScriptRuntime) {
|
||||
@Deprecated("使用AsyncTask代替")
|
||||
// @Overruled by SuperMonster003 on Jul 12, 2023.
|
||||
// ! Author: aiselp
|
||||
// ! Related PR:
|
||||
// ! http://pr.autojs6.com/75
|
||||
// ! Reason:
|
||||
// ! Sorry but my current capabilities are not sufficient
|
||||
// ! to fully understand everything from above pull request(s),
|
||||
// ! so most of the code will remain as is. :)
|
||||
class Loopers(runtime: ScriptRuntime) : IdleHandler {
|
||||
interface LooperQuitHandler {
|
||||
fun shouldQuit(): Boolean
|
||||
}
|
||||
|
||||
open class AsyncTask(private val describe: String) {
|
||||
private val allBind = ConcurrentLinkedQueue<Loopers>()
|
||||
var isEnd: Boolean = false
|
||||
private set
|
||||
|
||||
//线程即将退出时调用,返回true阻止线程退出,只要有一个task返回true线程就不会退出
|
||||
open fun onFinish(loopers: Loopers): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
fun end() {
|
||||
isEnd = true
|
||||
}
|
||||
|
||||
//线程正在退出,这里应该结束任务的执行,回收资源
|
||||
open fun onStop(loopers: Loopers) {}
|
||||
override fun toString(): String {
|
||||
return "AsyncTask: $describe"
|
||||
private val waitWhenIdle: ThreadLocal<Boolean> = object : ThreadLocal<Boolean>() {
|
||||
override fun initialValue(): Boolean {
|
||||
return Looper.myLooper() == Looper.getMainLooper()
|
||||
}
|
||||
}
|
||||
|
||||
private var waitWhenIdle: Boolean
|
||||
private val waitIds: ThreadLocal<HashSet<Int>> = object : ThreadLocal<HashSet<Int>>() {
|
||||
override fun initialValue(): HashSet<Int> {
|
||||
return HashSet()
|
||||
}
|
||||
}
|
||||
private val maxWaitId: ThreadLocal<Int> = object : ThreadLocal<Int>() {
|
||||
override fun initialValue(): Int {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
private val looperQuitHandlers = ThreadLocal<CopyOnWriteArrayList<LooperQuitHandler>>()
|
||||
|
||||
@Volatile
|
||||
private var mServantLooper: Looper? = null
|
||||
private val mTimers: Timers
|
||||
private var mMainLooperQuitHandler: LooperQuitHandler? = null
|
||||
private val allTasks = ConcurrentLinkedQueue<AsyncTask>()
|
||||
val mTimer: Timer
|
||||
val myLooper: Looper
|
||||
private val mMainHandler: Handler
|
||||
private val mMainLooper: Looper?
|
||||
private val mThreads: Threads
|
||||
private val mMainMessageQueue: MessageQueue
|
||||
|
||||
init {
|
||||
mTimers = runtime.timers
|
||||
mThreads = runtime.threads
|
||||
prepare()
|
||||
myLooper = Looper.myLooper()!!
|
||||
mTimer = Timer(runtime, myLooper)
|
||||
waitWhenIdle = myLooper == Looper.getMainLooper()
|
||||
mMainLooper = Looper.myLooper()
|
||||
mMainHandler = Handler(Looper.getMainLooper())
|
||||
mMainMessageQueue = Looper.myQueue()
|
||||
}
|
||||
|
||||
fun createAndAddAsyncTask(describe: String): AsyncTask {
|
||||
val task = AsyncTask(describe)
|
||||
allTasks.add(task)
|
||||
return task
|
||||
}
|
||||
|
||||
fun addAsyncTask(task: AsyncTask) {
|
||||
synchronized(myLooper) {
|
||||
allTasks.add(task)
|
||||
fun addLooperQuitHandler(handler: LooperQuitHandler) {
|
||||
var handlers = looperQuitHandlers.get()
|
||||
if (handlers == null) {
|
||||
handlers = CopyOnWriteArrayList()
|
||||
looperQuitHandlers.set(handlers)
|
||||
}
|
||||
handlers.add(handler)
|
||||
}
|
||||
|
||||
fun removeAsyncTask(task: AsyncTask) {
|
||||
synchronized(myLooper) {
|
||||
allTasks.remove(task)
|
||||
mTimer.post(EMPTY_RUNNABLE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkTask(): Boolean {
|
||||
allTasks.removeAll(allTasks.filter { it.isEnd }.toSet())
|
||||
for (task in allTasks) {
|
||||
if (task.onFinish(this)) return true
|
||||
}
|
||||
return false
|
||||
fun removeLooperQuitHandler(handler: LooperQuitHandler): Boolean {
|
||||
val handlers = looperQuitHandlers.get()
|
||||
return handlers != null && handlers.remove(handler)
|
||||
}
|
||||
|
||||
private fun shouldQuitLooper(): Boolean {
|
||||
synchronized(myLooper) {
|
||||
if (Thread.currentThread().isInterrupted) return true
|
||||
if (mTimer.hasPendingCallbacks()) return false
|
||||
//检查是否有运行中的线程
|
||||
if (checkTask()) return false
|
||||
if (waitWhenIdle) return false
|
||||
if ((Context.getCurrentContext() as AutoJsContext).hasPendingContinuation()) {
|
||||
return false
|
||||
}
|
||||
if (Thread.currentThread().isInterrupted) {
|
||||
return true
|
||||
}
|
||||
if (mTimers.hasPendingCallbacks()) {
|
||||
return false
|
||||
}
|
||||
if (waitWhenIdle.get() || !waitIds.get().isEmpty()) {
|
||||
return false
|
||||
}
|
||||
if ((Context.getCurrentContext() as AutoJsContext).hasPendingContinuation()) {
|
||||
return false
|
||||
}
|
||||
val handlers = looperQuitHandlers.get() ?: return true
|
||||
for (handler in handlers) {
|
||||
if (!handler.shouldQuit()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun initServantThread() {
|
||||
val lock = this@Loopers as Object
|
||||
ThreadCompat {
|
||||
Looper.prepare()
|
||||
val lock = this@Loopers as Object
|
||||
mServantLooper = Looper.myLooper()
|
||||
synchronized(lock) { lock.notifyAll() }
|
||||
Looper.loop()
|
||||
@@ -119,7 +117,7 @@ class Loopers(val runtime: ScriptRuntime) {
|
||||
get() {
|
||||
if (mServantLooper == null) {
|
||||
initServantThread()
|
||||
val lock = this as java.lang.Object
|
||||
val lock = this@Loopers as Object
|
||||
synchronized(lock) {
|
||||
try {
|
||||
lock.wait()
|
||||
@@ -131,56 +129,65 @@ class Loopers(val runtime: ScriptRuntime) {
|
||||
return mServantLooper!!
|
||||
}
|
||||
|
||||
@Deprecated("使用AsyncTask代替")
|
||||
fun waitWhenIdle(b: Boolean) {
|
||||
waitWhenIdle = b
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
Log.d(LOG_TAG, "recycle")
|
||||
for (task in allTasks.filter { !it.isEnd }) {
|
||||
try {
|
||||
task.onStop(this)
|
||||
} catch (e: Exception) {
|
||||
Log.w(LOG_TAG, e)
|
||||
}
|
||||
}
|
||||
private fun quitServantLooper() {
|
||||
mServantLooper?.quit()
|
||||
}
|
||||
|
||||
@Deprecated("使用AsyncTask代替")
|
||||
fun waitWhenIdle(): Int {
|
||||
val id = maxWaitId.get()
|
||||
Log.d(LOG_TAG, "waitWhenIdle: $id")
|
||||
maxWaitId.set(id + 1)
|
||||
waitIds.get().add(id)
|
||||
return id
|
||||
}
|
||||
|
||||
fun doNotWaitWhenIdle(waitId: Int) {
|
||||
Log.d(LOG_TAG, "doNotWaitWhenIdle: $waitId")
|
||||
waitIds.get().remove(waitId)
|
||||
}
|
||||
|
||||
fun waitWhenIdle(b: Boolean) {
|
||||
waitWhenIdle.set(b)
|
||||
}
|
||||
|
||||
fun recycle() {
|
||||
quitServantLooper()
|
||||
mMainMessageQueue.removeIdleHandler(this)
|
||||
}
|
||||
|
||||
fun setMainLooperQuitHandler(mainLooperQuitHandler: LooperQuitHandler?) {
|
||||
mMainLooperQuitHandler = mainLooperQuitHandler
|
||||
}
|
||||
|
||||
private fun prepare() {
|
||||
if (Looper.myLooper() == null) LooperHelper.prepare()
|
||||
Looper.myQueue().addIdleHandler(MessageQueue.IdleHandler {
|
||||
if (this == runtime.loopers) {
|
||||
Log.d(LOG_TAG, "main looper queueIdle")
|
||||
if (shouldQuitLooper() &&
|
||||
mMainLooperQuitHandler != null &&
|
||||
mMainLooperQuitHandler!!.shouldQuit()
|
||||
) {
|
||||
Log.d(LOG_TAG, "main looper quit")
|
||||
Looper.myLooper()!!.quitSafely()
|
||||
}
|
||||
} else {
|
||||
Log.d(LOG_TAG, "looper queueIdle $this")
|
||||
if (shouldQuitLooper()) {
|
||||
Log.d(LOG_TAG, "looper quit $this")
|
||||
Looper.myLooper()!!.quitSafely()
|
||||
}
|
||||
override fun queueIdle(): Boolean {
|
||||
val l = Looper.myLooper() ?: return true
|
||||
if (l == mMainLooper) {
|
||||
Log.d(LOG_TAG, "main looper queueIdle")
|
||||
if (shouldQuitLooper() && !mThreads.hasRunningThreads() && mMainLooperQuitHandler != null && mMainLooperQuitHandler!!.shouldQuit()) {
|
||||
Log.d(LOG_TAG, "main looper quit")
|
||||
l.quit()
|
||||
}
|
||||
return@IdleHandler true
|
||||
})
|
||||
} else {
|
||||
Log.d(LOG_TAG, "looper queueIdle: $l")
|
||||
if (shouldQuitLooper()) {
|
||||
l.quit()
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun prepare() {
|
||||
if (Looper.myLooper() == null) {
|
||||
LooperHelper.prepare()
|
||||
}
|
||||
Looper.myQueue().addIdleHandler(this)
|
||||
}
|
||||
|
||||
fun notifyThreadExit(thread: TimerThread) {
|
||||
Log.d(LOG_TAG, "notifyThreadExit: $thread")
|
||||
//当子线程退成时,主线程需要检查自身是否退出(主线程在所有子线程执行完成后才能退出,如果主线程已经执行完任务仍然要等待所有子线程),
|
||||
//此时通过向主线程发送一个空的Runnable,主线程执行完这个Runnable后会触发IdleHandler,从而检查自身是否退出
|
||||
//mHandler.post(EMPTY_RUNNABLE)
|
||||
// 当子线程退成时,主线程需要检查自身是否退出(主线程在所有子线程执行完成后才能退出,如果主线程已经执行完任务仍然要等待所有子线程),
|
||||
// 此时通过向主线程发送一个空的Runnable,主线程执行完这个Runnable后会触发IdleHandler,从而检查自身是否退出
|
||||
mMainHandler.post(EMPTY_RUNNABLE)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -3,134 +3,119 @@ package org.autojs.autojs.core.looper
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.os.SystemClock
|
||||
import android.util.SparseArray
|
||||
import org.autojs.autojs.concurrent.VolatileBox
|
||||
import org.autojs.autojs.runtime.ScriptRuntime
|
||||
import org.mozilla.javascript.BaseFunction
|
||||
import org.mozilla.javascript.Context
|
||||
import org.mozilla.javascript.Scriptable
|
||||
import org.mozilla.javascript.Undefined
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import kotlin.random.Random
|
||||
import kotlin.math.max
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/12/27.
|
||||
* Transformed by aiselp on Jun 4, 2023.
|
||||
* Modified by SuperMonster003 as of Jul 12, 2023.
|
||||
* Transformed by SuperMonster003 on Jul 12, 2023.
|
||||
*/
|
||||
class Timer(
|
||||
runtime: ScriptRuntime,
|
||||
looper: Looper
|
||||
) {
|
||||
private val myLooper: Looper = looper
|
||||
private val mHandlerCallbacks = ConcurrentHashMap<Int, Runnable?>()
|
||||
// @Overruled by SuperMonster003 on Jul 12, 2023.
|
||||
// ! Author: aiselp
|
||||
// ! Related PR:
|
||||
// ! http://pr.autojs6.com/75
|
||||
// ! http://pr.autojs6.com/78
|
||||
// ! Reason:
|
||||
// ! Sorry but my current capabilities are not sufficient
|
||||
// ! to fully understand everything from above pull request(s),
|
||||
// ! so most of the code will remain as is. :)
|
||||
class Timer @JvmOverloads constructor(runtime: ScriptRuntime, maxCallbackMillisForAllThread: VolatileBox<Long>, private val looper: Looper? = Looper.myLooper()) {
|
||||
|
||||
private val mHandlerCallbacks = SparseArray<Runnable?>()
|
||||
private var mCallbackMaxId = 0
|
||||
private val mRuntime: ScriptRuntime = runtime
|
||||
private val mHandler: Handler = Handler(looper)
|
||||
private val isUiLoop: Boolean = looper == Looper.getMainLooper()
|
||||
private val context: Context? by lazy { Context.getCurrentContext() }
|
||||
private val mHandler = looper?.let { Handler(it) } ?: Handler()
|
||||
private var mMaxCallbackUptimeMillis: Long = 0
|
||||
private val mMaxCallbackMillisForAllThread: VolatileBox<Long> = maxCallbackMillisForAllThread
|
||||
|
||||
constructor(runtime: ScriptRuntime) : this(runtime, Looper.myLooper()!!)
|
||||
|
||||
fun setTimeout(callback: Any, delay: Long, vararg args: Any?): Int {
|
||||
val id = createTimerId()
|
||||
fun setTimeout(callback: Any, delay: Long, vararg args: Array<out Any?>): Int {
|
||||
mCallbackMaxId++
|
||||
val id = mCallbackMaxId
|
||||
val r = Runnable {
|
||||
callFunction(callback, null, args)
|
||||
callFunction(callback, args)
|
||||
mHandlerCallbacks.remove(id)
|
||||
}
|
||||
mHandlerCallbacks[id] = r
|
||||
mHandlerCallbacks.put(id, r)
|
||||
postDelayed(r, delay)
|
||||
return id
|
||||
}
|
||||
|
||||
private fun callFunction(callback: Any, thisArg: Any?, args: Any?) {
|
||||
val func = callback as BaseFunction
|
||||
val map: Array<Any> =
|
||||
(args as? Array<*>)?.map { Context.javaToJS(it, callback.parentScope) }
|
||||
?.toTypedArray() ?: emptyArray()
|
||||
private fun callFunction(callback: Any, args: Array<out Any>) {
|
||||
try {
|
||||
func.call(
|
||||
context ?: Context.enter(), func.parentScope,
|
||||
thisArg as? Scriptable ?: Undefined.SCRIPTABLE_UNDEFINED, map
|
||||
)
|
||||
mRuntime.bridges.callFunction(callback, null, args)
|
||||
} catch (e: Exception) {
|
||||
if (isUiLoop) {
|
||||
if (Looper.myLooper() == Looper.getMainLooper()) {
|
||||
mRuntime.exit(e)
|
||||
} else throw e
|
||||
} finally {
|
||||
context ?: Context.exit()
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
private fun createTimerId(): Int {
|
||||
var id: Int
|
||||
do {
|
||||
id = Random.nextInt()
|
||||
} while (mHandlerCallbacks.containsKey(id))
|
||||
mHandlerCallbacks[id] = EMPTY_RUNNABLE
|
||||
return id
|
||||
}
|
||||
fun clearTimeout(id: Int) = clearCallback(id)
|
||||
|
||||
fun setInterval(listener: Any, interval: Long, vararg args: Any?): Int {
|
||||
val id = createTimerId()
|
||||
val r: Runnable = object : Runnable {
|
||||
fun setInterval(listener: Any, interval: Long, vararg args: Any): Int {
|
||||
mCallbackMaxId++
|
||||
val id = mCallbackMaxId
|
||||
val r = object : Runnable {
|
||||
override fun run() {
|
||||
if (mHandlerCallbacks[id] == null) return
|
||||
callFunction(listener, null, args)
|
||||
mHandlerCallbacks[id] ?: return
|
||||
callFunction(listener, args)
|
||||
postDelayed(this, interval)
|
||||
}
|
||||
}
|
||||
mHandlerCallbacks[id] = r
|
||||
mHandlerCallbacks.put(id, r)
|
||||
postDelayed(r, interval)
|
||||
return id
|
||||
}
|
||||
|
||||
fun postDelayed(r: Runnable, interval: Long) {
|
||||
synchronized(myLooper) {
|
||||
val uptime = SystemClock.uptimeMillis() + interval
|
||||
mHandler.postAtTime(r, uptime)
|
||||
}
|
||||
val uptime = SystemClock.uptimeMillis() + interval
|
||||
mHandler.postAtTime(r, uptime)
|
||||
mMaxCallbackUptimeMillis = mMaxCallbackUptimeMillis.coerceAtLeast(uptime)
|
||||
synchronized(mMaxCallbackMillisForAllThread) { mMaxCallbackMillisForAllThread.set(max(mMaxCallbackMillisForAllThread.get(), uptime)) }
|
||||
}
|
||||
|
||||
// @Reference to aiselp (https://github.com/aiselp) on Jul 18, 2023.
|
||||
fun post(r: Runnable) {
|
||||
synchronized(myLooper) {
|
||||
mHandler.post(r)
|
||||
looper?.let {
|
||||
synchronized(it) {
|
||||
mHandler.post(r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearInterval(id: Int): Boolean = clearCallback(id)
|
||||
fun clearImmediate(id: Int): Boolean = clearCallback(id)
|
||||
fun clearTimeout(id: Int): Boolean = clearCallback(id)
|
||||
fun clearInterval(id: Int) = clearCallback(id)
|
||||
|
||||
fun setImmediate(listener: Any, vararg args: Any?): Int {
|
||||
val id = createTimerId()
|
||||
fun setImmediate(listener: Any, vararg args: Any): Int {
|
||||
mCallbackMaxId++
|
||||
val id = mCallbackMaxId
|
||||
val r = Runnable {
|
||||
callFunction(listener, null, args)
|
||||
callFunction(listener, args)
|
||||
mHandlerCallbacks.remove(id)
|
||||
}
|
||||
mHandlerCallbacks[id] = r
|
||||
post(r)
|
||||
mHandlerCallbacks.put(id, r)
|
||||
postDelayed(r, 0)
|
||||
return id
|
||||
}
|
||||
|
||||
fun clearImmediate(id: Int) = clearCallback(id)
|
||||
|
||||
private fun clearCallback(id: Int): Boolean {
|
||||
val callback = mHandlerCallbacks[id]
|
||||
if (callback != null) {
|
||||
mHandler.removeCallbacks(callback)
|
||||
mHandlerCallbacks.remove(id)
|
||||
if (mHandlerCallbacks.isEmpty()) mHandler.post(EMPTY_RUNNABLE)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun hasPendingCallbacks(): Boolean {
|
||||
return mHandlerCallbacks.size > 0
|
||||
}
|
||||
fun hasPendingCallbacks() = mMaxCallbackUptimeMillis > SystemClock.uptimeMillis()
|
||||
|
||||
fun removeAllCallbacks() {
|
||||
mHandler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
fun removeAllCallbacks() = mHandler.removeCallbacksAndMessages(null)
|
||||
|
||||
companion object {
|
||||
private val EMPTY_RUNNABLE = Runnable {}
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,58 @@
|
||||
package org.autojs.autojs.core.looper
|
||||
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import androidx.annotation.CallSuper
|
||||
import org.autojs.autojs.concurrent.VolatileBox
|
||||
import org.autojs.autojs.engine.RhinoJavaScriptEngine
|
||||
import org.autojs.autojs.lang.ThreadCompat
|
||||
import org.autojs.autojs.runtime.ScriptRuntime
|
||||
import org.autojs.autojs.runtime.exception.ScriptInterruptedException
|
||||
import org.autojs.autojs.util.StringUtils.str
|
||||
import org.autojs.autojs6.R
|
||||
import org.mozilla.javascript.Context
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/12/27.
|
||||
* Transformed by aiselp on Jun 4, 2023.
|
||||
* Modified by SuperMonster003 as of Jul 12, 2023.
|
||||
* Transformed by SuperMonster003 on Jul 12, 2023.
|
||||
*/
|
||||
open class TimerThread(private val mRuntime: ScriptRuntime, private val mTarget: Runnable) :
|
||||
ThreadCompat(mTarget) {
|
||||
// @Overruled by SuperMonster003 on Jul 12, 2023.
|
||||
// ! Author: aiselp
|
||||
// ! Related PR:
|
||||
// ! http://pr.autojs6.com/75
|
||||
// ! Reason:
|
||||
// ! Sorry but my current capabilities are not sufficient
|
||||
// ! to fully understand everything from above pull request(s),
|
||||
// ! so most of the code will remain as is. :)
|
||||
open class TimerThread(
|
||||
private val scriptRuntime: ScriptRuntime,
|
||||
private val maxCallbackUptimeMillisForAllThreads: VolatileBox<Long>,
|
||||
private val target: Runnable
|
||||
) : ThreadCompat(target) {
|
||||
|
||||
private var mTimer: Timer? = null
|
||||
private var mRunning = false
|
||||
private val mRunningLock = Object()
|
||||
private val mAsyncTask = Loopers.AsyncTask("TimerThread")
|
||||
var loopers: Loopers? = null
|
||||
|
||||
init {
|
||||
mRuntime.loopers.addAsyncTask(mAsyncTask)
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
loopers = Loopers(mRuntime)
|
||||
mTimer = loopers!!.mTimer
|
||||
sTimerMap[currentThread()] = mTimer!!
|
||||
(mRuntime.engines.myEngine() as RhinoJavaScriptEngine).enterContext()
|
||||
scriptRuntime.loopers.prepare()
|
||||
mTimer = Timer(scriptRuntime, maxCallbackUptimeMillisForAllThreads).also {
|
||||
sTimerMap[currentThread()] = it
|
||||
}
|
||||
(scriptRuntime.engines.myEngine() as? RhinoJavaScriptEngine)?.enterContext()
|
||||
notifyRunning()
|
||||
mTimer!!.post(mTarget)
|
||||
Looper.myLooper()?.let {
|
||||
Handler(it).post(target)
|
||||
} ?: Handler().post(target)
|
||||
try {
|
||||
Looper.loop()
|
||||
} catch (e: Throwable) {
|
||||
if (!ScriptInterruptedException.causedByInterrupted(e)) {
|
||||
mRuntime.console.error(currentThread().toString() + ": ", e)
|
||||
scriptRuntime.console.error("${currentThread()}: $e")
|
||||
}
|
||||
} finally {
|
||||
//mRuntime.console.log("TimerThread exit");
|
||||
onExit()
|
||||
mTimer = null
|
||||
Context.exit()
|
||||
@@ -61,69 +74,49 @@ open class TimerThread(private val mRuntime: ScriptRuntime, private val mTarget:
|
||||
|
||||
@CallSuper
|
||||
protected open fun onExit() {
|
||||
mRuntime.loopers.removeAsyncTask(mAsyncTask)
|
||||
mRuntime.loopers.notifyThreadExit(this)
|
||||
}
|
||||
|
||||
fun setTimeout(callback: Any, delay: Long, vararg args: Any?): Int {
|
||||
return timer.setTimeout(callback, delay, *args as Array<out Any>)
|
||||
}
|
||||
|
||||
fun setTimeout(callback: Any): Int {
|
||||
return setTimeout(callback, 1)
|
||||
scriptRuntime.loopers.notifyThreadExit(this)
|
||||
}
|
||||
|
||||
val timer: Timer
|
||||
get() {
|
||||
checkNotNull(mTimer) { "thread is not alive" }
|
||||
return mTimer as Timer
|
||||
checkNotNull(mTimer) { str(R.string.error_thread_is_not_alive) }
|
||||
return mTimer!!
|
||||
}
|
||||
|
||||
fun clearTimeout(id: Int): Boolean {
|
||||
return timer.clearTimeout(id)
|
||||
}
|
||||
fun setTimeout(callback: Any, delay: Long, vararg args: Array<out Any?>) = timer.setTimeout(callback, delay, *args)
|
||||
|
||||
fun setInterval(listener: Any?, interval: Long, vararg args: Any?): Int {
|
||||
return timer.setInterval(listener!!, interval, *args as Array<out Any>)
|
||||
}
|
||||
fun clearTimeout(id: Int) = timer.clearTimeout(id)
|
||||
|
||||
fun setInterval(listener: Any?): Int {
|
||||
return setInterval(listener, 1)
|
||||
}
|
||||
fun setInterval(listener: Any, interval: Long, vararg args: Array<out Any?>) = timer.setInterval(listener, interval, *args)
|
||||
|
||||
fun clearInterval(id: Int): Boolean {
|
||||
return timer.clearInterval(id)
|
||||
}
|
||||
fun clearInterval(id: Int) = timer.clearInterval(id)
|
||||
|
||||
fun setImmediate(listener: Any, vararg args: Any?): Int {
|
||||
return timer.setImmediate(listener, *args as Array<out Any>)
|
||||
}
|
||||
fun setImmediate(listener: Any, vararg args: Array<out Any?>) = timer.setImmediate(listener, *args)
|
||||
|
||||
fun clearImmediate(id: Int): Boolean {
|
||||
return timer.clearImmediate(id)
|
||||
}
|
||||
fun clearImmediate(id: Int) = timer.clearImmediate(id)
|
||||
|
||||
@Throws(InterruptedException::class)
|
||||
fun waitFor() {
|
||||
synchronized(mRunningLock) {
|
||||
if (mRunning) return
|
||||
mRunningLock.wait()
|
||||
if (!mRunning) {
|
||||
mRunningLock.wait()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Thread[$name,$priority]"
|
||||
}
|
||||
override fun toString() = "Thread[$name,$priority]"
|
||||
|
||||
companion object {
|
||||
|
||||
private val sTimerMap = ConcurrentHashMap<Thread, Timer?>()
|
||||
|
||||
@JvmStatic
|
||||
fun getTimerForThread(thread: Thread): Timer? {
|
||||
return sTimerMap[thread]
|
||||
}
|
||||
fun getTimerForThread(thread: Thread) = sTimerMap[thread]
|
||||
|
||||
val timerForCurrentThread: Timer?
|
||||
@JvmStatic
|
||||
val timerForCurrentThread
|
||||
get() = getTimerForThread(currentThread())
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package org.autojs.autojs.core.permission;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import static android.content.pm.PackageManager.PERMISSION_DENIED;
|
||||
|
||||
public class Permissions {
|
||||
|
||||
private static final String[] EMPTY_STRING_ARRAY = new String[0];
|
||||
|
||||
static final int REQUEST_CODE = 18777;
|
||||
|
||||
public static String[] getPermissionsNeedToRequest(Context context, String[] permissions) {
|
||||
ArrayList<String> list = new ArrayList<>();
|
||||
for (String permission : permissions) {
|
||||
if (!permission.startsWith("android.permission.")) {
|
||||
permission = "android.permission." + permission.toUpperCase();
|
||||
}
|
||||
if (context.checkSelfPermission(permission) == PERMISSION_DENIED) {
|
||||
list.add(permission);
|
||||
}
|
||||
}
|
||||
return list.toArray(EMPTY_STRING_ARRAY);
|
||||
}
|
||||
|
||||
public static void requestPermissions(PermissionRequestProxyActivity activity, String[] permissions, OnRequestPermissionsResultCallback callback) {
|
||||
if (callback != null) {
|
||||
activity.addRequestPermissionsCallback((code, p, grantResults) -> {
|
||||
activity.removeRequestPermissionsCallback(callback);
|
||||
callback.onRequestPermissionsResult(code, p, grantResults);
|
||||
});
|
||||
}
|
||||
activity.requestPermissions(permissions, REQUEST_CODE);
|
||||
}
|
||||
|
||||
public static void requestPermissions(Context context, String[] permissions) {
|
||||
context.startActivity(new Intent(context, PermissionRequestActivity.class)
|
||||
.putExtra(PermissionRequestActivity.EXTRA_PERMISSIONS, permissions)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import org.autojs.autojs.core.ui.inflater.util.Ids
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/5/14.
|
||||
* Transformed by 抠脚本人 on Jul 10, 2023.
|
||||
* Transformed by 抠脚本人 (https://github.com/little-alei) on Jul 10, 2023.
|
||||
*/
|
||||
object JsViewHelper {
|
||||
@JvmStatic
|
||||
|
||||
@@ -21,8 +21,7 @@ public class JsDialogBuilder extends MaterialDialog.Builder {
|
||||
private final Timer mTimer;
|
||||
private final Loopers mLoopers;
|
||||
private JsDialog mDialog;
|
||||
private volatile Loopers.AsyncTask task;
|
||||
|
||||
private volatile int mWaitId = -1;
|
||||
|
||||
public JsDialogBuilder(Context context, ScriptRuntime runtime) {
|
||||
super(context);
|
||||
@@ -56,14 +55,14 @@ public class JsDialogBuilder extends MaterialDialog.Builder {
|
||||
}
|
||||
});
|
||||
dismissListener(dialog -> {
|
||||
mTimer.postDelayed(() -> mLoopers.removeAsyncTask(task), 0);
|
||||
mTimer.postDelayed(() -> mLoopers.doNotWaitWhenIdle(mWaitId), 0);
|
||||
emit("dismiss", dialog);
|
||||
});
|
||||
cancelListener(dialog -> emit("cancel", dialog));
|
||||
}
|
||||
|
||||
public void onShowCalled() {
|
||||
mTimer.postDelayed(() -> task = mLoopers.createAndAddAsyncTask("js-dialog"), 0);
|
||||
mTimer.postDelayed(() -> mWaitId = mLoopers.waitWhenIdle(), 0);
|
||||
}
|
||||
|
||||
public JsDialog getDialog() {
|
||||
|
||||
@@ -8,7 +8,7 @@ import androidx.appcompatlegacy.widget.AppCompatTextView
|
||||
* Created by SuperMonster003 on Mar 20, 2022.
|
||||
* Transformed by SuperMonster003 on May 22, 2023.
|
||||
*/
|
||||
// @Reference to TonyJiangWJ/Auto.js on Mar 20, 2022
|
||||
// @Reference to TonyJiangWJ/Auto.js (https://github.com/TonyJiangWJ/Auto.js) on Mar 20, 2022
|
||||
class JsTextViewLegacy : AppCompatTextView {
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
|
||||
@@ -83,7 +83,7 @@ object XmlConverter {
|
||||
.map(arrayOf("webview", "web"), JsWebView::class.java.name)
|
||||
.map(
|
||||
"text",
|
||||
// @Reference to TonyJiangWJ/Auto.js on Mar 20, 2022
|
||||
// @Reference to TonyJiangWJ/Auto.js (https://github.com/TonyJiangWJ/Auto.js) on Mar 20, 2022
|
||||
when (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
true -> JsTextViewLegacy::class.java.name
|
||||
else -> JsTextView::class.java.name
|
||||
|
||||
@@ -14,7 +14,7 @@ val runtime = AutoJs.instance.runtime
|
||||
/**
|
||||
* Created by SuperMonster003 on Apr 30, 2023.
|
||||
*/
|
||||
// @Reference to kkevsekk1/AutoX on Apr 30, 2023.
|
||||
// @Reference to kkevsekk1/AutoX (https://github.com/kkevsekk1/AutoX) on Apr 30, 2023.
|
||||
class WebSocket @JvmOverloads constructor(val client: OkHttpClient, val url: String, isInCurrentThread: Boolean = true) : EventEmitter(
|
||||
runtime.bridges, runtime.timers.timerForCurrentThread.takeIf { isInCurrentThread }
|
||||
), okhttp3.WebSocket {
|
||||
|
||||
@@ -5,8 +5,8 @@ import android.view.View
|
||||
import org.autojs.autojs.core.automator.UiObjectCollection
|
||||
import org.autojs.autojs.core.ui.ViewExtras
|
||||
import org.autojs.autojs.engine.module.AssetAndUrlModuleSourceProvider
|
||||
import org.autojs.autojs.engine.module.ScopeRequire
|
||||
import org.autojs.autojs.execution.ExecutionConfig
|
||||
import org.autojs.autojs.pio.PFiles
|
||||
import org.autojs.autojs.pio.UncheckedIOException
|
||||
import org.autojs.autojs.project.ScriptConfig
|
||||
import org.autojs.autojs.rhino.RhinoAndroidHelper
|
||||
@@ -18,6 +18,7 @@ import org.mozilla.javascript.Context
|
||||
import org.mozilla.javascript.Script
|
||||
import org.mozilla.javascript.Scriptable
|
||||
import org.mozilla.javascript.ScriptableObject
|
||||
import org.mozilla.javascript.commonjs.module.RequireBuilder
|
||||
import org.mozilla.javascript.commonjs.module.provider.SoftCachingModuleScriptProvider
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
@@ -118,10 +119,13 @@ open class RhinoJavaScriptEngine(private val mAndroidContext: android.content.Co
|
||||
|
||||
private fun initRequireBuilder(context: Context, scope: Scriptable) {
|
||||
val provider = AssetAndUrlModuleSourceProvider(
|
||||
mAndroidContext, MODULES_PATH,
|
||||
listOf<URI>(File("/").toURI())
|
||||
mAndroidContext, MODULES_ROOT_PATH, listOf<URI>(File(File.separator).toURI())
|
||||
)
|
||||
ScopeRequire(context, scope, SoftCachingModuleScriptProvider(provider)).install(scope)
|
||||
RequireBuilder()
|
||||
.setModuleScriptProvider(SoftCachingModuleScriptProvider(provider))
|
||||
.setSandboxed(true)
|
||||
.createRequire(context, scope)
|
||||
.install(scope)
|
||||
}
|
||||
|
||||
protected fun createScope(context: Context): TopLevelScope {
|
||||
@@ -164,8 +168,11 @@ open class RhinoJavaScriptEngine(private val mAndroidContext: android.content.Co
|
||||
|
||||
const val SOURCE_FILE_INIT = "init.js"
|
||||
const val SOURCE_NAME_INIT = "<init>"
|
||||
const val MODULES_ROOT_PATH = "modules"
|
||||
const val JS_BEAUTIFY_PATH = "js-beautify"
|
||||
|
||||
private const val MODULES_PATH = "modules"
|
||||
@JvmField
|
||||
val JS_BEAUTIFY_FILE = PFiles.join(JS_BEAUTIFY_PATH, "beautify.js")
|
||||
|
||||
private var sInitScript: Script? = null
|
||||
private val sContextEngineMap = ConcurrentHashMap<Context, RhinoJavaScriptEngine>()
|
||||
|
||||
@@ -1,160 +1,131 @@
|
||||
package org.autojs.autojs.engine.module
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import com.google.gson.Gson
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import org.autojs.autojs.engine.encryption.ScriptEncryption.decrypt
|
||||
import org.autojs.autojs.runtime.ScriptRuntime
|
||||
import org.autojs.autojs.script.EncryptedScriptFileHeader
|
||||
import org.autojs.autojs.script.EncryptedScriptFileHeader.isValidFile
|
||||
import org.mozilla.javascript.commonjs.module.provider.ModuleSource
|
||||
import org.mozilla.javascript.commonjs.module.provider.ModuleSourceProviderBase
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.File
|
||||
import java.io.File.separator
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.io.Reader
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.net.URLConnection
|
||||
import java.nio.charset.Charset
|
||||
import java.nio.charset.StandardCharsets
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/5/9.
|
||||
* Transformed by SuperMonster003 on Jul 14, 2023.
|
||||
*/
|
||||
// @Inspired by aiselp (https://github.com/aiselp) on Jul 14, 2023.
|
||||
// ! Related PR:
|
||||
// ! http://pr.autojs6.com/75
|
||||
// ! http://pr.autojs6.com/78
|
||||
// @Hint by SuperMonster003 on Jul 17, 2023.
|
||||
// ! Project-structured directories with package.json
|
||||
// ! was not yet adapted,
|
||||
// ! as it doesn't seem to matter as much. :)
|
||||
class AssetAndUrlModuleSourceProvider(
|
||||
context: Context,
|
||||
assetDirPath: String,
|
||||
list: List<URI>? = null
|
||||
) : ModuleSourceProviderBase() {
|
||||
val mContext = context
|
||||
private val okHttpClient = OkHttpClient.Builder().followRedirects(true).build()
|
||||
private val contentResolver: ContentResolver = context.contentResolver
|
||||
private val moduleSources: ArrayList<URI> = arrayListOf(mBaseURI, npmModuleSource)
|
||||
private val context: Context,
|
||||
private val assetDirPath: String,
|
||||
list: List<URI>
|
||||
) : UrlModuleSourceProvider(list, null) {
|
||||
|
||||
companion object {
|
||||
val mBaseURI: URI = URI.create("file:/android_asset/modules")
|
||||
val npmModuleSource: URI = URI.create("file:/android_asset/modules/npm")
|
||||
}
|
||||
private val mOkHttpClient by lazy { OkHttpClient.Builder().followRedirects(true).build() }
|
||||
private val mAssetBaseURI = URI.create("file:///android_asset$separator$assetDirPath")
|
||||
private val mAssetManager = context.assets
|
||||
private val mJavaScriptExtensionName = ".js"
|
||||
private val mRegexUrl = "^(https?|ftp|file)://[-a-zA-Z\\d+&@#/%?=~_|!:,.;]*[-a-zA-Z\\d+&@#/%=~_|]".toRegex()
|
||||
|
||||
// 初始化脚本以及启动文件只会从此方法加载模块,子模块加载没有以"./"或"../"开头的模块也会从此方法加载
|
||||
@Throws(IOException::class, URISyntaxException::class)
|
||||
override fun loadFromPrivilegedLocations(moduleId: String, validator: Any?): ModuleSource? {
|
||||
// println("加载私有模块:$moduleId")
|
||||
val uri = if (moduleId.startsWith("/")) {
|
||||
File(moduleId).toURI()
|
||||
} else if (moduleId.startsWith("http://") || moduleId.startsWith("https://")) {
|
||||
URI.create(moduleId)
|
||||
} else null
|
||||
if (uri != null) {
|
||||
return loadFromUri(uri, File(uri.path).parentFile?.toURI(), validator)
|
||||
}
|
||||
for (baseUri in moduleSources) {
|
||||
val sourceUri = URI.create("$baseUri/$moduleId")
|
||||
val moduleSource = loadFromUri(sourceUri, baseUri, validator)
|
||||
if (moduleSource != null) {
|
||||
return moduleSource
|
||||
return when (moduleId.matches(mRegexUrl)) {
|
||||
true -> loadFromURL(moduleId, validator)
|
||||
else -> try {
|
||||
loadFromAsset(moduleId, validator)
|
||||
} catch (e: Exception) {
|
||||
return when (moduleId.startsWith(separator)) {
|
||||
true -> loadFromFile(File(moduleId), validator = validator)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
// 这里处理node_module目录的模块
|
||||
override fun loadFromFallbackLocations(moduleId: String, validator: Any?): ModuleSource? {
|
||||
return super.loadFromFallbackLocations(moduleId, validator)
|
||||
}
|
||||
|
||||
// 子模块以相对路径加载时调用此方法
|
||||
override fun loadFromUri(uri: URI, base: URI?, validator: Any?): ModuleSource? {
|
||||
var uri = uri
|
||||
if (uri.scheme == null) uri = File(uri.path).toURI()
|
||||
// println("加载模块:$uri")
|
||||
if (uri.scheme == "http" || uri.scheme == "https") {
|
||||
return loadFromHttp(uri, base, validator)
|
||||
private fun loadFromAsset(moduleId: String, validator: Any?): ModuleSource? {
|
||||
val moduleIdWithExtension = when (moduleId.endsWith(mJavaScriptExtensionName, true)) {
|
||||
true -> moduleId
|
||||
else -> moduleId + mJavaScriptExtensionName
|
||||
}
|
||||
val moduleSource = loadAt(uri, base, validator) ?: loadAt(
|
||||
File(uri.path + ".js").toURI(), base, validator
|
||||
)
|
||||
if (moduleSource != null) {
|
||||
return moduleSource
|
||||
}
|
||||
// 尝试从目录加载
|
||||
// 尝试读取package.json指定的文件
|
||||
val mainFile: URI? = try {
|
||||
val packageFile = File(uri.path, "package.json")
|
||||
val json = Gson().fromJson<Map<String, Any>>(
|
||||
InputStreamReader(packageFile.inputStream()),
|
||||
Map::class.java
|
||||
return try {
|
||||
createModuleSource(
|
||||
mAssetManager.open("$assetDirPath$separator$moduleIdWithExtension"),
|
||||
URI("$mAssetBaseURI$separator$moduleIdWithExtension"),
|
||||
mAssetBaseURI,
|
||||
validator,
|
||||
)
|
||||
val main = json["main"] as String
|
||||
packageFile.toURI().resolve(main)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} catch (_: FileNotFoundException) {
|
||||
super.loadFromPrivilegedLocations(moduleId, validator)
|
||||
}
|
||||
val main: URI = mainFile ?: File(uri.path, "index.js").toURI()
|
||||
return loadAt(main, uri, validator)
|
||||
}
|
||||
|
||||
private fun loadAt(uri: URI, base: URI?, validator: Any?): ModuleSource? {
|
||||
if (uri.scheme == "http" || uri.scheme == "https") {
|
||||
return loadFromHttp(uri, base, validator)
|
||||
}
|
||||
private fun loadFromURL(url: String, validator: Any?): ModuleSource? {
|
||||
return try {
|
||||
val inputStream = if (uri.path.startsWith("/android_asset/")) {
|
||||
mContext.assets.open(uri.path.replace("/android_asset/", ""))
|
||||
} else contentResolver.openInputStream(Uri.parse(uri.toString()))
|
||||
if (inputStream != null) {
|
||||
createModuleEncryptionSource(inputStream, uri, base, validator)
|
||||
} else null
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun createModuleEncryptionSource(
|
||||
inputStream: InputStream,
|
||||
uri: URI,
|
||||
base: URI?,
|
||||
validator: Any?,
|
||||
): ModuleSource {
|
||||
val bytes = ByteArray(inputStream.available())
|
||||
inputStream.read(bytes)
|
||||
inputStream.close()
|
||||
val i = if (isValidFile(bytes)) {
|
||||
val clearText = decrypt(bytes, EncryptedScriptFileHeader.BLOCK_SIZE, bytes.size)
|
||||
ByteArrayInputStream(clearText)
|
||||
} else ByteArrayInputStream(bytes)
|
||||
return createModuleSource(i, uri, base, validator)
|
||||
}
|
||||
|
||||
private fun loadFromHttp(uri: URI, base: URI?, validator: Any?): ModuleSource? {
|
||||
return try {
|
||||
Request.Builder().url(uri.toString()).build().let { request ->
|
||||
val response = okHttpClient.newCall(request).execute()
|
||||
Request.Builder().url(url).build().let { request ->
|
||||
val response = mOkHttpClient.newCall(request).execute()
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
return null
|
||||
return null.also { response.close() }
|
||||
}
|
||||
response.body?.let {
|
||||
val charset = it.contentType()?.charset()?.toString() ?: "utf-8"
|
||||
return createModuleSource(it.byteStream(), uri, base, validator, charset)
|
||||
response.body.let {
|
||||
return createModuleSource(it.byteStream(), URI.create(url), null, validator, it.contentType()?.charset())
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
null.also { ScriptRuntime.popException(e.message) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createModuleSource(
|
||||
inputStream: InputStream,
|
||||
uri: URI,
|
||||
base: URI?,
|
||||
validator: Any?,
|
||||
charset: String? = null
|
||||
): ModuleSource {
|
||||
val id = if (uri.scheme == "file") {
|
||||
URI.create(uri.path)
|
||||
} else uri
|
||||
return ModuleSource(
|
||||
InputStreamReader(inputStream, charset ?: "utf-8"),
|
||||
null,
|
||||
id,
|
||||
base,
|
||||
validator
|
||||
)
|
||||
private fun loadFromFile(file: File, parentFile: File? = file.parentFile, validator: Any?): ModuleSource? {
|
||||
return loadFromFile(file.toURI(), parentFile?.toURI(), validator)
|
||||
}
|
||||
|
||||
private fun loadFromFile(uri: URI, parent: URI?, validator: Any?): ModuleSource? {
|
||||
val inputStream = context.contentResolver.openInputStream(Uri.parse(uri.toString())) ?: return null
|
||||
return try {
|
||||
createModuleSource(inputStream, uri, parent, validator)
|
||||
} catch (e: FileNotFoundException) {
|
||||
null.also { ScriptRuntime.popException(e.message) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createModuleSource(stream: InputStream, uri: URI, base: URI?, validator: Any?, charset: Charset? = null): ModuleSource {
|
||||
val streamReader = InputStreamReader(stream, charset ?: StandardCharsets.UTF_8)
|
||||
return ModuleSource(streamReader, null, uri, base, validator)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun getReader(urlConnection: URLConnection): Reader {
|
||||
val stream = urlConnection.getInputStream()
|
||||
val bytes = ByteArray(stream.available()).also {
|
||||
stream.read(it)
|
||||
stream.close()
|
||||
}
|
||||
return if (isValidFile(bytes)) {
|
||||
val clearText = decrypt(bytes, EncryptedScriptFileHeader.BLOCK_SIZE, bytes.size)
|
||||
InputStreamReader(ByteArrayInputStream(clearText))
|
||||
} else {
|
||||
InputStreamReader(ByteArrayInputStream(bytes))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,261 +0,0 @@
|
||||
package org.autojs.autojs.engine.module
|
||||
|
||||
import org.mozilla.javascript.*
|
||||
import org.mozilla.javascript.commonjs.module.ModuleScope
|
||||
import org.mozilla.javascript.commonjs.module.ModuleScript
|
||||
import org.mozilla.javascript.commonjs.module.ModuleScriptProvider
|
||||
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
open class ScopeRequire(
|
||||
cx: Context, private val nativeScope: Scriptable,
|
||||
private val moduleScriptProvider: ModuleScriptProvider, private val preExec: Script?,
|
||||
private val postExec: Script?, private val sandboxed: Boolean = true
|
||||
) : BaseFunction() {
|
||||
private var paths: Scriptable? = null
|
||||
private var mainModuleId: String? = null
|
||||
private var mainExports: Scriptable? = null
|
||||
|
||||
// Modules that completed loading; visible to all threads
|
||||
private val exportedModuleInterfaces: MutableMap<String, Scriptable?> = ConcurrentHashMap()
|
||||
private val loadLock = Any()
|
||||
|
||||
constructor(cx: Context, nativeScope: Scriptable, moduleScriptProvider: ModuleScriptProvider)
|
||||
: this(cx, nativeScope, moduleScriptProvider, null, null, false)
|
||||
|
||||
init {
|
||||
prototype = getFunctionPrototype(nativeScope)
|
||||
if (!sandboxed) {
|
||||
paths = cx.newArray(nativeScope, 0)
|
||||
defineReadOnlyProperty(this, "paths", paths)
|
||||
} else paths = null
|
||||
}
|
||||
|
||||
|
||||
fun requireMain(cx: Context, mainModuleId: String): Scriptable? {
|
||||
if (this.mainModuleId != null) {
|
||||
if (this.mainModuleId != mainModuleId) {
|
||||
throw IllegalStateException("Main module already set to " + this.mainModuleId)
|
||||
}
|
||||
return mainExports
|
||||
}
|
||||
val moduleScript: ModuleScript? = try {
|
||||
moduleScriptProvider.getModuleScript(cx, mainModuleId, null, null, paths)
|
||||
} catch (x: RuntimeException) {
|
||||
throw x
|
||||
} catch (x: Exception) {
|
||||
throw RuntimeException(x)
|
||||
}
|
||||
if (moduleScript != null) {
|
||||
mainExports = getExportedModuleInterface(
|
||||
cx, mainModuleId,
|
||||
null, null, true
|
||||
)
|
||||
} else if (!sandboxed) {
|
||||
var mainUri: URI? = try {
|
||||
URI(mainModuleId)
|
||||
} catch (_: URISyntaxException) {
|
||||
null
|
||||
}
|
||||
if (mainUri == null || !mainUri.isAbsolute) {
|
||||
val file = File(mainModuleId)
|
||||
if (!file.isFile) {
|
||||
throw ScriptRuntime.throwError(
|
||||
cx, nativeScope,
|
||||
"Module \"$mainModuleId\" not found."
|
||||
)
|
||||
}
|
||||
mainUri = file.toURI()
|
||||
}
|
||||
mainExports = getExportedModuleInterface(
|
||||
cx, mainUri.toString(),
|
||||
mainUri, null, true
|
||||
)
|
||||
}
|
||||
this.mainModuleId = mainModuleId
|
||||
return mainExports
|
||||
}
|
||||
|
||||
fun install(scope: Scriptable?) {
|
||||
putProperty(scope, "require", this)
|
||||
}
|
||||
|
||||
override fun call(cx: Context, scope: Scriptable, thisObj: Scriptable, args: Array<Any>?): Any {
|
||||
if (args == null || args.isEmpty()) {
|
||||
throw ScriptRuntime.throwError(
|
||||
cx, scope,
|
||||
"require() needs one argument"
|
||||
)
|
||||
}
|
||||
var id = Context.jsToJava(args[0], String::class.java) as String
|
||||
var uri: URI? = null
|
||||
var base: URI? = null
|
||||
if (id.startsWith("./") || id.startsWith("../")) {
|
||||
if (thisObj !is ModuleScope) {
|
||||
throw ScriptRuntime.throwError(
|
||||
cx, scope,
|
||||
"Can't resolve relative module ID \"" + id +
|
||||
"\" when require() is used outside of a module"
|
||||
)
|
||||
}
|
||||
base = thisObj.base
|
||||
val current = thisObj.uri
|
||||
uri = current.resolve(id)
|
||||
if (base == null) {
|
||||
id = uri.toString()
|
||||
} else {
|
||||
id = base.relativize(current).resolve(id).toString()
|
||||
if (id[0] == '.') {
|
||||
if (sandboxed) {
|
||||
throw ScriptRuntime.throwError(
|
||||
cx, scope,
|
||||
"Module \"$id\" is not contained in sandbox."
|
||||
)
|
||||
}
|
||||
id = uri.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
return (getExportedModuleInterface(cx, id, uri, base, false))!!
|
||||
}
|
||||
|
||||
override fun construct(cx: Context, scope: Scriptable, args: Array<Any>): Scriptable {
|
||||
throw ScriptRuntime.throwError(
|
||||
cx, scope,
|
||||
"require() can not be invoked as a constructor"
|
||||
)
|
||||
}
|
||||
|
||||
private fun getExportedModuleInterface(
|
||||
cx: Context, id: String, uri: URI?, base: URI?, isMain: Boolean
|
||||
): Scriptable? {
|
||||
// Check if the requested module is already completely loaded
|
||||
var exports = exportedModuleInterfaces[id]
|
||||
if (exports != null) {
|
||||
if (isMain) {
|
||||
throw IllegalStateException("Attempt to set main module after it was loaded")
|
||||
} else
|
||||
return exports
|
||||
}
|
||||
var threadLoadingModules: MutableMap<String, Scriptable>? =
|
||||
loadingModuleInterfaces.get() as? MutableMap<String, Scriptable>
|
||||
exports = threadLoadingModules?.get(id)
|
||||
if (exports != null) return exports
|
||||
|
||||
synchronized(loadLock) {
|
||||
exports = exportedModuleInterfaces[id]
|
||||
if (exports != null) return exports
|
||||
|
||||
val moduleScript: ModuleScript = getModule(cx, id, uri, base)
|
||||
if (sandboxed && !moduleScript.isSandboxed) {
|
||||
throw ScriptRuntime.throwError(
|
||||
cx, nativeScope, ("Module \"$id\" is not contained in sandbox.")
|
||||
)
|
||||
}
|
||||
exports = cx.newObject(nativeScope)
|
||||
val outermostLocked: Boolean = threadLoadingModules == null
|
||||
if (outermostLocked) {
|
||||
threadLoadingModules = HashMap()
|
||||
loadingModuleInterfaces.set(threadLoadingModules)
|
||||
}
|
||||
|
||||
threadLoadingModules?.set(id, exports!!)
|
||||
try {
|
||||
val newExports: Scriptable = executeModuleScript(
|
||||
cx, id, exports,
|
||||
moduleScript, isMain
|
||||
)
|
||||
if (exports !== newExports) {
|
||||
threadLoadingModules?.put(id, newExports)
|
||||
exports = newExports
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
threadLoadingModules?.remove(id)
|
||||
throw e
|
||||
} finally {
|
||||
if (outermostLocked) {
|
||||
exportedModuleInterfaces.putAll((threadLoadingModules!!))
|
||||
loadingModuleInterfaces.set(null)
|
||||
}
|
||||
}
|
||||
}
|
||||
return exports
|
||||
}
|
||||
|
||||
private fun executeModuleScript(
|
||||
cx: Context, id: String,
|
||||
exports: Scriptable?, moduleScript: ModuleScript, isMain: Boolean
|
||||
): Scriptable {
|
||||
val moduleObject = cx.newObject(nativeScope) as ScriptableObject
|
||||
val uri = moduleScript.uri
|
||||
val base = moduleScript.base
|
||||
defineReadOnlyProperty(moduleObject, "id", id)
|
||||
if (!sandboxed) {
|
||||
defineReadOnlyProperty(moduleObject, "uri", uri.toString())
|
||||
}
|
||||
val executionScope: Scriptable = ModuleScope(nativeScope, uri, base)
|
||||
executionScope.put("__filename", executionScope, File(uri.path).path)
|
||||
executionScope.put("__dirname", executionScope, File(uri.path).parent)
|
||||
executionScope.put("exports", executionScope, exports)
|
||||
executionScope.put("module", executionScope, moduleObject)
|
||||
moduleObject.put("exports", moduleObject, exports)
|
||||
install(executionScope)
|
||||
if (isMain) {
|
||||
defineReadOnlyProperty(this, "main", moduleObject)
|
||||
}
|
||||
//创建新作用域
|
||||
val funScope = cx.newObject(executionScope)
|
||||
funScope.parentScope = executionScope
|
||||
|
||||
executeOptionalScript(preExec, cx, funScope)
|
||||
moduleScript.script.exec(cx, funScope)
|
||||
executeOptionalScript(postExec, cx, funScope)
|
||||
return ScriptRuntime.toObject(
|
||||
cx, nativeScope,
|
||||
getProperty(moduleObject, "exports")
|
||||
)
|
||||
}
|
||||
|
||||
private fun getModule(cx: Context, id: String, uri: URI?, base: URI?): ModuleScript {
|
||||
try {
|
||||
return moduleScriptProvider.getModuleScript(cx, id, uri, base, paths)
|
||||
?: throw ScriptRuntime.throwError(
|
||||
cx, nativeScope, ("Module \"$id\" not found.")
|
||||
)
|
||||
} catch (e: RuntimeException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw Context.throwAsScriptRuntimeEx(e)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFunctionName() = "require"
|
||||
override fun getArity() = 1
|
||||
override fun getLength() = 1
|
||||
|
||||
companion object {
|
||||
private const val serialVersionUID = 1L
|
||||
|
||||
private val loadingModuleInterfaces = ThreadLocal<Map<String, Scriptable>>()
|
||||
private fun executeOptionalScript(
|
||||
script: Script?, cx: Context,
|
||||
executionScope: Scriptable
|
||||
) {
|
||||
script?.exec(cx, executionScope)
|
||||
}
|
||||
|
||||
private fun defineReadOnlyProperty(
|
||||
obj: ScriptableObject,
|
||||
name: String, value: Any?
|
||||
) {
|
||||
putProperty(obj, name, value)
|
||||
obj.setAttributes(
|
||||
name, READONLY or
|
||||
PERMANENT
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package org.autojs.autojs.engine.module;
|
||||
|
||||
import org.mozilla.javascript.commonjs.module.provider.DefaultUrlConnectionExpiryCalculator;
|
||||
import org.mozilla.javascript.commonjs.module.provider.ModuleSource;
|
||||
import org.mozilla.javascript.commonjs.module.provider.ModuleSourceProviderBase;
|
||||
import org.mozilla.javascript.commonjs.module.provider.ParsedContentType;
|
||||
import org.mozilla.javascript.commonjs.module.provider.UrlConnectionExpiryCalculator;
|
||||
import org.mozilla.javascript.commonjs.module.provider.UrlConnectionSecurityDomainProvider;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.Reader;
|
||||
import java.io.Serializable;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
/**
|
||||
* A URL-based script provider that can load modules against a set of base
|
||||
* privileged and fallback URIs. It is deliberately not named "URI provider"
|
||||
* but a "URL provider" since it actually only works against those URIs that
|
||||
* are URLs (and the JRE has a protocol handler for them). It creates cache
|
||||
* validators that are suitable for use with both file: and http: URL
|
||||
* protocols. Specifically, it is able to use both last-modified timestamps and
|
||||
* ETags for cache revalidation, and follows the HTTP cache expiry calculation
|
||||
* model, and allows for fallback heuristic expiry calculation when no server
|
||||
* specified expiry is provided.
|
||||
*
|
||||
* @author Attila Szegedi
|
||||
* @version $Id: UrlModuleSourceProvider.java,v 1.4 2011/04/07 20:26:12 hannes%helma.at Exp $
|
||||
*/
|
||||
public class UrlModuleSourceProvider extends ModuleSourceProviderBase {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final Iterable<URI> privilegedUris;
|
||||
private final Iterable<URI> fallbackUris;
|
||||
private final UrlConnectionSecurityDomainProvider
|
||||
urlConnectionSecurityDomainProvider;
|
||||
private final UrlConnectionExpiryCalculator urlConnectionExpiryCalculator;
|
||||
|
||||
/**
|
||||
* Creates a new module script provider that loads modules against a set of
|
||||
* privileged and fallback URIs. It will use a fixed default cache expiry
|
||||
* of 60 seconds, and provide no security domain objects for the resource.
|
||||
*
|
||||
* @param privilegedUris an iterable providing the privileged URIs. Can be
|
||||
* null if no privileged URIs are used.
|
||||
* @param fallbackUris an iterable providing the fallback URIs. Can be
|
||||
* null if no fallback URIs are used.
|
||||
*/
|
||||
public UrlModuleSourceProvider(Iterable<URI> privilegedUris,
|
||||
Iterable<URI> fallbackUris) {
|
||||
this(privilegedUris, fallbackUris,
|
||||
new DefaultUrlConnectionExpiryCalculator(), null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new module script provider that loads modules against a set of
|
||||
* privileged and fallback URIs. It will use the specified heuristic cache
|
||||
* expiry calculator and security domain provider.
|
||||
*
|
||||
* @param privilegedUris an iterable providing the privileged URIs. Can be
|
||||
* null if no privileged URIs are used.
|
||||
* @param fallbackUris an iterable providing the fallback URIs. Can be
|
||||
* null if no fallback URIs are used.
|
||||
* @param urlConnectionExpiryCalculator the calculator object for heuristic
|
||||
* calculation of the resource expiry, used when no expiry is provided by
|
||||
* the server of the resource. Can be null, in which case the maximum age
|
||||
* of cached entries without validation will be zero.
|
||||
* @param urlConnectionSecurityDomainProvider object that provides security
|
||||
* domain objects for the loaded sources. Can be null, in which case the
|
||||
* loaded sources will have no security domain associated with them.
|
||||
*/
|
||||
public UrlModuleSourceProvider(Iterable<URI> privilegedUris,
|
||||
Iterable<URI> fallbackUris,
|
||||
UrlConnectionExpiryCalculator urlConnectionExpiryCalculator,
|
||||
UrlConnectionSecurityDomainProvider urlConnectionSecurityDomainProvider) {
|
||||
this.privilegedUris = privilegedUris;
|
||||
this.fallbackUris = fallbackUris;
|
||||
this.urlConnectionExpiryCalculator = urlConnectionExpiryCalculator;
|
||||
this.urlConnectionSecurityDomainProvider =
|
||||
urlConnectionSecurityDomainProvider;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ModuleSource loadFromPrivilegedLocations(
|
||||
String moduleId, Object validator)
|
||||
throws IOException, URISyntaxException {
|
||||
return loadFromPathList(moduleId, validator, privilegedUris);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ModuleSource loadFromFallbackLocations(
|
||||
String moduleId, Object validator)
|
||||
throws IOException, URISyntaxException {
|
||||
return loadFromPathList(moduleId, validator, fallbackUris);
|
||||
}
|
||||
|
||||
private ModuleSource loadFromPathList(String moduleId,
|
||||
Object validator, Iterable<URI> paths)
|
||||
throws IOException, URISyntaxException {
|
||||
if (paths == null) {
|
||||
return null;
|
||||
}
|
||||
for (URI path : paths) {
|
||||
final ModuleSource moduleSource = loadFromUri(
|
||||
path.resolve(moduleId), path, validator);
|
||||
if (moduleSource != null) {
|
||||
return moduleSource;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ModuleSource loadFromUri(URI uri, URI base, Object validator)
|
||||
throws IOException, URISyntaxException {
|
||||
// We expect modules to have a ".js" file name extension ...
|
||||
URI fullUri = new URI(uri + ".js");
|
||||
ModuleSource source = loadFromActualUri(fullUri, base, validator);
|
||||
// ... but for compatibility we support modules without extension,
|
||||
// or ids with explicit extension.
|
||||
return source != null ?
|
||||
source : loadFromActualUri(uri, base, validator);
|
||||
}
|
||||
|
||||
protected ModuleSource loadFromActualUri(URI uri, URI base, Object validator)
|
||||
throws IOException {
|
||||
final URL url = new URL(base == null ? null : base.toURL(), uri.toString());
|
||||
final long request_time = System.currentTimeMillis();
|
||||
final URLConnection urlConnection = openUrlConnection(url);
|
||||
final URLValidator applicableValidator;
|
||||
if (validator instanceof final URLValidator uriValidator) {
|
||||
applicableValidator = uriValidator.appliesTo(uri) ? uriValidator :
|
||||
null;
|
||||
} else {
|
||||
applicableValidator = null;
|
||||
}
|
||||
if (applicableValidator != null) {
|
||||
applicableValidator.applyConditionals(urlConnection);
|
||||
}
|
||||
try {
|
||||
urlConnection.connect();
|
||||
if (applicableValidator != null &&
|
||||
applicableValidator.updateValidator(urlConnection,
|
||||
request_time, urlConnectionExpiryCalculator)) {
|
||||
close(urlConnection);
|
||||
return NOT_MODIFIED;
|
||||
}
|
||||
|
||||
return new ModuleSource(getReader(urlConnection),
|
||||
getSecurityDomain(urlConnection), uri, base,
|
||||
new URLValidator(uri, urlConnection, request_time,
|
||||
urlConnectionExpiryCalculator));
|
||||
} catch (FileNotFoundException e) {
|
||||
return null;
|
||||
} catch (RuntimeException | IOException e) {
|
||||
close(urlConnection);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
protected Reader getReader(URLConnection urlConnection)
|
||||
throws IOException {
|
||||
return new InputStreamReader(urlConnection.getInputStream(),
|
||||
getCharacterEncoding(urlConnection));
|
||||
}
|
||||
|
||||
protected String getCharacterEncoding(URLConnection urlConnection) {
|
||||
final ParsedContentType pct = new ParsedContentType(
|
||||
urlConnection.getContentType());
|
||||
final String encoding = pct.getEncoding();
|
||||
if (encoding != null) {
|
||||
return encoding;
|
||||
}
|
||||
final String contentType = pct.getContentType();
|
||||
if (contentType != null && contentType.startsWith("text/")) {
|
||||
return "8859_1";
|
||||
}
|
||||
return "utf-8";
|
||||
}
|
||||
|
||||
protected Object getSecurityDomain(URLConnection urlConnection) {
|
||||
return urlConnectionSecurityDomainProvider == null ? null :
|
||||
urlConnectionSecurityDomainProvider.getSecurityDomain(
|
||||
urlConnection);
|
||||
}
|
||||
|
||||
private void close(URLConnection urlConnection) {
|
||||
try {
|
||||
urlConnection.getInputStream().close();
|
||||
} catch (IOException e) {
|
||||
onFailedClosingUrlConnection(urlConnection, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Override if you want to get notified if the URL connection fails to
|
||||
* close. Does nothing by default.
|
||||
*
|
||||
* @param urlConnection the connection
|
||||
* @param cause the cause it failed to close.
|
||||
*/
|
||||
protected void onFailedClosingUrlConnection(URLConnection urlConnection,
|
||||
IOException cause) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be overridden in subclasses to customize the URL connection opening
|
||||
* process. By default, just calls {@link URL#openConnection()}.
|
||||
*
|
||||
* @param url the URL
|
||||
* @return a connection to the URL.
|
||||
* @throws IOException if an I/O error occurs.
|
||||
*/
|
||||
protected URLConnection openUrlConnection(URL url) throws IOException {
|
||||
return url.openConnection();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean entityNeedsRevalidation(Object validator) {
|
||||
return !(validator instanceof URLValidator)
|
||||
|| ((URLValidator) validator).entityNeedsRevalidation();
|
||||
}
|
||||
|
||||
private static class URLValidator implements Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private final URI uri;
|
||||
private final long lastModified;
|
||||
private final String entityTags;
|
||||
private long expiry;
|
||||
|
||||
public URLValidator(URI uri, URLConnection urlConnection,
|
||||
long request_time, UrlConnectionExpiryCalculator
|
||||
urlConnectionExpiryCalculator) {
|
||||
this.uri = uri;
|
||||
this.lastModified = urlConnection.getLastModified();
|
||||
this.entityTags = getEntityTags(urlConnection);
|
||||
expiry = calculateExpiry(urlConnection, request_time,
|
||||
urlConnectionExpiryCalculator);
|
||||
}
|
||||
|
||||
boolean updateValidator(URLConnection urlConnection, long request_time,
|
||||
UrlConnectionExpiryCalculator urlConnectionExpiryCalculator)
|
||||
throws IOException {
|
||||
boolean isResourceChanged = isResourceChanged(urlConnection);
|
||||
if (!isResourceChanged) {
|
||||
expiry = calculateExpiry(urlConnection, request_time,
|
||||
urlConnectionExpiryCalculator);
|
||||
}
|
||||
return isResourceChanged;
|
||||
}
|
||||
|
||||
private boolean isResourceChanged(URLConnection urlConnection)
|
||||
throws IOException {
|
||||
if (urlConnection instanceof HttpURLConnection) {
|
||||
return ((HttpURLConnection) urlConnection).getResponseCode() ==
|
||||
HttpURLConnection.HTTP_NOT_MODIFIED;
|
||||
}
|
||||
return lastModified != urlConnection.getLastModified();
|
||||
}
|
||||
|
||||
private long calculateExpiry(URLConnection urlConnection,
|
||||
long request_time, UrlConnectionExpiryCalculator
|
||||
urlConnectionExpiryCalculator) {
|
||||
if ("no-cache".equals(urlConnection.getHeaderField("Pragma"))) {
|
||||
return 0L;
|
||||
}
|
||||
final String cacheControl = urlConnection.getHeaderField(
|
||||
"Cache-Control");
|
||||
if (cacheControl != null) {
|
||||
if (cacheControl.contains("no-cache")) {
|
||||
return 0L;
|
||||
}
|
||||
final int max_age = getMaxAge(cacheControl);
|
||||
if (-1 != max_age) {
|
||||
final long response_time = System.currentTimeMillis();
|
||||
final long apparent_age = Math.max(0, response_time -
|
||||
urlConnection.getDate());
|
||||
final long corrected_received_age = Math.max(apparent_age,
|
||||
urlConnection.getHeaderFieldInt("Age", 0) * 1000L);
|
||||
final long response_delay = response_time - request_time;
|
||||
final long corrected_initial_age = corrected_received_age +
|
||||
response_delay;
|
||||
final long creation_time = response_time -
|
||||
corrected_initial_age;
|
||||
return max_age * 1000L + creation_time;
|
||||
}
|
||||
}
|
||||
final long explicitExpiry = urlConnection.getHeaderFieldDate(
|
||||
"Expires", -1L);
|
||||
if (explicitExpiry != -1L) {
|
||||
return explicitExpiry;
|
||||
}
|
||||
return urlConnectionExpiryCalculator == null ? 0L :
|
||||
urlConnectionExpiryCalculator.calculateExpiry(urlConnection);
|
||||
}
|
||||
|
||||
private int getMaxAge(String cacheControl) {
|
||||
final int maxAgeIndex = cacheControl.indexOf("max-age");
|
||||
if (maxAgeIndex == -1) {
|
||||
return -1;
|
||||
}
|
||||
final int eq = cacheControl.indexOf('=', maxAgeIndex + 7);
|
||||
if (eq == -1) {
|
||||
return -1;
|
||||
}
|
||||
final int comma = cacheControl.indexOf(',', eq + 1);
|
||||
final String strAge;
|
||||
if (comma == -1) {
|
||||
strAge = cacheControl.substring(eq + 1);
|
||||
} else {
|
||||
strAge = cacheControl.substring(eq + 1, comma);
|
||||
}
|
||||
try {
|
||||
return Integer.parseInt(strAge);
|
||||
} catch (NumberFormatException e) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
private String getEntityTags(URLConnection urlConnection) {
|
||||
final List<String> etags = urlConnection.getHeaderFields().get("ETag");
|
||||
if (etags == null || etags.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
final StringBuilder b = new StringBuilder();
|
||||
final Iterator<String> it = etags.iterator();
|
||||
b.append(it.next());
|
||||
while (it.hasNext()) {
|
||||
b.append(", ").append(it.next());
|
||||
}
|
||||
return b.toString();
|
||||
}
|
||||
|
||||
boolean appliesTo(URI uri) {
|
||||
return this.uri.equals(uri);
|
||||
}
|
||||
|
||||
void applyConditionals(URLConnection urlConnection) {
|
||||
if (lastModified != 0L) {
|
||||
urlConnection.setIfModifiedSince(lastModified);
|
||||
}
|
||||
if (entityTags != null && entityTags.length() > 0) {
|
||||
urlConnection.addRequestProperty("If-None-Match", entityTags);
|
||||
}
|
||||
}
|
||||
|
||||
boolean entityNeedsRevalidation() {
|
||||
return System.currentTimeMillis() > expiry;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package org.autojs.autojs.model.autocomplete
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2018/2/3.
|
||||
* Transformed by 抠脚本人 on Jul 11, 2023.
|
||||
* Transformed by 抠脚本人 (https://github.com/little-alei) on Jul 11, 2023.
|
||||
*/
|
||||
class CodeCompletion {
|
||||
val hint: String
|
||||
|
||||
@@ -2,7 +2,7 @@ package org.autojs.autojs.model.autocomplete
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/9/27.
|
||||
* Transformed by 抠脚本人 on Jul 11, 2023.
|
||||
* Transformed by 抠脚本人 (https://github.com/little-alei) on Jul 11, 2023.
|
||||
*/
|
||||
class CodeCompletions(val from: Int, private val mCompletions: List<CodeCompletion>) {
|
||||
fun size(): Int {
|
||||
|
||||
@@ -162,7 +162,7 @@ public class DevPluginResponseHandler implements Handler {
|
||||
File file = new File(WorkingDirectoryUtils.getPath(), name);
|
||||
PFiles.ensureDir(file.getPath());
|
||||
PFiles.write(file, script);
|
||||
ViewUtils.showToast(mContext, R.string.text_script_save_successfully, true);
|
||||
ViewUtils.showToast(mContext, R.string.text_script_save_succeeded, true);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ResultOfMethodCallIgnored")
|
||||
@@ -180,7 +180,7 @@ public class DevPluginResponseHandler implements Handler {
|
||||
};
|
||||
|
||||
Consumer<String> stringConsumer = dest -> ViewUtils
|
||||
.showToast(mContext, mContext.getString(R.string.text_project_save_success) + "\n" + dest);
|
||||
.showToast(mContext, mContext.getString(R.string.text_project_save_succeeded) + "\n" + dest);
|
||||
Consumer<Throwable> throwableConsumer = err -> ViewUtils
|
||||
.showToast(mContext, mContext.getString(R.string.text_project_save_error) + "\n" + err.getMessage());
|
||||
|
||||
|
||||
@@ -29,6 +29,8 @@ import java.io.IOException
|
||||
*/
|
||||
class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir: File) : ClassLoader(), GeneratedClassLoader {
|
||||
|
||||
private val mDexClassLoaders = HashMap<String, DexClassLoader>()
|
||||
|
||||
init {
|
||||
if (cacheDir.exists()) {
|
||||
deleteFilesOfDir(cacheDir)
|
||||
@@ -52,7 +54,7 @@ class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir:
|
||||
throw FileNotFoundException(str(R.string.file_not_exist_or_readable, path))
|
||||
}
|
||||
return DexClassLoader(path, cacheDir.path, null, parent).also {
|
||||
dexClassLoaders[path] = it
|
||||
mDexClassLoaders[path] = it
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +101,7 @@ class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir:
|
||||
@Throws(ClassNotFoundException::class)
|
||||
public override fun loadClass(name: String, resolve: Boolean): Class<*> {
|
||||
findLoadedClass(name)?.let { return it }
|
||||
for (dex in dexClassLoaders.values) try {
|
||||
for (dex in mDexClassLoaders.values) try {
|
||||
dex.loadClass(name)?.let { return it }
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
@@ -114,7 +116,7 @@ class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir:
|
||||
* @param aClass ignored
|
||||
*/
|
||||
override fun linkClass(aClass: Class<*>?) {
|
||||
//doesn't make sense on android
|
||||
// doesn't make sense on android
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,7 +184,6 @@ class AndroidClassLoader(private val parent: ClassLoader, private val cacheDir:
|
||||
companion object {
|
||||
|
||||
private val TAG = AndroidClassLoader::class.java.simpleName
|
||||
private val dexClassLoaders = HashMap<String, DexClassLoader>()
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,9 @@ import android.os.Build;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
|
||||
import org.autojs.autojs.core.permission.Permissions;
|
||||
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.accessibility.AccessibilityBridge;
|
||||
@@ -449,7 +451,7 @@ public class ScriptRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
public void load(String ...path) {
|
||||
public void load(String... path) {
|
||||
load(f -> isJarFile(f) || isDexFile(f), path);
|
||||
}
|
||||
|
||||
@@ -529,17 +531,18 @@ public class ScriptRuntime {
|
||||
}
|
||||
});
|
||||
|
||||
// 清空无障碍事件
|
||||
// @Hint by 抠脚本人 on Jul 10, 2023.
|
||||
// ! 清空无障碍事件.
|
||||
ignoresException(AccessibilityService::clearAccessibilityEventCallback);
|
||||
|
||||
ignoresException(RootUtils::resetRuntimeOverriddenRootModeState);
|
||||
ignoresException(ImageWrapper::recycleAll);
|
||||
|
||||
// 清除 interrupt 状态
|
||||
/* 清除 interrupt 状态. */
|
||||
ignoresException(ThreadCompat::interrupted);
|
||||
|
||||
// 浮动窗口需要第一时间关闭
|
||||
// 以免出现恶意脚本全屏浮动窗口遮蔽屏幕并且在 exit 中写死循环的问题
|
||||
/* 浮动窗口需要第一时间关闭. */
|
||||
/* 以免出现恶意脚本全屏浮动窗口遮蔽屏幕并且在 exit 中写死循环的问题. */
|
||||
ignoresException(floaty::closeAll);
|
||||
|
||||
ignoresException(() -> events.emit("exit"), "exception on exit: %s");
|
||||
@@ -639,4 +642,13 @@ public class ScriptRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
@ScriptInterface
|
||||
public void requestPermissions(String[] permissions) {
|
||||
Context context = uiHandler.getContext();
|
||||
permissions = Permissions.getPermissionsNeedToRequest(context, permissions);
|
||||
if (permissions.length == 0)
|
||||
return;
|
||||
Permissions.requestPermissions(context, permissions);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import android.view.KeyEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.autojs.autojs.annotation.ScriptInterface;
|
||||
import org.autojs.autojs.core.accessibility.AccessibilityBridge;
|
||||
import org.autojs.autojs.core.accessibility.AccessibilityNotificationObserver;
|
||||
import org.autojs.autojs.core.accessibility.AccessibilityService;
|
||||
@@ -21,6 +22,7 @@ import org.autojs.autojs.core.eventloop.EventEmitter;
|
||||
import org.autojs.autojs.core.inputevent.InputEventObserver;
|
||||
import org.autojs.autojs.core.inputevent.TouchObserver;
|
||||
import org.autojs.autojs.core.looper.Loopers;
|
||||
import org.autojs.autojs.core.looper.MainThreadProxy;
|
||||
import org.autojs.autojs.core.looper.Timer;
|
||||
import org.autojs.autojs.core.notification.Notification;
|
||||
import org.autojs.autojs.core.notification.NotificationListenerService;
|
||||
@@ -95,6 +97,11 @@ public class Events extends EventEmitter implements OnKeyListener, TouchObserver
|
||||
return new EventEmitter(mBridges, timer);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@ScriptInterface
|
||||
public EventEmitter emitter(MainThreadProxy mainThreadProxy) {
|
||||
return new EventEmitter(mBridges, mScriptRuntime.timers.getMainTimer());
|
||||
}
|
||||
|
||||
public void observeKey() {
|
||||
if (mListeningKey)
|
||||
|
||||
@@ -10,7 +10,7 @@ import org.autojs.autojs.core.image.ImageWrapper
|
||||
/**
|
||||
* Created by SuperMonster003 on Mar 18, 2023.
|
||||
*/
|
||||
// @Reference to TonyJiangWJ/Auto.js on Mar 18, 2023.
|
||||
// @Reference to TonyJiangWJ/Auto.js (https://github.com/TonyJiangWJ/Auto.js) on Mar 18, 2023.
|
||||
class MlKitOCR {
|
||||
|
||||
private var recognizer: TextRecognizer? = null
|
||||
|
||||
@@ -4,6 +4,7 @@ import android.graphics.Rect
|
||||
import kotlin.math.abs
|
||||
|
||||
// @Reference to com.baidu.paddle.lite.ocr.OcrResult on Mar 18, 2023.
|
||||
// ! https://github.com/PaddlePaddle/PaddleOCR
|
||||
class OcrResult(@JvmField val label: String, @JvmField val confidence: Float, @JvmField val bounds: Rect) : Comparable<OcrResult> {
|
||||
|
||||
override fun compareTo(other: OcrResult): Int {
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.annotation.NonNull;
|
||||
|
||||
import org.autojs.autojs.core.eventloop.EventEmitter;
|
||||
import org.autojs.autojs.core.looper.Loopers;
|
||||
import org.autojs.autojs.pref.Language;
|
||||
import org.autojs.autojs.runtime.ScriptBridges;
|
||||
import org.autojs.autojs.runtime.ScriptRuntime;
|
||||
import org.autojs.autojs.tool.MapBuilder;
|
||||
@@ -22,7 +23,7 @@ import java.util.Set;
|
||||
/**
|
||||
* Created by Stardust on 2018/2/5.
|
||||
*/
|
||||
public class Sensors extends EventEmitter {
|
||||
public class Sensors extends EventEmitter implements Loopers.LooperQuitHandler {
|
||||
|
||||
public class SensorEventEmitter extends EventEmitter implements SensorEventListener {
|
||||
|
||||
@@ -82,13 +83,6 @@ public class Sensors extends EventEmitter {
|
||||
private final ScriptBridges mScriptBridges;
|
||||
private final SensorEventEmitter mNoOpSensorEventEmitter;
|
||||
private final ScriptRuntime mScriptRuntime;
|
||||
private final Loopers.AsyncTask mAsyncTask = new Loopers.AsyncTask("Sensors"){
|
||||
@Override
|
||||
public boolean onFinish(@NonNull Loopers loopers) {
|
||||
return !mSensorEventEmitters.isEmpty();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
public Sensors(Context context, ScriptRuntime runtime) {
|
||||
super(runtime.bridges);
|
||||
@@ -96,7 +90,7 @@ public class Sensors extends EventEmitter {
|
||||
mScriptBridges = runtime.bridges;
|
||||
mNoOpSensorEventEmitter = new SensorEventEmitter(runtime.bridges);
|
||||
mScriptRuntime = runtime;
|
||||
runtime.loopers.addAsyncTask(mAsyncTask);
|
||||
runtime.loopers.addLooperQuitHandler(this);
|
||||
}
|
||||
|
||||
public SensorEventEmitter register(String sensorName) {
|
||||
@@ -128,8 +122,13 @@ public class Sensors extends EventEmitter {
|
||||
return emitter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldQuit() {
|
||||
return mSensorEventEmitters.isEmpty();
|
||||
}
|
||||
|
||||
public Sensor getSensor(String sensorName) {
|
||||
sensorName = sensorName.toUpperCase();
|
||||
sensorName = sensorName.toUpperCase(Language.getPrefLanguage().getLocale());
|
||||
Integer type = SENSORS.get(sensorName);
|
||||
type = type == null ? getSensorTypeByReflect(sensorName) : type;
|
||||
return type == null ? null : mSensorManager.getDefaultSensor(type);
|
||||
@@ -160,6 +159,6 @@ public class Sensors extends EventEmitter {
|
||||
}
|
||||
mSensorEventEmitters.clear();
|
||||
}
|
||||
mScriptRuntime.loopers.removeAsyncTask(mAsyncTask);
|
||||
mScriptRuntime.loopers.removeLooperQuitHandler(this);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package org.autojs.autojs.runtime.api
|
||||
|
||||
import org.autojs.autojs.concurrent.VolatileDispose
|
||||
import org.autojs.autojs.core.looper.Loopers
|
||||
import org.autojs.autojs.core.looper.MainThreadProxy
|
||||
import org.autojs.autojs.core.looper.TimerThread
|
||||
import org.autojs.autojs.runtime.ScriptRuntime
|
||||
import org.autojs.autojs.runtime.exception.ScriptInterruptedException
|
||||
import org.mozilla.javascript.BaseFunction
|
||||
import org.mozilla.javascript.Context
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ThreadFactory
|
||||
import org.autojs.autojs.util.StringUtils.str
|
||||
import org.autojs.autojs6.R
|
||||
import java.util.concurrent.atomic.AtomicLong
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
@@ -17,91 +13,58 @@ import java.util.concurrent.locks.ReentrantLock
|
||||
* Created by Stardust on 2017/12/3.
|
||||
*/
|
||||
class Threads(private val mRuntime: ScriptRuntime) {
|
||||
|
||||
private val mThreads = HashSet<Thread>()
|
||||
val mainThread: Thread = Thread.currentThread()
|
||||
private val mMainThreadProxy = MainThreadProxy(Thread.currentThread(), mRuntime)
|
||||
private var mSpawnCount = 0
|
||||
private var mTaskCount = AtomicLong(0)
|
||||
private var mExit = false
|
||||
private val looperTask = Loopers.AsyncTask("AsyncTaskThreadPool")
|
||||
private val threadPool = Executors.newFixedThreadPool(20, ThreadFactory {
|
||||
val thread = Thread(fun() {
|
||||
Context.enter()
|
||||
try {
|
||||
it.run()
|
||||
} finally {
|
||||
Context.exit()
|
||||
}
|
||||
})
|
||||
thread.name = mainThread.name + " (AsyncThread)"
|
||||
thread
|
||||
})
|
||||
|
||||
fun currentThread(): Any {
|
||||
val thread = Thread.currentThread()
|
||||
return if (thread === mainThread) mMainThreadProxy else thread
|
||||
}
|
||||
val mainThread: Thread = Thread.currentThread()
|
||||
|
||||
fun runTaskForThreadPool(runnable: BaseFunction) {
|
||||
if (mTaskCount.addAndGet(1) == 1L) mRuntime.loopers.addAsyncTask(looperTask)
|
||||
threadPool.execute {
|
||||
try {
|
||||
runnable.call(
|
||||
Context.getCurrentContext(), runnable.parentScope, runnable,
|
||||
emptyArray()
|
||||
)
|
||||
} catch (e: Throwable) {
|
||||
if (!ScriptInterruptedException.causedByInterrupted(e)) {
|
||||
mRuntime.console.error("$this: ", e)
|
||||
}
|
||||
} finally {
|
||||
if (mTaskCount.addAndGet(-1) == 0L) {
|
||||
mRuntime.loopers.removeAsyncTask(looperTask)
|
||||
}
|
||||
}
|
||||
}
|
||||
fun currentThread(): Any = Thread.currentThread().let { thread ->
|
||||
if (thread === mainThread) mMainThreadProxy else thread
|
||||
}
|
||||
|
||||
fun start(runnable: Runnable): TimerThread {
|
||||
val thread = createThread(runnable)
|
||||
synchronized(mThreads) {
|
||||
check(!mExit) { "script exiting" }
|
||||
mThreads.add(thread)
|
||||
thread.name = mainThread.name + " (Spawn-" + mSpawnCount + ")"
|
||||
mSpawnCount++
|
||||
thread.start()
|
||||
check(!mExit) { str(R.string.error_script_is_on_exiting) }
|
||||
thread.let {
|
||||
mThreads.add(it)
|
||||
it.name = "${mainThread.name} (Spawn-$mSpawnCount)"
|
||||
mSpawnCount++
|
||||
it.start()
|
||||
}
|
||||
}
|
||||
return thread
|
||||
}
|
||||
|
||||
private fun createThread(runnable: Runnable): TimerThread {
|
||||
return object : TimerThread(mRuntime, runnable) {
|
||||
val millis = mRuntime.timers.maxCallbackUptimeMillisForAllThreads
|
||||
return object : TimerThread(mRuntime, millis, runnable) {
|
||||
|
||||
override fun onExit() {
|
||||
synchronized(mThreads) { mThreads.remove(currentThread()) }
|
||||
super.onExit()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fun disposable(): VolatileDispose<*> {
|
||||
return VolatileDispose<Any?>()
|
||||
}
|
||||
fun disposable() = VolatileDispose<Any?>()
|
||||
|
||||
fun atomic(value: Long): AtomicLong {
|
||||
return AtomicLong(value)
|
||||
}
|
||||
fun atomic(value: Long) = AtomicLong(value)
|
||||
|
||||
fun atomic() = AtomicLong()
|
||||
|
||||
fun lock() = ReentrantLock()
|
||||
|
||||
fun shutDownAll() {
|
||||
threadPool.shutdownNow()
|
||||
synchronized(mThreads) {
|
||||
for (thread in mThreads) {
|
||||
thread.interrupt()
|
||||
mThreads.apply {
|
||||
forEach { it.interrupt() }
|
||||
clear()
|
||||
}
|
||||
mThreads.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -112,7 +75,6 @@ class Threads(private val mRuntime: ScriptRuntime) {
|
||||
}
|
||||
}
|
||||
|
||||
fun hasRunningThreads(): Boolean {
|
||||
synchronized(mThreads) { return mThreads.isNotEmpty() }
|
||||
}
|
||||
fun hasRunningThreads(): Boolean = synchronized(mThreads) { return mThreads.isNotEmpty() }
|
||||
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package org.autojs.autojs.runtime.api;
|
||||
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import org.autojs.autojs.concurrent.VolatileBox;
|
||||
import org.autojs.autojs.core.looper.Timer;
|
||||
import org.autojs.autojs.core.looper.TimerThread;
|
||||
import org.autojs.autojs.runtime.ScriptRuntime;
|
||||
@@ -8,24 +11,26 @@ import org.autojs.autojs.runtime.ScriptRuntime;
|
||||
/**
|
||||
* Created by Stardust on 2017/7/21.
|
||||
*/
|
||||
|
||||
public class Timers {
|
||||
|
||||
private static final String LOG_TAG = "Timers";
|
||||
private final VolatileBox<Long> mMaxCallbackUptimeMillisForAllThreads = new VolatileBox<>(0L);
|
||||
private final Threads mThreads;
|
||||
private final Timer mMainTimer;
|
||||
private final Timer mUiTimer;
|
||||
|
||||
//private VolatileBox<Long> mMaxCallbackUptimeMillisForAllThreads = new VolatileBox<>(0L);
|
||||
private Threads mThreads;
|
||||
private Timer mUiTimer;
|
||||
private ScriptRuntime mRuntime;
|
||||
|
||||
public Timers(ScriptRuntime runtime) {
|
||||
mUiTimer = new Timer(runtime, Looper.getMainLooper());
|
||||
mMainTimer = new Timer(runtime, mMaxCallbackUptimeMillisForAllThreads);
|
||||
mUiTimer = new Timer(runtime, mMaxCallbackUptimeMillisForAllThreads, Looper.getMainLooper());
|
||||
mThreads = runtime.threads;
|
||||
mRuntime = runtime;
|
||||
}
|
||||
|
||||
public Timer getMainTimer() {
|
||||
return mRuntime.loopers.getMTimer();
|
||||
return mMainTimer;
|
||||
}
|
||||
|
||||
VolatileBox<Long> getMaxCallbackUptimeMillisForAllThreads() {
|
||||
return mMaxCallbackUptimeMillisForAllThreads;
|
||||
}
|
||||
|
||||
public Timer getTimerForCurrentThread() {
|
||||
@@ -34,27 +39,19 @@ public class Timers {
|
||||
|
||||
public Timer getTimerForThread(Thread thread) {
|
||||
if (thread == mThreads.getMainThread()) {
|
||||
return mRuntime.loopers.getMTimer();
|
||||
return mMainTimer;
|
||||
}
|
||||
Timer timer = TimerThread.getTimerForThread(thread);
|
||||
if (timer == null && Looper.myLooper() == Looper.getMainLooper()) {
|
||||
return mUiTimer;
|
||||
}
|
||||
if (timer == null) {
|
||||
return mRuntime.loopers.getMTimer();
|
||||
} else {
|
||||
return timer;
|
||||
}
|
||||
return timer;
|
||||
}
|
||||
|
||||
public int setTimeout(Object callback, long delay, Object... args) {
|
||||
return getTimerForCurrentThread().setTimeout(callback, delay, args);
|
||||
}
|
||||
|
||||
public int setTimeout(Object callback) {
|
||||
return setTimeout(callback, 1);
|
||||
}
|
||||
|
||||
public boolean clearTimeout(int id) {
|
||||
return getTimerForCurrentThread().clearTimeout(id);
|
||||
}
|
||||
@@ -63,10 +60,6 @@ public class Timers {
|
||||
return getTimerForCurrentThread().setInterval(listener, interval, args);
|
||||
}
|
||||
|
||||
public int setInterval(Object listener) {
|
||||
return setInterval(listener, 1);
|
||||
}
|
||||
|
||||
public boolean clearInterval(int id) {
|
||||
return getTimerForCurrentThread().clearInterval(id);
|
||||
}
|
||||
@@ -79,8 +72,17 @@ public class Timers {
|
||||
return getTimerForCurrentThread().clearImmediate(id);
|
||||
}
|
||||
|
||||
public boolean hasPendingCallbacks() {
|
||||
// 如果是脚本主线程,则检查所有子线程中的定时回调。mFutureCallbackUptimeMillis用来记录所有子线程中定时最久的一个。
|
||||
if (mThreads.getMainThread() == Thread.currentThread()) {
|
||||
return mMaxCallbackUptimeMillisForAllThreads.get() > SystemClock.uptimeMillis();
|
||||
}
|
||||
// 否则检查当前线程的定时回调
|
||||
return getTimerForCurrentThread().hasPendingCallbacks();
|
||||
}
|
||||
|
||||
public void recycle() {
|
||||
mRuntime.loopers.getMTimer().removeAllCallbacks();
|
||||
mMainTimer.removeAllCallbacks();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package org.autojs.autojs.script;
|
||||
|
||||
import static org.autojs.autojs.engine.RhinoJavaScriptEngine.JS_BEAUTIFY_PATH;
|
||||
import static org.autojs.autojs.engine.RhinoJavaScriptEngine.JS_BEAUTIFY_FILE;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
@@ -37,15 +40,12 @@ public class JsBeautifier {
|
||||
private Function mJsBeautifyFunction;
|
||||
private org.mozilla.javascript.Context mScriptContext;
|
||||
private Scriptable mScriptable;
|
||||
private final String mBeautifyJsPath;
|
||||
private final String mBeautifyJsDir;
|
||||
|
||||
private View mView;
|
||||
|
||||
public JsBeautifier(View view, String beautifyJsDirPath) {
|
||||
public JsBeautifier(View view) {
|
||||
mContext = view.getContext();
|
||||
mView = view;
|
||||
mBeautifyJsDir = beautifyJsDirPath;
|
||||
mBeautifyJsPath = PFiles.join(beautifyJsDirPath, "beautify.js");
|
||||
}
|
||||
|
||||
public void beautify(final String code, final Callback callback) {
|
||||
@@ -82,8 +82,11 @@ public class JsBeautifier {
|
||||
importerTopLevel.initStandardObjects(mScriptContext, false);
|
||||
mScriptable = importerTopLevel;
|
||||
}
|
||||
AssetAndUrlModuleSourceProvider provider = new AssetAndUrlModuleSourceProvider(mContext, mBeautifyJsDir,
|
||||
Collections.singletonList(new File("/").toURI()));
|
||||
AssetAndUrlModuleSourceProvider provider = new AssetAndUrlModuleSourceProvider(
|
||||
mContext,
|
||||
JS_BEAUTIFY_PATH,
|
||||
Collections.singletonList(new File(File.separator).toURI())
|
||||
);
|
||||
new RequireBuilder()
|
||||
.setModuleScriptProvider(new SoftCachingModuleScriptProvider(provider))
|
||||
.setSandboxed(false)
|
||||
@@ -111,7 +114,7 @@ public class JsBeautifier {
|
||||
private void compile() {
|
||||
try {
|
||||
enterContext();
|
||||
InputStream is = mContext.getAssets().open(mBeautifyJsPath);
|
||||
InputStream is = mContext.getAssets().open(JS_BEAUTIFY_FILE);
|
||||
mJsBeautifyFunction = (Function) mScriptContext.evaluateString(mScriptable, PFiles.read(is), "<js_beautify>", 1, null);
|
||||
} catch (IOException e) {
|
||||
exitContext();
|
||||
@@ -119,7 +122,7 @@ public class JsBeautifier {
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown(){
|
||||
public void shutdown() {
|
||||
mExecutor.shutdownNow();
|
||||
mView = null;
|
||||
}
|
||||
|
||||
@@ -153,13 +153,11 @@ public class EditorMenu {
|
||||
private void importJavaPackageOrClass() {
|
||||
mEditor.getSelection()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(s ->
|
||||
new ClassSearchDialogBuilder(mContext)
|
||||
.setQuery(s)
|
||||
.itemClick((dialog, item, pos) -> showClassSearchingItem(dialog, item))
|
||||
.title(R.string.text_find_java_classes)
|
||||
.show()
|
||||
);
|
||||
.subscribe(s -> new ClassSearchDialogBuilder(mContext)
|
||||
.setQuery(s)
|
||||
.itemClick((dialog, item, pos) -> showClassSearchingItem(dialog, item))
|
||||
.title(R.string.text_find_java_classes)
|
||||
.show());
|
||||
}
|
||||
|
||||
private void showClassSearchingItem(MaterialDialog dialog, ClassSearchingItem item) {
|
||||
@@ -217,10 +215,12 @@ public class EditorMenu {
|
||||
.title(R.string.text_pinch_to_zoom)
|
||||
.items(R.array.values_editor_pinch_to_zoom_strategy)
|
||||
.itemsCallbackSingleChoice(defSelectedIndex, (dialog, itemView, which, text) -> {
|
||||
String newKey = itemKeys.get(which);
|
||||
if (!Objects.equals(newKey, itemKey)) {
|
||||
Pref.putString(key, newKey);
|
||||
mEditorView.editor.notifyPinchToZoomStrategyChanged(newKey);
|
||||
if (mEditorView.editor != null) {
|
||||
String newKey = itemKeys.get(which);
|
||||
if (!Objects.equals(newKey, itemKey)) {
|
||||
Pref.putString(key, newKey);
|
||||
mEditorView.editor.notifyPinchToZoomStrategyChanged(newKey);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
})
|
||||
@@ -283,6 +283,9 @@ public class EditorMenu {
|
||||
if (itemId == R.id.action_clear) {
|
||||
return tryDoing(() -> mEditor.setText(""));
|
||||
}
|
||||
if (itemId == R.id.action_comment) {
|
||||
return tryDoing(mEditor.commentHelper::handle);
|
||||
}
|
||||
if (itemId == R.id.action_beautify) {
|
||||
return tryDoing(mEditorView::beautifyCode);
|
||||
}
|
||||
@@ -359,12 +362,9 @@ public class EditorMenu {
|
||||
private void findOrReplace() {
|
||||
mEditor.getSelection()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(s ->
|
||||
new FindOrReplaceDialogBuilder(mContext, mEditorView)
|
||||
.setQueryIfNotEmpty(s)
|
||||
.show()
|
||||
);
|
||||
|
||||
.subscribe(s -> new FindOrReplaceDialogBuilder(mContext, mEditorView)
|
||||
.setQueryIfNotEmpty(s)
|
||||
.show());
|
||||
}
|
||||
|
||||
private void copyAll() {
|
||||
|
||||
@@ -198,7 +198,7 @@ open class EditorView : FrameLayout, OnHintClickListener, ClickCallback, Toolbar
|
||||
}
|
||||
}
|
||||
|
||||
fun setRestoredText(text: String?) {
|
||||
fun setRestoredText(text: String) {
|
||||
mRestoredText = text
|
||||
editor!!.text = text
|
||||
}
|
||||
@@ -231,7 +231,7 @@ open class EditorView : FrameLayout, OnHintClickListener, ClickCallback, Toolbar
|
||||
|
||||
private fun setInitialText(text: String) {
|
||||
if (mRestoredText != null) {
|
||||
editor!!.text = mRestoredText
|
||||
editor!!.text = mRestoredText!!
|
||||
mRestoredText = null
|
||||
return
|
||||
}
|
||||
@@ -281,19 +281,32 @@ open class EditorView : FrameLayout, OnHintClickListener, ClickCallback, Toolbar
|
||||
private fun setUpFunctionsKeyboard() {
|
||||
mFunctionsKeyboardHelper = FunctionsKeyboardHelper.with(context as Activity)
|
||||
.setContent(editor)
|
||||
.setFunctionsTrigger(mShowFunctionsButton)
|
||||
|
||||
// @Hint by SuperMonster003 on Jul 20, 2023.
|
||||
// ! Note the order in which setFunctionsView and setFunctionsTrigger are called.
|
||||
// ! zh-CN: 需留意 setFunctionsView 与 setFunctionsTrigger 的先后顺序.
|
||||
.setFunctionsView(mFunctionsKeyboard)
|
||||
.setFunctionsTrigger(mShowFunctionsButton)
|
||||
|
||||
.setEditView(editor!!.codeEditText)
|
||||
.build()
|
||||
//todo:不清楚作用,暂时注释掉
|
||||
|
||||
// @ArchivedTodo by 抠脚本人 on Jul 10, 2023.
|
||||
// ! 不清楚作用, 暂时注释掉.
|
||||
// @Hint by SuperMonster003 on Jul 12, 2023.
|
||||
// ! The click event callback is registered here
|
||||
// ! so that the properties name of the functional keyboard
|
||||
// ! can implement the functionality of the interface:
|
||||
// ! Click: auto-completion, with parentheses, periods, etc. as appropriate.
|
||||
// ! Long-press: display the method, property or module equivalent in a floating window (if exists).
|
||||
// ! zh-CN:
|
||||
// ! 此处的点击事件回调注册是为了使功能键盘智能提示的属性可以实现其接口对应的功能:
|
||||
// ! 点击: 自动补全, 并根据情况添加括号或句点符号等.
|
||||
// ! 长按: 以浮动窗口形式展示 [方法/属性/模块] 对应的文档内容 (如果存在的话).
|
||||
mFunctionsKeyboard!!.setClickCallback(this)
|
||||
|
||||
mShowFunctionsButton!!.setOnLongClickListener {
|
||||
editor!!.beautifyCode()
|
||||
true
|
||||
true.also { editor!!.beautifyCode() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +334,11 @@ open class EditorView : FrameLayout, OnHintClickListener, ClickCallback, Toolbar
|
||||
})
|
||||
editText.textSize = getEditorTextSize(pxToSp(context, editText.textSize).toInt()).toFloat()
|
||||
}
|
||||
editor.addCursorChangeCallback { line: String, cursor: Int -> autoComplete(line, cursor) }
|
||||
editor.addCursorChangeCallback(object : CodeEditor.CursorChangeCallback {
|
||||
override fun onCursorChange(line: String, cursor: Int) {
|
||||
autoComplete(line, cursor)
|
||||
}
|
||||
})
|
||||
editor.layoutDirection = LAYOUT_DIRECTION_LTR
|
||||
}
|
||||
}
|
||||
@@ -507,7 +524,7 @@ open class EditorView : FrameLayout, OnHintClickListener, ClickCallback, Toolbar
|
||||
fun setTextSize(value: Int) {
|
||||
setEditorTextSize(value)
|
||||
editor!!.codeEditText.textSize = value.toFloat()
|
||||
editor!!.setLastTextSize(value)
|
||||
editor!!.lastTextSize = value
|
||||
}
|
||||
|
||||
private fun selectEditorTheme(themes: List<Theme?>) {
|
||||
@@ -529,7 +546,7 @@ open class EditorView : FrameLayout, OnHintClickListener, ClickCallback, Toolbar
|
||||
}
|
||||
|
||||
@Throws(CheckedPatternSyntaxException::class)
|
||||
fun find(keywords: String?, usingRegex: Boolean) {
|
||||
fun find(keywords: String, usingRegex: Boolean) {
|
||||
editor!!.find(keywords, usingRegex)
|
||||
showSearchToolbar(false)
|
||||
}
|
||||
@@ -544,13 +561,13 @@ open class EditorView : FrameLayout, OnHintClickListener, ClickCallback, Toolbar
|
||||
}
|
||||
|
||||
@Throws(CheckedPatternSyntaxException::class)
|
||||
fun replace(keywords: String?, replacement: String?, usingRegex: Boolean) {
|
||||
fun replace(keywords: String, replacement: String, usingRegex: Boolean) {
|
||||
editor!!.replace(keywords, replacement, usingRegex)
|
||||
showSearchToolbar(true)
|
||||
}
|
||||
|
||||
@Throws(CheckedPatternSyntaxException::class)
|
||||
fun replaceAll(keywords: String?, replacement: String?, usingRegex: Boolean) {
|
||||
fun replaceAll(keywords: String, replacement: String, usingRegex: Boolean) {
|
||||
editor!!.replaceAll(keywords, replacement, usingRegex)
|
||||
}
|
||||
|
||||
@@ -584,21 +601,52 @@ open class EditorView : FrameLayout, OnHintClickListener, ClickCallback, Toolbar
|
||||
|
||||
override fun onHintClick(completions: CodeCompletions, pos: Int) {
|
||||
val completion = completions[pos]
|
||||
//todo:增加行注释
|
||||
if (completion.insertText=="/") {
|
||||
editor!!.commentLine()
|
||||
} else editor!!.insert(completion.insertText)
|
||||
|
||||
// @Overruled by SuperMonster003 on Jul 12, 2023.
|
||||
// ! Author: 抠脚本人
|
||||
// ! Related PR:
|
||||
// ! http://pr.autojs6.com/98
|
||||
// ! Reason:
|
||||
// ! In any case, only the simplest input functions should be realized
|
||||
// ! when clicking on the keys of the function keyboard.
|
||||
// ! zh-CN: 在任何情况下, 单击功能键盘的按键时, 均应实现且仅实现最简单的输入功能.
|
||||
// !
|
||||
// @ArchivedTodo by 抠脚本人 on Jul 11, 2023.
|
||||
// ! 增加行注释
|
||||
// if (completion.insertText == "/") {
|
||||
// editor!!.commentLine()
|
||||
// } else editor!!.insert(completion.insertText)
|
||||
|
||||
editor!!.insert(completion.insertText)
|
||||
}
|
||||
|
||||
override fun onHintLongClick(completions: CodeCompletions, pos: Int) {
|
||||
val completion = completions[pos]
|
||||
//todo:增加块注释
|
||||
if (completion.insertText=="/") {
|
||||
editor!!.commentBlock()
|
||||
return
|
||||
|
||||
// @Overruled by SuperMonster003 on Jul 12, 2023.
|
||||
// ! Author: 抠脚本人
|
||||
// ! Related PR:
|
||||
// ! http://pr.autojs6.com/98
|
||||
// ! Reason:
|
||||
// ! Given the confusion caused by combinations of
|
||||
// ! block comments and certain syntactic of RegEx,
|
||||
// ! multi-line comments are also commented with double slashes.
|
||||
// ! zh-CN: 鉴于块注释与正则表达式的某些句法组合造成混淆, 多行注释也采用双斜杠注释方式.
|
||||
// !
|
||||
// val completion = completions[pos]
|
||||
// @ArchivedTodo by 抠脚本人 on Jul 10, 2023.
|
||||
// ! 增加块注释
|
||||
// if (completion.insertText == "/") {
|
||||
// editor!!.commentBlock()
|
||||
// return
|
||||
// }
|
||||
|
||||
completions[pos].let {
|
||||
when {
|
||||
// @Inspired by 抠脚本人 (https://github.com/little-alei) on Jul 13, 2023.
|
||||
it.insertText == "/" -> editor!!.commentHelper.handle()
|
||||
it.url != null -> showManual(it.url, it.hint)
|
||||
}
|
||||
}
|
||||
if (completion.url == null) return
|
||||
showManual(completion.url, completion.hint)
|
||||
}
|
||||
|
||||
private fun showManual(urlSuffix: String, title: String) {
|
||||
@@ -654,6 +702,7 @@ open class EditorView : FrameLayout, OnHintClickListener, ClickCallback, Toolbar
|
||||
|
||||
override fun onRestoreInstanceState(state: Parcelable) {
|
||||
val bundle = state as Bundle
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val superData = bundle.getParcelable<Parcelable>("super_data")
|
||||
scriptExecutionId = bundle.getInt("script_execution_id", ScriptExecution.NO_ID)
|
||||
@@ -662,7 +711,7 @@ open class EditorView : FrameLayout, OnHintClickListener, ClickCallback, Toolbar
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
editor?.destroy()
|
||||
editor!!.destroy()
|
||||
mAutoCompletion?.shutdown()
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,6 @@ class CodeEditText : AppCompatEditText {
|
||||
}
|
||||
|
||||
private fun drawLineHighlights(canvas: Canvas) {
|
||||
val currentLine = currentLine
|
||||
val debugHighlightLine = mDebuggingLine
|
||||
if (debugHighlightLine != currentLine) {
|
||||
// 绘制当前行高亮
|
||||
@@ -165,6 +164,7 @@ class CodeEditText : AppCompatEditText {
|
||||
setPadding(gutterWidth.toInt(), 0, 0, 0)
|
||||
}
|
||||
}
|
||||
|
||||
// 该方法中内联了很多函数来提高效率 但是 这是必要的吗???
|
||||
// 绘制文本着色
|
||||
private fun drawText(canvas: Canvas) {
|
||||
@@ -352,7 +352,7 @@ class CodeEditText : AppCompatEditText {
|
||||
}
|
||||
|
||||
override fun setSelection(index: Int) {
|
||||
super.setSelection(index.coerceAtMost(text!!.length).coerceAtLeast(0))
|
||||
super.setSelection(index.coerceIn(0, text?.length ?: 0))
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(): Parcelable {
|
||||
@@ -402,7 +402,7 @@ class CodeEditText : AppCompatEditText {
|
||||
mTouchValid = false
|
||||
}
|
||||
if (event.action == MotionEvent.ACTION_UP) {
|
||||
//当触摸有效时, 对那一行设置断点或取消断点
|
||||
// 当触摸有效时, 对那一行设置断点或取消断点
|
||||
if (mTouchValid) {
|
||||
if (!removeBreakpoint(mTouchedLine)) {
|
||||
addBreakpoint(mTouchedLine)
|
||||
|
||||
@@ -1,579 +0,0 @@
|
||||
package org.autojs.autojs.ui.edit.editor;
|
||||
|
||||
import static org.autojs.autojs.util.StringUtils.key;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.text.Editable;
|
||||
import android.text.Layout;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ScaleGestureDetector;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import org.autojs.autojs.pref.Pref;
|
||||
import org.autojs.autojs.script.JsBeautifier;
|
||||
import org.autojs.autojs.ui.edit.theme.Theme;
|
||||
import org.autojs.autojs.util.ClipboardUtils;
|
||||
import org.autojs.autojs.util.DisplayUtils;
|
||||
import org.autojs.autojs.util.StringUtils;
|
||||
import org.autojs.autojs.util.ViewUtils;
|
||||
import org.autojs.autojs6.R;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Objects;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.regex.PatternSyntaxException;
|
||||
|
||||
import io.reactivex.Observable;
|
||||
|
||||
/**
|
||||
* Copyright 2018 WHO<980008027@qq.com>
|
||||
* <p>
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
* <p>
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
* <p>
|
||||
* Modified by project: https://github.com/980008027/JsDroidEditor
|
||||
*/
|
||||
public class CodeEditor extends HVScrollView {
|
||||
|
||||
private CodeEditText mCodeEditText;
|
||||
private TextViewUndoRedo mTextViewRedoUndo;
|
||||
private JavaScriptHighlighter mJavaScriptHighlighter;
|
||||
private ScaleGestureDetector mScaleGestureDetector;
|
||||
private ScaleGestureDetector mScaleGestureDetectorForChangeTextSize;
|
||||
private Theme mTheme;
|
||||
private JsBeautifier mJsBeautifier;
|
||||
private MaterialDialog mProcessDialog;
|
||||
|
||||
private CharSequence mReplacement = "";
|
||||
private String mKeywords;
|
||||
private Matcher mMatcher;
|
||||
private int mFoundIndex = -1;
|
||||
|
||||
private double mLastScaleFactor = 1;
|
||||
private int mLastTextSize = 0;
|
||||
|
||||
private int mMinTextSize = 0;
|
||||
private int mMaxTextSize = 0;
|
||||
|
||||
public CodeEditor(Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public CodeEditor(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public void setLastTextSize(int size) {
|
||||
mLastTextSize = size;
|
||||
}
|
||||
|
||||
private void init() {
|
||||
// setFillViewport(true);
|
||||
inflate(getContext(), R.layout.code_editor, this);
|
||||
mCodeEditText = findViewById(R.id.code_edit_text);
|
||||
mCodeEditText.addTextChangedListener(new AutoIndent(mCodeEditText));
|
||||
mLastTextSize = Pref.getEditorTextSize((int) DisplayUtils.pxToSp(getContext(), mCodeEditText.getTextSize()));
|
||||
mMinTextSize = Integer.parseInt(getContext().getString(R.string.text_text_size_min_value));
|
||||
mMaxTextSize = Integer.parseInt(getContext().getString(R.string.text_text_size_max_value));
|
||||
mTextViewRedoUndo = new TextViewUndoRedo(mCodeEditText);
|
||||
mJavaScriptHighlighter = new JavaScriptHighlighter(mTheme, mCodeEditText);
|
||||
mJsBeautifier = new JsBeautifier(this, "js-beautify");
|
||||
mScaleGestureDetectorForChangeTextSize = new ScaleGestureDetector(getContext(), getSimpleOnScaleGestureListener());
|
||||
applyScaleGesture();
|
||||
}
|
||||
|
||||
private void applyScaleGesture() {
|
||||
applyScaleGesture(null);
|
||||
}
|
||||
|
||||
private void applyScaleGesture(String key) {
|
||||
if (key == null) {
|
||||
String defKey = key(R.string.default_key_editor_pinch_to_zoom_strategy);
|
||||
key = Pref.getString(key(R.string.key_editor_pinch_to_zoom_strategy), defKey);
|
||||
}
|
||||
if (Objects.equals(key, key(R.string.key_editor_pinch_to_zoom_change_text_size))) {
|
||||
mScaleGestureDetector = mScaleGestureDetectorForChangeTextSize;
|
||||
} else if (Objects.equals(key, key(R.string.key_editor_pinch_to_zoom_scale_view))) {
|
||||
|
||||
// TODO by SuperMonster003 on Oct 17, 2022.
|
||||
|
||||
} else if (Objects.equals(key, key(R.string.key_editor_pinch_to_zoom_disable))) {
|
||||
mScaleGestureDetector = null;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private ScaleGestureDetector.SimpleOnScaleGestureListener getSimpleOnScaleGestureListener() {
|
||||
return new ScaleGestureDetector.SimpleOnScaleGestureListener() {
|
||||
@Override
|
||||
public boolean onScale(@NonNull ScaleGestureDetector detector) {
|
||||
double currentFactor = Math.floor(detector.getScaleFactor() * 10) / 10;
|
||||
if (currentFactor > 0 && mLastScaleFactor != currentFactor) {
|
||||
int currentTextSize = mLastTextSize + (currentFactor > mLastScaleFactor ? 1 : -1);
|
||||
mLastTextSize = Math.max(mMinTextSize, Math.min(mMaxTextSize, currentTextSize));
|
||||
mCodeEditText.setTextSize(mLastTextSize);
|
||||
|
||||
mLastScaleFactor = currentFactor;
|
||||
}
|
||||
return super.onScale(detector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScaleBegin(@NonNull ScaleGestureDetector detector) {
|
||||
// TODO by SuperMonster003 on Oct 16, 2022.
|
||||
// ! Show a floating text size changing bar.
|
||||
|
||||
return super.onScaleBegin(detector);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScaleEnd(@NonNull ScaleGestureDetector detector) {
|
||||
// TODO by SuperMonster003 on Oct 16, 2022.
|
||||
// ! Dismiss a floating text size changing bar in 2 seconds.
|
||||
|
||||
mLastScaleFactor = 1.0;
|
||||
Pref.setEditorTextSize(mLastTextSize);
|
||||
super.onScaleEnd(detector);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(MotionEvent ev) {
|
||||
if (mScaleGestureDetector == null) {
|
||||
return super.onTouchEvent(ev);
|
||||
}
|
||||
mScaleGestureDetector.onTouchEvent(ev);
|
||||
return !mScaleGestureDetector.isInProgress() && super.onTouchEvent(ev);
|
||||
}
|
||||
|
||||
public Observable<Integer> getLineCount() {
|
||||
return Observable.just(mCodeEditText.getLayout().getLineCount());
|
||||
}
|
||||
|
||||
public void copyLine() {
|
||||
Layout layout = mCodeEditText.getLayout();
|
||||
int line = LayoutHelper.getLineOfChar(layout, mCodeEditText.getSelectionStart());
|
||||
if (line >= 0 && line < layout.getLineCount()) {
|
||||
Editable text = mCodeEditText.getText();
|
||||
CharSequence lineText = null;
|
||||
if (text != null) {
|
||||
lineText = text.subSequence(layout.getLineStart(line), layout.getLineEnd(line));
|
||||
}
|
||||
ClipboardUtils.setClip(getContext(), lineText);
|
||||
ViewUtils.showSnack(this, R.string.text_already_copied_to_clip, false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void deleteLine() {
|
||||
Layout layout = mCodeEditText.getLayout();
|
||||
int line = LayoutHelper.getLineOfChar(layout, mCodeEditText.getSelectionStart());
|
||||
if (line >= 0 && line < layout.getLineCount()) {
|
||||
Editable text = mCodeEditText.getText();
|
||||
if (text != null) {
|
||||
text.replace(layout.getLineStart(line), layout.getLineEnd(line), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void jumpToStart() {
|
||||
mCodeEditText.setSelection(0);
|
||||
}
|
||||
|
||||
public void jumpToEnd() {
|
||||
Editable text = mCodeEditText.getText();
|
||||
if (text != null) {
|
||||
mCodeEditText.setSelection(text.length());
|
||||
}
|
||||
}
|
||||
|
||||
public void jumpToLineStart() {
|
||||
Layout layout = mCodeEditText.getLayout();
|
||||
int line = LayoutHelper.getLineOfChar(layout, mCodeEditText.getSelectionStart());
|
||||
if (line >= 0 && line < layout.getLineCount()) {
|
||||
mCodeEditText.setSelection(layout.getLineStart(line));
|
||||
}
|
||||
}
|
||||
|
||||
public void jumpToLineEnd() {
|
||||
Layout layout = mCodeEditText.getLayout();
|
||||
int line = LayoutHelper.getLineOfChar(layout, mCodeEditText.getSelectionStart());
|
||||
if (line >= 0 && line < layout.getLineCount()) {
|
||||
mCodeEditText.setSelection(layout.getLineEnd(line) - 1);
|
||||
}
|
||||
}
|
||||
|
||||
public void setTheme(Theme theme) {
|
||||
mTheme = theme;
|
||||
setBackgroundColor(mTheme.getBackgroundColor());
|
||||
mJavaScriptHighlighter.setTheme(theme);
|
||||
Editable text = mCodeEditText.getText();
|
||||
if (text != null) {
|
||||
mJavaScriptHighlighter.updateTokens(text.toString());
|
||||
}
|
||||
mCodeEditText.setTheme(mTheme);
|
||||
invalidate();
|
||||
}
|
||||
|
||||
public boolean isTextChanged() {
|
||||
return mTextViewRedoUndo.isTextChanged();
|
||||
}
|
||||
|
||||
public boolean canUndo() {
|
||||
return mTextViewRedoUndo.canUndo();
|
||||
}
|
||||
|
||||
public boolean canRedo() {
|
||||
return mTextViewRedoUndo.canRedo();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onScrollChanged(int l, int t, int oldl, int oldt) {
|
||||
super.onScrollChanged(l, t, oldl, oldt);
|
||||
mCodeEditText.postInvalidate();
|
||||
}
|
||||
|
||||
public CodeEditText getCodeEditText() {
|
||||
return mCodeEditText;
|
||||
}
|
||||
|
||||
public void setInitialText(String text) {
|
||||
mCodeEditText.setText(text);
|
||||
mTextViewRedoUndo.setDefaultText(text);
|
||||
}
|
||||
|
||||
public void jumpTo(int line, int col) {
|
||||
Layout layout = mCodeEditText.getLayout();
|
||||
if (line >= 0 && (layout == null || line < layout.getLineCount())) {
|
||||
mCodeEditText.setSelection(mCodeEditText.getLayout().getLineStart(line) + col);
|
||||
}
|
||||
}
|
||||
|
||||
public void setReadOnly(boolean readOnly) {
|
||||
mCodeEditText.setEnabled(!readOnly);
|
||||
}
|
||||
|
||||
public void setRedoUndoEnabled(boolean enabled) {
|
||||
mTextViewRedoUndo.setEnabled(enabled);
|
||||
}
|
||||
|
||||
public void setProgress(boolean progress) {
|
||||
if (mProcessDialog != null) {
|
||||
mProcessDialog.dismiss();
|
||||
}
|
||||
mProcessDialog = !progress ? null : new MaterialDialog.Builder(getContext())
|
||||
.content(R.string.text_processing)
|
||||
.progress(true, 0)
|
||||
.cancelable(false)
|
||||
.show();
|
||||
}
|
||||
|
||||
public void setText(String text) {
|
||||
mCodeEditText.setText(text);
|
||||
}
|
||||
|
||||
public void addCursorChangeCallback(CursorChangeCallback callback) {
|
||||
mCodeEditText.addCursorChangeCallback(callback);
|
||||
}
|
||||
|
||||
public void removeCursorChangeCallback(CursorChangeCallback callback) {
|
||||
mCodeEditText.removeCursorChangeCallback(callback);
|
||||
}
|
||||
|
||||
public void undo() {
|
||||
mTextViewRedoUndo.undo();
|
||||
}
|
||||
|
||||
public void redo() {
|
||||
mTextViewRedoUndo.redo();
|
||||
}
|
||||
|
||||
public void find(String keywords, boolean usingRegex) throws CheckedPatternSyntaxException {
|
||||
if (usingRegex) {
|
||||
try {
|
||||
Editable text = mCodeEditText.getText();
|
||||
if (text != null) {
|
||||
mMatcher = Pattern.compile(keywords).matcher(text);
|
||||
}
|
||||
} catch (PatternSyntaxException e) {
|
||||
throw new CheckedPatternSyntaxException(e);
|
||||
}
|
||||
mKeywords = null;
|
||||
} else {
|
||||
mKeywords = keywords;
|
||||
mMatcher = null;
|
||||
}
|
||||
findNext();
|
||||
}
|
||||
|
||||
public void replace(String keywords, String replacement, boolean usingRegex) throws CheckedPatternSyntaxException {
|
||||
mReplacement = replacement == null ? "" : replacement;
|
||||
find(keywords, usingRegex);
|
||||
}
|
||||
|
||||
public void replaceAll(String keywords, String replacement, boolean usingRegex) throws CheckedPatternSyntaxException {
|
||||
if (!usingRegex) {
|
||||
keywords = Pattern.quote(keywords);
|
||||
}
|
||||
Editable codeEditTextText = mCodeEditText.getText();
|
||||
String text = null;
|
||||
if (codeEditTextText != null) {
|
||||
text = codeEditTextText.toString();
|
||||
}
|
||||
try {
|
||||
if (text != null) {
|
||||
text = text.replaceAll(keywords, replacement);
|
||||
}
|
||||
} catch (PatternSyntaxException e) {
|
||||
throw new CheckedPatternSyntaxException(e);
|
||||
}
|
||||
setText(text);
|
||||
}
|
||||
|
||||
public void findNext() {
|
||||
int foundIndex;
|
||||
if (mMatcher == null) {
|
||||
if (mKeywords == null) {
|
||||
return;
|
||||
}
|
||||
Editable text = mCodeEditText.getText();
|
||||
if (text != null) {
|
||||
foundIndex = StringUtils.indexOf(text, mKeywords, mFoundIndex + 1);
|
||||
} else {
|
||||
foundIndex = -1;
|
||||
}
|
||||
if (foundIndex >= 0)
|
||||
mCodeEditText.setSelection(foundIndex, foundIndex + mKeywords.length());
|
||||
} else if (mMatcher.find(mFoundIndex + 1)) {
|
||||
foundIndex = mMatcher.start();
|
||||
mCodeEditText.setSelection(foundIndex, foundIndex + mMatcher.group().length());
|
||||
} else {
|
||||
foundIndex = -1;
|
||||
}
|
||||
if (foundIndex < 0 && mFoundIndex >= 0) {
|
||||
mFoundIndex = -1;
|
||||
findNext();
|
||||
} else {
|
||||
mFoundIndex = foundIndex;
|
||||
}
|
||||
}
|
||||
|
||||
public void findPrev() {
|
||||
if (mMatcher != null) {
|
||||
ViewUtils.showToast(getContext(), R.string.error_regex_find_prev, true);
|
||||
return;
|
||||
}
|
||||
int len = mCodeEditText.getText().length();
|
||||
if (mFoundIndex <= 0) {
|
||||
mFoundIndex = len;
|
||||
}
|
||||
int index = mCodeEditText.getText().toString().lastIndexOf(mKeywords, mFoundIndex - 1);
|
||||
if (index < 0) {
|
||||
if (mFoundIndex != len) {
|
||||
mFoundIndex = len;
|
||||
findPrev();
|
||||
}
|
||||
return;
|
||||
}
|
||||
mFoundIndex = index;
|
||||
mCodeEditText.setSelection(index, index + mKeywords.length());
|
||||
}
|
||||
|
||||
public void replaceSelection() {
|
||||
mCodeEditText.getText().replace(mCodeEditText.getSelectionStart(), mCodeEditText.getSelectionEnd(), mReplacement);
|
||||
}
|
||||
|
||||
public void beautifyCode() {
|
||||
setProgress(true);
|
||||
int pos = mCodeEditText.getSelectionStart();
|
||||
mJsBeautifier.beautify(mCodeEditText.getText().toString(), new JsBeautifier.Callback() {
|
||||
@Override
|
||||
public void onSuccess(String beautifiedCode) {
|
||||
setProgress(false);
|
||||
mCodeEditText.setText(beautifiedCode);
|
||||
// 格式化后恢复光标位置
|
||||
mCodeEditText.setSelection(pos);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onException(Exception e) {
|
||||
setProgress(false);
|
||||
e.printStackTrace();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void commentLine() {
|
||||
//如果没有选中,则添加文本/,否则选中的行前加//
|
||||
String selectionText = getSelectionRaw();
|
||||
if (selectionText.equals("")) {
|
||||
insert("/");
|
||||
} else {
|
||||
String[] lines = selectionText.split("\\n");
|
||||
StringBuilder commentedText = new StringBuilder();
|
||||
//处理取消注释
|
||||
if (lines[0].startsWith("//")) {
|
||||
for (String line : lines) {
|
||||
commentedText.append(line.substring(2)).append("\n");
|
||||
}
|
||||
} else {
|
||||
for (String line : lines) {
|
||||
commentedText.append("//").append(line).append("\n");
|
||||
}
|
||||
}
|
||||
mReplacement = commentedText.toString().replaceAll("\\n$", "");
|
||||
replaceSelection();
|
||||
}
|
||||
}
|
||||
|
||||
public void commentBlock() {
|
||||
String selectionText = getSelectionRaw();
|
||||
if (!selectionText.isEmpty()) {
|
||||
String regex = "/\\*([^*]|\\*+[^*/])*\\*/";
|
||||
if (selectionText.matches(regex)) {
|
||||
// 取消块注释
|
||||
mReplacement = selectionText.substring(2, selectionText.length() - 2);
|
||||
} else {
|
||||
// 增加块注释
|
||||
mReplacement = "/*" + selectionText + "*/";
|
||||
}
|
||||
replaceSelection();
|
||||
}
|
||||
}
|
||||
|
||||
public void insert(String insertText) {
|
||||
int selection = Math.max(mCodeEditText.getSelectionStart(), 0);
|
||||
mCodeEditText.getText().insert(selection, insertText);
|
||||
}
|
||||
|
||||
public void insert(int line, String insertText) {
|
||||
int selection = mCodeEditText.getLayout().getLineStart(line);
|
||||
mCodeEditText.getText().insert(selection, insertText);
|
||||
}
|
||||
|
||||
public void moveCursor(int dCh) {
|
||||
mCodeEditText.setSelection(mCodeEditText.getSelectionStart() + dCh);
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return mCodeEditText.getText().toString();
|
||||
}
|
||||
|
||||
public String getSelectionRaw() {
|
||||
int s = mCodeEditText.getSelectionStart();
|
||||
int e = mCodeEditText.getSelectionEnd();
|
||||
if (s == e) {
|
||||
return "";
|
||||
}
|
||||
return mCodeEditText.getText().toString().substring(s, e);
|
||||
}
|
||||
|
||||
public Observable<String> getSelection() {
|
||||
return Observable.just(getSelectionRaw());
|
||||
}
|
||||
|
||||
|
||||
public void markTextAsSaved() {
|
||||
mTextViewRedoUndo.markTextAsUnchanged();
|
||||
}
|
||||
|
||||
public LinkedHashMap<Integer, Breakpoint> getBreakpoints() {
|
||||
return mCodeEditText.getBreakpoints();
|
||||
}
|
||||
|
||||
public void setDebuggingLine(int line) {
|
||||
mCodeEditText.setDebuggingLine(line);
|
||||
}
|
||||
|
||||
public void setBreakpointChangeListener(BreakpointChangeListener listener) {
|
||||
mCodeEditText.setBreakpointChangeListener(listener);
|
||||
}
|
||||
|
||||
public void addOrRemoveBreakpoint(int line) {
|
||||
if (!mCodeEditText.removeBreakpoint(line)) {
|
||||
mCodeEditText.addBreakpoint(line);
|
||||
}
|
||||
}
|
||||
|
||||
public void addOrRemoveBreakpointAtCurrentLine() {
|
||||
Layout layout = mCodeEditText.getLayout();
|
||||
int line = LayoutHelper.getLineOfChar(layout, mCodeEditText.getSelectionStart());
|
||||
if (line >= 0 && line < layout.getLineCount()) {
|
||||
addOrRemoveBreakpoint(line);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeAllBreakpoints() {
|
||||
mCodeEditText.removeAllBreakpoints();
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
mJavaScriptHighlighter.shutdown();
|
||||
mJsBeautifier.shutdown();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
int codeWidth = getWidth() - getPaddingLeft() - getPaddingRight();
|
||||
int codeHeight = getHeight() - getPaddingTop() - getPaddingBottom();
|
||||
if (mCodeEditText.getMinWidth() != codeWidth || mCodeEditText.getMinWidth() != codeWidth) {
|
||||
mCodeEditText.setMinWidth(codeWidth);
|
||||
mCodeEditText.setMinHeight(codeHeight);
|
||||
invalidate();
|
||||
}
|
||||
super.onDraw(canvas);
|
||||
}
|
||||
|
||||
public static class Breakpoint {
|
||||
|
||||
public int line;
|
||||
public boolean enabled = true;
|
||||
|
||||
public Breakpoint(int line) {
|
||||
this.line = line;
|
||||
}
|
||||
}
|
||||
|
||||
public interface BreakpointChangeListener {
|
||||
void onBreakpointChange(int line, boolean enabled);
|
||||
|
||||
void onAllBreakpointRemoved(int count);
|
||||
}
|
||||
|
||||
public void notifyPinchToZoomStrategyChanged(String newKey) {
|
||||
applyScaleGesture(newKey);
|
||||
}
|
||||
|
||||
public static class CheckedPatternSyntaxException extends Exception {
|
||||
|
||||
public CheckedPatternSyntaxException(PatternSyntaxException cause) {
|
||||
super(cause);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public interface CursorChangeCallback {
|
||||
|
||||
void onCursorChange(String line, int ch);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
634
app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.kt
Normal file
634
app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.kt
Normal file
@@ -0,0 +1,634 @@
|
||||
package org.autojs.autojs.ui.edit.editor
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.ScaleGestureDetector
|
||||
import android.view.ScaleGestureDetector.SimpleOnScaleGestureListener
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import io.reactivex.Observable
|
||||
import org.autojs.autojs.pref.Pref.getEditorTextSize
|
||||
import org.autojs.autojs.pref.Pref.getString
|
||||
import org.autojs.autojs.pref.Pref.setEditorTextSize
|
||||
import org.autojs.autojs.script.JsBeautifier
|
||||
import org.autojs.autojs.ui.edit.theme.Theme
|
||||
import org.autojs.autojs.util.ClipboardUtils.setClip
|
||||
import org.autojs.autojs.util.DisplayUtils.pxToSp
|
||||
import org.autojs.autojs.util.StringUtils.indexOf
|
||||
import org.autojs.autojs.util.StringUtils.key
|
||||
import org.autojs.autojs.util.ViewUtils.showSnack
|
||||
import org.autojs.autojs.util.ViewUtils.showToast
|
||||
import org.autojs.autojs6.R
|
||||
import org.autojs.autojs6.databinding.CodeEditorBinding
|
||||
import java.util.regex.Matcher
|
||||
import java.util.regex.Pattern
|
||||
import java.util.regex.PatternSyntaxException
|
||||
import kotlin.math.floor
|
||||
|
||||
/**
|
||||
* Copyright 2018 WHO<980008027@qq.com>
|
||||
*
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
*
|
||||
* Modified by project: https://github.com/980008027/JsDroidEditor
|
||||
*/
|
||||
/**
|
||||
* Modified by SuperMonster003 as of Jul 16, 2023.
|
||||
* Transformed by SuperMonster003 on Jul 16, 2023.
|
||||
*/
|
||||
class CodeEditor : HVScrollView {
|
||||
|
||||
val binding = CodeEditorBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
|
||||
val codeEditText: CodeEditText = binding.codeEditText.also {
|
||||
it.addTextChangedListener(AutoIndent(it))
|
||||
lastTextSize = getEditorTextSize(pxToSp(it.context, it.textSize).toInt())
|
||||
}
|
||||
|
||||
val lineCount
|
||||
get() = Observable.just(codeEditText.layout.lineCount)
|
||||
|
||||
val isTextChanged
|
||||
get() = mTextViewRedoUndo.isTextChanged
|
||||
|
||||
val selection: Observable<String>
|
||||
get() = Observable.just(selectionText)
|
||||
|
||||
val breakpoints: LinkedHashMap<Int, Breakpoint>
|
||||
get() = codeEditText.breakpoints
|
||||
|
||||
var lastTextSize = 0
|
||||
|
||||
var text: String
|
||||
get() = codeEditText.text?.toString() ?: ""
|
||||
set(text) {
|
||||
codeEditText.setText(text)
|
||||
}
|
||||
|
||||
@JvmField
|
||||
val commentHelper = CommentHelper()
|
||||
|
||||
private val simpleOnScaleGestureListener
|
||||
get() = object : SimpleOnScaleGestureListener() {
|
||||
override fun onScale(detector: ScaleGestureDetector): Boolean {
|
||||
val currentFactor = floor((detector.scaleFactor * 10).toDouble()) / 10
|
||||
if (currentFactor > 0 && mLastScaleFactor != currentFactor) {
|
||||
val currentTextSize = lastTextSize + if (currentFactor > mLastScaleFactor) 1 else -1
|
||||
lastTextSize = currentTextSize.coerceIn(mMinTextSize, mMaxTextSize).also {
|
||||
codeEditText.textSize = it.toFloat()
|
||||
}
|
||||
mLastScaleFactor = currentFactor
|
||||
}
|
||||
return super.onScale(detector)
|
||||
}
|
||||
|
||||
// TODO by SuperMonster003 on Oct 16, 2022.
|
||||
// ! Show a floating text size changing bar.
|
||||
|
||||
// override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
|
||||
// return super.onScaleBegin(detector)
|
||||
// }
|
||||
|
||||
override fun onScaleEnd(detector: ScaleGestureDetector) {
|
||||
// TODO by SuperMonster003 on Oct 16, 2022.
|
||||
// ! Dismiss a floating text size changing bar in 2 seconds.
|
||||
mLastScaleFactor = 1.0
|
||||
setEditorTextSize(lastTextSize)
|
||||
super.onScaleEnd(detector)
|
||||
}
|
||||
}
|
||||
|
||||
private val selectionText: String
|
||||
get() {
|
||||
val s = codeEditText.selectionStart
|
||||
val e = codeEditText.selectionEnd
|
||||
return if (s == e) "" else codeEditText.text?.substring(s, e) ?: ""
|
||||
}
|
||||
|
||||
private var mMinTextSize = context.getString(R.string.text_text_size_min_value).toInt()
|
||||
private var mMaxTextSize = context.getString(R.string.text_text_size_max_value).toInt()
|
||||
private var mTextViewRedoUndo = TextViewUndoRedo(codeEditText)
|
||||
|
||||
private var mTheme: Theme? = null
|
||||
private var mJavaScriptHighlighter = JavaScriptHighlighter(mTheme, codeEditText)
|
||||
|
||||
private var mJsBeautifier = JsBeautifier(this)
|
||||
private var mScaleGestureDetectorForChangeTextSize = ScaleGestureDetector(context, simpleOnScaleGestureListener)
|
||||
private var mScaleGestureDetector: ScaleGestureDetector? = null
|
||||
private var mProcessDialog: MaterialDialog? = null
|
||||
private var mReplacement: CharSequence = ""
|
||||
private var mKeywords: String? = null
|
||||
private var mMatcher: Matcher? = null
|
||||
private var mFoundIndex = -1
|
||||
private var mLastScaleFactor = 1.0
|
||||
|
||||
constructor(context: Context?) : super(context)
|
||||
|
||||
constructor(context: Context?, attrs: AttributeSet?) : super(context, attrs)
|
||||
|
||||
init {
|
||||
applyScaleGesture()
|
||||
}
|
||||
|
||||
private fun applyScaleGesture(key: String? = null) {
|
||||
var niceKey = key
|
||||
if (niceKey == null) {
|
||||
val defKey = key(R.string.default_key_editor_pinch_to_zoom_strategy)
|
||||
niceKey = getString(key(R.string.key_editor_pinch_to_zoom_strategy), defKey)
|
||||
}
|
||||
when (niceKey) {
|
||||
key(R.string.key_editor_pinch_to_zoom_change_text_size) -> {
|
||||
mScaleGestureDetector = mScaleGestureDetectorForChangeTextSize
|
||||
}
|
||||
key(R.string.key_editor_pinch_to_zoom_scale_view) -> {
|
||||
// TODO by SuperMonster003 on Oct 17, 2022.
|
||||
}
|
||||
key(R.string.key_editor_pinch_to_zoom_disable) -> {
|
||||
mScaleGestureDetector = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouchEvent(ev: MotionEvent): Boolean {
|
||||
return mScaleGestureDetector?.let {
|
||||
it.onTouchEvent(ev)
|
||||
!it.isInProgress && super.onTouchEvent(ev)
|
||||
} ?: super.onTouchEvent(ev)
|
||||
}
|
||||
|
||||
override fun onScrollChanged(l: Int, t: Int, oldl: Int, oldt: Int) {
|
||||
super.onScrollChanged(l, t, oldl, oldt)
|
||||
codeEditText.postInvalidate()
|
||||
}
|
||||
|
||||
fun copyLine() {
|
||||
val layout = codeEditText.layout
|
||||
val line = LayoutHelper.getLineOfChar(layout, codeEditText.selectionStart)
|
||||
if (line >= 0 && line < layout.lineCount) {
|
||||
val text = codeEditText.text
|
||||
var lineText: CharSequence? = null
|
||||
if (text != null) {
|
||||
lineText = text.subSequence(layout.getLineStart(line), layout.getLineEnd(line))
|
||||
}
|
||||
setClip(context, lineText)
|
||||
showSnack(this, R.string.text_already_copied_to_clip, false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getCoveredLinesText(): CharSequence {
|
||||
val layout = codeEditText.layout
|
||||
val lineStart = LayoutHelper.getLineOfChar(layout, codeEditText.selectionStart)
|
||||
val lineEnd = LayoutHelper.getLineOfChar(layout, codeEditText.selectionEnd)
|
||||
if (lineStart >= 0 && lineStart < layout.lineCount) {
|
||||
if (lineEnd >= 0 && lineEnd < layout.lineCount) {
|
||||
return text.subSequence(layout.getLineStart(lineStart), layout.getLineEnd(lineEnd))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
private fun replaceSelectedLines(feature: Regex, transform: (MatchResult) -> CharSequence) {
|
||||
val text = codeEditText.text ?: return
|
||||
val layout = codeEditText.layout
|
||||
val lineStart = LayoutHelper.getLineOfChar(layout, codeEditText.selectionStart)
|
||||
val lineEnd = LayoutHelper.getLineOfChar(layout, codeEditText.selectionEnd)
|
||||
val newText = getCoveredLinesText().split("\n").joinToString("\n") { it.replace(feature, transform) }
|
||||
text.replace(layout.getLineStart(lineStart), layout.getLineEnd(lineEnd), newText)
|
||||
}
|
||||
|
||||
fun deleteLine() {
|
||||
val text = codeEditText.text ?: return
|
||||
val layout = codeEditText.layout
|
||||
val line = LayoutHelper.getLineOfChar(layout, codeEditText.selectionStart)
|
||||
if (line >= 0 && line < layout.lineCount) {
|
||||
text.replace(layout.getLineStart(line), layout.getLineEnd(line), "")
|
||||
}
|
||||
}
|
||||
|
||||
fun jumpToStart() {
|
||||
codeEditText.setSelection(0)
|
||||
}
|
||||
|
||||
fun jumpToEnd() {
|
||||
val text = codeEditText.text
|
||||
if (text != null) {
|
||||
codeEditText.setSelection(text.length)
|
||||
}
|
||||
}
|
||||
|
||||
fun jumpToLineStart() {
|
||||
val layout = codeEditText.layout
|
||||
val line = LayoutHelper.getLineOfChar(layout, codeEditText.selectionStart)
|
||||
if (line >= 0 && line < layout.lineCount) {
|
||||
codeEditText.setSelection(layout.getLineStart(line))
|
||||
}
|
||||
}
|
||||
|
||||
fun jumpToLineEnd() {
|
||||
val layout = codeEditText.layout
|
||||
val line = LayoutHelper.getLineOfChar(layout, codeEditText.selectionStart)
|
||||
if (line >= 0 && line < layout.lineCount) {
|
||||
codeEditText.setSelection(layout.getLineEnd(line) - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun setTheme(theme: Theme?) {
|
||||
mTheme = theme
|
||||
setBackgroundColor(mTheme!!.backgroundColor)
|
||||
mJavaScriptHighlighter.setTheme(theme)
|
||||
val text = codeEditText.text
|
||||
if (text != null) {
|
||||
mJavaScriptHighlighter.updateTokens(text.toString())
|
||||
}
|
||||
codeEditText.setTheme(mTheme!!)
|
||||
invalidate()
|
||||
}
|
||||
|
||||
fun canUndo(): Boolean {
|
||||
return mTextViewRedoUndo.canUndo()
|
||||
}
|
||||
|
||||
fun canRedo(): Boolean {
|
||||
return mTextViewRedoUndo.canRedo()
|
||||
}
|
||||
|
||||
fun setInitialText(text: String?) {
|
||||
codeEditText.setText(text)
|
||||
mTextViewRedoUndo.setDefaultText(text)
|
||||
}
|
||||
|
||||
fun jumpTo(line: Int, col: Int) {
|
||||
val layout = codeEditText.layout
|
||||
if (line >= 0 && (layout == null || line < layout.lineCount)) {
|
||||
codeEditText.setSelection(codeEditText.layout.getLineStart(line) + col)
|
||||
}
|
||||
}
|
||||
|
||||
fun setReadOnly(readOnly: Boolean) {
|
||||
codeEditText.isEnabled = !readOnly
|
||||
}
|
||||
|
||||
fun setRedoUndoEnabled(enabled: Boolean) {
|
||||
mTextViewRedoUndo.isEnabled = enabled
|
||||
}
|
||||
|
||||
fun setProgress(progress: Boolean) {
|
||||
if (mProcessDialog != null) {
|
||||
mProcessDialog!!.dismiss()
|
||||
}
|
||||
mProcessDialog = if (!progress) null else MaterialDialog.Builder(context)
|
||||
.content(R.string.text_processing)
|
||||
.progress(true, 0)
|
||||
.cancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
fun addCursorChangeCallback(callback: CursorChangeCallback?) {
|
||||
codeEditText.addCursorChangeCallback(callback!!)
|
||||
}
|
||||
|
||||
fun removeCursorChangeCallback(callback: CursorChangeCallback?) {
|
||||
codeEditText.removeCursorChangeCallback(callback!!)
|
||||
}
|
||||
|
||||
fun undo() {
|
||||
mTextViewRedoUndo.undo()
|
||||
}
|
||||
|
||||
fun redo() {
|
||||
mTextViewRedoUndo.redo()
|
||||
}
|
||||
|
||||
@Throws(CheckedPatternSyntaxException::class)
|
||||
fun find(keywords: String, usingRegex: Boolean) {
|
||||
if (usingRegex) {
|
||||
try {
|
||||
val text = codeEditText.text
|
||||
if (text != null) {
|
||||
mMatcher = Pattern.compile(keywords).matcher(text)
|
||||
}
|
||||
} catch (e: PatternSyntaxException) {
|
||||
throw CheckedPatternSyntaxException(e)
|
||||
}
|
||||
mKeywords = null
|
||||
} else {
|
||||
mKeywords = keywords
|
||||
mMatcher = null
|
||||
}
|
||||
findNext()
|
||||
}
|
||||
|
||||
@Throws(CheckedPatternSyntaxException::class)
|
||||
fun replace(keywords: String, replacement: String, usingRegex: Boolean) {
|
||||
mReplacement = replacement
|
||||
find(keywords, usingRegex)
|
||||
}
|
||||
|
||||
@Throws(CheckedPatternSyntaxException::class)
|
||||
fun replaceAll(keywords: String, replacement: String?, usingRegex: Boolean) {
|
||||
var niceKeywords = keywords
|
||||
if (!usingRegex) {
|
||||
niceKeywords = Pattern.quote(niceKeywords)
|
||||
}
|
||||
val codeEditTextText = codeEditText.text
|
||||
var text: String? = null
|
||||
if (codeEditTextText != null) {
|
||||
text = codeEditTextText.toString()
|
||||
}
|
||||
try {
|
||||
if (text != null) {
|
||||
text = text.replace(niceKeywords.toRegex(), replacement!!)
|
||||
}
|
||||
} catch (e: PatternSyntaxException) {
|
||||
throw CheckedPatternSyntaxException(e)
|
||||
}
|
||||
if (text != null) {
|
||||
this.text = text
|
||||
}
|
||||
}
|
||||
|
||||
fun findNext() {
|
||||
val foundIndex: Int
|
||||
if (mMatcher == null) {
|
||||
if (mKeywords == null) {
|
||||
return
|
||||
}
|
||||
val text = codeEditText.text
|
||||
foundIndex = if (text != null) {
|
||||
indexOf(text, mKeywords!!, mFoundIndex + 1)
|
||||
} else {
|
||||
-1
|
||||
}
|
||||
if (foundIndex >= 0) codeEditText.setSelection(foundIndex, foundIndex + mKeywords!!.length)
|
||||
} else if (mMatcher!!.find(mFoundIndex + 1)) {
|
||||
foundIndex = mMatcher!!.start()
|
||||
codeEditText.setSelection(foundIndex, foundIndex + mMatcher!!.group().length)
|
||||
} else {
|
||||
foundIndex = -1
|
||||
}
|
||||
if (foundIndex < 0 && mFoundIndex >= 0) {
|
||||
mFoundIndex = -1
|
||||
findNext()
|
||||
} else {
|
||||
mFoundIndex = foundIndex
|
||||
}
|
||||
}
|
||||
|
||||
fun findPrev() {
|
||||
if (mMatcher != null) {
|
||||
showToast(context, R.string.error_regex_find_prev, true)
|
||||
return
|
||||
}
|
||||
val len = codeEditText.text!!.length
|
||||
if (mFoundIndex <= 0) {
|
||||
mFoundIndex = len
|
||||
}
|
||||
val index = codeEditText.text.toString().lastIndexOf(mKeywords!!, mFoundIndex - 1)
|
||||
if (index < 0) {
|
||||
if (mFoundIndex != len) {
|
||||
mFoundIndex = len
|
||||
findPrev()
|
||||
}
|
||||
return
|
||||
}
|
||||
mFoundIndex = index
|
||||
codeEditText.setSelection(index, index + mKeywords!!.length)
|
||||
}
|
||||
|
||||
fun replaceSelection() {
|
||||
codeEditText.text?.replace(codeEditText.selectionStart, codeEditText.selectionEnd, mReplacement)
|
||||
}
|
||||
|
||||
fun beautifyCode() {
|
||||
setProgress(true)
|
||||
val pos = codeEditText.selectionStart
|
||||
mJsBeautifier.beautify(codeEditText.text.toString(), object : JsBeautifier.Callback {
|
||||
override fun onSuccess(beautifiedCode: String) {
|
||||
codeEditText.setText(beautifiedCode)
|
||||
// @Hint by 抠脚本人 on Jul 11, 2023.
|
||||
// ! 格式化后恢复光标位置
|
||||
codeEditText.setSelection(pos)
|
||||
setProgress(false)
|
||||
showToast(context, R.string.text_formatting_completed)
|
||||
}
|
||||
|
||||
override fun onException(e: Exception) {
|
||||
setProgress(false)
|
||||
showToast(context, R.string.text_failed_to_format, true)
|
||||
e.printStackTrace()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun insert(insertText: String?) {
|
||||
val selection = codeEditText.selectionStart.coerceAtLeast(0)
|
||||
codeEditText.text!!.insert(selection, insertText)
|
||||
}
|
||||
|
||||
fun insert(line: Int, insertText: String?) {
|
||||
val selection = codeEditText.layout.getLineStart(line)
|
||||
codeEditText.text!!.insert(selection, insertText)
|
||||
}
|
||||
|
||||
fun moveCursor(dCh: Int) {
|
||||
codeEditText.setSelection(codeEditText.selectionStart + dCh)
|
||||
}
|
||||
|
||||
fun markTextAsSaved() {
|
||||
mTextViewRedoUndo.markTextAsUnchanged()
|
||||
}
|
||||
|
||||
fun setDebuggingLine(line: Int) {
|
||||
codeEditText.debuggingLine = line
|
||||
}
|
||||
|
||||
fun setBreakpointChangeListener(listener: BreakpointChangeListener?) {
|
||||
codeEditText.breakpointChangeListener = listener
|
||||
}
|
||||
|
||||
private fun addOrRemoveBreakpoint(line: Int) {
|
||||
if (!codeEditText.removeBreakpoint(line)) {
|
||||
codeEditText.addBreakpoint(line)
|
||||
}
|
||||
}
|
||||
|
||||
fun addOrRemoveBreakpointAtCurrentLine() {
|
||||
val layout = codeEditText.layout
|
||||
val line = LayoutHelper.getLineOfChar(layout, codeEditText.selectionStart)
|
||||
if (line >= 0 && line < layout.lineCount) {
|
||||
addOrRemoveBreakpoint(line)
|
||||
}
|
||||
}
|
||||
|
||||
fun removeAllBreakpoints() {
|
||||
codeEditText.removeAllBreakpoints()
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
mJavaScriptHighlighter.shutdown()
|
||||
mJsBeautifier.shutdown()
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
val codeWidth = width - paddingLeft - paddingRight
|
||||
val codeHeight = height - paddingTop - paddingBottom
|
||||
if (codeEditText.minWidth != codeWidth || codeEditText.minWidth != codeWidth) {
|
||||
codeEditText.minWidth = codeWidth
|
||||
codeEditText.minHeight = codeHeight
|
||||
invalidate()
|
||||
}
|
||||
super.onDraw(canvas)
|
||||
}
|
||||
|
||||
fun notifyPinchToZoomStrategyChanged(newKey: String?) {
|
||||
applyScaleGesture(newKey)
|
||||
}
|
||||
|
||||
class Breakpoint(@JvmField var line: Int) {
|
||||
@JvmField
|
||||
var enabled = true
|
||||
}
|
||||
|
||||
class CheckedPatternSyntaxException(cause: PatternSyntaxException?) : Exception(cause)
|
||||
|
||||
interface BreakpointChangeListener {
|
||||
fun onBreakpointChange(line: Int, enabled: Boolean)
|
||||
fun onAllBreakpointRemoved(count: Int)
|
||||
}
|
||||
|
||||
interface CursorChangeCallback {
|
||||
fun onCursorChange(line: String, cursor: Int)
|
||||
}
|
||||
|
||||
inner class CommentHelper : CodeEditorCommentHelper {
|
||||
|
||||
private val prefix = "//"
|
||||
|
||||
override fun handle() = toggle()
|
||||
|
||||
override fun comment() {
|
||||
var selectionEnd = codeEditText.selectionEnd
|
||||
val insetPosition = getProperWhitespaceAmount()
|
||||
var hasEverMatched = false
|
||||
|
||||
@Suppress("RegExpSimplifiable")
|
||||
replaceSelectedLines(Regex("^(\\s{$insetPosition})(\\s*\\S+)")) { matchResult ->
|
||||
"$prefix\u0020"
|
||||
.also { selectionEnd += it.length }
|
||||
.also { hasEverMatched = true }
|
||||
.let { "${matchResult.groupValues[1]}$it${matchResult.groupValues[2]}" }
|
||||
}
|
||||
|
||||
@Suppress("ControlFlowWithEmptyBody")
|
||||
if (!hasEverMatched) {
|
||||
// FIXME by SuperMonster003 on Jul 20, 2023.
|
||||
// ! Behaves abnormally for empty line(s).
|
||||
// replaceSelectedLines(Regex(".*")) { matchResult ->
|
||||
// prefix
|
||||
// .also { selectionEnd += it.length }
|
||||
// .let { "$it${matchResult.value}" }
|
||||
// }
|
||||
}
|
||||
|
||||
codeEditText.setSelection(selectionEnd)
|
||||
}
|
||||
|
||||
override fun uncomment() {
|
||||
var selectionEnd = codeEditText.selectionEnd
|
||||
|
||||
replaceSelectedLines(Regex("(\\s*)($prefix\\s?)(.*)")) { matchResult ->
|
||||
selectionEnd -= matchResult.groupValues[2].length
|
||||
"${matchResult.groupValues[1]}${matchResult.groupValues[3]}"
|
||||
}
|
||||
|
||||
codeEditText.setSelection(selectionEnd)
|
||||
}
|
||||
|
||||
override fun isCommented(): Boolean {
|
||||
var atLeastOneWithPrefix = false
|
||||
val allMatched = getCoveredLinesText().split("\n").all {
|
||||
it.matches(Regex("\\s*")) || it
|
||||
.contains(Regex("^\\s*$prefix"))
|
||||
.also { atLeastOneWithPrefix = true }
|
||||
}
|
||||
return allMatched and atLeastOneWithPrefix
|
||||
}
|
||||
|
||||
private fun getProperWhitespaceAmount(): Int {
|
||||
var result = Int.MAX_VALUE
|
||||
val threshold = 0
|
||||
getCoveredLinesText().split("\n").forEach {
|
||||
Regex("^\\s*(?=\\S+)").find(it)?.let { matchResult ->
|
||||
val len = matchResult.value.length
|
||||
if (len < result) result = len
|
||||
if (result == threshold) return threshold
|
||||
}
|
||||
}
|
||||
return if (result == Int.MAX_VALUE) threshold else result
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// @Archived by SuperMonster003 on Jul 12, 2023.
|
||||
// ! Author: 抠脚本人
|
||||
// ! Reason: Replaced with CommentHelper class.
|
||||
// fun commentLine() {
|
||||
// // 如果没有选中,则添加文本/,否则选中的行前加//
|
||||
// val selectionText: String = getSelectionRaw()
|
||||
// if (selectionText == "") {
|
||||
// insert("/")
|
||||
// } else {
|
||||
// val lines = selectionText.split("\\n".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
// val commentedText = StringBuilder()
|
||||
// // 处理取消注释
|
||||
// if (lines[0].startsWith("//")) {
|
||||
// for (line in lines) {
|
||||
// commentedText.append(line.substring(2)).append("\n")
|
||||
// }
|
||||
// } else {
|
||||
// for (line in lines) {
|
||||
// commentedText.append("//").append(line).append("\n")
|
||||
// }
|
||||
// }
|
||||
// mReplacement = commentedText.toString().replace("\\n$".toRegex(), "")
|
||||
// replaceSelection()
|
||||
// }
|
||||
// }
|
||||
|
||||
// @Archived by SuperMonster003 on Jul 12, 2023.
|
||||
// ! Author: 抠脚本人
|
||||
// ! Reason: Replaced with CommentHelper class.
|
||||
// fun commentBlock() {
|
||||
// val selectionText: String = getSelectionRaw()
|
||||
// if (!selectionText.isEmpty()) {
|
||||
// val regex = "/\\*([^*]|\\*+[^*/])*\\*/"
|
||||
// mReplacement = if (selectionText.matches(regex)) {
|
||||
// // 取消块注释
|
||||
// selectionText.substring(2, selectionText.length - 2)
|
||||
// } else {
|
||||
// // 增加块注释
|
||||
// "/*$selectionText*/"
|
||||
// }
|
||||
// replaceSelection()
|
||||
// }
|
||||
// }
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package org.autojs.autojs.ui.edit.editor;
|
||||
|
||||
public interface CodeEditorCommentHelper {
|
||||
|
||||
void handle();
|
||||
|
||||
void comment();
|
||||
|
||||
void uncomment();
|
||||
|
||||
boolean isCommented();
|
||||
|
||||
default void toggle() {
|
||||
if (isCommented()) {
|
||||
uncomment();
|
||||
} else {
|
||||
comment();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -186,7 +186,7 @@ public class CircularMenu implements Recorder.OnStateChangedListener, LayoutInsp
|
||||
mSettingsDialog = new CircularMenuOperationDialogBuilder(mContext)
|
||||
.item(R.drawable.ic_accessibility_black_48dp, R.string.text_manage_a11y_service, v1 -> {
|
||||
dismissSettingsDialog();
|
||||
getAccessibilityTool().getService().enable();
|
||||
getAccessibilityTool().getService().launchSettings();
|
||||
})
|
||||
.item(R.drawable.ic_text_fields_black_48dp, mContext.getString(R.string.text_latest_package) + ":\n" + getRunningPackage(), v1 -> {
|
||||
dismissSettingsDialog();
|
||||
|
||||
@@ -17,7 +17,7 @@ import org.autojs.autojs.util.ClipboardUtils
|
||||
import org.autojs.autojs.util.ViewUtils
|
||||
import org.autojs.autojs6.R
|
||||
|
||||
abstract class LayoutFloatyWindow(private val rootNode: NodeInfo?, private val context: Context, private val isServiceRelied: Boolean) : FullScreenFloatyWindow(){
|
||||
abstract class LayoutFloatyWindow(private val rootNode: NodeInfo?, private val context: Context, private val isServiceRelied: Boolean) : FullScreenFloatyWindow() {
|
||||
|
||||
private lateinit var mServiceContext: Context
|
||||
private lateinit var mActions: LinkedHashMap<Int, Runnable>
|
||||
@@ -29,12 +29,22 @@ abstract class LayoutFloatyWindow(private val rootNode: NodeInfo?, private val c
|
||||
private val mNodeInfoDialog by lazy {
|
||||
AppLevelThemeDialogBuilder(mServiceContext)
|
||||
.customView(mNodeInfoView, false)
|
||||
.positiveText("生成")
|
||||
.onPositive { _, _ ->
|
||||
ViewUtils.showToast(context, "TODO")
|
||||
val selector = mNodeInfoView.getCheckedDate().joinToString(".")
|
||||
if (selector.isNotEmpty()) ClipboardUtils.setClip(context, selector)
|
||||
}
|
||||
|
||||
// @Overruled by SuperMonster003 on Jul 21, 2023.
|
||||
// ! Author: 抠脚本人
|
||||
// ! Related PR:
|
||||
// ! http://pr.autojs6.com/98
|
||||
// ! Reason:
|
||||
// ! Pending processing.
|
||||
// ! zh-CN: 将于后续版本继续处理.
|
||||
// !
|
||||
// .positiveText("生成")
|
||||
// .onPositive { _, _ ->
|
||||
// ViewUtils.showToast(context, "TODO")
|
||||
// val selector = mNodeInfoView.getCheckedDate().joinToString(".")
|
||||
// if (selector.isNotEmpty()) ClipboardUtils.setClip(context, selector)
|
||||
// }
|
||||
|
||||
.build()
|
||||
.also { it.window!!.setType(FloatyWindowManger.getWindowType()) }
|
||||
}
|
||||
|
||||
@@ -36,7 +36,8 @@ public class LayoutHierarchyView extends MultiLevelListView {
|
||||
public interface OnItemLongClickListener {
|
||||
void onItemLongClick(View view, NodeInfo nodeInfo);
|
||||
}
|
||||
private final Map<NodeInfo,ViewHolder> nodeMap = new LinkedHashMap<>();
|
||||
|
||||
private final Map<NodeInfo, ViewHolder> nodeMap = new LinkedHashMap<>();
|
||||
private Adapter mAdapter;
|
||||
private OnItemLongClickListener mOnItemLongClickListener;
|
||||
private final AdapterView.OnItemLongClickListener mOnItemLongClickListenerProxy = new AdapterView.OnItemLongClickListener() {
|
||||
@@ -121,25 +122,34 @@ public class LayoutHierarchyView extends MultiLevelListView {
|
||||
}
|
||||
|
||||
private void drawListItem(NodeInfo info, Boolean draw) {
|
||||
ArrayList<ViewHolder> list = new ArrayList<>();
|
||||
NodeInfo currentInfo = info;
|
||||
while (true) {
|
||||
currentInfo = currentInfo.getParent();
|
||||
if (currentInfo == null) break;
|
||||
ViewHolder vh = nodeMap.get(currentInfo);
|
||||
list.add(vh);
|
||||
}
|
||||
//todo:选用能适应深色模式的字体颜色
|
||||
//fixme:列表滑动时listview数据错乱
|
||||
if (draw) {
|
||||
for (ViewHolder vh : list) {
|
||||
vh.nameView.setTextColor(Color.RED); // 设置字体颜色为红色
|
||||
}
|
||||
} else {
|
||||
for (ViewHolder vh : list) {
|
||||
vh.nameView.setTextColor(Color.BLACK); // 设置字体颜色为红色
|
||||
}
|
||||
}
|
||||
|
||||
// @Overruled by SuperMonster003 on Jul 21, 2023.
|
||||
// ! Author: 抠脚本人
|
||||
// ! Related PR:
|
||||
// ! http://pr.autojs6.com/98
|
||||
// ! Reason:
|
||||
// ! Pending processing.
|
||||
// ! zh-CN: 将于后续版本继续处理.
|
||||
// !
|
||||
// ArrayList<ViewHolder> list = new ArrayList<>();
|
||||
// NodeInfo currentInfo = info;
|
||||
// while (true) {
|
||||
// currentInfo = currentInfo.getParent();
|
||||
// if (currentInfo == null) break;
|
||||
// ViewHolder vh = nodeMap.get(currentInfo);
|
||||
// list.add(vh);
|
||||
// }
|
||||
// // TODO: 选用能适应深色模式的字体颜色
|
||||
// // FIXME: 列表滑动时 ListView 数据错乱
|
||||
// if (draw) {
|
||||
// for (ViewHolder vh : list) {
|
||||
// vh.nameView.setTextColor(Color.RED); // 设置字体颜色为红色
|
||||
// }
|
||||
// } else {
|
||||
// for (ViewHolder vh : list) {
|
||||
// vh.nameView.setTextColor(Color.BLACK); // 设置字体颜色为红色
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private void initPaint() {
|
||||
@@ -221,10 +231,8 @@ public class LayoutHierarchyView extends MultiLevelListView {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private class Adapter extends MultiLevelListAdapter {
|
||||
|
||||
|
||||
@Override
|
||||
protected List<?> getSubObjects(Object object) {
|
||||
return ((NodeInfo) object).getChildren();
|
||||
@@ -252,9 +260,21 @@ public class LayoutHierarchyView extends MultiLevelListView {
|
||||
} else {
|
||||
viewHolder = (ViewHolder) convertView.getTag();
|
||||
}
|
||||
nodeMap.put(nodeInfo,viewHolder);
|
||||
//对于id,desc,text,clickable,longClickable不为空的显示额外信息
|
||||
viewHolder.nameView.setText(extraInfo(nodeInfo));
|
||||
nodeMap.put(nodeInfo, viewHolder);
|
||||
|
||||
// @Overruled by SuperMonster003 on Jul 12, 2023.
|
||||
// ! Author: 抠脚本人
|
||||
// ! Related PR:
|
||||
// ! http://pr.autojs6.com/98
|
||||
// ! Reason:
|
||||
// ! Pending processing.
|
||||
// ! zh-CN: 将于后续版本继续处理.
|
||||
// !
|
||||
// @Hint by 抠脚本人 on Jul 10, 2023.
|
||||
// ! 对于 id, desc, text, clickable, longClickable 不为空的显示额外信息
|
||||
// viewHolder.nameView.setText(extraInfo(nodeInfo));
|
||||
viewHolder.nameView.setText(simplifyClassName(nodeInfo.getClassName()));
|
||||
|
||||
viewHolder.nodeInfo = nodeInfo;
|
||||
if (viewHolder.infoView.getVisibility() == VISIBLE)
|
||||
viewHolder.infoView.setText(getItemInfoDsc(itemInfo));
|
||||
@@ -306,7 +326,7 @@ public class LayoutHierarchyView extends MultiLevelListView {
|
||||
if (nodeInfo.getLongClickable()) {
|
||||
info.add("longClickable");
|
||||
}
|
||||
//字符串拼接
|
||||
// 字符串拼接
|
||||
String others = String.join(", ", info);
|
||||
if (!others.isEmpty()) {
|
||||
return extra + " [" + others + "]";
|
||||
|
||||
@@ -22,7 +22,9 @@ import java.lang.reflect.Field
|
||||
* Modified by SuperMonster003 as of Dec 1, 2021.
|
||||
*/
|
||||
class NodeInfoView : RecyclerView {
|
||||
//todo:调整数据结构,对话框关闭后根据已勾选的属性,生成选择器
|
||||
|
||||
// TODO by 抠脚本人 on Jul 12, 2023.
|
||||
// ! 调整数据结构, 对话框关闭后根据已勾选的属性, 生成选择器
|
||||
private val data = Array(FIELDS.size + 1) { Array(2) { "" } }
|
||||
|
||||
constructor(context: Context) : super(context)
|
||||
@@ -75,7 +77,8 @@ class NodeInfoView : RecyclerView {
|
||||
}
|
||||
|
||||
fun getCheckedDate(): Array<String> {
|
||||
//todo:数据增加checked属性,区分已选中项目
|
||||
// TODO by 抠脚本人 on Jul 12, 2023.
|
||||
// ! 数据增加 checked 属性, 区分已选中项目
|
||||
val checkedArr = data.filter { it[0] == "id" || it[0] == "text" }
|
||||
return Array(checkedArr.size) {
|
||||
dataToFx(checkedArr[it])
|
||||
@@ -95,7 +98,7 @@ class NodeInfoView : RecyclerView {
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.apply {
|
||||
data[position].let {
|
||||
//attrChecked.isChecked = false
|
||||
// attrChecked.isChecked = false
|
||||
attrName.text = it[0]
|
||||
attrValue.text = it[1]
|
||||
}
|
||||
@@ -109,7 +112,7 @@ class NodeInfoView : RecyclerView {
|
||||
}
|
||||
|
||||
internal inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
//val attrChecked: CheckBox = itemView.findViewById(R.id.generate)
|
||||
// val attrChecked: CheckBox = itemView.findViewById(R.id.generate)
|
||||
val attrName: TextView = itemView.findViewById(R.id.name)
|
||||
val attrValue: TextView = itemView.findViewById(R.id.value)
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import org.autojs.autojs.core.accessibility.NodeInfo
|
||||
|
||||
/**
|
||||
* Created by Stardust on 2017/3/10.
|
||||
* Transformed by 抠脚本人 on Jul 10, 2023.
|
||||
*/
|
||||
fun interface OnNodeInfoSelectListener {
|
||||
fun onNodeSelect(info: NodeInfo)
|
||||
|
||||
@@ -292,8 +292,8 @@ public class BuildActivity extends BaseActivity implements ApkBuilder.ProgressCa
|
||||
mProgressDialog.dismiss();
|
||||
mProgressDialog = null;
|
||||
new MaterialDialog.Builder(this)
|
||||
.title(R.string.text_build_successfully)
|
||||
.content(getString(R.string.format_build_successfully, outApk.getPath()))
|
||||
.title(R.string.text_build_succeeded)
|
||||
.content(getString(R.string.format_build_succeeded, outApk.getPath()))
|
||||
.positiveText(R.string.text_install)
|
||||
.negativeText(R.string.text_cancel)
|
||||
.onPositive((dialog, which) -> IntentUtils.installApkOrToast(BuildActivity.this, outApk.getPath(), AppFileProvider.AUTHORITY))
|
||||
|
||||
@@ -14,7 +14,7 @@ public final class ResourceMonitor {
|
||||
|
||||
private static final String LOG_TAG = "ResourceMonitor";
|
||||
|
||||
// @Reference to TonyJiangWJ/Auto.js on Nov 22, 2021
|
||||
// @Reference to TonyJiangWJ/Auto.js (https://github.com/TonyJiangWJ/Auto.js) on Nov 22, 2021
|
||||
private static class LockedResource {
|
||||
private ReentrantLock lock;
|
||||
private SparseArray<Exception> resource;
|
||||
|
||||
Reference in New Issue
Block a user