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 0000000000000000000000000000000000000000..70a1a6ef284fdf4c246c7d13da1d33cea21c57ba --- /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 8fd44df3aacd44217ce179f8b04960bf6ec6c6d4..55fa7df5427e5d578296aed2a4c8b3357f4760ac 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 edc3900c51933f2962df136a0bedf42cc0688dd3..991774d304ec9ab27b37f85a9dab7c4c4038a6d6 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 82bdc6f3a85328831863d6a6c10eff8408d2d7c7..5aef0fd143df64ab9a970a4e6963069f9dfa2a70 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 0000000000000000000000000000000000000000..77b748fde5f15e472ae8ea38eae95a2a449597a8 --- /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 525ba055ee5f5dc7e187d1c96beef7b9f4e0c366..55a2ec6b899c89464c1e835434181eae020b7033 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 51b5727156e06961424379ff51327b036f16bfd8..f03d1c2e8ea91aa9f608f58fa693e150615fd8c4 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 6304044003391c9139a3a4462ef4bd53b7bbd807..b53100add6665244e9af5eac2eb999b94ff50db5 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 d02bab3ebd716369a6dd988571cb47fb2ec7039b..103f95ad3659072732e15558e6f708579b8f6fbd 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 d71d2de95151ae6293c320f762e0d03b5180f116..6318f44a727704b9b26cd6745bee550cd98993bb 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 ded9ab6836c30c90d8f771e9669b0b3d94588761..36c7338881b6c060a9b5c2f9e269c0bab3119386 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 ab2a6bd1f78c55e2df6c2b4620fb76ff952dfe3f..499b9c849b424bdc1bb3b6d1f1cb0ccdda463b98 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 0000000000000000000000000000000000000000..773514703651c5d4b7864d0bc238925b0e829553 --- /dev/null +++ b/jadx-gui/src/main/resources/icons/debugger/trash.svg @@ -0,0 +1,3 @@ + + +