├── .gitignore ├── README.md ├── pom.xml ├── src └── main │ ├── java │ └── com │ │ └── majeur │ │ └── ars │ │ ├── AdbDeviceScreenComponent.java │ │ ├── AdbRemoteScreen.java │ │ ├── Constants.java │ │ ├── InputKeysDialog.java │ │ ├── JRadioButtonMenu.java │ │ ├── LogDialog.java │ │ ├── Logger.java │ │ ├── MainFrame.java │ │ ├── TitleManager.java │ │ ├── Utils.java │ │ └── adb │ │ ├── AdbCommandBuilder.java │ │ ├── AdbDevice.java │ │ ├── AdbDevicesWatcher.java │ │ ├── AdbHelper.java │ │ └── CommandExec.java │ └── resources │ ├── adb_2701_linux │ ├── adb_2701_win │ └── app_icon.png └── web_demo.png /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | *.iml 3 | target 4 | *~ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Adb-Remote-Screen 2 | A simple tool to control your device through ADB. Useful to retrieve data for broken devices. 3 | Supports touch, swipe and key inputs. 4 | 5 | [**DOWNLOAD**](https://github.com/MajeurAndroid/Adb-Remote-Screen/releases/download/3.0/AdbRemoteScreen-3.0.jar) 6 | 7 | ![alt tag](https://raw.githubusercontent.com/MajeurAndroid/Adb-Remote-Screen/master/web_demo.png) 8 | 9 | ### How to use 10 | 11 | ###### From UI: 12 | 13 | Right click on jar file > open with > Oracle Java X Runtime or OpenJDK X Runtime. 14 | 15 | ###### In command line : 16 | ```shell 17 | cd path/to/jar/file 18 | java -jar AdbRemoteScreen.jar 19 | #or if custome adb binary 20 | java -jar AdbRemoteScreen.jar --adb-path=/path/to/adb/binary 21 | #or if any issue related to UI frameworks on linux 22 | java -jar AdbRemoteScreen.jar --use-default-theme 23 | #(or both arguments obviously) 24 | ``` 25 | 26 | ### New features in 3.0 27 | - Reviewed UI 28 | - Platform specific adb binaries are now bundled with the app 29 | - Optimizations and better resource handling 30 | - Fixed bugs 31 | 32 | ### TODOs 33 | - Add support for MAC OS 34 | - Add directory dialog to let user choose adb file through UI 35 | - Better handle device changes 36 | - ... 37 | 38 | ### License 39 | 40 | Copyright Majeur 2015-2020 41 | 42 | Licensed under Apache License 2.0 43 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | com.majeur 8 | adb-remote-screen 9 | 3.0 10 | jar 11 | 12 | 13 | UTF-8 14 | UTF-8 15 | 8 16 | 8 17 | 18 | 19 | 20 | 21 | 22 | org.apache.maven.plugins 23 | maven-jar-plugin 24 | 3.2.0 25 | 26 | 27 | 28 | true 29 | com.majeur.ars.AdbRemoteScreen 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/AdbDeviceScreenComponent.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars; 2 | 3 | import com.majeur.ars.adb.AdbHelper; 4 | 5 | import javax.imageio.IIOException; 6 | import javax.swing.*; 7 | import java.awt.*; 8 | import java.awt.event.*; 9 | import java.awt.image.BufferedImage; 10 | import java.io.IOException; 11 | 12 | public class AdbDeviceScreenComponent extends JComponent implements MouseListener, KeyListener { 13 | 14 | private static final long MIN_SCREEN_REFRESH_INTERVAL = 250; 15 | private static final Dimension DEFAULT_SCREEN_SIZE = new Dimension(720, 1080); 16 | 17 | private AdbHelper mAdbHelper; 18 | private volatile BufferedImage mImage; 19 | private int mDownX, mDownY; 20 | private long mSwipeStartTime; 21 | 22 | protected volatile Thread mUpdateThread; 23 | 24 | public AdbDeviceScreenComponent(AdbHelper helper) { 25 | mAdbHelper = helper; 26 | addMouseListener(this); 27 | addKeyListener(this); 28 | 29 | setPreferredSize(new Dimension((int) (DEFAULT_SCREEN_SIZE.getWidth() * 0.5), 30 | (int) (DEFAULT_SCREEN_SIZE.getHeight() * 0.5))); 31 | 32 | addComponentListener(new ComponentAdapter() { 33 | @Override 34 | public void componentHidden(ComponentEvent e) { 35 | if (isRendering()) 36 | stopUpdate(); 37 | } 38 | 39 | @Override 40 | public void componentResized(ComponentEvent e) { 41 | requestFocus(); 42 | requestFocusInWindow(); 43 | } 44 | }); 45 | } 46 | 47 | boolean isRendering() { 48 | return mUpdateThread != null && !mUpdateThread.isInterrupted(); 49 | } 50 | 51 | public void startUpdate() { 52 | if (isRendering()) 53 | throw new IllegalStateException("Already started"); 54 | 55 | Logger.i("Start rendering device screen"); 56 | mUpdateThread = new UpdateThread(); 57 | mUpdateThread.start(); 58 | } 59 | 60 | public void stopUpdate() { 61 | if (!isRendering()) 62 | throw new IllegalStateException("Already stoped"); 63 | 64 | Logger.i("Stop rendering device screen"); 65 | mUpdateThread.interrupt(); 66 | mUpdateThread = null; 67 | mImage = null; 68 | repaint(); 69 | } 70 | 71 | public void setScale(double scale) { 72 | Dimension d; 73 | if (mImage == null) 74 | d = new Dimension((int) (DEFAULT_SCREEN_SIZE.getWidth() * scale), 75 | (int) (DEFAULT_SCREEN_SIZE.getHeight() * scale)); 76 | else 77 | d = new Dimension((int) (mImage.getWidth() * scale), (int) (mImage.getHeight() * scale)); 78 | 79 | if (!getPreferredSize().equals(d)) { 80 | setPreferredSize(d); 81 | ((JFrame) getTopLevelAncestor()).pack(); 82 | } 83 | } 84 | 85 | @Override 86 | protected void paintComponent(Graphics g) { 87 | super.paintComponent(g); 88 | 89 | if (mImage != null) { 90 | double dRatio = mImage.getWidth() / (double) mImage.getHeight(); 91 | double fRatio = getWidth() / (double) getHeight(); 92 | if (!Double.valueOf(dRatio).equals(fRatio)) 93 | setScale(0.5); 94 | 95 | g.drawImage(mImage, 0, 0, getWidth(), getHeight(), null); 96 | 97 | } else { 98 | g.setColor(Color.BLACK); 99 | g.fillRect(0, 0, getWidth(), getHeight()); 100 | 101 | String text = "Nothing to show"; 102 | g.setColor(Color.WHITE); 103 | int textWidth = g.getFontMetrics().stringWidth(text); 104 | g.drawString(text, (getWidth() - textWidth) / 2, getHeight() / 2); 105 | } 106 | } 107 | 108 | private double computeTransformValue() { 109 | return (mImage == null ? DEFAULT_SCREEN_SIZE.getWidth() : mImage.getWidth()) / (double) getWidth(); 110 | } 111 | 112 | @Override 113 | public void keyTyped(KeyEvent e) { 114 | // TODO Auto-generated method stub 115 | 116 | } 117 | 118 | @Override 119 | public void keyPressed(KeyEvent e) { 120 | // TODO Auto-generated method stub 121 | 122 | } 123 | 124 | @Override 125 | public void keyReleased(KeyEvent e) { 126 | // TODO Auto-generated method stub 127 | 128 | } 129 | 130 | @Override 131 | public void mouseClicked(MouseEvent e) { 132 | double transform = computeTransformValue(); 133 | mAdbHelper.performClick(e.getX() * transform, e.getY() * transform); 134 | } 135 | 136 | @Override 137 | public void mousePressed(MouseEvent e) { 138 | mDownX = e.getX(); 139 | mDownY = e.getY(); 140 | mSwipeStartTime = System.currentTimeMillis(); 141 | } 142 | 143 | @Override 144 | public void mouseReleased(MouseEvent e) { 145 | int upX = e.getX(), upY = e.getY(); 146 | 147 | int dx = Math.abs(mDownX - upX); 148 | int dy = Math.abs(mDownY - upY); 149 | 150 | if (dx > 5 && dy > 5) { 151 | double transform = computeTransformValue(); 152 | long duration = System.currentTimeMillis() - mSwipeStartTime; 153 | mAdbHelper.performSwipe(mDownX * transform, mDownY * transform, upX * transform, upY * transform, duration); 154 | } 155 | } 156 | 157 | @Override 158 | public void mouseEntered(MouseEvent e) { 159 | // TODO Auto-generated method stub 160 | 161 | } 162 | 163 | @Override 164 | public void mouseExited(MouseEvent e) { 165 | // TODO Auto-generated method stub 166 | 167 | } 168 | 169 | private class UpdateThread extends Thread { 170 | 171 | @Override 172 | public void run() { 173 | super.run(); 174 | long previousFrameTime = System.currentTimeMillis(); 175 | 176 | while (!Thread.interrupted()) { 177 | long currentFrameTime = System.currentTimeMillis(); 178 | long dT = currentFrameTime - previousFrameTime; 179 | previousFrameTime = currentFrameTime; 180 | 181 | if (dT < MIN_SCREEN_REFRESH_INTERVAL) 182 | Utils.sleep(MIN_SCREEN_REFRESH_INTERVAL - dT); 183 | 184 | if (Thread.interrupted()) 185 | break; 186 | 187 | try { 188 | mImage = mAdbHelper.retrieveScreenShot(); 189 | 190 | long t2 = System.currentTimeMillis(); 191 | postFpsCount((t2 - currentFrameTime) / 1000d); 192 | 193 | if (Thread.interrupted()) 194 | mImage = null; 195 | 196 | if (mImage == null) 197 | abort(); 198 | else 199 | repaintPanel(); 200 | 201 | } catch (IIOException e) { 202 | e.printStackTrace(); 203 | Logger.w("Corrupted data received from device, skipping one frame"); 204 | // Screencap is partially corrupted for that frame, let's try at the next one 205 | } catch (IOException e) { 206 | e.printStackTrace(); 207 | abort(); 208 | } 209 | } 210 | } 211 | 212 | private void abort() { 213 | interrupt(); 214 | mUpdateThread = null; 215 | mImage = null; 216 | 217 | Utils.executeOnUiThread(() -> { 218 | Logger.e("Unable to retrieve screen capture, aborting rendering..."); 219 | repaint(); 220 | }); 221 | } 222 | 223 | private void repaintPanel() { 224 | Utils.executeOnUiThread(AdbDeviceScreenComponent.this::repaint); 225 | } 226 | 227 | private void postFpsCount(final double dt) { 228 | Utils.executeOnUiThread(() -> ((MainFrame) getTopLevelAncestor()).getTitleManger().postFpsCount(1d / dt)); 229 | } 230 | } 231 | 232 | } 233 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/AdbRemoteScreen.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars; 2 | 3 | import com.majeur.ars.Utils.OS; 4 | import com.majeur.ars.Utils.OS.OSName; 5 | import com.majeur.ars.adb.AdbHelper; 6 | 7 | import javax.swing.*; 8 | import java.awt.*; 9 | import java.io.*; 10 | import java.net.URL; 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | public class AdbRemoteScreen implements Runnable { 15 | 16 | private static final Map ARGS = new HashMap<>(); 17 | 18 | public static void main(String... args) { 19 | for (String arg : args) { 20 | if (arg.contains("=")) { 21 | ARGS.put(arg.substring(0, arg.indexOf('=')), 22 | arg.substring(arg.indexOf('=') + 1)); 23 | } else { 24 | ARGS.put(arg, ""); 25 | } 26 | } 27 | EventQueue.invokeLater(new AdbRemoteScreen()); 28 | } 29 | 30 | @Override 31 | public void run() { 32 | boolean skipSysLookAndFeel = ARGS.get("--use-default-theme") != null; 33 | try { 34 | if (!skipSysLookAndFeel) 35 | UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 36 | } catch (ClassNotFoundException | InstantiationException | IllegalAccessException 37 | | UnsupportedLookAndFeelException e) { 38 | e.printStackTrace(); 39 | } 40 | 41 | String customAdbPath = ARGS.get("--adb-path"); 42 | File adbExecutable = loadCustomAdbExecutable(customAdbPath); 43 | 44 | if (adbExecutable == null) { 45 | adbExecutable = loadInternalAdbExecutable(); 46 | if (adbExecutable == null) { 47 | System.out.println("Fatal error, cannot load adb binary."); 48 | return; 49 | } 50 | } 51 | 52 | new MainFrame(new AdbHelper(adbExecutable)); 53 | } 54 | 55 | private File loadCustomAdbExecutable(String path) { 56 | if (path == null) 57 | return null; 58 | 59 | File adbExecutable = new File(path); 60 | if (adbExecutable.exists()) { 61 | Logger.i("Adb custom path used: %s", adbExecutable.getPath()); 62 | return adbExecutable; 63 | } else { 64 | Logger.w("No valid adb executable found at \"%s\". Falling back to default internal executable.", path); 65 | return null; 66 | } 67 | } 68 | 69 | private File loadInternalAdbExecutable() { 70 | URL adbExecutableURL = null; 71 | switch (OS.name) { 72 | case WINDOWS: 73 | adbExecutableURL = getClass().getResource("/adb_2701_win"); 74 | break; 75 | case LINUX: 76 | adbExecutableURL = getClass().getResource("/adb_2701_linux"); 77 | break; 78 | case MAC: 79 | JOptionPane.showMessageDialog(null, 80 | "Adb Remote Screen does not support MAC OS yet.", "Error", 81 | JOptionPane.ERROR_MESSAGE); 82 | return null; 83 | } 84 | 85 | File tempDirectory = new File(Utils.getRunningJarPath() + File.separator + "Temp"); 86 | if (!tempDirectory.exists()) 87 | tempDirectory.mkdirs(); 88 | tempDirectory.deleteOnExit(); 89 | 90 | final File tempAdbExecutable = new File(tempDirectory, "adb"); 91 | tempAdbExecutable.deleteOnExit(); 92 | try { 93 | if (!tempAdbExecutable.exists()) 94 | tempAdbExecutable.createNewFile(); 95 | createTempExecutable(adbExecutableURL, tempAdbExecutable); 96 | } catch (IOException e) { 97 | e.printStackTrace(); 98 | JOptionPane.showMessageDialog(null, e.toString(), "Error", JOptionPane.ERROR_MESSAGE); 99 | return null; 100 | } 101 | 102 | Logger.i("Adb executable loaded for %s into %s", OS.name, tempAdbExecutable.getPath()); 103 | 104 | return tempAdbExecutable; 105 | } 106 | 107 | private void createTempExecutable(URL adbSourceExecutableURL, File destFile) throws IOException { 108 | InputStream is = adbSourceExecutableURL.openStream(); 109 | OutputStream os = new FileOutputStream(destFile); 110 | 111 | byte[] b = new byte[2048]; 112 | int length; 113 | while ((length = is.read(b)) != -1) { 114 | os.write(b, 0, length); 115 | } 116 | 117 | is.close(); 118 | os.close(); 119 | 120 | if (OS.name == OSName.LINUX) 121 | destFile.setExecutable(true, false);// TODO Fix this 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/Constants.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars; 2 | 3 | public final class Constants { 4 | 5 | public static final Object[][] VIEW_SCALE_KEY_MAP = {{0.25, "25%"}, {0.50, "50%"}, {0.75, "75%"}, 6 | {1.00, "100%"}, {1.25, "125%"}, {1.50, "150%"}, {2.00, "200%"}}; 7 | 8 | public static final class Strings { 9 | 10 | public static final String WINDOW_TILE_LOG = "Adb Remote Screen: Log"; 11 | 12 | public static final String WINDOW_TILE_INPUT = "Adb Remote Screen: Input Keys"; 13 | 14 | public static final String WINDOW_TILE_REGULAR = "Adb Remote Screen"; 15 | public static final String WINDOW_TILE_DEVICE = "Adb Remote Screen - %s - %.1f FPS"; 16 | 17 | public static final String MESSAGE_NO_ADB_PATH = "Make shure you specified adb path in local.properties file. This file must be in the same folder as .jar file.\n" 18 | + "Content must be:\nadbPath=/path/to/adb binary"; 19 | public static final String TITLE_NO_ADB_PATH = "Error: No adb path specified"; 20 | 21 | public static final String MESSAGE_NO_ADB_FILE = "Specified adb path isn't valid, cannot find adb binary."; 22 | public static final String TITLE_NO_ADB_FILE = "Error: Adb binary not found"; 23 | 24 | public static final String GITHUB_PROJECT_URL = "https://github.com/MajeurAndroid/Adb-Remote-Screen"; 25 | public static final String GITHUB_MAJEUR_URL = "https://github.com/MajeurAndroid"; 26 | 27 | public static final String TITLE_NO_DEVICE = "No device found"; 28 | public static final String MESSAGE_NO_DEVICE = "Please connect a device to your computer.\n(Make sure you have enabled USB Debugging and unlocked your device if any security PIN or pattern has been set)"; 29 | 30 | public static final String ABOUT_MESSAGE = "

Adb Remote Screen

Version 3.0

Author

This program is developed by MajeurAndroid (majeur.android@gmail.com).

Source

This program is Open Source. Source code can be found on GitHub.

License

Copyright 2020 MajeurAndroid
Licensed under the Apache License, Version 2.0 (the \"License\"); you may not use this
file except in compliance with the License."; 31 | } 32 | 33 | public static final class Adb { 34 | 35 | public static final Object[][] INPUT_KEY_MAP = {{0, "UNKNOWN"}, {1, "MENU"}, {2, "SOFT_RIGHT"}, 36 | {3, "HOME"}, {4, "BACK"}, {5, "CALL"}, {6, "ENDCALL"}, {7, "0"}, {8, "1"}, {9, "2"}, 37 | {10, "3"}, {11, "4"}, {12, "5"}, {13, "6"}, {14, "7"}, {15, "8"}, {16, "9"}, 38 | {17, "STAR"}, {18, "POUND"}, {19, "DPAD_UP"}, {20, "DPAD_DOWN"}, {21, "DPAD_LEFT"}, 39 | {22, "DPAD_RIGHT"}, {23, "DPAD_CENTER"}, {24, "VOLUME_UP"}, {25, "VOLUME_DOWN"}, 40 | {26, "POWER"}, {27, "CAMERA"}, {28, "CLEAR"}, {29, "A"}, {30, "B"}, {31, "C"}, {32, "D"}, 41 | {33, "E"}, {34, "F"}, {35, "G"}, {36, "H"}, {37, "I"}, {38, "J"}, {39, "K"}, {40, "L"}, 42 | {41, "M"}, {42, "N"}, {43, "O"}, {44, "P"}, {45, "Q"}, {46, "R"}, {47, "S"}, {48, "T"}, 43 | {49, "U"}, {50, "V"}, {51, "W"}, {52, "X"}, {53, "Y"}, {54, "Z"}, {55, "COMMA"}, 44 | {56, "PERIOD"}, {57, "ALT_LEFT"}, {58, "ALT_RIGHT"}, {59, "SHIFT_LEFT"}, {60, "SHIFT_RIGHT"}, 45 | {61, "TAB"}, {62, "SPACE"}, {63, "SYM"}, {64, "EXPLORER"}, {65, "ENVELOPE"}, {66, "ENTER"}, 46 | {67, "DEL"}, {68, "GRAVE"}, {69, "MINUS"}, {70, "EQUALS"}, {71, "LEFT_BRACKET"}, 47 | {72, "RIGHT_BRACKET"}, {73, "BACKSLASH"}, {74, "SEMICOLON"}, {75, "APOSTROPHE"}, 48 | {76, "SLASH"}, {77, "AT"}, {78, "NUM"}, {79, "HEADSETHOOK"}, {80, "FOCUS"}, {81, "PLUS"}, 49 | {82, "MENU"}, {83, "NOTIFICATION"}, {84, "SEARCH"}, {85, "TAG_LAST_KEYCODE"}}; 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/InputKeysDialog.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars; 2 | 3 | import com.majeur.ars.adb.AdbHelper; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.awt.event.ActionEvent; 8 | import java.awt.event.ActionListener; 9 | import java.net.URL; 10 | 11 | public class InputKeysDialog extends JDialog implements ActionListener { 12 | 13 | private AdbHelper mAdbHelper; 14 | 15 | public InputKeysDialog(Frame owner, AdbHelper adbHelper) { 16 | super(owner, Constants.Strings.WINDOW_TILE_INPUT); 17 | setVisible(true); 18 | setSize(800, 400); 19 | setResizable(false); 20 | setLocationRelativeTo(owner); 21 | URL iconURL = getClass().getResource("/app_icon.png"); 22 | setIconImage(new ImageIcon(iconURL).getImage()); 23 | setLayout(new GridLayout(15, 4)); 24 | 25 | mAdbHelper = adbHelper; 26 | 27 | for (int i = 0; i < Constants.Adb.INPUT_KEY_MAP.length; i++) { 28 | JButton jButton = new JButton((String) Constants.Adb.INPUT_KEY_MAP[i][1]); 29 | jButton.setName(String.valueOf(i)); 30 | jButton.addActionListener(this); 31 | add(jButton); 32 | } 33 | } 34 | 35 | @Override 36 | public void actionPerformed(ActionEvent e) { 37 | int index = Integer.parseInt(((JButton) e.getSource()).getName()); 38 | mAdbHelper.performInputKey((Integer) Constants.Adb.INPUT_KEY_MAP[index][0]); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/JRadioButtonMenu.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars; 2 | 3 | import javax.swing.*; 4 | 5 | public class JRadioButtonMenu extends JMenu { 6 | 7 | private ButtonGroup mButtonGroup; 8 | 9 | public JRadioButtonMenu(String title) { 10 | super(title); 11 | mButtonGroup = new ButtonGroup(); 12 | } 13 | 14 | @Override 15 | public void removeAll() { 16 | super.removeAll(); 17 | mButtonGroup = new ButtonGroup(); 18 | } 19 | 20 | public void add(JRadioButtonMenuItem menuItem) { 21 | super.add(menuItem); 22 | mButtonGroup.add(menuItem); 23 | } 24 | 25 | public void setSelectedIndex(int index) { 26 | mButtonGroup.clearSelection(); 27 | ((JRadioButtonMenuItem) getMenuComponent(index)).setSelected(true); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/LogDialog.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.event.WindowEvent; 6 | import java.awt.event.WindowListener; 7 | import java.net.URL; 8 | 9 | public class LogDialog extends JDialog implements WindowListener { 10 | 11 | public LogDialog(Frame owner) { 12 | super(owner, Constants.Strings.WINDOW_TILE_LOG); 13 | setVisible(true); 14 | setSize(650, 450); 15 | setLocationRelativeTo(owner); 16 | URL iconURL = getClass().getResource("/app_icon.png"); 17 | setIconImage(new ImageIcon(iconURL).getImage()); 18 | 19 | addWindowListener(this); 20 | 21 | JTextPane textArea = new JTextPane(); 22 | Logger.bindUI(textArea); 23 | 24 | getContentPane().add(new JScrollPane(textArea)); 25 | } 26 | 27 | @Override 28 | public void windowOpened(WindowEvent e) { 29 | 30 | } 31 | 32 | @Override 33 | public void windowClosing(WindowEvent e) { 34 | Logger.unbindUI(); 35 | } 36 | 37 | @Override 38 | public void windowClosed(WindowEvent e) { 39 | 40 | } 41 | 42 | @Override 43 | public void windowIconified(WindowEvent e) { 44 | 45 | } 46 | 47 | @Override 48 | public void windowDeiconified(WindowEvent e) { 49 | 50 | } 51 | 52 | @Override 53 | public void windowActivated(WindowEvent e) { 54 | 55 | } 56 | 57 | @Override 58 | public void windowDeactivated(WindowEvent e) { 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/Logger.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars; 2 | 3 | import javax.swing.*; 4 | import java.lang.ref.WeakReference; 5 | import java.util.Calendar; 6 | 7 | public class Logger { 8 | 9 | private static final StringBuilder sStringBuilder = new StringBuilder(""); 10 | private static WeakReference sTextAreaRef = new WeakReference<>(null); 11 | 12 | static { 13 | begin(); 14 | } 15 | 16 | private static void begin() { 17 | log(new LogEntry(LogEntry.TYPE_BEGIN, "Beginning of LOG")); 18 | } 19 | 20 | public static void d(String msg) { 21 | log(new LogEntry(LogEntry.TYPE_DEBUG, msg)); 22 | updateUI(); 23 | } 24 | 25 | public static void i(String msg, Object... args) { 26 | i(String.format(msg, args)); 27 | } 28 | 29 | public static void i(String msg) { 30 | log(new LogEntry(LogEntry.TYPE_INFO, msg)); 31 | updateUI(); 32 | } 33 | 34 | public static void w(String msg, Object... args) { 35 | w(String.format(msg, args)); 36 | } 37 | 38 | public static void w(String msg) { 39 | log(new LogEntry(LogEntry.TYPE_WARN, msg)); 40 | updateUI(); 41 | } 42 | 43 | public static void e(String msg, Object... args) { 44 | e(String.format(msg, args)); 45 | } 46 | 47 | public static void e(String msg) { 48 | log(new LogEntry(LogEntry.TYPE_ERR, msg)); 49 | updateUI(); 50 | } 51 | 52 | private static void log(LogEntry entry) { 53 | sStringBuilder.append("") 56 | .append(entry.timeStamp) 57 | .append("    ") 58 | .append(entry.readableType()) 59 | .append(": ") 60 | .append(entry.message) 61 | .append("") 62 | .append("
"); 63 | } 64 | 65 | private static void updateUI() { 66 | JTextPane textArea; 67 | if ((textArea = sTextAreaRef.get()) != null) { 68 | textArea.setText(sStringBuilder.toString() + ""); 69 | 70 | JScrollPane scrollPane; 71 | JScrollBar scrollBar; 72 | if (textArea.getParent() instanceof JScrollPane && (scrollPane = (JScrollPane) textArea.getParent()) != null 73 | && (scrollBar = scrollPane.getVerticalScrollBar()) != null) 74 | scrollBar.setValue(scrollBar.getMaximum()); 75 | } 76 | } 77 | 78 | static void bindUI(JTextPane textArea) { 79 | textArea.setContentType("text/html"); 80 | sTextAreaRef = new WeakReference<>(textArea); 81 | updateUI(); 82 | } 83 | 84 | static void unbindUI() { 85 | sTextAreaRef.clear(); 86 | } 87 | 88 | private static class LogEntry { 89 | static final int TYPE_INFO = 0; 90 | static final int TYPE_WARN = 1; 91 | static final int TYPE_ERR = 2; 92 | static final int TYPE_DEBUG = 3; 93 | static final int TYPE_BEGIN = 4; 94 | 95 | final String message; 96 | final String timeStamp; 97 | final int type; 98 | 99 | LogEntry(int type, String message) { 100 | this.type = type; 101 | this.message = message; 102 | Calendar calendar = Calendar.getInstance(); 103 | this.timeStamp = String.format("%02d:%02d:%02d.%03d", calendar.get(Calendar.HOUR), 104 | calendar.get(Calendar.MINUTE), calendar.get(Calendar.SECOND), calendar.get(Calendar.MILLISECOND)); 105 | } 106 | 107 | String readableType() { 108 | switch (type) { 109 | case TYPE_INFO: 110 | return "INFO"; 111 | case TYPE_WARN: 112 | return "WARNING"; 113 | case TYPE_ERR: 114 | return "ERROR"; 115 | case TYPE_DEBUG: 116 | return "DEBUG"; 117 | case TYPE_BEGIN: 118 | return "LOG"; 119 | default: 120 | return null; 121 | } 122 | } 123 | 124 | String htmlColor() { 125 | switch (type) { 126 | case TYPE_INFO: 127 | return "black"; 128 | case TYPE_WARN: 129 | return "orange"; 130 | case TYPE_ERR: 131 | return "red"; 132 | case TYPE_DEBUG: 133 | return "green"; 134 | case TYPE_BEGIN: 135 | return "blue"; 136 | default: 137 | return null; 138 | } 139 | } 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/MainFrame.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars; 2 | 3 | import com.majeur.ars.adb.AdbDevice; 4 | import com.majeur.ars.adb.AdbHelper; 5 | import com.majeur.ars.adb.AdbHelper.OnAttachedDevicesChangedListener; 6 | 7 | import javax.swing.*; 8 | import java.awt.event.ActionEvent; 9 | import java.awt.event.ActionListener; 10 | import java.awt.event.WindowAdapter; 11 | import java.awt.event.WindowEvent; 12 | import java.net.URL; 13 | import java.util.List; 14 | 15 | public class MainFrame extends JFrame implements OnAttachedDevicesChangedListener { 16 | 17 | private AdbHelper mAdbHelper; 18 | private AdbDeviceScreenComponent mScreenPanel; 19 | private TitleManager mTitleManager; 20 | 21 | private JRadioButtonMenu mDevicesMenu; 22 | 23 | private JDialog mInputKeysDialog; 24 | private JDialog mLogDialog; 25 | 26 | public MainFrame(final AdbHelper adbHelper) { 27 | setSize(200, 500); 28 | setResizable(false); 29 | setLocationRelativeTo(null); 30 | setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); 31 | setVisible(true); 32 | URL iconURL = getClass().getResource("/app_icon.png"); 33 | setIconImage(new ImageIcon(iconURL).getImage()); 34 | initMenu(); 35 | 36 | mTitleManager = new TitleManager(this); 37 | 38 | mAdbHelper = adbHelper; 39 | mAdbHelper.setAttachedDevicesChangedListener(this); 40 | 41 | mScreenPanel = new AdbDeviceScreenComponent(adbHelper); 42 | getContentPane().add(mScreenPanel); 43 | 44 | pack(); 45 | } 46 | 47 | TitleManager getTitleManger() { 48 | return mTitleManager; 49 | } 50 | 51 | private void setCurrentDevice(int deviceIndex) { 52 | if (deviceIndex == AdbHelper.INVALID_DEVICE_INDEX) { 53 | Logger.i("Selected device: None"); 54 | 55 | if (mScreenPanel.isRendering()) 56 | mScreenPanel.stopUpdate(); 57 | 58 | mAdbHelper.setCurrentDevice(AdbHelper.INVALID_DEVICE_INDEX); 59 | mDevicesMenu.setSelectedIndex(0); // None menuItem index 60 | mTitleManager.toggleMode(false); 61 | 62 | } else { 63 | mAdbHelper.setCurrentDevice(deviceIndex); 64 | AdbDevice device = mAdbHelper.getCurrentDevice(); 65 | Logger.i("Selected device: " + device.getReadableName()); 66 | mTitleManager.postDeviceName(device.getReadableName()); 67 | mTitleManager.toggleMode(true); 68 | 69 | mDevicesMenu.setSelectedIndex(deviceIndex + 1); // Offset to match menuItem index 70 | mScreenPanel.startUpdate(); 71 | mScreenPanel.setScale(0.5); 72 | } 73 | } 74 | 75 | @Override 76 | public void onAttachedDevicesChanged() { 77 | Logger.i("Attached devices changed"); 78 | MenuSelectionManager.defaultManager().clearSelectedPath(); 79 | ButtonGroup buttonGroup = new ButtonGroup(); 80 | mDevicesMenu.removeAll(); 81 | JMenuItem noneMenuItem = new JRadioButtonMenuItem("None"); 82 | buttonGroup.add(noneMenuItem); 83 | mDevicesMenu.add(noneMenuItem); 84 | noneMenuItem.addActionListener(e -> setCurrentDevice(AdbHelper.INVALID_DEVICE_INDEX)); 85 | 86 | List devices = mAdbHelper.getAttachedDevices(); 87 | for (int i = 0; i < devices.size(); i++) { 88 | AdbDevice device = devices.get(i); 89 | JMenuItem menuItem = new JRadioButtonMenuItem(device.getReadableName()); 90 | menuItem.setEnabled(device.isAvailable()); 91 | buttonGroup.add(menuItem); 92 | mDevicesMenu.add(menuItem); 93 | final int deviceIndex = i; 94 | menuItem.addActionListener(e -> setCurrentDevice(deviceIndex)); 95 | Logger.i(String.format("Device #%d: ", i) + device.toString()); 96 | } 97 | setCurrentDevice(AdbHelper.INVALID_DEVICE_INDEX); 98 | } 99 | 100 | private void initMenu() { 101 | JMenuBar menuBar = new JMenuBar(); 102 | 103 | menuBar.add(buildDevicesMenu()); 104 | menuBar.add(buildInputMenu()); 105 | menuBar.add(buildViewMenu()); 106 | menuBar.add(buildAboutMenu()); 107 | setJMenuBar(menuBar); 108 | } 109 | 110 | private JMenu buildDevicesMenu() { 111 | mDevicesMenu = new JRadioButtonMenu("Devices"); 112 | return mDevicesMenu; 113 | } 114 | 115 | private JMenu buildInputMenu() { 116 | JMenu inputMenu = new JMenu("Inputs"); 117 | 118 | JMenuItem menuItem = new JMenuItem("Send KeyInputs"); 119 | menuItem.addActionListener(e -> { 120 | if (mInputKeysDialog == null) { 121 | mInputKeysDialog = new InputKeysDialog(MainFrame.this, mAdbHelper); 122 | mInputKeysDialog.addWindowListener(new WindowAdapter() { 123 | @Override 124 | public void windowClosing(WindowEvent windowEvent) { 125 | mInputKeysDialog.dispose(); 126 | mInputKeysDialog = null; 127 | } 128 | }); 129 | } else { 130 | mInputKeysDialog.toFront(); 131 | } 132 | }); 133 | inputMenu.add(menuItem); 134 | return inputMenu; 135 | } 136 | 137 | private JMenu buildViewMenu() { 138 | JMenu viewMenu = new JMenu("View"); 139 | JMenu scaleSubMenu = new JMenu("Scale"); 140 | for (final Object[] entry : Constants.VIEW_SCALE_KEY_MAP) { 141 | JMenuItem scaleMenuItem = new JMenuItem((String) entry[1]); 142 | scaleMenuItem.addActionListener(e -> mScreenPanel.setScale((double) entry[0])); 143 | scaleSubMenu.add(scaleMenuItem); 144 | } 145 | viewMenu.add(scaleSubMenu); 146 | return viewMenu; 147 | } 148 | 149 | private JMenu buildAboutMenu() { 150 | JMenu helpMenu = new JMenu("Help"); 151 | 152 | JMenuItem logMenuItem = new JMenuItem("Debugging LOG"); 153 | logMenuItem.addActionListener(e -> { 154 | if (mLogDialog == null) { 155 | mLogDialog = new LogDialog(MainFrame.this); 156 | mLogDialog.addWindowListener(new WindowAdapter() { 157 | @Override 158 | public void windowClosing(WindowEvent windowEvent) { 159 | mLogDialog.dispose(); 160 | mLogDialog = null; 161 | } 162 | }); 163 | } else { 164 | mLogDialog.toFront(); 165 | } 166 | }); 167 | helpMenu.add(logMenuItem); 168 | helpMenu.addSeparator(); 169 | 170 | JMenuItem aboutMenuItem = new JMenuItem("About Adb Remote Screen"); 171 | aboutMenuItem.addActionListener(new ActionListener() { 172 | @Override 173 | public void actionPerformed(ActionEvent e) { 174 | URL iconURL = getClass().getResource("/app_icon.png"); 175 | JOptionPane.showMessageDialog(MainFrame.this, Constants.Strings.ABOUT_MESSAGE, "About", 176 | JOptionPane.PLAIN_MESSAGE, new ImageIcon(iconURL)); 177 | } 178 | }); 179 | helpMenu.add(aboutMenuItem); 180 | 181 | return helpMenu; 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/TitleManager.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars; 2 | 3 | import com.majeur.ars.Constants.Strings; 4 | 5 | import javax.swing.*; 6 | 7 | public class TitleManager { 8 | 9 | private JFrame mFrame; 10 | private boolean toggled; 11 | private String mDeviceName; 12 | private double mFps; 13 | 14 | public TitleManager(JFrame frame) { 15 | mFrame = frame; 16 | update(); 17 | } 18 | 19 | private void update() { 20 | if (toggled) 21 | mFrame.setTitle(String.format(Strings.WINDOW_TILE_DEVICE, mDeviceName, mFps)); 22 | else 23 | mFrame.setTitle(Strings.WINDOW_TILE_REGULAR); 24 | } 25 | 26 | void toggleMode(boolean b) { 27 | toggled = b; 28 | update(); 29 | } 30 | 31 | void postDeviceName(String deviceName) { 32 | mDeviceName = deviceName; 33 | update(); 34 | } 35 | 36 | void postFpsCount(double fpsCount) { 37 | mFps = fpsCount; 38 | update(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/Utils.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.io.File; 6 | import java.io.IOException; 7 | import java.net.URI; 8 | import java.net.URISyntaxException; 9 | import java.util.Collection; 10 | import java.util.List; 11 | import java.util.Locale; 12 | import java.util.Scanner; 13 | 14 | public class Utils { 15 | 16 | public static void openUrlInBrowser(String url) { 17 | Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null; 18 | if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) { 19 | try { 20 | desktop.browse(new URI(url)); 21 | } catch (IOException | URISyntaxException exception) { 22 | exception.printStackTrace(); 23 | } 24 | } 25 | } 26 | 27 | static String streamToString(java.io.InputStream is) { 28 | java.util.Scanner s = new Scanner(is).useDelimiter("\\A"); 29 | return s.hasNext() ? s.next() : ""; 30 | } 31 | 32 | static String getRunningJarPath() { 33 | try { 34 | String path = Utils.class.getProtectionDomain().getCodeSource().getLocation().toURI().getPath(); 35 | File classFile = new File(path); 36 | return classFile.getParent(); 37 | 38 | } catch (URISyntaxException e) { 39 | e.printStackTrace(); 40 | return null; 41 | } 42 | } 43 | 44 | public static void executeOnUiThread(Runnable r) { 45 | SwingUtilities.invokeLater(r); 46 | } 47 | 48 | public static boolean isEmpty(String str) { 49 | for (Character c : str.toCharArray()) 50 | if (Character.isLetterOrDigit(c)) 51 | return false; 52 | return true; 53 | } 54 | 55 | public static String toString(double d) { 56 | return String.format(Locale.US, "%.3f", d); 57 | } 58 | 59 | public static boolean equalsOrder(List c1, List c2) { 60 | if (c1 == null || c2 == null || (c1.size() != c2.size())) 61 | return false; 62 | for (int i = 0; i < c1.size(); i++) 63 | if (!c1.get(i).equals(c2.get(i))) 64 | return false; 65 | return true; 66 | } 67 | 68 | public static String toString(Collection c) { 69 | StringBuilder builder = new StringBuilder("("); 70 | for (T t : c) { 71 | builder.append(t.toString()); 72 | builder.append(", "); 73 | } 74 | builder.delete(builder.length() - 3, builder.length() - 1); 75 | builder.append(")"); 76 | return builder.toString(); 77 | } 78 | 79 | public static void sleep(long t) { 80 | try { 81 | Thread.sleep(t); 82 | } catch (InterruptedException e) { 83 | e.printStackTrace(); 84 | } 85 | } 86 | 87 | public static class OS { 88 | 89 | public enum OSName { 90 | WINDOWS, LINUX, MAC 91 | } 92 | 93 | public static OSName name; 94 | 95 | static { 96 | String os = System.getProperty("os.name").toLowerCase(); 97 | if (os.contains("win")) 98 | name = OSName.WINDOWS; 99 | else if (os.contains("osx")) 100 | name = OSName.MAC; 101 | else if (os.contains("nix") || os.contains("aix") || os.contains("nux")) 102 | name = OSName.LINUX; 103 | } 104 | 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/adb/AdbCommandBuilder.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars.adb; 2 | 3 | import com.majeur.ars.Utils; 4 | 5 | import java.util.LinkedList; 6 | import java.util.List; 7 | 8 | public class AdbCommandBuilder { 9 | 10 | private final AdbHelper mAdbHelper; 11 | 12 | public AdbCommandBuilder(AdbHelper adbHelper) { 13 | mAdbHelper = adbHelper; 14 | } 15 | 16 | public List buildTapCommand(double x, double y) { 17 | List command = buildInputCommand(); 18 | command.add("tap"); 19 | command.add(Utils.toString(x)); 20 | command.add(Utils.toString(y)); 21 | return command; 22 | } 23 | 24 | public List buildKeyEventCommand(int keyCode) { 25 | List command = buildInputCommand(); 26 | command.add("keyevent"); 27 | command.add(Integer.toString(keyCode)); 28 | return command; 29 | } 30 | 31 | public List buildSwipeCommand(double x1, double y1, double x2, double y2, long dt) { 32 | List command = buildInputCommand(); 33 | command.add("swipe"); 34 | command.add(Utils.toString(x1)); 35 | command.add(Utils.toString(y1)); 36 | command.add(Utils.toString(x2)); 37 | command.add(Utils.toString(y2)); 38 | command.add(Long.toString(dt)); 39 | return command; 40 | } 41 | 42 | public List buildInputCommand() { 43 | List command = buildShellCommand(); 44 | command.add("input"); 45 | return command; 46 | } 47 | 48 | public List buildShellCommand() { 49 | List command = buildDeviceSpecificCommand(); 50 | command.add("shell"); 51 | return command; 52 | } 53 | 54 | public List buildScreencapCommand() { 55 | List command = buildDeviceSpecificCommand(); 56 | command.add("exec-out"); 57 | command.add("screencap"); 58 | command.add("-p"); 59 | return command; 60 | } 61 | 62 | public List buildDeviceSpecificCommand() { 63 | List command = buildAdbCommand(); 64 | command.add("-s"); 65 | command.add(mAdbHelper.getCurrentDevice().serial); 66 | return command; 67 | } 68 | 69 | public List buildDevicesCommand() { 70 | List command = buildAdbCommand(); 71 | command.add("devices"); 72 | command.add("-l"); 73 | return command; 74 | } 75 | 76 | public List buildKillServerCommand() { 77 | List command = buildAdbCommand(); 78 | command.add("kill-server"); 79 | command.add("-l"); 80 | return command; 81 | } 82 | 83 | public List buildAdbCommand() { 84 | List command = new LinkedList<>(); 85 | command.add(mAdbHelper.getAdbPath()); 86 | return command; 87 | } 88 | 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/adb/AdbDevice.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars.adb; 2 | 3 | import com.majeur.ars.Utils; 4 | 5 | public class AdbDevice implements Comparable { 6 | 7 | String serial; 8 | boolean available; 9 | 10 | String product; 11 | String model; 12 | String device; 13 | 14 | AdbDevice(String line) { 15 | String[] array = line.split(" "); 16 | serial = array[0]; 17 | 18 | for (int i = 1; i < array.length; i++) { 19 | if (!Utils.isEmpty(array[i])) { 20 | available = "device".equals(array[i]); 21 | break; 22 | } 23 | } 24 | 25 | for (String s : array) { 26 | if (s.startsWith("product:")) 27 | product = s.substring("product:".length()); 28 | else if (s.startsWith("model:")) 29 | model = s.substring("model:".length()); 30 | else if (s.startsWith("device:")) 31 | device = s.substring("device:".length()); 32 | } 33 | 34 | if (!available) 35 | model = serial.substring(0, Math.min(serial.length(), 4)) + "... (UNAVAILABLE)"; 36 | } 37 | 38 | public String getReadableName() { 39 | return model; 40 | } 41 | 42 | public boolean isAvailable() { 43 | return available; 44 | } 45 | 46 | @Override 47 | public boolean equals(Object object) { 48 | if (object instanceof AdbDevice) 49 | return serial.equals(((AdbDevice) object).serial) && available == ((AdbDevice) object).available; 50 | return false; 51 | } 52 | 53 | @Override 54 | public int compareTo(AdbDevice device) { 55 | return serial.compareTo(device.serial); 56 | } 57 | 58 | @Override 59 | public String toString() { 60 | return available ? model : "unavailable" + " [" + serial + "]"; 61 | } 62 | 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/adb/AdbDevicesWatcher.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars.adb; 2 | 3 | import com.majeur.ars.Utils; 4 | 5 | import javax.swing.*; 6 | import java.util.Collections; 7 | import java.util.LinkedList; 8 | import java.util.List; 9 | 10 | public class AdbDevicesWatcher implements Runnable { 11 | 12 | private final AdbHelper mAdbHelper; 13 | private Thread mWatchThread; 14 | private List mPreviouslyConnectedDevices; 15 | 16 | public AdbDevicesWatcher(AdbHelper adbHelper) { 17 | mAdbHelper = adbHelper; 18 | } 19 | 20 | void startWatch() { 21 | mWatchThread = new Thread(this); 22 | mWatchThread.start(); 23 | } 24 | 25 | void stopWatch() { 26 | mWatchThread.interrupt(); 27 | mWatchThread = null; 28 | } 29 | 30 | @Override 31 | public void run() { 32 | while (!Thread.interrupted()) { 33 | 34 | final List devices = new LinkedList<>(); 35 | 36 | AdbCommandBuilder commandBuilder = mAdbHelper.getCommandBuilder(); 37 | byte[] data = CommandExec.exec(commandBuilder.buildDevicesCommand()); 38 | if (data != null) { 39 | String[] lines = new String(data).split("\\n"); 40 | for (String line : lines) { 41 | if (line.startsWith("*") || line.startsWith("List") || Utils.isEmpty(line)) // line.startsWith("adb 42 | // server") 43 | continue; 44 | devices.add(new AdbDevice(line)); 45 | } 46 | } 47 | 48 | Collections.sort(devices); 49 | if (!Utils.equalsOrder(devices, mPreviouslyConnectedDevices)) { 50 | mPreviouslyConnectedDevices = devices; 51 | SwingUtilities.invokeLater(() -> mAdbHelper.onDevicesChanged(devices)); 52 | } 53 | 54 | Utils.sleep(5000); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/adb/AdbHelper.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars.adb; 2 | 3 | import com.majeur.ars.Logger; 4 | 5 | import javax.imageio.ImageIO; 6 | import java.awt.image.BufferedImage; 7 | import java.io.ByteArrayInputStream; 8 | import java.io.File; 9 | import java.io.IOException; 10 | import java.util.Collections; 11 | import java.util.List; 12 | 13 | public class AdbHelper { 14 | 15 | public static final int INVALID_DEVICE_INDEX = -1; 16 | 17 | private final String mAdbPath; 18 | private final AdbCommandBuilder mCommandBuilder; 19 | private final AdbDevicesWatcher mDevicesWatcher; 20 | 21 | private OnAttachedDevicesChangedListener mAttachedDevicesChangedListener; 22 | private List mAttachedDevices; 23 | private int mCurrentDeviceIndex; 24 | 25 | public AdbHelper(File adbFile) { 26 | mAdbPath = adbFile.getAbsolutePath(); 27 | mCommandBuilder = new AdbCommandBuilder(this); 28 | 29 | Runtime.getRuntime().addShutdownHook(new Thread(() -> executeCommand(mCommandBuilder.buildKillServerCommand()))); 30 | 31 | mDevicesWatcher = new AdbDevicesWatcher(this); 32 | mDevicesWatcher.startWatch(); 33 | } 34 | 35 | public void release() { 36 | mDevicesWatcher.stopWatch(); 37 | } 38 | 39 | public AdbDevice getCurrentDevice() { 40 | return mAttachedDevices.get(mCurrentDeviceIndex); 41 | } 42 | 43 | public List getAttachedDevices() { 44 | return Collections.unmodifiableList(mAttachedDevices); 45 | } 46 | 47 | public void setAttachedDevicesChangedListener(OnAttachedDevicesChangedListener listener) { 48 | mAttachedDevicesChangedListener = listener; 49 | } 50 | 51 | public void setCurrentDevice(int index) { 52 | mCurrentDeviceIndex = index; 53 | } 54 | 55 | String getAdbPath() { 56 | return mAdbPath; 57 | } 58 | 59 | AdbCommandBuilder getCommandBuilder() { 60 | return mCommandBuilder; 61 | } 62 | 63 | void onDevicesChanged(List newDevices) { 64 | mCurrentDeviceIndex = INVALID_DEVICE_INDEX; 65 | mAttachedDevices = newDevices; 66 | if (mAttachedDevicesChangedListener != null) 67 | mAttachedDevicesChangedListener.onAttachedDevicesChanged(); 68 | } 69 | 70 | public void performInputKey(int keyCode) { 71 | if (mCurrentDeviceIndex == INVALID_DEVICE_INDEX) 72 | return; 73 | 74 | Logger.i("Key pressed (code: %d)", keyCode); 75 | executeCommand(mCommandBuilder.buildKeyEventCommand(keyCode)); 76 | } 77 | 78 | public void performClick(double x, double y) { 79 | if (mCurrentDeviceIndex == INVALID_DEVICE_INDEX) 80 | return; 81 | 82 | Logger.i("Click at %.1fx%.1f", x, y); 83 | executeCommand(mCommandBuilder.buildTapCommand(x, y)); 84 | } 85 | 86 | public void performSwipe(double x1, double y1, double x2, double y2, long duration) { 87 | if (mCurrentDeviceIndex == INVALID_DEVICE_INDEX) 88 | return; 89 | 90 | Logger.i("Swipe from %.1fx%.1f to %.1fx%.1f during %d ms", x1, y1, x2, y2, duration); 91 | executeCommand(mCommandBuilder.buildSwipeCommand(x1, y1, x2, y2, duration)); 92 | } 93 | 94 | public BufferedImage retrieveScreenShot() throws IOException { 95 | if (mCurrentDeviceIndex == INVALID_DEVICE_INDEX) { 96 | Logger.e("No device selected, screenshot aborted"); 97 | return null; 98 | } 99 | 100 | byte[] data = CommandExec.exec(mCommandBuilder.buildScreencapCommand()); 101 | if (data == null) { 102 | Logger.e("Fatal error retrieving screenshot"); 103 | return null; 104 | } 105 | ByteArrayInputStream inputStream = new ByteArrayInputStream(data); 106 | // No need to call close() for ByteArrayInputStream 107 | return ImageIO.read(inputStream); 108 | } 109 | 110 | private void executeCommand(List command) { 111 | CommandExec.execAsync(command, null); 112 | } 113 | 114 | public interface OnAttachedDevicesChangedListener { 115 | void onAttachedDevicesChanged(); 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /src/main/java/com/majeur/ars/adb/CommandExec.java: -------------------------------------------------------------------------------- 1 | package com.majeur.ars.adb; 2 | 3 | import java.io.ByteArrayOutputStream; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.List; 7 | 8 | public class CommandExec { 9 | 10 | interface Callback { 11 | void callback(byte[] data); 12 | } 13 | 14 | static void execAsync(final List command, final Callback callback) { 15 | new Thread(() -> { 16 | byte[] data = exec(command); 17 | if (callback != null) 18 | callback.callback(data); 19 | }).start(); 20 | } 21 | 22 | static byte[] exec(List command) { 23 | //Logger.d(Arrays.toString(command.toArray())); 24 | ProcessBuilder builder = new ProcessBuilder(command); 25 | builder.redirectErrorStream(true); 26 | Process process; 27 | try { 28 | process = builder.start(); 29 | 30 | InputStream inputStream = process.getInputStream(); 31 | ByteArrayOutputStream buffer = new ByteArrayOutputStream(); 32 | 33 | int nRead; 34 | byte[] dataChunk = new byte[16384]; 35 | while ((nRead = inputStream.read(dataChunk, 0, dataChunk.length)) != -1) 36 | buffer.write(dataChunk, 0, nRead); 37 | buffer.flush(); 38 | 39 | byte[] data = buffer.toByteArray(); 40 | 41 | buffer.close(); 42 | inputStream.close(); 43 | 44 | return data; 45 | 46 | } catch (IOException e) { 47 | e.printStackTrace(); 48 | return null; 49 | } 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /src/main/resources/adb_2701_linux: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajeurAndroid/java-adb-remote-screen/195903dbd25daf2ec30c386d3d2a7a33fc18f3e6/src/main/resources/adb_2701_linux -------------------------------------------------------------------------------- /src/main/resources/adb_2701_win: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajeurAndroid/java-adb-remote-screen/195903dbd25daf2ec30c386d3d2a7a33fc18f3e6/src/main/resources/adb_2701_win -------------------------------------------------------------------------------- /src/main/resources/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajeurAndroid/java-adb-remote-screen/195903dbd25daf2ec30c386d3d2a7a33fc18f3e6/src/main/resources/app_icon.png -------------------------------------------------------------------------------- /web_demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MajeurAndroid/java-adb-remote-screen/195903dbd25daf2ec30c386d3d2a7a33fc18f3e6/web_demo.png --------------------------------------------------------------------------------