diff --git a/.idea/caches/build_file_checksums.ser b/.idea/caches/build_file_checksums.ser index bf0552a3..5994ddd6 100644 Binary files a/.idea/caches/build_file_checksums.ser and b/.idea/caches/build_file_checksums.ser differ diff --git a/app/build.gradle b/app/build.gradle index ea809434..1b8f1c78 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -8,8 +8,8 @@ android { applicationId "org.autojs.autojs" minSdkVersion 17 targetSdkVersion 23 - versionCode 406 - versionName "4.0.2 Alpha2" + versionCode 408 + versionName "4.0.2 Alpha3" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" multiDexEnabled true ndk { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 81cdb5c1..ad608775 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -73,6 +73,7 @@ @@ -81,7 +82,10 @@ android:name=".ui.settings.SettingsActivity_" android:theme="@style/AppTheme.Settings"/> - + PFiles.read(file)) @@ -225,9 +242,19 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC private void setMenuItemStatus(int id, boolean enabled) { - findViewById(id).setEnabled(enabled); + mMenuItemStatus.put(id, enabled); + ToolbarFragment fragment = (ToolbarFragment) getActivity().getSupportFragmentManager() + .findFragmentById(R.id.toolbar_menu); + if (fragment == null) { + mNormalToolbar.setMenuItemStatus(id, enabled); + } else { + fragment.setMenuItemStatus(id, enabled); + } } + public boolean getMenuItemStatus(int id, boolean defValue) { + return mMenuItemStatus.get(id, defValue); + } @AfterViews void init() { @@ -238,9 +265,18 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC setMenuItemStatus(R.id.save, false); mDocsWebView.getWebView().getSettings().setDisplayZoomControls(true); mDocsWebView.getWebView().loadUrl(Pref.getDocumentationUrl() + "index.html"); - Themes.getCurrent(getContext()). - observeOn(AndroidSchedulers.mainThread()) + Themes.getCurrent(getContext()) + .observeOn(AndroidSchedulers.mainThread()) .subscribe(this::setTheme); + initNormalToolbar(); + } + + private void initNormalToolbar() { + mNormalToolbar.setOnMenuItemClickListener(this); + Fragment fragment = getActivity().getSupportFragmentManager().findFragmentById(R.id.toolbar_menu); + if(fragment == null){ + showNormalToolbar(); + } } private void setUpFunctionsKeyboard() { @@ -299,8 +335,37 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC return false; } + @Override + public void onToolbarMenuItemClick(int id) { + switch (id) { + case R.id.run: + runAndSaveFileIfNeeded(); + break; + case R.id.save: + saveFile(); + break; + case R.id.undo: + undo(); + break; + case R.id.redo: + redo(); + break; + case R.id.replace: + replace(); + break; + case R.id.find_next: + findNext(); + break; + case R.id.find_prev: + findPrev(); + break; + case R.id.cancel_search: + cancelSearch(); + break; + } + } - @Click(R.id.run) + @SuppressLint("CheckResult") public void runAndSaveFileIfNeeded() { save().observeOn(AndroidSchedulers.mainThread()) .subscribe(s -> run()); @@ -308,17 +373,15 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC public void run() { Snackbar.make(this, R.string.text_start_running, Snackbar.LENGTH_SHORT).show(); - mScriptExecution = Scripts.runWithBroadcastSender(mFile); + mScriptExecutionId = Scripts.runWithBroadcastSender(mFile).getId(); setMenuItemStatus(R.id.run, false); } - @Click(R.id.undo) public void undo() { mEditor.undo(); } - @Click(R.id.redo) public void redo() { mEditor.redo(); } @@ -335,32 +398,49 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC } public void forceStop() { - if (mScriptExecution != null) { - mScriptExecution.getEngine().forceStop(); + doWithCurrentEngine(ScriptEngine::forceStop); + } + + private void doWithCurrentEngine(Callback callback) { + ScriptExecution execution = AutoJs.getInstance().getScriptEngineService().getScriptExecution(mScriptExecutionId); + if (execution != null) { + ScriptEngine engine = execution.getEngine(); + if (engine != null) { + callback.call(engine); + } } } - @Click(R.id.save) public void saveFile() { save().subscribe(); } - @Click(R.id.find_next) void findNext() { mEditor.findNext(); } - @Click(R.id.find_prev) void findPrev() { mEditor.findPrev(); } - @Click(R.id.cancel) void cancelSearch() { - mToolbarSwitcher.showFirst(); + showNormalToolbar(); + } + + private void showNormalToolbar() { + getActivity().getSupportFragmentManager().beginTransaction() + .replace(R.id.toolbar_menu, mNormalToolbar) + .commitAllowingStateLoss(); + } + + FragmentActivity getActivity() { + Context context = getContext(); + while (!(context instanceof Activity) && context instanceof ContextWrapper) { + context = ((ContextWrapper) context).getBaseContext(); + } + return (FragmentActivity) context; } - @Click(R.id.replace) void replace() { mEditor.replaceSelection(); } @@ -374,9 +454,7 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC } public void showConsole() { - if (mScriptExecution != null) { - ((JavaScriptEngine) mScriptExecution.getEngine()).getRuntime().console.show(); - } + doWithCurrentEngine(engine -> ((JavaScriptEngine) engine).getRuntime().console.show()); } public void openByOtherApps() { @@ -433,14 +511,22 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC public void find(String keywords, boolean usingRegex) { mEditor.find(keywords, usingRegex); - mReplaceMenuItem.setVisibility(GONE); - mToolbarSwitcher.showSecond(); + showSearchToolbar(false); + } + + private void showSearchToolbar(boolean showReplaceItem) { + SearchToolbarFragment searchToolbarFragment = SearchToolbarFragment_.builder() + .arg(SearchToolbarFragment.ARGUMENT_SHOW_REPLACE_ITEM, showReplaceItem) + .build(); + searchToolbarFragment.setOnMenuItemClickListener(this); + getActivity().getSupportFragmentManager().beginTransaction() + .replace(R.id.toolbar_menu, searchToolbarFragment) + .commit(); } public void replace(String keywords, String replacement, boolean usingRegex) { mEditor.replace(keywords, replacement, usingRegex); - mReplaceMenuItem.setVisibility(VISIBLE); - mToolbarSwitcher.showSecond(); + showSearchToolbar(true); } public void replaceAll(String keywords, String replacement, boolean usingRegex) { @@ -448,6 +534,26 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC } + public void launchDebugger() { + DebugToolbarFragment debugToolbarFragment = DebugToolbarFragment_.builder() + .build(); + getActivity().getSupportFragmentManager().beginTransaction() + .replace(R.id.toolbar_menu, debugToolbarFragment) + .commit(); + mDebugging = true; + } + + public void exitDebugging() { + FragmentManager fragmentManager = getActivity().getSupportFragmentManager(); + Fragment fragment = fragmentManager.findFragmentById(R.id.toolbar_menu); + if (fragment instanceof DebugToolbarFragment) { + ((DebugToolbarFragment) fragment).detachDebugger(); + } + showNormalToolbar(); + mEditor.setDebuggingLine(-1); + mDebugging = false; + } + private void showErrorMessage(String msg) { Snackbar.make(EditorView.this, getResources().getString(R.string.text_error) + ": " + msg, Snackbar.LENGTH_LONG) .setAction(R.string.text_detail, v -> LogActivity_.intent(getContext()).start()) @@ -511,5 +617,31 @@ public class EditorView extends FrameLayout implements CodeCompletionBar.OnHintC } } + public int getScriptExecutionId() { + return mScriptExecutionId; + } + @Nullable + public ScriptExecution getScriptExecution(){ + return AutoJs.getInstance().getScriptEngineService().getScriptExecution(mScriptExecutionId); + } + + @Nullable + @Override + protected Parcelable onSaveInstanceState() { + Bundle bundle = new Bundle(); + Parcelable superData = super.onSaveInstanceState(); + bundle.putParcelable("super_data", superData); + bundle.putInt("script_execution_id", mScriptExecutionId); + return bundle; + } + + @Override + protected void onRestoreInstanceState(Parcelable state) { + Bundle bundle = (Bundle) state; + Parcelable superData = bundle.getParcelable("super_data"); + mScriptExecutionId = bundle.getInt("script_execution_id", ScriptExecution.NO_ID); + super.onRestoreInstanceState(superData); + setMenuItemStatus(R.id.run, mScriptExecutionId == ScriptExecution.NO_ID); + } } diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.java b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.java index 79602b90..c8e8489a 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.java +++ b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditText.java @@ -22,6 +22,7 @@ import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.Typeface; +import android.os.Bundle; import android.os.Parcelable; import android.support.v7.widget.AppCompatEditText; import android.text.Editable; @@ -31,14 +32,19 @@ import android.util.Log; import android.util.TimingLogger; import android.view.Gravity; -import org.autojs.autojs.BuildConfig; +import org.autojs.autojs.R; import org.autojs.autojs.ui.edit.theme.Theme; import org.autojs.autojs.ui.edit.theme.TokenMapping; + +import com.stardust.autojs.execution.ScriptExecution; import com.stardust.util.ClipboardUtil; import com.stardust.util.TextUtils; import org.mozilla.javascript.Token; +import java.util.ArrayList; +import java.util.LinkedHashMap; + import static org.autojs.autojs.ui.edit.editor.BracketMatching.UNMATCHED_BRACKET; /** @@ -62,6 +68,8 @@ public class CodeEditText extends AppCompatEditText { private int mFirstLineForDraw = -1, mLastLineForDraw; private int[] mMatchingBrackets = {-1, -1}; private int mUnmatchedBracket = -1; + private LinkedHashMap mBreakpoints = new LinkedHashMap<>(); + private int mDebuggingLine = -1; public CodeEditText(Context context) { @@ -87,6 +95,10 @@ public class CodeEditText extends AppCompatEditText { mLineHighlightPaint.setStyle(Paint.Style.FILL); } + public LinkedHashMap getBreakpoints() { + return mBreakpoints; + } + public void setTheme(Theme theme) { mTheme = theme; invalidate(); @@ -101,8 +113,8 @@ public class CodeEditText extends AppCompatEditText { updatePaddingForGutter(); updateLineRangeForDraw(canvas); - //绘制当前行高亮需要在绘制光标之前 - drawLineHighlight(canvas, mLineHighlightPaint, getCurrentLine()); + //绘制行高亮需要在绘制光标之前 + drawLineHighlights(canvas); //调用super.onDraw绘制光标和选择高亮。因为字体颜色被设置为透明因此super.onDraw()绘制的字体不显示 // TODO: 2018/2/24 优化效率。不绘制透明字体。 @@ -118,6 +130,30 @@ public class CodeEditText extends AppCompatEditText { mLogger.dumpToLog(); } + public int getDebuggingLine() { + return mDebuggingLine; + } + + public void setDebuggingLine(int debuggingLine) { + mDebuggingLine = debuggingLine; + invalidate(); + } + + private void drawLineHighlights(Canvas canvas) { + int currentLine = getCurrentLine(); + int debugHighlightLine = mDebuggingLine; + if (debugHighlightLine != currentLine) { + //绘制当前行高亮 + mLineHighlightPaint.setColor(mTheme.getLineHighlightBackgroundColor()); + drawLineHighlight(canvas, mLineHighlightPaint, getCurrentLine()); + } + if (debugHighlightLine != -1) { + mLineHighlightPaint.setColor(mTheme.getDebuggingLineBackgroundColor()); + drawLineHighlight(canvas, mLineHighlightPaint, debugHighlightLine); + } + + } + private void updateLineRangeForDraw(Canvas canvas) { Layout layout = getLayout(); if (layout == null) @@ -159,15 +195,22 @@ public class CodeEditText extends AppCompatEditText { int scrollX = Math.max(getRealScrollX() - paddingLeft, 0); Paint paint = getPaint(); int lineNumberColor = mTheme.getLineNumberColor(); + int breakPointColor = mTheme.getBreakpointColor(); if (DEBUG) Log.d(LOG_TAG, "draw line: " + (mLastLineForDraw - mFirstLineForDraw + 1)); mLogger.addSplit("before draw line"); for (int line = mFirstLineForDraw; line <= mLastLineForDraw && line < lineCount; line++) { int lineBottom = layout.getLineTop(line + 1); + int lineTop = layout.getLineTop(line); int lineBaseline = lineBottom - layout.getLineDescent(line); //drawLineNumber String lineNumberText = Integer.toString(line + 1); + // if there is a breakpoint at this line, draw highlight background for line number + if (mBreakpoints.containsKey(line)) { + paint.setColor(breakPointColor); + canvas.drawRect(0, lineTop, paddingLeft - 10, lineBottom, paint); + } paint.setColor(lineNumberColor); canvas.drawText(lineNumberText, 0, lineNumberText.length(), 10, lineBaseline, paint); @@ -225,7 +268,6 @@ public class CodeEditText extends AppCompatEditText { } int lineTop = getLayout().getLineTop(line); int lineBottom = getLayout().getLineTop(line + 1); - paint.setColor(mTheme.getLineHighlightBackgroundColor()); canvas.drawRect(0, lineTop, canvas.getWidth(), lineBottom, paint); } @@ -363,4 +405,34 @@ public class CodeEditText extends AppCompatEditText { super.setSelection(index); } + + @Override + public Parcelable onSaveInstanceState() { + Bundle bundle = new Bundle(); + Parcelable superData = super.onSaveInstanceState(); + bundle.putParcelable("super_data", superData); + bundle.putInt("debugging_line", mDebuggingLine); + int[] breakpoints = new int[mBreakpoints.size()]; + int i = 0; + for (CodeEditor.Breakpoint breakpoint : mBreakpoints.values()) { + breakpoints[i++] = breakpoint.line; + } + bundle.putIntArray("breakpoints", breakpoints); + return bundle; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + Bundle bundle = (Bundle) state; + Parcelable superData = bundle.getParcelable("super_data"); + mDebuggingLine = bundle.getInt("debugging_line", -1); + int[] breakpoints = bundle.getIntArray("breakpoints"); + if(breakpoints != null){ + for (int breakpoint : breakpoints) { + mBreakpoints.put(breakpoint, new CodeEditor.Breakpoint(breakpoint)); + } + } + super.onRestoreInstanceState(superData); + } + } diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.java b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.java index 5f894d70..5e7e9520 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.java +++ b/app/src/main/java/org/autojs/autojs/ui/edit/editor/CodeEditor.java @@ -3,17 +3,20 @@ package org.autojs.autojs.ui.edit.editor; import android.content.Context; import android.graphics.Canvas; import android.support.design.widget.Snackbar; +import android.text.Layout; import android.util.AttributeSet; import android.widget.Toast; import com.afollestad.materialdialogs.MaterialDialog; -import com.android.dx.util.IntList; import com.stardust.autojs.script.JsBeautifier; + import org.autojs.autojs.R; import org.autojs.autojs.ui.edit.theme.Theme; + import com.stardust.util.ClipboardUtil; import com.stardust.util.TextUtils; +import java.util.LinkedHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -53,7 +56,6 @@ public class CodeEditor extends HVScrollView { private JsBeautifier mJsBeautifier; private MaterialDialog mProcessDialog; - private CharSequence mReplacement = ""; private String mKeywords; private Matcher mMatcher; @@ -73,7 +75,7 @@ public class CodeEditor extends HVScrollView { private void init() { //setFillViewport(true); inflate(getContext(), R.layout.code_editor, this); - mCodeEditText = (CodeEditText) findViewById(R.id.code_edit_text); + mCodeEditText = findViewById(R.id.code_edit_text); mCodeEditText.addTextChangedListener(new AutoIndent(mCodeEditText)); mTextViewRedoUndo = new TextViewRedoUndo(mCodeEditText); mJavaScriptHighlighter = new JavaScriptHighlighter(mTheme, mCodeEditText); @@ -166,7 +168,8 @@ public class CodeEditor extends HVScrollView { } public void jumpTo(int line, int col) { - if (line >= mCodeEditText.getLayout().getLineCount() || line < 0) { + Layout layout = mCodeEditText.getLayout(); + if (line < 0 || (layout != null && line >= layout.getLineCount())) { return; } mCodeEditText.setSelection(mCodeEditText.getLayout().getLineStart(line) + col); @@ -260,7 +263,7 @@ public class CodeEditor extends HVScrollView { } public void findPrev() { - if (mMatcher != null){ + if (mMatcher != null) { Toast.makeText(getContext(), R.string.error_regex_find_prev, Toast.LENGTH_SHORT).show(); return; } @@ -329,6 +332,31 @@ public class CodeEditor extends HVScrollView { mTextViewRedoUndo.markTextAsUnchanged(); } + public LinkedHashMap getBreakpoints() { + return mCodeEditText.getBreakpoints(); + } + + public void setDebuggingLine(int line) { + jumpTo(line, 0); + mCodeEditText.setDebuggingLine(line); + } + + public void addOrRemoveBreakpoint(int line) { + LinkedHashMap breakpoints = mCodeEditText.getBreakpoints(); + if (breakpoints.remove(line) == null) { + breakpoints.put(line, new Breakpoint(line)); + } + mCodeEditText.invalidate(); + } + + public void addOrRemoveBreakpointAtCurrentLine() { + int line = LayoutHelper.getLineOfChar(mCodeEditText.getLayout(), mCodeEditText.getSelectionStart()); + if (line < 0 || line >= mCodeEditText.getLayout().getLineCount()) + return; + addOrRemoveBreakpoint(line); + } + + @Override protected void onDraw(Canvas canvas) { int codeWidth = getWidth() - getPaddingLeft() - getPaddingRight(); @@ -340,4 +368,14 @@ public class CodeEditor extends HVScrollView { } super.onDraw(canvas); } + + public static class Breakpoint { + + public int line; + public boolean enabled = true; + + public Breakpoint(int line) { + this.line = line; + } + } } diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/theme/Theme.java b/app/src/main/java/org/autojs/autojs/ui/edit/theme/Theme.java index 2c05413e..27e490b4 100644 --- a/app/src/main/java/org/autojs/autojs/ui/edit/theme/Theme.java +++ b/app/src/main/java/org/autojs/autojs/ui/edit/theme/Theme.java @@ -25,6 +25,8 @@ public class Theme { private int mImeBarForegroundColor = Color.WHITE; private EditorTheme mEditorTheme; private int mLineHighlightBackground; + private int mBreakpointColor; + private int mDebuggingLineBackground; public Theme(EditorTheme theme) { mEditorTheme = theme; @@ -34,6 +36,8 @@ public class Theme { mImeBarBackgroundColor = parseColor(theme.getEditorColors().getImeBackgroundColor(), mImeBarBackgroundColor); mImeBarForegroundColor = parseColor(theme.getEditorColors().getImeForegroundColor(), mImeBarForegroundColor); mLineHighlightBackground = parseColor(theme.getEditorColors().getLineHighlightBackground(), mLineHighlightBackground); + mDebuggingLineBackground = parseColor(theme.getEditorColors().getDebuggingLineBackground(), mDebuggingLineBackground); + mBreakpointColor = parseColor(theme.getEditorColors().getBreakpointForeground(), mBackgroundColor); for (TokenColor tokenColor : theme.getTokenColors()) { String foregroundStr = tokenColor.getSettings().getForeground(); @@ -141,4 +145,11 @@ public class Theme { } + public int getBreakpointColor() { + return mBreakpointColor; + } + + public int getDebuggingLineBackgroundColor() { + return mDebuggingLineBackground; + } } diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/DebugToolbarFragment.java b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/DebugToolbarFragment.java new file mode 100644 index 00000000..21feb219 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/DebugToolbarFragment.java @@ -0,0 +1,163 @@ +package org.autojs.autojs.ui.edit.toolbar; + +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.Nullable; +import android.util.Log; +import android.view.View; + + +import com.stardust.autojs.engine.RhinoJavaScriptEngine; +import com.stardust.autojs.execution.ScriptExecution; +import com.stardust.autojs.rhino.debug.Dim; +import com.stardust.autojs.rhino.debug.DebugCallback; + +import org.androidannotations.annotations.Click; +import org.androidannotations.annotations.EFragment; +import org.autojs.autojs.R; +import org.autojs.autojs.autojs.AutoJs; +import org.autojs.autojs.ui.edit.EditorView; +import org.autojs.autojs.ui.edit.editor.CodeEditor; +import org.mozilla.javascript.ContextFactory; + +import java.util.Arrays; +import java.util.List; + +@EFragment(R.layout.fragment_debug_toolbar) +public class DebugToolbarFragment extends ToolbarFragment implements DebugCallback { + + private static final String LOG_TAG = "DebugToolbarFragment"; + private Dim mDim; + private EditorView mEditorView; + private Handler mHandler; + + public DebugToolbarFragment() { + Log.d(LOG_TAG, "DebugToolbarFragment()"); + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mHandler = new Handler(); + } + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + mEditorView = findEditorView(view); + ScriptExecution scriptExecution = mEditorView.getScriptExecution(); + if (scriptExecution != null) { + mDim = (Dim) scriptExecution.getEngine().getTag(Dim.TAG); + } + if (mDim == null) { + mDim = new Dim(); + mDim.setBreak(); + mDim.setBreakOnExceptions(true); + mDim.attachTo(AutoJs.getInstance().getScriptEngineService(), ContextFactory.getGlobal()); + mDim.setGuiCallback(this); + setInterrupted(false); + mEditorView.run(); + } else { + mDim.setGuiCallback(this); + } + Log.d(LOG_TAG, "onViewCreated"); + } + + private void setInterrupted(boolean interrupted) { + setMenuItemStatus(R.id.step_into, interrupted); + setMenuItemStatus(R.id.step_over, interrupted); + setMenuItemStatus(R.id.step_out, interrupted); + setMenuItemStatus(R.id.resume_script, interrupted); + if (!interrupted) { + mEditorView.getEditor().setDebuggingLine(-1); + } + } + + public void detachDebugger() { + mDim.detach(); + mDim.setGuiCallback(null); + } + + @Click(R.id.step_over) + void stepOver() { + setInterrupted(false); + mDim.setReturnValue(Dim.STEP_OVER); + } + + @Click(R.id.step_into) + void stepInto() { + setInterrupted(false); + mDim.setReturnValue(Dim.STEP_INTO); + } + + @Click(R.id.step_out) + void stepOut() { + setInterrupted(false); + mDim.setReturnValue(Dim.STEP_OUT); + } + + @Click(R.id.stop_script) + void stopScript() { + mEditorView.forceStop(); + } + + @Click(R.id.resume_script) + void resumeScript() { + setInterrupted(false); + mDim.setReturnValue(Dim.GO); + } + + @Override + public void updateSourceText(Dim.SourceInfo sourceInfo) { + Log.d(LOG_TAG, "updateSourceText: url = " + sourceInfo.url()); + if (!sourceInfo.url().equals(mEditorView.getFile().toString())) { + return; + } + sourceInfo.removeAllBreakpoints(); + for (CodeEditor.Breakpoint breakpoint : mEditorView.getEditor().getBreakpoints().values()) { + int line = breakpoint.line + 1; + if (sourceInfo.breakableLine(line)) { + sourceInfo.breakpoint(line, breakpoint.enabled); + Log.d(LOG_TAG, "not breakable: " + line); + } + } + } + + @Override + public void enterInterrupt(Dim.StackFrame stackFrame, String threadName, String s1) { + Log.d(LOG_TAG, "enterInterrupt: threadName = " + threadName + ", url = " + stackFrame.getUrl() + ", line = " + stackFrame.getLineNumber()); + if (stackFrame.getUrl().equals(mEditorView.getFile().toString())) { + final int line = stackFrame.getLineNumber() - 1; + mHandler.post(() -> { + mEditorView.getEditor().setDebuggingLine(line); + setInterrupted(true); + }); + + } else { + mHandler.post(this::resumeScript); + } + } + + @Override + public boolean isGuiEventThread() { + return Looper.getMainLooper() == Looper.myLooper(); + } + + @Override + public void dispatchNextGuiEvent() { + } + + @Override + public boolean shouldAttachDebugger(RhinoJavaScriptEngine engine) { + ScriptExecution execution = mEditorView.getScriptExecution(); + return execution != null && execution.getId() == engine.getId(); + + } + + @Override + public List getMenuItemIds() { + return Arrays.asList(R.id.step_over, R.id.step_into, R.id.step_out, R.id.resume_script, R.id.stop_script); + } + +} diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/NormalToolbarFragment.java b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/NormalToolbarFragment.java new file mode 100644 index 00000000..1f718d2f --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/NormalToolbarFragment.java @@ -0,0 +1,20 @@ +package org.autojs.autojs.ui.edit.toolbar; + +import android.support.v4.app.Fragment; + +import org.androidannotations.annotations.Click; +import org.androidannotations.annotations.EFragment; +import org.autojs.autojs.R; + +import java.util.Arrays; +import java.util.List; + +@EFragment(R.layout.fragment_normal_toolbar) +public class NormalToolbarFragment extends ToolbarFragment { + + + @Override + public List getMenuItemIds() { + return Arrays.asList(R.id.run, R.id.undo, R.id.redo, R.id.save); + } +} diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/SearchToolbarFragment.java b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/SearchToolbarFragment.java new file mode 100644 index 00000000..ac947588 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/SearchToolbarFragment.java @@ -0,0 +1,31 @@ +package org.autojs.autojs.ui.edit.toolbar; + +import android.os.Bundle; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.view.View; + +import org.androidannotations.annotations.EFragment; +import org.autojs.autojs.R; + +import java.util.Arrays; +import java.util.List; + +@EFragment(R.layout.fragment_search_toolbar) +public class SearchToolbarFragment extends ToolbarFragment { + + public static final String ARGUMENT_SHOW_REPLACE_ITEM = "show_replace_item"; + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + boolean showReplaceItem = getArguments().getBoolean(ARGUMENT_SHOW_REPLACE_ITEM, false); + view.findViewById(R.id.replace).setVisibility(showReplaceItem ? View.VISIBLE : View.GONE); + } + + @Override + public List getMenuItemIds() { + return Arrays.asList(R.id.replace, R.id.find_next, R.id.find_prev, R.id.cancel_search); + } + +} diff --git a/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/ToolbarFragment.java b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/ToolbarFragment.java new file mode 100644 index 00000000..aed9a309 --- /dev/null +++ b/app/src/main/java/org/autojs/autojs/ui/edit/toolbar/ToolbarFragment.java @@ -0,0 +1,87 @@ +package org.autojs.autojs.ui.edit.toolbar; + +import android.os.Bundle; +import android.os.Parcelable; +import android.support.annotation.Nullable; +import android.support.v4.app.Fragment; +import android.util.SparseBooleanArray; +import android.view.View; + +import org.autojs.autojs.ui.edit.EditorView; + +import java.util.List; + +public abstract class ToolbarFragment extends Fragment implements View.OnClickListener { + + public interface OnMenuItemClickListener { + void onToolbarMenuItemClick(int id); + } + + private OnMenuItemClickListener mOnMenuItemClickListener; + private List mMenuItemIds; + + public void setOnMenuItemClickListener(OnMenuItemClickListener listener) { + mOnMenuItemClickListener = listener; + } + + public abstract List getMenuItemIds(); + + @Override + public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + updateMenuItemStatus(view); + } + + protected EditorView findEditorView(View view) { + while (!(view instanceof EditorView) && view.getParent() != null) { + view = (View) view.getParent(); + } + if (!(view instanceof EditorView)) { + throw new IllegalStateException("cannot find EditorView from child: " + view); + } + return (EditorView) view; + } + + + private void updateMenuItemStatus(View rootView) { + if (rootView == null) { + return; + } + EditorView editorView = findEditorView(rootView); + if (mMenuItemIds == null) { + mMenuItemIds = getMenuItemIds(); + } + for (int id : mMenuItemIds) { + View view = rootView.findViewById(id); + view.setOnClickListener(this); + view.setEnabled(editorView.getMenuItemStatus(id, view.isEnabled())); + } + } + + + @Override + public void onClick(View view) { + if (mOnMenuItemClickListener != null) { + mOnMenuItemClickListener.onToolbarMenuItemClick(view.getId()); + } + } + + public void setMenuItemStatus(int id, boolean enabled) { + if (mMenuItemIds == null) { + mMenuItemIds = getMenuItemIds(); + } + if (!mMenuItemIds.contains(id)) { + return; + } + View rootView = getView(); + if (rootView == null) { + return; + } + View view = rootView.findViewById(id); + if (view == null) { + return; + } + view.setEnabled(enabled); + } + +} diff --git a/app/src/main/java/org/autojs/autojs/ui/main/task/Task.java b/app/src/main/java/org/autojs/autojs/ui/main/task/Task.java index 9e59665c..f1ae6097 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/task/Task.java +++ b/app/src/main/java/org/autojs/autojs/ui/main/task/Task.java @@ -1,18 +1,13 @@ package org.autojs.autojs.ui.main.task; -import android.os.Parcel; -import android.os.Parcelable; - import com.stardust.app.GlobalAppContext; -import com.stardust.autojs.engine.JavaScriptEngine; import com.stardust.autojs.engine.ScriptEngine; -import com.stardust.autojs.engine.ScriptEngineFactory; +import com.stardust.autojs.execution.ScriptExecution; import com.stardust.autojs.script.AutoFileSource; import com.stardust.autojs.script.JavaScriptSource; import com.stardust.autojs.script.ScriptSource; -import com.stardust.pio.PFile; import com.stardust.pio.PFiles; -import org.autojs.autojs.App; + import org.autojs.autojs.R; import org.autojs.autojs.timing.TimedTask; import org.autojs.autojs.timing.TimedTaskManager; @@ -23,7 +18,7 @@ import org.joda.time.format.DateTimeFormat; * Created by Stardust on 2017/11/28. */ -public abstract class Task { +public abstract class Task { public abstract String getName(); @@ -78,46 +73,37 @@ public abstract class Task { } public static class RunningTask extends Task { - private final ScriptEngine mScriptEngine; + private final ScriptExecution mScriptExecution; - public RunningTask(ScriptEngine scriptEngine) { - mScriptEngine = scriptEngine; + public RunningTask(ScriptExecution scriptExecution) { + mScriptExecution = scriptExecution; } - public ScriptEngine getScriptEngine() { - return mScriptEngine; + public ScriptExecution getScriptExecution() { + return mScriptExecution; } @Override public String getName() { - ScriptSource source = (ScriptSource) mScriptEngine.getTag(ScriptEngine.TAG_SOURCE); - if (source == null) { - return null; - } - return source.getName(); + return mScriptExecution.getSource().getName(); } @Override public String getDesc() { - ScriptSource source = (ScriptSource) mScriptEngine.getTag(ScriptEngine.TAG_SOURCE); - if (source == null) { - return null; - } - return source.toString(); + return mScriptExecution.getSource().toString(); } @Override public void cancel() { - mScriptEngine.forceStop(); + ScriptEngine engine = mScriptExecution.getEngine(); + if (engine != null) { + engine.forceStop(); + } } @Override public String getEngineName() { - ScriptSource source = (ScriptSource) mScriptEngine.getTag(ScriptEngine.TAG_SOURCE); - if (source == null) { - return null; - } - return source.getEngineName(); + return mScriptExecution.getSource().getEngineName(); } } } diff --git a/app/src/main/java/org/autojs/autojs/ui/main/task/TaskGroup.java b/app/src/main/java/org/autojs/autojs/ui/main/task/TaskGroup.java index 2ff28001..a1aef3d6 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/task/TaskGroup.java +++ b/app/src/main/java/org/autojs/autojs/ui/main/task/TaskGroup.java @@ -4,12 +4,15 @@ import android.content.Context; import com.bignerdranch.expandablerecyclerview.model.Parent; import com.stardust.autojs.engine.ScriptEngine; +import com.stardust.autojs.execution.ScriptExecution; + import org.autojs.autojs.R; import org.autojs.autojs.autojs.AutoJs; import org.autojs.autojs.timing.TimedTask; import org.autojs.autojs.timing.TimedTaskManager; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Set; @@ -98,20 +101,20 @@ public abstract class TaskGroup implements Parent { @Override public void refresh() { - Set scriptEngines = AutoJs.getInstance().getScriptEngineService().getEngines(); + Collection executions = AutoJs.getInstance().getScriptEngineService().getScriptExecutions(); mTasks.clear(); - for (ScriptEngine engine : scriptEngines) { - mTasks.add(new Task.RunningTask(engine)); + for (ScriptExecution execution : executions) { + mTasks.add(new Task.RunningTask(execution)); } } - public int addTask(ScriptEngine engine) { + public int addTask(ScriptExecution engine) { int pos = mTasks.size(); mTasks.add(new Task.RunningTask(engine)); return pos; } - public int removeTask(ScriptEngine engine) { + public int removeTask(ScriptExecution engine) { int i = indexOf(engine); if (i >= 0) { mTasks.remove(i); @@ -119,9 +122,9 @@ public abstract class TaskGroup implements Parent { return i; } - public int indexOf(ScriptEngine engine) { + public int indexOf(ScriptExecution engine) { for (int i = 0; i < mTasks.size(); i++) { - if (((Task.RunningTask) mTasks.get(i)).getScriptEngine().equals(engine)) { + if (((Task.RunningTask) mTasks.get(i)).getScriptExecution().equals(engine)) { return i; } } diff --git a/app/src/main/java/org/autojs/autojs/ui/main/task/TaskListRecyclerView.java b/app/src/main/java/org/autojs/autojs/ui/main/task/TaskListRecyclerView.java index b716490f..aba82132 100644 --- a/app/src/main/java/org/autojs/autojs/ui/main/task/TaskListRecyclerView.java +++ b/app/src/main/java/org/autojs/autojs/ui/main/task/TaskListRecyclerView.java @@ -12,6 +12,7 @@ import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; + import com.stardust.autojs.workground.WrapContentLinearLayoutManager; import com.bignerdranch.expandablerecyclerview.ChildViewHolder; @@ -25,6 +26,7 @@ import com.stardust.autojs.execution.ScriptExecutionListener; import com.stardust.autojs.execution.SimpleScriptExecutionListener; import com.stardust.autojs.engine.ScriptEngine; import com.stardust.autojs.script.AutoFileSource; + import org.autojs.autojs.R; import org.autojs.autojs.autojs.AutoJs; import org.autojs.autojs.storage.database.ModelChange; @@ -32,6 +34,7 @@ import org.autojs.autojs.timing.TaskReceiver; import org.autojs.autojs.timing.TimedTask; import org.autojs.autojs.timing.TimedTaskManager; import org.autojs.autojs.ui.timing.TimedTaskSettingActivity_; + import com.yqritc.recyclerviewflexibledivider.HorizontalDividerItemDecoration; import java.util.ArrayList; @@ -47,7 +50,7 @@ import io.reactivex.disposables.Disposable; * Created by Stardust on 2017/3/24. */ -public class TaskListRecyclerView extends ThemeColorRecyclerView implements ScriptEngineManager.EngineLifecycleCallback { +public class TaskListRecyclerView extends ThemeColorRecyclerView { private static final String LOG_TAG = "TaskListRecyclerView"; @@ -61,15 +64,28 @@ public class TaskListRecyclerView extends ThemeColorRecyclerView implements Scri private ScriptExecutionListener mScriptExecutionListener = new SimpleScriptExecutionListener() { @Override public void onStart(final ScriptExecution execution) { - post(() -> { - int position = mRunningTaskGroup.indexOf(execution.getEngine()); - if (position >= 0) { - mAdapter.notifyChildChanged(0, position); + post(()-> mAdapter.notifyChildInserted(0, mRunningTaskGroup.addTask(execution))); + } + + @Override + public void onSuccess(ScriptExecution execution, Object result) { + onFinish(execution); + } + + @Override + public void onException(ScriptExecution execution, Exception e) { + onFinish(execution); + } + + private void onFinish(ScriptExecution execution){ + post(()->{ + final int i = mRunningTaskGroup.removeTask(execution); + if (i >= 0) { + mAdapter.notifyChildRemoved(0, i); } else { refresh(); } }); - } }; @@ -116,7 +132,6 @@ public class TaskListRecyclerView extends ThemeColorRecyclerView implements Scri @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - mScriptEngineService.registerEngineLifecycleCallback(this); AutoJs.getInstance().getScriptEngineService().registerGlobalScriptExecutionListener(mScriptExecutionListener); mTimedTaskChangeDisposable = TimedTaskManager.getInstance().getTimeTaskChanges() .observeOn(AndroidSchedulers.mainThread()) @@ -134,7 +149,6 @@ public class TaskListRecyclerView extends ThemeColorRecyclerView implements Scri @Override protected void onDetachedFromWindow() { super.onDetachedFromWindow(); - mScriptEngineService.unregisterEngineLifecycleCallback(this); AutoJs.getInstance().getScriptEngineService().unregisterGlobalScriptExecutionListener(mScriptExecutionListener); mTimedTaskChangeDisposable.dispose(); } @@ -162,26 +176,6 @@ public class TaskListRecyclerView extends ThemeColorRecyclerView implements Scri } } - @Override - public void onEngineCreate(final ScriptEngine engine) { - post(() -> - mAdapter.notifyChildInserted(0, mRunningTaskGroup.addTask(engine)) - ); - } - - @Override - public void onEngineRemove(final ScriptEngine engine) { - post(() -> { - final int i = mRunningTaskGroup.removeTask(engine); - if (i >= 0) { - mAdapter.notifyChildRemoved(0, i); - } else { - refresh(); - } - - }); - } - private class Adapter extends ExpandableRecyclerAdapter { public Adapter(@NonNull List parentList) { diff --git a/app/src/main/res/drawable-xhdpi/ic_debug.png b/app/src/main/res/drawable-xhdpi/ic_debug.png new file mode 100644 index 00000000..c766543b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_debug.png differ diff --git a/app/src/main/res/drawable/ic_debug_step_into.png b/app/src/main/res/drawable/ic_debug_step_into.png new file mode 100644 index 00000000..bf7f1a2a Binary files /dev/null and b/app/src/main/res/drawable/ic_debug_step_into.png differ diff --git a/app/src/main/res/drawable/ic_debug_step_out.png b/app/src/main/res/drawable/ic_debug_step_out.png new file mode 100644 index 00000000..468e1975 Binary files /dev/null and b/app/src/main/res/drawable/ic_debug_step_out.png differ diff --git a/app/src/main/res/drawable/ic_debug_step_over.png b/app/src/main/res/drawable/ic_debug_step_over.png new file mode 100644 index 00000000..3860385c Binary files /dev/null and b/app/src/main/res/drawable/ic_debug_step_over.png differ diff --git a/app/src/main/res/layout/code_editor.xml b/app/src/main/res/layout/code_editor.xml index 85c20411..72b09fd7 100644 --- a/app/src/main/res/layout/code_editor.xml +++ b/app/src/main/res/layout/code_editor.xml @@ -5,6 +5,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:scrollHorizontally="true" + android:imeOptions="flagNoExtractUi" android:textColor="@android:color/transparent" android:textCursorDrawable="@drawable/code_edit_text_cursor" android:textSize="15sp"/> diff --git a/app/src/main/res/layout/editor_view.xml b/app/src/main/res/layout/editor_view.xml index c1808bf7..29112e7c 100644 --- a/app/src/main/res/layout/editor_view.xml +++ b/app/src/main/res/layout/editor_view.xml @@ -19,89 +19,11 @@ android:title="@string/_app_name" app:popupTheme="@style/AppTheme.PopupOverlay"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:layout_gravity="right"/> diff --git a/app/src/main/res/layout/fragment_debug_toolbar.xml b/app/src/main/res/layout/fragment_debug_toolbar.xml new file mode 100644 index 00000000..0831e974 --- /dev/null +++ b/app/src/main/res/layout/fragment_debug_toolbar.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_normal_toolbar.xml b/app/src/main/res/layout/fragment_normal_toolbar.xml new file mode 100644 index 00000000..a5ca5924 --- /dev/null +++ b/app/src/main/res/layout/fragment_normal_toolbar.xml @@ -0,0 +1,40 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_search_toolbar.xml b/app/src/main/res/layout/fragment_search_toolbar.xml new file mode 100644 index 00000000..7c829637 --- /dev/null +++ b/app/src/main/res/layout/fragment_search_toolbar.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/menu_editor.xml b/app/src/main/res/menu/menu_editor.xml index e2456bdd..bc7ae83f 100644 --- a/app/src/main/res/menu/menu_editor.xml +++ b/app/src/main/res/menu/menu_editor.xml @@ -79,6 +79,24 @@ + + + + + + + + + + key_script_dir_path /脚本/ 发生错误: %s + 调试 + 断点 + 启动调试 + 跳出 + 进入 + 单步 + 继续 + 停止 diff --git a/autojs/src/main/java/com/stardust/autojs/AutoJs.java b/autojs/src/main/java/com/stardust/autojs/AutoJs.java index c6fc77c1..2f95ba50 100644 --- a/autojs/src/main/java/com/stardust/autojs/AutoJs.java +++ b/autojs/src/main/java/com/stardust/autojs/AutoJs.java @@ -101,6 +101,7 @@ public abstract class AutoJs { engine.setRuntime(createRuntime()); return engine; }); + LoopBasedJavaScriptEngine.initEngine(); mScriptEngineManager.registerEngine(AutoFileSource.ENGINE, () -> new RootAutomatorEngine(mContext)); } diff --git a/autojs/src/main/java/com/stardust/autojs/ScriptEngineService.java b/autojs/src/main/java/com/stardust/autojs/ScriptEngineService.java index ffc41460..05769528 100644 --- a/autojs/src/main/java/com/stardust/autojs/ScriptEngineService.java +++ b/autojs/src/main/java/com/stardust/autojs/ScriptEngineService.java @@ -1,6 +1,8 @@ package com.stardust.autojs; import android.content.Context; +import android.os.Parcelable; +import android.support.annotation.Nullable; import com.stardust.autojs.engine.JavaScriptEngine; import com.stardust.autojs.engine.ScriptEngine; @@ -24,12 +26,18 @@ import com.stardust.util.UiHandler; import org.greenrobot.eventbus.EventBus; import org.greenrobot.eventbus.Subscribe; +import org.mozilla.javascript.RhinoException; +import org.mozilla.javascript.ScriptStackElement; +import org.mozilla.javascript.WrappedException; import java.io.BufferedReader; import java.io.IOException; import java.io.PipedReader; import java.io.PipedWriter; import java.io.PrintWriter; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Set; @@ -83,8 +91,16 @@ public class ScriptEngineService { private UiHandler mUiHandler; private final Console mGlobalConsole; private final ScriptEngineManager mScriptEngineManager; - private final EngineLifecycleObserver mEngineLifecycleObserver = new EngineLifecycleObserver(); + private final EngineLifecycleObserver mEngineLifecycleObserver = new EngineLifecycleObserver() { + + @Override + public void onEngineRemove(ScriptEngine engine) { + mScriptExecutions.remove(engine.getId()); + super.onEngineRemove(engine); + } + }; private ScriptExecutionObserver mScriptExecutionObserver = new ScriptExecutionObserver(); + private LinkedHashMap mScriptExecutions = new LinkedHashMap<>(); ScriptEngineService(ScriptEngineServiceBuilder builder) { mUiHandler = builder.mUiHandler; @@ -119,6 +135,12 @@ public class ScriptEngineService { } public ScriptExecution execute(ScriptExecutionTask task) { + ScriptExecution execution = executeInternal(task); + mScriptExecutions.put(execution.getId(), execution); + return execution; + } + + private ScriptExecution executeInternal(ScriptExecutionTask task) { if (task.getListener() != null) { task.setExecutionListener(new ScriptExecutionObserver.Wrapper(mScriptExecutionObserver, task.getListener())); } else { @@ -185,6 +207,18 @@ public class ScriptEngineService { return mScriptEngineManager.getEngines(); } + public Collection getScriptExecutions() { + return mScriptExecutions.values(); + } + + @Nullable + public ScriptExecution getScriptExecution(int id) { + if (id == ScriptExecution.NO_ID) { + return null; + } + return mScriptExecutions.get(id); + } + private static class EngineLifecycleObserver implements ScriptEngineManager.EngineLifecycleCallback { private final Set mEngineLifecycleCallbacks = new LinkedHashSet<>(); @@ -245,7 +279,17 @@ public class ScriptEngineService { } } - private static String getScriptTrace(Exception e) { + public static String getScriptTrace(Exception e) { + StringBuilder scriptTrace = new StringBuilder(); + if (e instanceof RhinoException) { + RhinoException rhinoException = (RhinoException) e; + scriptTrace.append(rhinoException.details()).append("\n"); + for (ScriptStackElement element : rhinoException.getScriptStack()) { + element.renderV8Style(scriptTrace); + scriptTrace.append("\n"); + } + scriptTrace.append("- - - - - - - - - - -\n"); + } try { PipedReader reader = new PipedReader(8192); PrintWriter writer = new PrintWriter(new PipedWriter(reader)); @@ -253,10 +297,9 @@ public class ScriptEngineService { writer.close(); BufferedReader bufferedReader = new BufferedReader(reader); String line; - StringBuilder scriptTrace = new StringBuilder(TextUtils.toEmptyIfNull(e.getMessage())); + //scriptTrace.append(TextUtils.toEmptyIfNull(e.getMessage())); while ((line = bufferedReader.readLine()) != null) { - if (line.trim().startsWith("at script")) - scriptTrace.append("\n").append(line); + scriptTrace.append("\n").append(line); } return scriptTrace.toString(); } catch (IOException e1) { diff --git a/autojs/src/main/java/com/stardust/autojs/core/looper/TimerThread.java b/autojs/src/main/java/com/stardust/autojs/core/looper/TimerThread.java index 71a75086..2f7e834c 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/looper/TimerThread.java +++ b/autojs/src/main/java/com/stardust/autojs/core/looper/TimerThread.java @@ -4,6 +4,7 @@ import android.os.Handler; import android.os.Looper; import android.support.annotation.CallSuper; +import com.stardust.autojs.ScriptEngineService; import com.stardust.autojs.engine.RhinoJavaScriptEngine; import com.stardust.autojs.runtime.ScriptBridges; import com.stardust.autojs.runtime.ScriptRuntime; @@ -48,7 +49,7 @@ public class TimerThread extends ThreadCompat { Looper.loop(); } catch (Exception e) { if (!ScriptInterruptedException.causedByInterrupted(e)) { - mRuntime.console.error(Thread.currentThread().toString() + ": " + e); + mRuntime.console.error(Thread.currentThread().toString() + ": " + ScriptEngineService.getScriptTrace(e)); } } finally { onExit(); diff --git a/autojs/src/main/java/com/stardust/autojs/core/ui/inflater/DynamicLayoutInflater.java b/autojs/src/main/java/com/stardust/autojs/core/ui/inflater/DynamicLayoutInflater.java index a7c8bf26..6d1f1e17 100644 --- a/autojs/src/main/java/com/stardust/autojs/core/ui/inflater/DynamicLayoutInflater.java +++ b/autojs/src/main/java/com/stardust/autojs/core/ui/inflater/DynamicLayoutInflater.java @@ -176,7 +176,6 @@ public class DynamicLayoutInflater { } } - @SuppressWarnings("unchecked") public View inflate(Node node, @Nullable ViewGroup parent, boolean attachToParent) { View view = doInflation(node, parent, attachToParent); if (view != null && view instanceof ShouldCallOnFinishInflate) { diff --git a/autojs/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java b/autojs/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java index 4cb03c4b..c2b6243a 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/LoopBasedJavaScriptEngine.java @@ -86,5 +86,9 @@ public class LoopBasedJavaScriptEngine extends RhinoJavaScriptEngine { super.init(); } + public static void initEngine(){ + RhinoJavaScriptEngine.initEngine(); + } + } diff --git a/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.java b/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.java index d50a8e4a..dfd1b86b 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/RhinoJavaScriptEngine.java @@ -3,6 +3,7 @@ package com.stardust.autojs.engine; import android.os.Looper; import android.util.Log; +import com.stardust.app.GlobalAppContext; import com.stardust.autojs.BuildConfig; import com.stardust.autojs.rhino.AndroidContextFactory; import com.stardust.autojs.rhino.NativeJavaClassWithPrototype; @@ -33,6 +34,7 @@ import java.io.Reader; import java.net.URI; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.concurrent.ConcurrentHashMap; @@ -43,10 +45,13 @@ import java.util.concurrent.ConcurrentHashMap; public class RhinoJavaScriptEngine extends JavaScriptEngine { + public static final String SOURCE_NAME_INIT = ""; + private static final String LOG_TAG = "RhinoJavaScriptEngine"; private static int contextCount = 0; private static StringScriptSource sInitScript; + private static final ConcurrentHashMap sContextEngineMap = new ConcurrentHashMap<>(); private Context mContext; private Scriptable mScriptable; @@ -69,7 +74,7 @@ public class RhinoJavaScriptEngine extends JavaScriptEngine { Reader reader = source.getNonNullScriptReader(); try { reader = preprocess(reader); - return mContext.evaluateReader(mScriptable, reader, "<" + source.getName() + ">", 1, null); + return mContext.evaluateReader(mScriptable, reader, source.toString(), 1, null); } catch (IOException e) { throw new UncheckedIOException(e); } @@ -90,6 +95,7 @@ public class RhinoJavaScriptEngine extends JavaScriptEngine { public synchronized void destroy() { super.destroy(); Log.d(LOG_TAG, "on destroy"); + sContextEngineMap.remove(getContext()); Context.exit(); contextCount--; Log.d(LOG_TAG, "contextCount = " + contextCount); @@ -105,7 +111,7 @@ public class RhinoJavaScriptEngine extends JavaScriptEngine { mThread = Thread.currentThread(); ScriptableObject.putProperty(mScriptable, "__engine__", this); initRequireBuilder(mContext, mScriptable); - mContext.evaluateString(mScriptable, getInitScript().getScript(), "", 1, null); + mContext.evaluateString(mScriptable, getInitScript().getScript(), SOURCE_NAME_INIT, 1, null); } private JavaScriptSource getInitScript() { @@ -148,12 +154,10 @@ public class RhinoJavaScriptEngine extends JavaScriptEngine { } public Context createContext() { - if (!ContextFactory.hasExplicitGlobal()) { - ContextFactory.initGlobal(new InterruptibleAndroidContextFactory(new File(mAndroidContext.getCacheDir(), "classes"))); - } Context context = new RhinoAndroidHelper(mAndroidContext).enterContext(); contextCount++; setupContext(context); + sContextEngineMap.put(context, this); return context; } @@ -164,6 +168,17 @@ public class RhinoJavaScriptEngine extends JavaScriptEngine { context.setWrapFactory(new WrapFactory()); } + public static void initEngine() { + if (!ContextFactory.hasExplicitGlobal()) { + android.content.Context context = GlobalAppContext.get(); + ContextFactory.initGlobal(new InterruptibleAndroidContextFactory(new File(context.getCacheDir(), "classes"))); + } + } + + public static RhinoJavaScriptEngine getEngineOfContext(Context context) { + return sContextEngineMap.get(context); + } + private class WrapFactory extends org.mozilla.javascript.WrapFactory { diff --git a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.java b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.java index 010a304e..f0a2c09a 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngine.java @@ -2,17 +2,18 @@ package com.stardust.autojs.engine; import android.support.annotation.CallSuper; -import com.stardust.autojs.runtime.exception.ScriptException; +import com.stardust.autojs.execution.ScriptExecution; import com.stardust.autojs.script.ScriptSource; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; /** * Created by Stardust on 2017/4/2. *

*

- * A ScriptEngine is created by {@link ScriptEngineManager#createEngine(String)} ()}, and then can be + * A ScriptEngine is created by {@link ScriptEngineManager#createEngine(String, int)} ()}, and then can be * used to execute script with {@link ScriptEngine#execute(ScriptSource)} in the **same** thread. * When the execution finish successfully, the engine should be destroy in the thread that created it. *

@@ -46,6 +47,9 @@ public interface ScriptEngine { Exception getUncaughtException(); + void setId(int id); + + int getId(); /** * @hide @@ -68,7 +72,7 @@ public interface ScriptEngine { private OnDestroyListener mOnDestroyListener; private boolean mDestroyed = false; private Exception mUncaughtException; - + private volatile AtomicInteger mId = new AtomicInteger(ScriptExecution.NO_ID); @Override public synchronized void setTag(String key, Object value) { @@ -116,5 +120,15 @@ public interface ScriptEngine { public Exception getUncaughtException() { return mUncaughtException; } + + @Override + public void setId(int id) { + mId.compareAndSet(ScriptExecution.NO_ID, id); + } + + @Override + public int getId() { + return mId.get(); + } } } diff --git a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java index c73a8cda..91507838 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineManager.java @@ -4,6 +4,7 @@ import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; +import com.stardust.autojs.execution.ScriptExecution; import com.stardust.autojs.script.ScriptSource; import com.stardust.util.Supplier; @@ -96,37 +97,34 @@ public class ScriptEngineManager { @Nullable - public ScriptEngine createEngine(String name) { + public ScriptEngine createEngine(String name, int id) { Supplier s = mEngineSuppliers.get(name); if (s == null) { return null; } ScriptEngine engine = s.get(); + engine.setId(id); putProperties(engine); addEngine(engine); return engine; } @Nullable - public ScriptEngine createEngineOfSource(ScriptSource source) { - return createEngine(source.getEngineName()); + public ScriptEngine createEngineOfSource(ScriptSource source, int id) { + return createEngine(source.getEngineName(), id); } - @NonNull - public ScriptEngine createEngineByNameOrThrow(String name) { - ScriptEngine engine = createEngine(name); + public ScriptEngine createEngineOfSourceOrThrow(ScriptSource source, int id) { + ScriptEngine engine = createEngineOfSource(source, id); if (engine == null) - throw new ScriptEngineFactory.EngineNotFoundException("name: " + name); + throw new ScriptEngineFactory.EngineNotFoundException("source: " + source.toString()); return engine; } @NonNull public ScriptEngine createEngineOfSourceOrThrow(ScriptSource source) { - ScriptEngine engine = createEngineOfSource(source); - if (engine == null) - throw new ScriptEngineFactory.EngineNotFoundException("source: " + source.toString()); - return engine; + return createEngineOfSourceOrThrow(source, ScriptExecution.NO_ID); } public void registerEngine(String name, Supplier supplier) { diff --git a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineProxy.java b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineProxy.java index d59e68ac..298b2323 100644 --- a/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineProxy.java +++ b/autojs/src/main/java/com/stardust/autojs/engine/ScriptEngineProxy.java @@ -77,4 +77,14 @@ public class ScriptEngineProxy implements ScriptEngine urlToSourceInfo = + Collections.synchronizedMap(new HashMap()); + + /** + * Table mapping function names to information about the function. + */ + private final Map functionNames = + Collections.synchronizedMap(new HashMap()); + + /** + * Table mapping functions to information about the function. + */ + private final Map functionToSource = + Collections.synchronizedMap(new HashMap()); + + /** + * ContextFactory.Listener instance attached to {@link #contextFactory}. + */ + private DimIProxy listener; + + /** + * Sets the GuiCallback object to use. + */ + public void setGuiCallback(DebugCallback callback) { + this.callback = callback; + } + + /** + * Tells the debugger to break at the next opportunity. + */ + public void setBreak() { + this.breakFlag = true; + } + + /** + * Sets the ScopeProvider to be used. + */ + public void setScopeProvider(ScopeProvider scopeProvider) { + this.scopeProvider = scopeProvider; + } + + /** + * Sets the ScopeProvider to be used. + */ + public void setSourceProvider(final SourceProvider sourceProvider) { + this.sourceProvider = sourceProvider; + } + + /** + * Switches context to the stack frame with the given index. + */ + public void contextSwitch(int frameIndex) { + this.frameIndex = frameIndex; + } + + /** + * Sets whether the debugger should break on exceptions. + */ + public void setBreakOnExceptions(boolean breakOnExceptions) { + this.breakOnExceptions = breakOnExceptions; + } + + /** + * Sets whether the debugger should break on function entering. + */ + public void setBreakOnEnter(boolean breakOnEnter) { + this.breakOnEnter = breakOnEnter; + } + + /** + * Sets whether the debugger should break on function return. + */ + public void setBreakOnReturn(boolean breakOnReturn) { + this.breakOnReturn = breakOnReturn; + } + + /** + * Attaches the debugger to the given ContextFactory. + */ + public void attachTo(ScriptEngineService scriptEngineService, ContextFactory factory) { + detach(); + this.contextFactory = factory; + this.scriptEngineService = scriptEngineService; + this.listener = new DimIProxy(this, IPROXY_LISTEN); + scriptEngineService.registerEngineLifecycleCallback(this.listener); + } + + /** + * Detaches the debugger from the current ContextFactory. + */ + public void detach() { + if (listener != null) { + scriptEngineService.unregisterEngineLifecycleCallback(listener); + contextFactory = null; + scriptEngineService = null; + listener = null; + } + } + + /** + * Releases resources associated with this debugger. + */ + public void dispose() { + detach(); + } + + /** + * Returns the FunctionSource object for the given script or function. + */ + private FunctionSource getFunctionSource(DebuggableScript fnOrScript) { + FunctionSource fsource = functionSource(fnOrScript); + if (fsource == null) { + String url = getNormalizedUrl(fnOrScript); + SourceInfo si = sourceInfo(url); + if (si == null) { + if (!fnOrScript.isGeneratedScript()) { + // Not eval or Function, try to load it from URL + String source = loadSource(url); + if (source != null) { + DebuggableScript top = fnOrScript; + for (; ; ) { + DebuggableScript parent = top.getParent(); + if (parent == null) { + break; + } + top = parent; + } + registerTopScript(top, source); + fsource = functionSource(fnOrScript); + } + } + } + } + return fsource; + } + + /** + * Loads the script at the given URL. + */ + private String loadSource(String sourceUrl) { + String source = null; + int hash = sourceUrl.indexOf('#'); + if (hash >= 0) { + sourceUrl = sourceUrl.substring(0, hash); + } + try { + InputStream is; + openStream: + { + if (sourceUrl.indexOf(':') < 0) { + // Can be a file name + try { + if (sourceUrl.startsWith("~/")) { + String home = SecurityUtilities.getSystemProperty("user.home"); + if (home != null) { + String pathFromHome = sourceUrl.substring(2); + File f = new File(new File(home), pathFromHome); + if (f.exists()) { + is = new FileInputStream(f); + break openStream; + } + } + } + File f = new File(sourceUrl); + if (f.exists()) { + is = new FileInputStream(f); + break openStream; + } + } catch (SecurityException ex) { + } + // No existing file, assume missed http:// + if (sourceUrl.startsWith("//")) { + sourceUrl = "http:" + sourceUrl; + } else if (sourceUrl.startsWith("/")) { + sourceUrl = "http://127.0.0.1" + sourceUrl; + } else { + sourceUrl = "http://" + sourceUrl; + } + } + + is = (new URL(sourceUrl)).openStream(); + } + + try { + source = Kit.readReader(new InputStreamReader(is)); + } finally { + is.close(); + } + } catch (IOException ex) { + System.err.println + ("Failed to load source from " + sourceUrl + ": " + ex); + } + return source; + } + + /** + * Registers the given script as a top-level script in the debugger. + */ + private void registerTopScript(DebuggableScript topScript, String source) { + if (!topScript.isTopLevel()) { + throw new IllegalArgumentException(); + } + String url = getNormalizedUrl(topScript); + DebuggableScript[] functions = getAllFunctions(topScript); + if (sourceProvider != null) { + final String providedSource = sourceProvider.getSource(topScript); + if (providedSource != null) { + source = providedSource; + } + } + + final SourceInfo sourceInfo = new SourceInfo(source, functions, url); + + synchronized (urlToSourceInfo) { + SourceInfo old = urlToSourceInfo.get(url); + if (old != null) { + sourceInfo.copyBreakpointsFrom(old); + } + urlToSourceInfo.put(url, sourceInfo); + for (int i = 0; i != sourceInfo.functionSourcesTop(); ++i) { + FunctionSource fsource = sourceInfo.functionSource(i); + String name = fsource.name(); + if (name.length() != 0) { + functionNames.put(name, fsource); + } + } + } + + synchronized (functionToSource) { + for (int i = 0; i != functions.length; ++i) { + FunctionSource fsource = sourceInfo.functionSource(i); + functionToSource.put(functions[i], fsource); + } + } + + callback.updateSourceText(sourceInfo); + } + + /** + * Returns the FunctionSource object for the given function or script. + */ + private FunctionSource functionSource(DebuggableScript fnOrScript) { + return functionToSource.get(fnOrScript); + } + + /** + * Returns an array of all function names. + */ + public String[] functionNames() { + synchronized (urlToSourceInfo) { + return functionNames.keySet().toArray(new String[functionNames.size()]); + } + } + + /** + * Returns the FunctionSource object for the function with the given name. + */ + public FunctionSource functionSourceByName(String functionName) { + return functionNames.get(functionName); + } + + /** + * Returns the SourceInfo object for the given URL. + */ + public SourceInfo sourceInfo(String url) { + return urlToSourceInfo.get(url); + } + + /** + * Returns the source URL for the given script or function. + */ + private String getNormalizedUrl(DebuggableScript fnOrScript) { + String url = fnOrScript.getSourceName(); + if (url == null) { + url = ""; + } else { + // Not to produce window for eval from different lines, + // strip line numbers, i.e. replace all #[0-9]+\(eval\) by + // (eval) + // Option: similar teatment for Function? + char evalSeparator = '#'; + StringBuilder sb = null; + int urlLength = url.length(); + int cursor = 0; + for (; ; ) { + int searchStart = url.indexOf(evalSeparator, cursor); + if (searchStart < 0) { + break; + } + String replace = null; + int i = searchStart + 1; + while (i != urlLength) { + int c = url.charAt(i); + if (!('0' <= c && c <= '9')) { + break; + } + ++i; + } + if (i != searchStart + 1) { + // i points after #[0-9]+ + if ("(eval)".regionMatches(0, url, i, 6)) { + cursor = i + 6; + replace = "(eval)"; + } + } + if (replace == null) { + break; + } + if (sb == null) { + sb = new StringBuilder(); + sb.append(url.substring(0, searchStart)); + } + sb.append(replace); + } + if (sb != null) { + if (cursor != urlLength) { + sb.append(url.substring(cursor)); + } + url = sb.toString(); + } + } + return url; + } + + /** + * Returns an array of all functions in the given script. + */ + private static DebuggableScript[] getAllFunctions + (DebuggableScript function) { + ObjArray functions = new ObjArray(); + collectFunctions_r(function, functions); + DebuggableScript[] result = new DebuggableScript[functions.size()]; + functions.toArray(result); + return result; + } + + /** + * Helper function for {@link #getAllFunctions(DebuggableScript)}. + */ + private static void collectFunctions_r(DebuggableScript function, + ObjArray array) { + array.add(function); + for (int i = 0; i != function.getFunctionCount(); ++i) { + collectFunctions_r(function.getFunction(i), array); + } + } + + /** + * Clears all breakpoints. + */ + public void clearAllBreakpoints() { + for (SourceInfo si : urlToSourceInfo.values()) { + si.removeAllBreakpoints(); + } + } + + /** + * Called when a breakpoint has been hit. + */ + private void handleBreakpointHit(StackFrame frame, Context cx) { + breakFlag = false; + interrupted(cx, frame, null); + } + + /** + * Called when a script exception has been thrown. + */ + private void handleExceptionThrown(Context cx, Throwable ex, + StackFrame frame) { + if (breakOnExceptions) { + ContextData cd = frame.contextData(); + if (cd.lastProcessedException != ex) { + interrupted(cx, frame, ex); + cd.lastProcessedException = ex; + } + } + } + + /** + * Returns the current ContextData object. + */ + public ContextData currentContextData() { + return interruptedContextData; + } + + /** + * Sets the action to perform to end interruption. + */ + public void setReturnValue(int returnValue) { + synchronized (monitor) { + this.returnValue = returnValue; + monitor.notify(); + } + } + + /** + * Resumes execution of script. + */ + public void go() { + synchronized (monitor) { + this.returnValue = GO; + monitor.notifyAll(); + } + } + + /** + * Evaluates the given script. + */ + public String eval(String expr) { + String result = "undefined"; + if (expr == null) { + return result; + } + ContextData contextData = currentContextData(); + if (contextData == null || frameIndex >= contextData.frameCount()) { + return result; + } + StackFrame frame = contextData.getFrame(frameIndex); + if (contextData.eventThreadFlag) { + Context cx = Context.getCurrentContext(); + result = do_eval(cx, frame, expr); + } else { + synchronized (monitor) { + if (insideInterruptLoop) { + evalRequest = expr; + evalFrame = frame; + monitor.notify(); + do { + try { + monitor.wait(); + } catch (InterruptedException exc) { + Thread.currentThread().interrupt(); + break; + } + } while (evalRequest != null); + result = evalResult; + } + } + } + return result; + } + + /** + * Compiles the given script. + */ + public void compileScript(String url, String text) { + DimIProxy action = new DimIProxy(this, IPROXY_COMPILE_SCRIPT); + action.url = url; + action.text = text; + action.withContext(); + } + + /** + * Evaluates the given script. + */ + public void evalScript(final String url, final String text) { + DimIProxy action = new DimIProxy(this, IPROXY_EVAL_SCRIPT); + action.url = url; + action.text = text; + action.withContext(); + } + + /** + * Converts the given script object to a string. + */ + public String objectToString(Object object) { + DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_TO_STRING); + action.object = object; + action.withContext(); + return action.stringResult; + } + + /** + * Returns whether the given string is syntactically valid script. + */ + public boolean stringIsCompilableUnit(String str) { + DimIProxy action = new DimIProxy(this, IPROXY_STRING_IS_COMPILABLE); + action.text = str; + action.withContext(); + return action.booleanResult; + } + + /** + * Returns the value of a property on the given script object. + */ + public Object getObjectProperty(Object object, Object id) { + DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_PROPERTY); + action.object = object; + action.id = id; + action.withContext(); + return action.objectResult; + } + + /** + * Returns an array of the property names on the given script object. + */ + public Object[] getObjectIds(Object object) { + DimIProxy action = new DimIProxy(this, IPROXY_OBJECT_IDS); + action.object = object; + action.withContext(); + return action.objectArrayResult; + } + + /** + * Returns the value of a property on the given script object. + */ + private Object getObjectPropertyImpl(Context cx, Object object, + Object id) { + Scriptable scriptable = (Scriptable) object; + Object result; + if (id instanceof String) { + String name = (String) id; + if (name.equals("this")) { + result = scriptable; + } else if (name.equals("__proto__")) { + result = scriptable.getPrototype(); + } else if (name.equals("__parent__")) { + result = scriptable.getParentScope(); + } else { + result = ScriptableObject.getProperty(scriptable, name); + if (result == ScriptableObject.NOT_FOUND) { + result = Undefined.instance; + } + } + } else { + int index = ((Integer) id).intValue(); + result = ScriptableObject.getProperty(scriptable, index); + if (result == ScriptableObject.NOT_FOUND) { + result = Undefined.instance; + } + } + return result; + } + + /** + * Returns an array of the property names on the given script object. + */ + private Object[] getObjectIdsImpl(Context cx, Object object) { + if (!(object instanceof Scriptable) || object == Undefined.instance) { + return Context.emptyArgs; + } + + Object[] ids; + Scriptable scriptable = (Scriptable) object; + if (scriptable instanceof DebuggableObject) { + ids = ((DebuggableObject) scriptable).getAllIds(); + } else { + ids = scriptable.getIds(); + } + + Scriptable proto = scriptable.getPrototype(); + Scriptable parent = scriptable.getParentScope(); + int extra = 0; + if (proto != null) { + ++extra; + } + if (parent != null) { + ++extra; + } + if (extra != 0) { + Object[] tmp = new Object[extra + ids.length]; + System.arraycopy(ids, 0, tmp, extra, ids.length); + ids = tmp; + extra = 0; + if (proto != null) { + ids[extra++] = "__proto__"; + } + if (parent != null) { + ids[extra++] = "__parent__"; + } + } + + return ids; + } + + /** + * Interrupts script execution. + */ + private void interrupted(Context cx, final StackFrame frame, + Throwable scriptException) { + ContextData contextData = frame.contextData(); + boolean eventThreadFlag = callback.isGuiEventThread(); + contextData.eventThreadFlag = eventThreadFlag; + + boolean recursiveEventThreadCall = false; + + interruptedCheck: + synchronized (eventThreadMonitor) { + if (eventThreadFlag) { + if (interruptedContextData != null) { + recursiveEventThreadCall = true; + break interruptedCheck; + } + } else { + while (interruptedContextData != null) { + try { + eventThreadMonitor.wait(); + } catch (InterruptedException exc) { + return; + } + } + } + interruptedContextData = contextData; + } + + if (recursiveEventThreadCall) { + // XXX: For now the following is commented out as on Linux + // too deep recursion of dispatchNextGuiEvent causes GUI lockout. + // Note: it can make GUI unresponsive if long-running script + // will be called on GUI thread while processing another interrupt + if (false) { + // Run event dispatch until gui sets a flag to exit the initial + // call to interrupted. + while (this.returnValue == -1) { + try { + callback.dispatchNextGuiEvent(); + } catch (InterruptedException exc) { + } + } + } + return; + } + + if (interruptedContextData == null) Kit.codeBug(); + + try { + do { + int frameCount = contextData.frameCount(); + this.frameIndex = frameCount - 1; + + final String threadTitle = Thread.currentThread().toString(); + final String alertMessage; + if (scriptException == null) { + alertMessage = null; + } else { + alertMessage = scriptException.toString(); + } + + int returnValue = -1; + if (!eventThreadFlag) { + synchronized (monitor) { + if (insideInterruptLoop) Kit.codeBug(); + this.insideInterruptLoop = true; + this.evalRequest = null; + this.returnValue = -1; + callback.enterInterrupt(frame, threadTitle, + alertMessage); + try { + for (; ; ) { + try { + monitor.wait(); + } catch (InterruptedException exc) { + Thread.currentThread().interrupt(); + break; + } + if (evalRequest != null) { + this.evalResult = null; + try { + evalResult = do_eval(cx, evalFrame, + evalRequest); + } finally { + evalRequest = null; + evalFrame = null; + monitor.notify(); + } + continue; + } + if (this.returnValue != -1) { + returnValue = this.returnValue; + break; + } + } + } finally { + insideInterruptLoop = false; + } + } + } else { + this.returnValue = -1; + callback.enterInterrupt(frame, threadTitle, alertMessage); + while (this.returnValue == -1) { + try { + callback.dispatchNextGuiEvent(); + } catch (InterruptedException exc) { + } + } + returnValue = this.returnValue; + } + switch (returnValue) { + case STEP_OVER: + contextData.breakNextLine = true; + contextData.stopAtFrameDepth = contextData.frameCount(); + break; + case STEP_INTO: + contextData.breakNextLine = true; + contextData.stopAtFrameDepth = -1; + break; + case STEP_OUT: + if (contextData.frameCount() > 1) { + contextData.breakNextLine = true; + contextData.stopAtFrameDepth + = contextData.frameCount() - 1; + } + break; + } + } while (false); + } finally { + synchronized (eventThreadMonitor) { + interruptedContextData = null; + eventThreadMonitor.notifyAll(); + } + } + + } + + /** + * Evaluates script in the given stack frame. + */ + private static String do_eval(Context cx, StackFrame frame, String expr) { + String resultString; + Debugger saved_debugger = cx.getDebugger(); + Object saved_data = cx.getDebuggerContextData(); + int saved_level = cx.getOptimizationLevel(); + + cx.setDebugger(null, null); + cx.setOptimizationLevel(-1); + cx.setGeneratingDebug(false); + try { + Callable script = (Callable) cx.compileString(expr, "", 0, null); + Object result = script.call(cx, frame.scope, frame.thisObj, + ScriptRuntime.emptyArgs); + if (result == Undefined.instance) { + resultString = ""; + } else { + resultString = ScriptRuntime.toString(result); + } + } catch (Exception exc) { + resultString = exc.getMessage(); + } finally { + cx.setGeneratingDebug(true); + cx.setOptimizationLevel(saved_level); + cx.setDebugger(saved_debugger, saved_data); + } + if (resultString == null) { + resultString = "null"; + } + return resultString; + } + + /** + * Proxy class to implement debug interfaces without bloat of class + * files. + */ + private class DimIProxy + implements ContextAction, ScriptEngineManager.EngineLifecycleCallback, Debugger { + + /** + * The debugger. + */ + private Dim dim; + + /** + * The interface implementation type. One of the IPROXY_* constants + * defined in {@link Dim}. + */ + private int type; + + /** + * The URL origin of the script to compile or evaluate. + */ + private String url; + + /** + * The text of the script to compile, evaluate or test for compilation. + */ + private String text; + + /** + * The object to convert, get a property from or enumerate. + */ + private Object object; + + /** + * The property to look up in {@link #object}. + */ + private Object id; + + /** + * The boolean result of the action. + */ + private boolean booleanResult; + + /** + * The String result of the action. + */ + private String stringResult; + + /** + * The Object result of the action. + */ + private Object objectResult; + + /** + * The Object[] result of the action. + */ + private Object[] objectArrayResult; + + /** + * Creates a new DimIProxy. + */ + private DimIProxy(Dim dim, int type) { + this.dim = dim; + this.type = type; + } + + // ContextAction + + /** + * Performs the action given by {@link #type}. + */ + public Object run(Context cx) { + switch (type) { + case IPROXY_COMPILE_SCRIPT: + cx.compileString(text, url, 1, null); + break; + + case IPROXY_EVAL_SCRIPT: { + Scriptable scope = null; + if (dim.scopeProvider != null) { + scope = dim.scopeProvider.getScope(); + } + if (scope == null) { + scope = new ImporterTopLevel(cx); + } + cx.evaluateString(scope, text, url, 1, null); + } + break; + + case IPROXY_STRING_IS_COMPILABLE: + booleanResult = cx.stringIsCompilableUnit(text); + break; + + case IPROXY_OBJECT_TO_STRING: + if (object == Undefined.instance) { + stringResult = "undefined"; + } else if (object == null) { + stringResult = "null"; + } else if (object instanceof NativeCall) { + stringResult = "[object Call]"; + } else { + stringResult = Context.toString(object); + } + break; + + case IPROXY_OBJECT_PROPERTY: + objectResult = dim.getObjectPropertyImpl(cx, object, id); + break; + + case IPROXY_OBJECT_IDS: + objectArrayResult = dim.getObjectIdsImpl(cx, object); + break; + + default: + throw Kit.codeBug(); + } + return null; + } + + /** + * Performs the action given by {@link #type} with the attached + * {@link ContextFactory}. + */ + private void withContext() { + dim.contextFactory.call(this); + } + + @Override + public void onEngineCreate(ScriptEngine engine) { + if (type != IPROXY_LISTEN) Kit.codeBug(); + if (!(engine instanceof RhinoJavaScriptEngine) || + !callback.shouldAttachDebugger((RhinoJavaScriptEngine) engine)) { + return; + } + + Context cx = ((RhinoJavaScriptEngine) engine).getContext(); + ContextData contextData = new ContextData(); + Debugger debugger = new DimIProxy(dim, IPROXY_DEBUG); + cx.setDebugger(debugger, contextData); + cx.setGeneratingDebug(true); + cx.setOptimizationLevel(-1); + engine.setTag(TAG, Dim.this); + } + + @Override + public void onEngineRemove(ScriptEngine engine) { + if (type != IPROXY_LISTEN) Kit.codeBug(); + } + + // Debugger + + /** + * Returns a StackFrame for the given function or script. + */ + public DebugFrame getFrame(Context cx, DebuggableScript fnOrScript) { + if (type != IPROXY_DEBUG) Kit.codeBug(); + + FunctionSource item = dim.getFunctionSource(fnOrScript); + if (item == null) { + // Can not debug if source is not available + return null; + } + return new StackFrame(cx, dim, item); + } + + /** + * Called when compilation is finished. + */ + public void handleCompilationDone(Context cx, + DebuggableScript fnOrScript, + String source) { + if (type != IPROXY_DEBUG) Kit.codeBug(); + + if (!fnOrScript.isTopLevel()) { + return; + } + dim.registerTopScript(fnOrScript, source); + } + } + + /** + * Class to store information about a stack. + */ + public static class ContextData { + + /** + * The stack frames. + */ + private ObjArray frameStack = new ObjArray(); + + /** + * Whether the debugger should break at the next line in this context. + */ + private boolean breakNextLine; + + /** + * The frame depth the debugger should stop at. Used to implement + * "step over" and "step out". + */ + private int stopAtFrameDepth = -1; + + /** + * Whether this context is in the event thread. + */ + private boolean eventThreadFlag; + + /** + * The last exception that was processed. + */ + private Throwable lastProcessedException; + + /** + * Returns the ContextData for the given Context. + */ + public static ContextData get(Context cx) { + return (ContextData) cx.getDebuggerContextData(); + } + + /** + * Returns the number of stack frames. + */ + public int frameCount() { + return frameStack.size(); + } + + /** + * Returns the stack frame with the given index. + */ + public StackFrame getFrame(int frameNumber) { + int num = frameStack.size() - frameNumber - 1; + return (StackFrame) frameStack.get(num); + } + + /** + * Pushes a stack frame on to the stack. + */ + private void pushFrame(StackFrame frame) { + frameStack.push(frame); + } + + /** + * Pops a stack frame from the stack. + */ + private void popFrame() { + frameStack.pop(); + } + } + + /** + * Object to represent one stack frame. + */ + public static class StackFrame implements DebugFrame { + + /** + * The debugger. + */ + private Dim dim; + + /** + * The ContextData for the Context being debugged. + */ + private ContextData contextData; + + /** + * The scope. + */ + private Scriptable scope; + + /** + * The 'this' object. + */ + private Scriptable thisObj; + + /** + * Information about the function. + */ + private FunctionSource fsource; + + /** + * Array of breakpoint state for each source line. + */ + private boolean[] breakpoints; + + /** + * Current line number. + */ + private int lineNumber; + + /** + * Creates a new StackFrame. + */ + private StackFrame(Context cx, Dim dim, FunctionSource fsource) { + this.dim = dim; + this.contextData = ContextData.get(cx); + this.fsource = fsource; + this.breakpoints = fsource.sourceInfo().breakpoints; + this.lineNumber = fsource.firstLine(); + } + + /** + * Called when the stack frame is entered. + */ + public void onEnter(Context cx, Scriptable scope, + Scriptable thisObj, Object[] args) { + contextData.pushFrame(this); + this.scope = scope; + this.thisObj = thisObj; + if (dim.breakOnEnter) { + dim.handleBreakpointHit(this, cx); + } + } + + /** + * Called when the current position has changed. + */ + public void onLineChange(Context cx, int lineno) { + this.lineNumber = lineno; + + if (!breakpoints[lineno] && !dim.breakFlag) { + boolean lineBreak = contextData.breakNextLine; + if (lineBreak && contextData.stopAtFrameDepth >= 0) { + lineBreak = (contextData.frameCount() + <= contextData.stopAtFrameDepth); + } + if (!lineBreak) { + return; + } + contextData.stopAtFrameDepth = -1; + contextData.breakNextLine = false; + } + + dim.handleBreakpointHit(this, cx); + } + + /** + * Called when an exception has been thrown. + */ + public void onExceptionThrown(Context cx, Throwable exception) { + dim.handleExceptionThrown(cx, exception, this); + } + + /** + * Called when the stack frame has been left. + */ + public void onExit(Context cx, boolean byThrow, + Object resultOrException) { + if (dim.breakOnReturn && !byThrow) { + dim.handleBreakpointHit(this, cx); + } + contextData.popFrame(); + } + + /** + * Called when a 'debugger' statement is executed. + */ + public void onDebuggerStatement(Context cx) { + dim.handleBreakpointHit(this, cx); + } + + /** + * Returns the SourceInfo object for the function. + */ + public SourceInfo sourceInfo() { + return fsource.sourceInfo(); + } + + /** + * Returns the ContextData object for the Context. + */ + public ContextData contextData() { + return contextData; + } + + /** + * Returns the scope object for this frame. + */ + public Object scope() { + return scope; + } + + /** + * Returns the 'this' object for this frame. + */ + public Object thisObj() { + return thisObj; + } + + /** + * Returns the source URL. + */ + public String getUrl() { + return fsource.sourceInfo().url(); + } + + /** + * Returns the current line number. + */ + public int getLineNumber() { + return lineNumber; + } + + /** + * Returns the current function name. + */ + public String getFunctionName() { + return fsource.name(); + } + } + + /** + * Class to store information about a function. + */ + public static class FunctionSource { + + /** + * Information about the source of the function. + */ + private SourceInfo sourceInfo; + + /** + * Line number of the first line of the function. + */ + private int firstLine; + + /** + * The function name. + */ + private String name; + + /** + * Creates a new FunctionSource. + */ + private FunctionSource(SourceInfo sourceInfo, int firstLine, + String name) { + if (name == null) throw new IllegalArgumentException(); + this.sourceInfo = sourceInfo; + this.firstLine = firstLine; + this.name = name; + } + + /** + * Returns the SourceInfo object that describes the source of the + * function. + */ + public SourceInfo sourceInfo() { + return sourceInfo; + } + + /** + * Returns the line number of the first line of the function. + */ + public int firstLine() { + return firstLine; + } + + /** + * Returns the name of the function. + */ + public String name() { + return name; + } + } + + /** + * Class to store information about a script source. + */ + public static class SourceInfo { + + /** + * An empty array of booleans. + */ + private static final boolean[] EMPTY_BOOLEAN_ARRAY = new boolean[0]; + + /** + * The script. + */ + private String source; + + /** + * The URL of the script. + */ + private String url; + + /** + * Array indicating which lines can have breakpoints set. + */ + private boolean[] breakableLines; + + /** + * Array indicating whether a breakpoint is set on the line. + */ + private boolean[] breakpoints; + + /** + * Array of FunctionSource objects for the functions in the script. + */ + private FunctionSource[] functionSources; + + /** + * Creates a new SourceInfo object. + */ + private SourceInfo(String source, DebuggableScript[] functions, + String normilizedUrl) { + this.source = source; + this.url = normilizedUrl; + + int N = functions.length; + int[][] lineArrays = new int[N][]; + for (int i = 0; i != N; ++i) { + lineArrays[i] = functions[i].getLineNumbers(); + } + + int minAll = 0, maxAll = -1; + int[] firstLines = new int[N]; + for (int i = 0; i != N; ++i) { + int[] lines = lineArrays[i]; + if (lines == null || lines.length == 0) { + firstLines[i] = -1; + } else { + int min, max; + min = max = lines[0]; + for (int j = 1; j != lines.length; ++j) { + int line = lines[j]; + if (line < min) { + min = line; + } else if (line > max) { + max = line; + } + } + firstLines[i] = min; + if (minAll > maxAll) { + minAll = min; + maxAll = max; + } else { + if (min < minAll) { + minAll = min; + } + if (max > maxAll) { + maxAll = max; + } + } + } + } + + if (minAll > maxAll) { + // No line information + this.breakableLines = EMPTY_BOOLEAN_ARRAY; + this.breakpoints = EMPTY_BOOLEAN_ARRAY; + } else { + if (minAll < 0) { + // Line numbers can not be negative + throw new IllegalStateException(String.valueOf(minAll)); + } + int linesTop = maxAll + 1; + this.breakableLines = new boolean[linesTop]; + this.breakpoints = new boolean[linesTop]; + for (int i = 0; i != N; ++i) { + int[] lines = lineArrays[i]; + if (lines != null && lines.length != 0) { + for (int j = 0; j != lines.length; ++j) { + int line = lines[j]; + this.breakableLines[line] = true; + } + } + } + } + this.functionSources = new FunctionSource[N]; + for (int i = 0; i != N; ++i) { + String name = functions[i].getFunctionName(); + if (name == null) { + name = ""; + } + this.functionSources[i] + = new FunctionSource(this, firstLines[i], name); + } + } + + /** + * Returns the source text. + */ + public String source() { + return this.source; + } + + /** + * Returns the script's origin URL. + */ + public String url() { + return this.url; + } + + /** + * Returns the number of FunctionSource objects stored in this object. + */ + public int functionSourcesTop() { + return functionSources.length; + } + + /** + * Returns the FunctionSource object with the given index. + */ + public FunctionSource functionSource(int i) { + return functionSources[i]; + } + + /** + * Copies the breakpoints from the given SourceInfo object into this + * one. + */ + private void copyBreakpointsFrom(SourceInfo old) { + int end = old.breakpoints.length; + if (end > this.breakpoints.length) { + end = this.breakpoints.length; + } + for (int line = 0; line != end; ++line) { + if (old.breakpoints[line]) { + this.breakpoints[line] = true; + } + } + } + + /** + * Returns whether the given line number can have a breakpoint set on + * it. + */ + public boolean breakableLine(int line) { + return (line < this.breakableLines.length) + && this.breakableLines[line]; + } + + /** + * Returns whether there is a breakpoint set on the given line. + */ + public boolean breakpoint(int line) { + if (!breakableLine(line)) { + throw new IllegalArgumentException(String.valueOf(line)); + } + return line < this.breakpoints.length && this.breakpoints[line]; + } + + /** + * Sets or clears the breakpoint flag for the given line. + */ + public boolean breakpoint(int line, boolean value) { + if (!breakableLine(line)) { + throw new IllegalArgumentException(String.valueOf(line)); + } + boolean changed; + synchronized (breakpoints) { + if (breakpoints[line] != value) { + breakpoints[line] = value; + changed = true; + } else { + changed = false; + } + } + return changed; + } + + /** + * Removes all breakpoints from the script. + */ + public void removeAllBreakpoints() { + synchronized (breakpoints) { + for (int line = 0; line != breakpoints.length; ++line) { + breakpoints[line] = false; + } + } + } + } +} \ No newline at end of file diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java b/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java index b91b0157..b90069d5 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java +++ b/autojs/src/main/java/com/stardust/autojs/runtime/ScriptRuntime.java @@ -372,7 +372,7 @@ public class ScriptRuntime { try { events.emit("exit"); } catch (Exception ignored) { - console.error("exception on exit: " + ignored); + console.error("exception on exit: " + ScriptEngineService.getScriptTrace(ignored)); } ignoresException(threads::shutDownAll); ignoresException(events::recycle); diff --git a/autojs/src/main/java/com/stardust/autojs/runtime/exception/ScriptException.java b/autojs/src/main/java/com/stardust/autojs/runtime/exception/ScriptException.java index e9022c46..4d3774d5 100644 --- a/autojs/src/main/java/com/stardust/autojs/runtime/exception/ScriptException.java +++ b/autojs/src/main/java/com/stardust/autojs/runtime/exception/ScriptException.java @@ -3,11 +3,14 @@ package com.stardust.autojs.runtime.exception; import org.mozilla.javascript.EvaluatorException; import org.mozilla.javascript.RhinoException; +import java.util.concurrent.atomic.AtomicInteger; + /** * Created by Stardust on 2017/1/29. */ public class ScriptException extends RuntimeException { + public ScriptException(String message, Throwable cause) { super(message, cause); } diff --git a/common/release/output.json b/common/release/output.json index fc21bded..8103d30f 100644 --- a/common/release/output.json +++ b/common/release/output.json @@ -1 +1 @@ -[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":401},"path":"commonRelease-4.0.0 Alpha1.apk","properties":{"packageId":"org.autojs.autojs","split":"","minSdkVersion":"17"}}] \ No newline at end of file +[{"outputType":{"type":"APK"},"apkInfo":{"type":"MAIN","splits":[],"versionCode":408},"path":"commonRelease-4.0.2 Alpha3.apk","properties":{"packageId":"org.autojs.autojs","split":"","minSdkVersion":"17"}}] \ No newline at end of file diff --git a/inrt/src/main/java/com/stardust/auojs/inrt/launch/AssetsProjectLauncher.java b/inrt/src/main/java/com/stardust/auojs/inrt/launch/AssetsProjectLauncher.java index cc01165d..fd931d09 100644 --- a/inrt/src/main/java/com/stardust/auojs/inrt/launch/AssetsProjectLauncher.java +++ b/inrt/src/main/java/com/stardust/auojs/inrt/launch/AssetsProjectLauncher.java @@ -10,6 +10,7 @@ import com.stardust.auojs.inrt.BuildConfig; import com.stardust.auojs.inrt.LogActivity; import com.stardust.auojs.inrt.Pref; import com.stardust.auojs.inrt.autojs.AutoJs; +import com.stardust.autojs.ScriptEngineService; import com.stardust.autojs.execution.ExecutionConfig; import com.stardust.autojs.execution.ScriptExecution; import com.stardust.autojs.project.ProjectConfig; @@ -73,7 +74,7 @@ public class AssetsProjectLauncher { mScriptExecution = AutoJs.getInstance().getScriptEngineService().execute(source, new ExecutionConfig() .executePath(mProjectDir)); } catch (Exception e) { - AutoJs.getInstance().getGlobalConsole().error(e); + AutoJs.getInstance().getGlobalConsole().error(ScriptEngineService.getScriptTrace(e)); } }