From 9114821fb12558874e01421bf38b0d34fb39df72 Mon Sep 17 00:00:00 2001 From: The Cobra Chicken <38409554+TheCobraChicken@users.noreply.github.com> Date: Thu, 8 Sep 2022 04:18:55 -1000 Subject: [PATCH] feat(debugger): add logcat output (#1411)(PR #1666) * Adding logcatController class and writing adb / debugger panel information to the controller. * Finished parsing logcat binary output and writing an arraylist containing all events. * added highlighting of logcat output based on type. Added timestamp parsing. * Updated code to only get new log messages. * Added additional code for select all * Completed Check and uncheckall options. * Changed log highlighting to log color. Changed from JTextArea to JTextPane. Logcat pane will now autoscroll only if it is already scrolled to the bottom. Debugger exit will now stop logcat as well. * Moved labels into NLS rather than using hardcoded strings. * Implemented the ability to autoselect attached process. Changed the formatting of logcat messages. * Moved labels into NLS rather than using hardcoded strings. * updating to use info getter methods rather than directly accessing variable * Added Logcat Pause Button * Added Clear button * Updated clear icon * Cleaning warnings * cleaning * Changed behavior to only show logcat for debugged process to start with. * cleaning * cleaning * cleaning * applying spotless * Fixing bug with switch * fixed formatting issue * add missing localization strings Co-authored-by: green9317 <38409554+green9317@users.noreply.github.com> Co-authored-by: TheCobraChicken Co-authored-by: Skylot --- .../gui/device/debugger/LogcatController.java | 390 +++++++++++++++ .../jadx/gui/device/protocol/ADBDevice.java | 53 ++- .../java/jadx/gui/ui/dialog/ADBDialog.java | 4 +- .../jadx/gui/ui/panel/JDebuggerPanel.java | 23 +- .../java/jadx/gui/ui/panel/LogcatPanel.java | 450 ++++++++++++++++++ .../resources/i18n/Messages_de_DE.properties | 20 + .../resources/i18n/Messages_en_US.properties | 22 +- .../resources/i18n/Messages_es_ES.properties | 20 + .../resources/i18n/Messages_ko_KR.properties | 20 + .../resources/i18n/Messages_pt_BR.properties | 20 + .../resources/i18n/Messages_zh_CN.properties | 20 + .../resources/i18n/Messages_zh_TW.properties | 20 + .../main/resources/icons/debugger/trash.svg | 3 + 13 files changed, 1056 insertions(+), 9 deletions(-) create mode 100644 jadx-gui/src/main/java/jadx/gui/device/debugger/LogcatController.java create mode 100644 jadx-gui/src/main/java/jadx/gui/ui/panel/LogcatPanel.java create mode 100644 jadx-gui/src/main/resources/icons/debugger/trash.svg diff --git a/jadx-gui/src/main/java/jadx/gui/device/debugger/LogcatController.java b/jadx-gui/src/main/java/jadx/gui/device/debugger/LogcatController.java new file mode 100644 index 00000000..70a1a6ef --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/device/debugger/LogcatController.java @@ -0,0 +1,390 @@ +package jadx.gui.device.debugger; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.time.Instant; +import java.time.ZoneId; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Timer; +import java.util.TimerTask; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.device.protocol.ADBDevice; +import jadx.gui.ui.panel.LogcatPanel; + +public class LogcatController { + private static final Logger LOG = LoggerFactory.getLogger(LogcatController.class); + + private final ADBDevice adbDevice; + private final LogcatPanel logcatPanel; + private Timer timer; + private final String timezone; + private LogcatInfo recent = null; + private ArrayList events = new ArrayList<>(); + private LogcatFilter filter = new LogcatFilter(null, null); + private String status = "null"; + + public LogcatController(LogcatPanel logcatPanel, ADBDevice adbDevice) throws IOException { + this.adbDevice = adbDevice; + this.logcatPanel = logcatPanel; + this.timezone = adbDevice.getTimezone(); + this.startLogcat(); + } + + public void startLogcat() { + timer = new Timer(); + timer.schedule(new TimerTask() { + @Override + public void run() { + getLog(); + } + }, 0, 1000); + this.status = "running"; + } + + public void stopLogcat() { + timer.cancel(); + this.status = "stopped"; + } + + public String getStatus() { + return this.status; + } + + public void clearLogcat() { + try { + adbDevice.clearLogcat(); + clearEvents(); + } catch (IOException e) { + LOG.error("Failed to clear Logcat", e); + } + } + + private void getLog() { + if (!logcatPanel.isReady()) { + return; + } + try { + byte[] buf; + if (recent == null) { + buf = adbDevice.getBinaryLogcat(); + } else { + buf = adbDevice.getBinaryLogcat(recent.getAfterTimestamp()); + } + if (buf == null) { + return; + } + ByteBuffer in = ByteBuffer.wrap(buf); + in.order(ByteOrder.LITTLE_ENDIAN); + while (in.remaining() > 20) { + + LogcatInfo eInfo = null; + byte[] msgBuf; + short eLen = in.getShort(); + short eHdrLen = in.getShort(); + if (eLen + eHdrLen > in.remaining()) { + return; + } + switch (eHdrLen) { + case 20: // header length 20 == version 1 + eInfo = new LogcatInfo(eLen, eHdrLen, in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.get()); + msgBuf = new byte[eLen]; + in.get(msgBuf, 0, eLen - 1); + eInfo.setMsg(msgBuf); + break; + case 24: // header length 24 == version 2 / 3 + eInfo = new LogcatInfo(eLen, eHdrLen, in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.get()); + msgBuf = new byte[eLen]; + in.get(msgBuf, 0, eLen - 1); + eInfo.setMsg(msgBuf); + break; + case 28: // header length 28 == version 4 + eInfo = new LogcatInfo(eLen, eHdrLen, in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(), in.getInt(), + in.get()); + msgBuf = new byte[eLen]; + in.get(msgBuf, 0, eLen - 1); + eInfo.setMsg(msgBuf); + break; + default: + + break; + } + if (eInfo == null) { + return; + } + if (recent == null) { + recent = eInfo; + } else if (recent.getInstant().isBefore(eInfo.getInstant())) { + recent = eInfo; + } + + if (filter.doFilter(eInfo)) { + logcatPanel.log(eInfo); + } + events.add(eInfo); + } + + } catch (Exception e) { + LOG.error("Failed to get logcat message", e); + } + } + + public boolean reload() { + stopLogcat(); + boolean ok = logcatPanel.clearLogcatArea(); + if (ok) { + events.forEach((eInfo) -> { + if (filter.doFilter(eInfo)) { + logcatPanel.log(eInfo); + } + }); + startLogcat(); + } + return true; + } + + public void clearEvents() { + this.recent = null; + this.events = new ArrayList<>(); + } + + public void exit() { + stopLogcat(); + filter = new LogcatFilter(null, null); + recent = null; + } + + public LogcatFilter getFilter() { + return this.filter; + } + + public class LogcatFilter { + private final ArrayList pid; + private ArrayList msgType = new ArrayList() { + { + add((byte) 1); + add((byte) 2); + add((byte) 3); + add((byte) 4); + add((byte) 5); + add((byte) 6); + add((byte) 7); + add((byte) 8); + } + }; + + public LogcatFilter(ArrayList pid, ArrayList msgType) { + if (pid != null) { + this.pid = pid; + } else { + this.pid = new ArrayList<>(); + } + + if (msgType != null) { + this.msgType = msgType; + } + } + + public void addPid(int pid) { + + if (!this.pid.contains(pid)) { + this.pid.add(pid); + } + } + + public void removePid(int pid) { + int pidPos = this.pid.indexOf(pid); + if (pidPos >= 0) { + this.pid.remove(pidPos); + } + } + + public void togglePid(int pid, boolean state) { + if (state) { + addPid(pid); + } else { + removePid(pid); + } + } + + public void addMsgType(byte msgType) { + if (!this.msgType.contains(msgType)) { + this.msgType.add(msgType); + } + } + + public void removeMsgType(byte msgType) { + int typePos = this.msgType.indexOf(msgType); + if (typePos >= 0) { + this.msgType.remove(typePos); + } + } + + public void toggleMsgType(byte msgType, boolean state) { + if (state) { + addMsgType(msgType); + } else { + removeMsgType(msgType); + } + } + + public boolean doFilter(LogcatInfo inInfo) { + if (pid.contains(inInfo.getPid())) { + return msgType.contains(inInfo.getMsgType()); + } + return false; + } + + public ArrayList getFilteredList(ArrayList inInfoList) { + ArrayList outInfoList = new ArrayList(); + inInfoList.forEach((inInfo) -> { + if (doFilter(inInfo)) { + outInfoList.add(inInfo); + } + }); + return outInfoList; + } + } + + public class LogcatInfo { + private String msg; + private final byte msgType; + private final int nsec; + private final int pid; + private final int sec; + private final int tid; + private final short hdrSize; + private final short len; + private final short version; + private int lid; + private int uid; + + public LogcatInfo(short len, short hdrSize, int pid, int tid, int sec, int nsec, byte msgType) { + this.hdrSize = hdrSize; + this.len = len; + this.msgType = msgType; + this.nsec = nsec; + this.pid = pid; + this.sec = sec; + this.tid = tid; + this.version = 1; + } + + // Version 2 and 3 both have the same arguments + public LogcatInfo(short len, short hdrSize, int pid, int tid, int sec, int nsec, int lid, byte msgType) { + this.hdrSize = hdrSize; + this.len = len; + this.lid = lid; + this.msgType = msgType; + this.nsec = nsec; + this.pid = pid; + this.sec = sec; + this.tid = tid; + this.version = 3; + } + + public LogcatInfo(short len, short hdrSize, int pid, int tid, int sec, int nsec, int lid, int uid, byte msgType) { + this.hdrSize = hdrSize; + this.len = len; + this.lid = lid; + this.msgType = msgType; + this.nsec = nsec; + this.pid = pid; + this.sec = sec; + this.tid = tid; + this.uid = uid; + this.version = 4; + } + + public void setMsg(byte[] msg) { + this.msg = new String(msg); + } + + public short getVersion() { + return this.version; + } + + public short getLen() { + return this.len; + } + + public short getHeaderLen() { + return this.hdrSize; + } + + public int getPid() { + return this.pid; + } + + public int getTid() { + return this.tid; + } + + public int getSec() { + return this.sec; + } + + public int getNSec() { + return this.nsec; + } + + public int getLid() { + return this.lid; + } + + public int getUid() { + return this.uid; + } + + public Instant getInstant() { + return Instant.ofEpochSecond(getSec(), getNSec()); + } + + public String getTimestamp() { + DateTimeFormatter dtFormat = DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.of(timezone)); + return dtFormat.format(getInstant()); + } + + public String getAfterTimestamp() { + DateTimeFormatter dtFormat = DateTimeFormatter.ofPattern("MM-dd HH:mm:ss.SSS").withZone(ZoneId.of(timezone)); + return dtFormat.format(getInstant().plusMillis(1)); + } + + public byte getMsgType() { + return this.msgType; + } + + public String getMsgTypeString() { + switch (getMsgType()) { + case 0: + return "Unknown"; + case 1: + return "Default"; + case 2: + return "Verbose"; + case 3: + return "Debug"; + case 4: + return "Info"; + case 5: + return "Warn"; + case 6: + return "Error"; + case 7: + return "Fatal"; + case 8: + return "Silent"; + default: + return "Unknown"; + } + } + + public String getMsg() { + return this.msg; + } + } +} diff --git a/jadx-gui/src/main/java/jadx/gui/device/protocol/ADBDevice.java b/jadx-gui/src/main/java/jadx/gui/device/protocol/ADBDevice.java index 8fd44df3..55fa7df5 100644 --- a/jadx-gui/src/main/java/jadx/gui/device/protocol/ADBDevice.java +++ b/jadx-gui/src/main/java/jadx/gui/device/protocol/ADBDevice.java @@ -10,6 +10,8 @@ import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.Executors; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,7 +27,7 @@ public class ADBDevice { private static final Logger LOG = LoggerFactory.getLogger(ADBDevice.class); private static final String CMD_TRACK_JDWP = "000atrack-jdwp"; - + private static final Pattern TIMESTAMP_FORMAT = Pattern.compile("^[0-9]{2}\\-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}$"); ADBDeviceInfo info; String androidReleaseVer; volatile Socket jdwpListenerSock; @@ -120,6 +122,49 @@ public class ADBDevice { return -1; } + /** + * @Return binary output of logcat + */ + public byte[] getBinaryLogcat() throws IOException { + + Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort()); + String cmd = "logcat -dB"; + return ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream()); + } + + /** + * @Return binary output of logcat after provided timestamp + * Timestamp is in the format 09-08 02:18:03.131 + */ + public byte[] getBinaryLogcat(String timestamp) throws IOException { + Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort()); + Matcher matcher = TIMESTAMP_FORMAT.matcher(timestamp); + if (!matcher.find()) { + LOG.error("Invalid Logcat Timestamp " + timestamp); + } + String cmd = "logcat -dB -t \"" + timestamp + "\""; + return ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream()); + } + + /** + * @Return binary output of logcat -c + */ + public void clearLogcat() throws IOException { + Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort()); + String cmd = "logcat -c"; + ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream()); + } + + /** + * @return Timezone for the attached android device + */ + public String getTimezone() throws IOException { + Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort()); + String cmd = "getprop persist.sys.timezone"; + byte[] tz = ADB.execShellCommandRaw(info.getSerial(), cmd, socket.getOutputStream(), socket.getInputStream()); + return new String(tz).trim(); + } + public String getAndroidReleaseVersion() { if (!StringUtils.isEmpty(androidReleaseVer)) { return androidReleaseVer; @@ -160,15 +205,15 @@ public class ADBDevice { } public List getProcessByPkg(String pkg) throws IOException { - return getProcessList("ps | grep " + pkg, 0); + return getProcessList("ps | grep " + pkg); } @NonNull public List getProcessList() throws IOException { - return getProcessList("ps", 1); + return getProcessList("ps"); } - private List getProcessList(String cmd, int index) throws IOException { + private List getProcessList(String cmd) throws IOException { try (Socket socket = ADB.connect(info.getAdbHost(), info.getAdbPort())) { List procs = new ArrayList<>(); byte[] payload = ADB.execShellCommandRaw(info.getSerial(), cmd, diff --git a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ADBDialog.java b/jadx-gui/src/main/java/jadx/gui/ui/dialog/ADBDialog.java index edc3900c..991774d3 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/dialog/ADBDialog.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/dialog/ADBDialog.java @@ -361,7 +361,9 @@ public class ADBDialog extends JDialog implements ADB.DeviceStateListener, ADB.J debugSetter.name, debugSetter.device.getDeviceInfo().getAdbHost(), debugSetter.forwardTcpPort, - debugSetter.ver); + debugSetter.ver, + debugSetter.device, + debugSetter.pid); } catch (Exception e) { LOG.error("Failed to attach to process", e); return false; diff --git a/jadx-gui/src/main/java/jadx/gui/ui/panel/JDebuggerPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/panel/JDebuggerPanel.java index 82bdc6f3..5aef0fd1 100644 --- a/jadx-gui/src/main/java/jadx/gui/ui/panel/JDebuggerPanel.java +++ b/jadx-gui/src/main/java/jadx/gui/ui/panel/JDebuggerPanel.java @@ -29,6 +29,7 @@ import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JSplitPane; +import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JToolBar; import javax.swing.JTree; @@ -39,10 +40,14 @@ import javax.swing.tree.DefaultTreeModel; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import io.reactivex.annotations.Nullable; import jadx.core.utils.StringUtils; import jadx.gui.device.debugger.DebugController; +import jadx.gui.device.protocol.ADBDevice; import jadx.gui.treemodel.JClass; import jadx.gui.ui.MainWindow; import jadx.gui.ui.codearea.SmaliArea; @@ -53,6 +58,7 @@ import jadx.gui.utils.UiUtils; public class JDebuggerPanel extends JPanel { private static final long serialVersionUID = -1111111202102181631L; + private static final Logger LOG = LoggerFactory.getLogger(LogcatPanel.class); private static final ImageIcon ICON_RUN = UiUtils.openSvgIcon("debugger/execute"); private static final ImageIcon ICON_RERUN = UiUtils.openSvgIcon("debugger/rerun"); @@ -76,6 +82,7 @@ public class JDebuggerPanel extends JPanel { private final transient JSplitPane rightSplitter; private final transient JSplitPane leftSplitter; private final transient IDebugController controller; + private final LogcatPanel logcatPanel; private final transient VarTreePopupMenu varTreeMenu; private transient KeyEventDispatcher controllerShortCutDispatcher; @@ -131,11 +138,14 @@ public class JDebuggerPanel extends JPanel { varTreeMenu = new VarTreePopupMenu(mainWindow); - JPanel loggerPanel = new JPanel(new CardLayout()); + JTabbedPane loggerPanel = new JTabbedPane(); logger = new JTextArea(); logger.setEditable(false); logger.setLineWrap(true); - loggerPanel.add(new JScrollPane(logger)); + JScrollPane loggerScroll = new JScrollPane(logger); + loggerPanel.addTab("Debugger Log", null, loggerScroll, null); + this.logcatPanel = new LogcatPanel(this); + loggerPanel.addTab(NLS.str("logcat.logcat"), null, logcatPanel, null); leftSplitter.setLeftComponent(stackFramePanel); leftSplitter.setRightComponent(rightSplitter); @@ -159,6 +169,7 @@ public class JDebuggerPanel extends JPanel { JOptionPane.OK_CANCEL_OPTION); if (what == JOptionPane.OK_OPTION) { controller.exit(); + logcatPanel.exit(); } else { return; } @@ -373,10 +384,16 @@ public class JDebuggerPanel extends JPanel { } } - public boolean showDebugger(String procName, String host, int port, int androidVer) { + public boolean showDebugger(String procName, String host, int port, int androidVer, ADBDevice device, String pid) { boolean ok = controller.startDebugger(this, host, port, androidVer); if (ok) { log(String.format("Attached %s %s:%d", procName, host, port)); + try { + logcatPanel.init(device, pid); + } catch (Exception e) { + log(NLS.str("logcat.error_fail_start")); + LOG.error("Logcat failed to start", e); + } leftSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerStackFrameSplitterLoc()); rightSplitter.setDividerLocation(mainWindow.getSettings().getDebuggerVarTreeSplitterLoc()); mainWindow.showDebuggerPanel(); diff --git a/jadx-gui/src/main/java/jadx/gui/ui/panel/LogcatPanel.java b/jadx-gui/src/main/java/jadx/gui/ui/panel/LogcatPanel.java new file mode 100644 index 00000000..77b748fd --- /dev/null +++ b/jadx-gui/src/main/java/jadx/gui/ui/panel/LogcatPanel.java @@ -0,0 +1,450 @@ +package jadx.gui.ui.panel; + +import java.awt.BorderLayout; +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.EventQueue; +import java.awt.GridBagConstraints; +import java.awt.GridBagLayout; +import java.awt.Insets; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.BoundedRangeModel; +import javax.swing.Box; +import javax.swing.ImageIcon; +import javax.swing.JCheckBox; +import javax.swing.JComboBox; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JMenuItem; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollBar; +import javax.swing.JScrollPane; +import javax.swing.JTextPane; +import javax.swing.JToolBar; +import javax.swing.ListCellRenderer; +import javax.swing.text.AttributeSet; +import javax.swing.text.SimpleAttributeSet; +import javax.swing.text.StyleConstants; +import javax.swing.text.StyleContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import jadx.gui.device.debugger.LogcatController; +import jadx.gui.device.protocol.ADB; +import jadx.gui.device.protocol.ADBDevice; +import jadx.gui.utils.NLS; +import jadx.gui.utils.UiUtils; + +public class LogcatPanel extends JPanel { + private static final Logger LOG = LoggerFactory.getLogger(LogcatPanel.class); + + StyleContext sc = StyleContext.getDefaultStyleContext(); + private final AttributeSet defaultAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#6c71c4")); + private final AttributeSet verboseAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#2aa198")); + private final AttributeSet debugAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#859900")); + private final AttributeSet infoAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#586e75")); + private final AttributeSet warningAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#b58900")); + private final AttributeSet errorAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#dc322f")); + private final AttributeSet fatalAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#d33682")); + private final AttributeSet silentAset = sc.addAttribute(SimpleAttributeSet.EMPTY, StyleConstants.Foreground, Color.decode("#002b36")); + + private static final ImageIcon ICON_PAUSE = UiUtils.openSvgIcon("debugger/threadFrozen"); + private static final ImageIcon ICON_RUN = UiUtils.openSvgIcon("debugger/execute"); + private static final ImageIcon CLEAR_LOGCAT = UiUtils.openSvgIcon("debugger/trash"); + + private transient JTextPane logcatPane; + private final transient JDebuggerPanel debugPanel; + private LogcatController logcatController; + private boolean ready = false; + private List procs; + + public LogcatPanel(JDebuggerPanel debugPanel) { + this.debugPanel = debugPanel; + } + + private ArrayList pids; + private JScrollPane logcatScroll; + private int pid; + + private final AbstractAction pauseButton = new AbstractAction(NLS.str("logcat.pause"), ICON_PAUSE) { + @Override + public void actionPerformed(ActionEvent e) { + toggleLogcat(); + } + }; + + private final AbstractAction clearButton = new AbstractAction(NLS.str("logcat.clear"), CLEAR_LOGCAT) { + @Override + public void actionPerformed(ActionEvent e) { + clearLogcat(); + } + }; + + public boolean showLogcat() { + ArrayList pkgs = new ArrayList<>(); + pids = new ArrayList<>(); + JPanel procBox; + for (ADB.Process proc : procs.subList(1, procs.size())) { // skipping first element because it contains the column label + pkgs.add(String.format("[pid: %-6s] %s", proc.pid, proc.name)); + pids.add(Integer.valueOf(proc.pid)); + } + + String[] msgTypes = { + NLS.str("logcat.default"), + NLS.str("logcat.verbose"), + NLS.str("logcat.debug"), + NLS.str("logcat.info"), + NLS.str("logcat.warn"), + NLS.str("logcat.error"), + NLS.str("logcat.fatal"), + NLS.str("logcat.silent") }; + Integer[] msgIndex = { 1, 2, 3, 4, 5, 6, 7, 8 }; + + this.setLayout(new BorderLayout()); + logcatPane = new JTextPane(); + logcatPane.setEditable(false); + logcatScroll = new JScrollPane(logcatPane); + JToolBar menuPanel = new JToolBar(); + + CheckCombo procObj = new CheckCombo(NLS.str("logcat.process"), 1, pids.toArray(new Integer[0]), pkgs.toArray(new String[0])); + procBox = procObj.getContent(); + procObj.selectAllBut(this.pids.indexOf(this.pid)); + + JPanel msgTypeBox = new CheckCombo(NLS.str("logcat.level"), 2, msgIndex, msgTypes).getContent(); + + menuPanel.add(procBox); + menuPanel.add(Box.createRigidArea(new Dimension(5, 0))); + menuPanel.add(msgTypeBox); + menuPanel.add(Box.createRigidArea(new Dimension(5, 0))); + menuPanel.add(pauseButton); + menuPanel.add(Box.createRigidArea(new Dimension(5, 0))); + menuPanel.add(clearButton); + + this.add(menuPanel, BorderLayout.NORTH); + this.add(logcatScroll, BorderLayout.CENTER); + + return true; + } + + public boolean clearLogcatArea() { + logcatPane.setText(""); + return true; + } + + public boolean init(ADBDevice device, String pid) { + this.pid = Integer.parseInt(pid); + try { + this.logcatController = new LogcatController(this, device); + this.procs = device.getProcessList(); + if (!this.showLogcat()) { + debugPanel.log(NLS.str("logcat.error_fail_start")); + } + } catch (Exception e) { + this.ready = false; + LOG.error("Failed to start logcat", e); + return false; + } + this.ready = true; + return true; + } + + private void toggleLogcat() { + if (Objects.equals(this.logcatController.getStatus(), "running")) { + this.logcatController.stopLogcat(); + this.pauseButton.putValue(Action.SMALL_ICON, ICON_RUN); + this.pauseButton.putValue(Action.NAME, NLS.str("logcat.start")); + } else if (Objects.equals(this.logcatController.getStatus(), "stopped")) { + this.logcatController.startLogcat(); + this.pauseButton.putValue(Action.SMALL_ICON, ICON_PAUSE); + this.pauseButton.putValue(Action.NAME, NLS.str("logcat.pause")); + } + } + + private void clearLogcat() { + boolean running = false; + if (Objects.equals(this.logcatController.getStatus(), "running")) { + this.logcatController.stopLogcat(); + running = true; + } + this.logcatController.clearLogcat(); + clearLogcatArea(); + this.debugPanel.log(this.logcatController.getStatus()); + if (running) { + this.logcatController.startLogcat(); + } + } + + public boolean isReady() { + return this.ready; + } + + private boolean isAtBottom(JScrollBar scrollbar) { + BoundedRangeModel model = scrollbar.getModel(); + return (model.getExtent() + model.getValue()) == model.getMaximum(); + } + + public void log(LogcatController.LogcatInfo logcatInfo) { + boolean atBottom = false; + + int len = logcatPane.getDocument().getLength(); + JScrollBar scrollbar = logcatScroll.getVerticalScrollBar(); + if (isAtBottom(scrollbar)) { + atBottom = true; + } + + StringBuilder sb = new StringBuilder(); + sb.append(" > ") + .append(logcatInfo.getTimestamp()) + .append(" [pid: ") + .append(logcatInfo.getPid()) + .append("] ") + .append(logcatInfo.getMsgTypeString()) + .append(": ") + .append(logcatInfo.getMsg()) + .append("\n"); + + try { + switch (logcatInfo.getMsgType()) { + case 0: // Unknown + break; + case 1: // Default + logcatPane.getDocument().insertString(len, sb.toString(), defaultAset); + break; + case 2: // Verbose + logcatPane.getDocument().insertString(len, sb.toString(), verboseAset); + break; + case 3: // Debug + logcatPane.getDocument().insertString(len, sb.toString(), debugAset); + break; + case 4: // Info + logcatPane.getDocument().insertString(len, sb.toString(), infoAset); + break; + case 5: // Warn + logcatPane.getDocument().insertString(len, sb.toString(), warningAset); + break; + case 6: // Error + logcatPane.getDocument().insertString(len, sb.toString(), errorAset); + break; + case 7: // Fatal + logcatPane.getDocument().insertString(len, sb.toString(), fatalAset); + break; + case 8: // Silent + logcatPane.getDocument().insertString(len, sb.toString(), silentAset); + break; + default: + logcatPane.getDocument().insertString(len, sb.toString(), null); + break; + } + } catch (Exception e) { + LOG.error("Failed to write logcat message", e); + } + + if (atBottom) { + EventQueue.invokeLater(() -> scrollbar.setValue(scrollbar.getMaximum())); + } + } + + public void exit() { + logcatController.exit(); + clearLogcatArea(); + this.logcatController.clearEvents(); + + } + + class CheckCombo implements ActionListener { + private final String[] ids; + private final int type; + private final String label; + private final Integer[] index; + private JComboBox combo; + + public CheckCombo(String label, int type, Integer[] index, String[] ids) { + this.ids = ids; + this.type = type; + this.label = label; + this.index = index; + + } + + public void actionPerformed(ActionEvent e) { + JComboBox cb = (JComboBox) e.getSource(); + CheckComboStore store = (CheckComboStore) cb.getSelectedItem(); + CheckComboRenderer ccr = (CheckComboRenderer) cb.getRenderer(); + store.state = !store.state; + ccr.checkBox.setSelected(store.state); + + switch (this.type) { + case 1: // process + logcatController.getFilter().togglePid(store.index, store.state); + logcatController.reload(); + break; + case 2: // label + logcatController.getFilter().toggleMsgType((byte) store.index, store.state); + logcatController.reload(); + break; + default: + LOG.error("Invalid Logcat Filter Type"); + break; + } + } + + public JPanel getContent() { + JLabel label = new JLabel(this.label + ": "); + CheckComboStore[] stores = new CheckComboStore[ids.length]; + for (int j = 0; j < ids.length; j++) { + stores[j] = new CheckComboStore(index[j], ids[j], Boolean.TRUE); + } + combo = new JComboBox<>(stores); + combo.setRenderer(new CheckComboRenderer()); + JPanel panel = new JPanel(); + panel.setLayout(new GridBagLayout()); + GridBagConstraints c = new GridBagConstraints(); + c.weightx = 0; + c.gridwidth = 1; + c.insets = new Insets(0, 1, 0, 1); + panel.add(label, c); + c.weightx = 1; + c.gridwidth = GridBagConstraints.REMAINDER; + c.anchor = GridBagConstraints.WEST; + c.insets = new Insets(0, 1, 0, 1); + panel.add(combo, c); + combo.addActionListener(this); + combo.addMouseListener(new FilterClickListener(this)); + return panel; + } + + public void toggleAll(boolean checked) { + for (int i = 0; i < combo.getItemCount(); i++) { + CheckComboStore ccs = combo.getItemAt(i); + ccs.state = checked; + switch (type) { + case 1: // process + logcatController.getFilter().togglePid(ccs.index, checked); + break; + case 2: // level + logcatController.getFilter().toggleMsgType((byte) ccs.index, checked); + break; + default: + LOG.error("Invalid Logcat Toggle Filter Encountered"); + break; + } + } + logcatController.reload(); + } + + public void selectAllBut(int ind) { + for (int i = 0; i < combo.getItemCount(); i++) { + CheckComboStore ccs = combo.getItemAt(i); + if (i != ind) { + ccs.state = false; + } else { + ccs.state = true; + } + switch (type) { + case 1: // process + logcatController.getFilter().togglePid(ccs.index, ccs.state); + break; + case 2: // level + logcatController.getFilter().toggleMsgType((byte) ccs.index, ccs.state); + break; + default: + LOG.error("Invalid Logcat selectAllBut filter encountered"); + break; + } + } + logcatController.reload(); + } + } + + class CheckComboRenderer implements ListCellRenderer { + JCheckBox checkBox; + ArrayList boxes = new ArrayList<>(); + + public CheckComboRenderer() { + checkBox = new JCheckBox(); + } + + public Component getListCellRendererComponent(JList list, Object value, + int index, boolean isSelected, boolean cellHasFocus) { + CheckComboStore store = (CheckComboStore) value; + checkBox.setText(store.id); + checkBox.setSelected(store.state); + boxes.add(checkBox); + return checkBox; + } + } + + static class CheckComboStore { + String id; + Boolean state; + int index; + + public CheckComboStore(int index, String id, Boolean state) { + this.id = id; + this.state = state; + this.index = index; + } + } + + class FilterClickListener extends MouseAdapter { + CheckCombo combo; + + public FilterClickListener(CheckCombo combo) { + this.combo = combo; + } + + public void mousePressed(MouseEvent e) { + if (e.isPopupTrigger()) { + doPop(e); + } + } + + public void mouseReleased(MouseEvent e) { + if (e.isPopupTrigger()) { + doPop(e); + } + } + + private void doPop(MouseEvent e) { + FilterPopup menu = new FilterPopup(combo); + menu.show(e.getComponent(), e.getX(), e.getY()); + } + } + + class FilterPopup extends JPopupMenu { + CheckCombo combo; + JMenuItem selectAll; + JMenuItem unselectAll; + JMenuItem selectAttached; + + public FilterPopup(CheckCombo combo) { + this.combo = combo; + selectAll = new JMenuItem(NLS.str("logcat.select_all")); + selectAll.addActionListener(actionEvent -> combo.toggleAll(true)); + + unselectAll = new JMenuItem(NLS.str("logcat.unselect_all")); + unselectAll.addActionListener(actionEvent -> combo.toggleAll(false)); + + if (combo.type == 1) { + selectAttached = new JMenuItem(NLS.str("logcat.select_attached")); + selectAttached.addActionListener(actionEvent -> combo.selectAllBut(pids.indexOf(pid))); + add(selectAttached); + } + + add(selectAll); + add(unselectAll); + } + } +} diff --git a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties index 525ba055..55a2ec6b 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_de_DE.properties @@ -288,6 +288,26 @@ debugger.popup_change_to_zero=Wechsel zu 0 debugger.popup_change_to_one=Wechsel zu 1 debugger.popup_copy_value=Wert kopieren +#logcat.pause=Pause Logcat +#logcat.start=Resume Logcat +#logcat.clear=Clear Logcat + +#logcat.error_fail_start=Failed to start logcat +#logcat.process=Process +#logcat.level=Level +#logcat.default=Default +#logcat.verbose=Verbose +#logcat.debug=Debug +#logcat.info=Info +#logcat.warn=Warn +#logcat.error=Error +#logcat.fatal=Fatal +#logcat.silent=Silent +#logcat.logcat=Logcat +#logcat.select_attached=Select Attached +#logcat.select_all=Select All +#logcat.unselect_all=Unselect All + set_value_dialog.label_value=Wert set_value_dialog.btn_set=Wert einstellen set_value_dialog.title=Wert Einstellen diff --git a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties index 51b57271..f03d1c2e 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_en_US.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_en_US.properties @@ -12,7 +12,7 @@ menu.alwaysSelectOpened=Always Select Opened File/Class menu.navigation=Navigation menu.text_search=Text search menu.class_search=Class search -menu.comment_search=Comment search +menu.comment_search=Comment searchF menu.tools=Tools menu.deobfuscation=Deobfuscation menu.log=Log Viewer @@ -288,6 +288,26 @@ debugger.popup_change_to_zero=Change to 0 debugger.popup_change_to_one=Change to 1 debugger.popup_copy_value=Copy Value +logcat.pause=Pause Logcat +logcat.start=Resume Logcat +logcat.clear=Clear Logcat + +logcat.error_fail_start=Failed to start logcat +logcat.process=Process +logcat.level=Level +logcat.default=Default +logcat.verbose=Verbose +logcat.debug=Debug +logcat.info=Info +logcat.warn=Warn +logcat.error=Error +logcat.fatal=Fatal +logcat.silent=Silent +logcat.logcat=Logcat +logcat.select_attached=Select Attached +logcat.select_all=Select All +logcat.unselect_all=Unselect All + set_value_dialog.label_value=Value set_value_dialog.btn_set=Set Value set_value_dialog.title=Set Value diff --git a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties index 63040440..b53100ad 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_es_ES.properties @@ -288,6 +288,26 @@ certificate.serialPubKeyY=Y #debugger.popup_change_to_one=Change to 1 #debugger.popup_copy_value=Copy Value +#logcat.pause=Pause Logcat +#logcat.start=Resume Logcat +#logcat.clear=Clear Logcat + +#logcat.error_fail_start=Failed to start logcat +#logcat.process=Process +#logcat.level=Level +#logcat.default=Default +#logcat.verbose=Verbose +#logcat.debug=Debug +#logcat.info=Info +#logcat.warn=Warn +#logcat.error=Error +#logcat.fatal=Fatal +#logcat.silent=Silent +#logcat.logcat=Logcat +#logcat.select_attached=Select Attached +#logcat.select_all=Select All +#logcat.unselect_all=Unselect All + #set_value_dialog.label_value=Value #set_value_dialog.btn_set=Set Value #set_value_dialog.title=Set Value diff --git a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties index d02bab3e..103f95ad 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_ko_KR.properties @@ -288,6 +288,26 @@ debugger.popup_change_to_zero=0으로 변경 debugger.popup_change_to_one=1로 변경 debugger.popup_copy_value=값 복사 +#logcat.pause=Pause Logcat +#logcat.start=Resume Logcat +#logcat.clear=Clear Logcat + +#logcat.error_fail_start=Failed to start logcat +#logcat.process=Process +#logcat.level=Level +#logcat.default=Default +#logcat.verbose=Verbose +#logcat.debug=Debug +#logcat.info=Info +#logcat.warn=Warn +#logcat.error=Error +#logcat.fatal=Fatal +#logcat.silent=Silent +#logcat.logcat=Logcat +#logcat.select_attached=Select Attached +#logcat.select_all=Select All +#logcat.unselect_all=Unselect All + set_value_dialog.label_value=값 set_value_dialog.btn_set=값 설정 set_value_dialog.title=값 설정 diff --git a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties index d71d2de9..6318f44a 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_pt_BR.properties @@ -288,6 +288,26 @@ debugger.popup_change_to_zero=Alterar para 0 debugger.popup_change_to_one=Alterar para 1 debugger.popup_copy_value=Copiar valor +#logcat.pause=Pause Logcat +#logcat.start=Resume Logcat +#logcat.clear=Clear Logcat + +#logcat.error_fail_start=Failed to start logcat +#logcat.process=Process +#logcat.level=Level +#logcat.default=Default +#logcat.verbose=Verbose +#logcat.debug=Debug +#logcat.info=Info +#logcat.warn=Warn +#logcat.error=Error +#logcat.fatal=Fatal +#logcat.silent=Silent +#logcat.logcat=Logcat +#logcat.select_attached=Select Attached +#logcat.select_all=Select All +#logcat.unselect_all=Unselect All + set_value_dialog.label_value=Valor set_value_dialog.btn_set=Definir Valor set_value_dialog.title=Definir Valor diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties index ded9ab68..36c73388 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_CN.properties @@ -288,6 +288,26 @@ debugger.popup_change_to_zero=修改为0 debugger.popup_change_to_one=修改为1 debugger.popup_copy_value=复制值 +#logcat.pause=Pause Logcat +#logcat.start=Resume Logcat +#logcat.clear=Clear Logcat + +#logcat.error_fail_start=Failed to start logcat +#logcat.process=Process +#logcat.level=Level +#logcat.default=Default +#logcat.verbose=Verbose +#logcat.debug=Debug +#logcat.info=Info +#logcat.warn=Warn +#logcat.error=Error +#logcat.fatal=Fatal +#logcat.silent=Silent +#logcat.logcat=Logcat +#logcat.select_attached=Select Attached +#logcat.select_all=Select All +#logcat.unselect_all=Unselect All + set_value_dialog.label_value=值 set_value_dialog.btn_set=修改 set_value_dialog.title=设置新值 diff --git a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties index ab2a6bd1..499b9c84 100644 --- a/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties +++ b/jadx-gui/src/main/resources/i18n/Messages_zh_TW.properties @@ -288,6 +288,26 @@ debugger.popup_change_to_zero=修改為 0 debugger.popup_change_to_one=修改為 1 debugger.popup_copy_value=複製數值 +#logcat.pause=Pause Logcat +#logcat.start=Resume Logcat +#logcat.clear=Clear Logcat + +#logcat.error_fail_start=Failed to start logcat +#logcat.process=Process +#logcat.level=Level +#logcat.default=Default +#logcat.verbose=Verbose +#logcat.debug=Debug +#logcat.info=Info +#logcat.warn=Warn +#logcat.error=Error +#logcat.fatal=Fatal +#logcat.silent=Silent +#logcat.logcat=Logcat +#logcat.select_attached=Select Attached +#logcat.select_all=Select All +#logcat.unselect_all=Unselect All + set_value_dialog.label_value=數值 set_value_dialog.btn_set=設置數值 set_value_dialog.title=設置數值 diff --git a/jadx-gui/src/main/resources/icons/debugger/trash.svg b/jadx-gui/src/main/resources/icons/debugger/trash.svg new file mode 100644 index 00000000..77351470 --- /dev/null +++ b/jadx-gui/src/main/resources/icons/debugger/trash.svg @@ -0,0 +1,3 @@ + + + -- GitLab