6.3.3 - PR Review; 新增代码注释功能; 修复 ClassLoader 栈溢出 / VSCode 插件问题

This commit is contained in:
SuperMonster003
2023-07-21 23:46:44 +08:00
parent 413454796c
commit 49e9961a86
166 changed files with 2292 additions and 1743 deletions

View File

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

View File

@@ -0,0 +1,9 @@
package org.autojs.autojs.core.accessibility
/**
* Created by 抠脚本人 on Jul 10, 2023.
*/
interface AccessibilityServiceCallback {
fun onConnected()
fun onDisconnected()
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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()
// }
// }
}

View File

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

View File

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

View File

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

View File

@@ -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 + "]";

View File

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

View File

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

View File

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

View File

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