├── lib ├── adb.exe ├── fastboot.exe ├── AdbWinApi.dll └── AdbWinUsbApi.dll ├── app.properties ├── .travis.yml ├── src └── main │ ├── java │ └── com │ │ └── github │ │ └── xsavikx │ │ └── androidscreencast │ │ ├── api │ │ ├── command │ │ │ ├── Command.java │ │ │ ├── executor │ │ │ │ ├── CommandExecutor.java │ │ │ │ └── ShellCommandExecutor.java │ │ │ ├── TapCommand.java │ │ │ ├── InputCommand.java │ │ │ ├── exception │ │ │ │ ├── AdbShellCommandExecutionException.java │ │ │ │ └── CommandExecutionException.java │ │ │ ├── SwipeCommand.java │ │ │ ├── KeyCommand.java │ │ │ └── factory │ │ │ │ └── AdbInputCommandFactory.java │ │ ├── AndroidDevice.java │ │ ├── injector │ │ │ ├── MultiLineReceiverPrinter.java │ │ │ ├── KeyCodeConverter.java │ │ │ ├── Injector.java │ │ │ ├── OutputStreamShellOutputReceiver.java │ │ │ ├── ScreenCaptureThread.java │ │ │ └── InputKeyEvent.java │ │ ├── file │ │ │ └── FileInfo.java │ │ ├── recording │ │ │ ├── FilterImageOutputStream.java │ │ │ └── DataAtomOutputStream.java │ │ └── AndroidDeviceImpl.java │ │ ├── app │ │ ├── Application.java │ │ ├── GUIApplication.java │ │ ├── SwingApplication.java │ │ ├── AndroidScreencastApplication.java │ │ └── DeviceChooserApplication.java │ │ ├── ui │ │ ├── interaction │ │ │ ├── KeyEventDispatcherFactory.java │ │ │ ├── KeyboardActionListenerFactory.java │ │ │ ├── MouseActionAdapterFactory.java │ │ │ ├── KeyboardActionListener.java │ │ │ ├── KeyEventDispatcherImpl.java │ │ │ └── MouseActionAdapter.java │ │ ├── JDialogError.java │ │ ├── JSplashScreen.java │ │ ├── explorer │ │ │ ├── LazyMutableTreeNode.java │ │ │ ├── JFrameExplorer.java │ │ │ └── LazyLoadingTreeNode.java │ │ ├── JDialogUrl.java │ │ ├── JPanelScreen.java │ │ ├── model │ │ │ ├── InputKeyEventTableModel.java │ │ │ └── InputKeyEventTable.java │ │ ├── JDialogDeviceList.java │ │ ├── JDialogExecuteKeyEvent.java │ │ ├── worker │ │ │ ├── AccumulativeRunnable.java │ │ │ └── SwingWorker.java │ │ ├── JFrameMain.java │ │ └── MultiLineLabelUI.java │ │ ├── constant │ │ └── Constants.java │ │ ├── Main.java │ │ └── spring │ │ └── config │ │ ├── ApplicationContextProvider.java │ │ └── ApplicationConfiguration.java │ └── resources │ └── log4j.properties ├── README.md ├── .gitignore ├── pom.xml └── LICENSE /lib/adb.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speed/AndroidScreencast/master/lib/adb.exe -------------------------------------------------------------------------------- /app.properties: -------------------------------------------------------------------------------- 1 | adb.path=adb.exe 2 | default.window.width=1024 3 | default.window.height=768 -------------------------------------------------------------------------------- /lib/fastboot.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speed/AndroidScreencast/master/lib/fastboot.exe -------------------------------------------------------------------------------- /lib/AdbWinApi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speed/AndroidScreencast/master/lib/AdbWinApi.dll -------------------------------------------------------------------------------- /lib/AdbWinUsbApi.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/speed/AndroidScreencast/master/lib/AdbWinUsbApi.dll -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: java 3 | 4 | branches: 5 | only: 6 | - master 7 | 8 | jdk: 9 | - openjdk7 10 | - oraclejdk7 11 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/command/Command.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.command; 2 | 3 | public interface Command { 4 | String INPUT = "input"; 5 | String WHITESPACE = " "; 6 | 7 | String getFormattedCommand(); 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/command/executor/CommandExecutor.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.command.executor; 2 | 3 | import com.github.xsavikx.androidscreencast.api.command.Command; 4 | 5 | public interface CommandExecutor { 6 | void execute(Command command); 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/app/Application.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.app; 2 | 3 | public interface Application { 4 | 5 | void close(); 6 | 7 | void handleException(Thread thread, Throwable ex); 8 | 9 | void start(); 10 | 11 | void init(); 12 | } 13 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyEventDispatcherFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui.interaction; 2 | 3 | import java.awt.*; 4 | 5 | public final class KeyEventDispatcherFactory { 6 | public static KeyEventDispatcher getKeyEventDispatcher(Window frame) { 7 | return new KeyEventDispatcherImpl(frame); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyboardActionListenerFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui.interaction; 2 | 3 | import com.github.xsavikx.androidscreencast.api.injector.InputKeyEvent; 4 | 5 | public final class KeyboardActionListenerFactory { 6 | public static KeyboardActionListener getInstance(InputKeyEvent inputKeyEvent) { 7 | return new KeyboardActionListener(inputKeyEvent.getCode()); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/AndroidDevice.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api; 2 | 3 | import com.github.xsavikx.androidscreencast.api.file.FileInfo; 4 | 5 | import java.io.File; 6 | import java.util.List; 7 | 8 | public interface AndroidDevice { 9 | String executeCommand(String command); 10 | 11 | List list(String path); 12 | 13 | void openUrl(String url); 14 | 15 | void pullFile(String remoteFrom, File localTo); 16 | 17 | void pushFile(File localFrom, String remoteTo); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/command/TapCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.command; 2 | 3 | public class TapCommand extends InputCommand { 4 | private int x; 5 | private int y; 6 | 7 | public TapCommand(int x, int y) { 8 | this.x = x; 9 | this.y = y; 10 | } 11 | 12 | @Override 13 | protected String getCommandPart() { 14 | StringBuilder stringBuilder = new StringBuilder(); 15 | stringBuilder.append("tap ").append(x).append(' ').append(y); 16 | return stringBuilder.toString(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/injector/MultiLineReceiverPrinter.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.injector; 2 | 3 | import com.android.ddmlib.MultiLineReceiver; 4 | import org.springframework.stereotype.Component; 5 | 6 | @Component 7 | public class MultiLineReceiverPrinter extends MultiLineReceiver { 8 | 9 | @Override 10 | public boolean isCancelled() { 11 | return false; 12 | } 13 | 14 | @Override 15 | public void processNewLines(String[] arg0) { 16 | for (String elem : arg0) { 17 | System.out.println(elem); 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/command/InputCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.command; 2 | 3 | public abstract class InputCommand implements Command { 4 | private static final String TO_STRING_PATTERN = "%s [%s]"; 5 | 6 | @Override 7 | public String toString() { 8 | return String.format(TO_STRING_PATTERN, this.getClass().getSimpleName(), getFormattedCommand()); 9 | } 10 | 11 | @Override 12 | public String getFormattedCommand() { 13 | return INPUT + WHITESPACE + getCommandPart(); 14 | } 15 | 16 | protected abstract String getCommandPart(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/constant/Constants.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.constant; 2 | 3 | import javax.annotation.Resource; 4 | 5 | @Resource 6 | public final class Constants { 7 | public static final String APP_NATIVE_LOOK_PROPERTY = "app.nativeLook"; 8 | public static final String ADB_PATH_PROPERTY = "adb.path"; 9 | public static final String DEFAULT_WINDOW_WIDTH = "default.window.width"; 10 | public static final String DEFAULT_WINDOW_HEIGHT = "default.window.height"; 11 | 12 | public static final boolean DEFAULT_APP_NATIVE_LOOK = true; 13 | public static final int DEFAULT_ADB_PORT = 2345; 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/MouseActionAdapterFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui.interaction; 2 | 3 | import com.github.xsavikx.androidscreencast.api.injector.Injector; 4 | import com.github.xsavikx.androidscreencast.ui.JPanelScreen; 5 | 6 | public final class MouseActionAdapterFactory { 7 | public static MouseActionAdapter getInstance(JPanelScreen jPanelScreen) { 8 | return new MouseActionAdapter(jPanelScreen); 9 | } 10 | 11 | public static MouseActionAdapter getInstance(JPanelScreen jPanelScreen, Injector injector) { 12 | return new MouseActionAdapter(jPanelScreen, injector); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/command/exception/AdbShellCommandExecutionException.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.command.exception; 2 | 3 | import com.github.xsavikx.androidscreencast.api.command.Command; 4 | 5 | public class AdbShellCommandExecutionException extends CommandExecutionException { 6 | private static final String ERROR_MESSAGE = "Error while executing command: %s"; 7 | 8 | private static final long serialVersionUID = -503890452151627952L; 9 | 10 | public AdbShellCommandExecutionException(Command command, Throwable cause) { 11 | super(String.format(ERROR_MESSAGE, command.getFormattedCommand()), cause); 12 | } 13 | 14 | } 15 | -------------------------------------------------------------------------------- /src/main/resources/log4j.properties: -------------------------------------------------------------------------------- 1 | # Root logger option 2 | log4j.rootLogger=TRACE, stdout, file 3 | # Direct log messages to a log file 4 | log4j.appender.file=org.apache.log4j.RollingFileAppender 5 | log4j.appender.file.File=AndroidScreencast.log 6 | log4j.appender.file.MaxFileSize=1MB 7 | log4j.appender.file.MaxBackupIndex=1 8 | log4j.appender.file.layout=org.apache.log4j.PatternLayout 9 | log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%-5p] [%C{1}:%L] - %m%n 10 | # Direct log messages to stdout 11 | log4j.appender.stdout=org.apache.log4j.ConsoleAppender 12 | log4j.appender.stdout.Target=System.out 13 | log4j.appender.stdout.layout=org.apache.log4j.PatternLayout 14 | log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss} [%-5p] [%C{1}] - %m%n 15 | # Set levels for appenders 16 | log4j.appender.file.Threshold=DEBUG 17 | log4j.appender.stdout.Threshold=DEBUG 18 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogError.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.io.PrintWriter; 6 | import java.io.StringWriter; 7 | 8 | public class JDialogError extends JDialog { 9 | 10 | private static final long serialVersionUID = -2562084286663149628L; 11 | 12 | public JDialogError(Throwable ex) { 13 | getRootPane().setLayout(new BorderLayout()); 14 | JTextArea l = new JTextArea(); 15 | StringWriter w = new StringWriter(); 16 | if (ex.getClass() == RuntimeException.class && ex.getCause() != null) 17 | ex = ex.getCause(); 18 | ex.printStackTrace(new PrintWriter(w)); 19 | l.setText(w.toString()); 20 | getRootPane().add(l, BorderLayout.CENTER); 21 | pack(); 22 | setLocationRelativeTo(null); 23 | setAlwaysOnTop(true); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/JSplashScreen.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | 6 | public class JSplashScreen extends JWindow { 7 | 8 | private static final long serialVersionUID = -4537199368044671301L; 9 | private JLabel label; 10 | 11 | public JSplashScreen(String text) { 12 | label = new JLabel("Loading...", SwingConstants.CENTER); 13 | initialize(); 14 | setText(text); 15 | } 16 | 17 | private void initialize() { 18 | setLayout(new BorderLayout()); 19 | label.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); 20 | // createLineBorder(Color.BLACK)); 21 | add(label, BorderLayout.CENTER); 22 | } 23 | 24 | public void setText(String text) { 25 | label.setText(text); 26 | pack(); 27 | setLocationRelativeTo(null); 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/injector/KeyCodeConverter.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.injector; 2 | 3 | import org.apache.log4j.Logger; 4 | 5 | import java.awt.event.KeyEvent; 6 | 7 | public class KeyCodeConverter { 8 | private static final Logger LOGGER = Logger.getLogger(KeyCodeConverter.class); 9 | 10 | public static int getKeyCode(KeyEvent e) { 11 | LOGGER.debug("getKeyCode(KeyEvent e=" + e + ") - start"); 12 | int code = InputKeyEvent.KEYCODE_UNKNOWN.getCode(); 13 | char c = e.getKeyChar(); 14 | int keyCode = e.getKeyCode(); 15 | InputKeyEvent inputKeyEvent = InputKeyEvent.getByCharacterOrKeyCode(Character.toLowerCase(c), keyCode); 16 | if (inputKeyEvent != null) { 17 | code = inputKeyEvent.getCode(); 18 | } 19 | LOGGER.debug("getKeyCode(KeyEvent e=" + e + ") - end"); 20 | return code; 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/app/GUIApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.app; 2 | 3 | import java.lang.Thread.UncaughtExceptionHandler; 4 | 5 | public abstract class GUIApplication implements Application { 6 | 7 | public GUIApplication() { 8 | Runtime.getRuntime().addShutdownHook(new Thread() { 9 | @Override 10 | public void run() { 11 | close(); 12 | } 13 | }); 14 | Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { 15 | @Override 16 | public void uncaughtException(Thread arg0, Throwable ex) { 17 | try { 18 | handleException(arg0, ex); 19 | } catch (Exception ex2) { 20 | // ignored 21 | ex2.printStackTrace(); 22 | } 23 | } 24 | }); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/file/FileInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.file; 2 | 3 | import com.github.xsavikx.androidscreencast.api.AndroidDeviceImpl; 4 | import org.springframework.stereotype.Component; 5 | 6 | import java.io.File; 7 | 8 | @Component 9 | public class FileInfo { 10 | public AndroidDeviceImpl device; 11 | public String path; 12 | public String attribs; 13 | public boolean directory; 14 | public String name; 15 | 16 | public File downloadTemporary() { 17 | try { 18 | File tempFile = File.createTempFile("android", name); 19 | device.pullFile(path + name, tempFile); 20 | tempFile.deleteOnExit(); 21 | return tempFile; 22 | } catch (Exception ex) { 23 | throw new RuntimeException(ex); 24 | } 25 | } 26 | 27 | @Override 28 | public String toString() { 29 | return name; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/Main.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast; 2 | 3 | import com.github.xsavikx.androidscreencast.app.AndroidScreencastApplication; 4 | import com.github.xsavikx.androidscreencast.app.Application; 5 | import com.github.xsavikx.androidscreencast.spring.config.ApplicationContextProvider; 6 | import org.apache.log4j.Logger; 7 | 8 | import java.util.Arrays; 9 | 10 | public class Main { 11 | private static final Logger LOGGER = Logger.getLogger(Main.class); 12 | 13 | public static void main(String args[]) { 14 | LOGGER.debug("main(String[] args=" + Arrays.toString(args) + ") - start"); 15 | Application application = ApplicationContextProvider.getApplicationContext() 16 | .getBean(AndroidScreencastApplication.class); 17 | application.init(); 18 | application.start(); 19 | 20 | LOGGER.debug("main(String[] args=" + Arrays.toString(args) + ") - end"); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/command/exception/CommandExecutionException.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.command.exception; 2 | 3 | public class CommandExecutionException extends RuntimeException { 4 | 5 | private static final long serialVersionUID = 8676432388325401069L; 6 | 7 | public CommandExecutionException() { 8 | super(); 9 | } 10 | 11 | public CommandExecutionException(String message, Throwable cause, boolean enableSuppression, 12 | boolean writableStackTrace) { 13 | super(message, cause, enableSuppression, writableStackTrace); 14 | } 15 | 16 | public CommandExecutionException(String message, Throwable cause) { 17 | super(message, cause); 18 | } 19 | 20 | public CommandExecutionException(String message) { 21 | super(message); 22 | } 23 | 24 | public CommandExecutionException(Throwable cause) { 25 | super(cause); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/injector/Injector.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.injector; 2 | 3 | import org.apache.log4j.Logger; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.stereotype.Service; 6 | 7 | @Service 8 | public class Injector { 9 | private static final Logger LOGGER = Logger.getLogger(Injector.class); 10 | @Autowired 11 | public ScreenCaptureThread screencapture; 12 | 13 | public void restart() { 14 | LOGGER.debug("restart() - start"); 15 | 16 | close(); 17 | start(); 18 | 19 | LOGGER.debug("restart() - end"); 20 | } 21 | 22 | public void close() { 23 | LOGGER.debug("close() - start"); 24 | 25 | screencapture.interrupt(); 26 | 27 | LOGGER.debug("close() - end"); 28 | } 29 | 30 | public void start() { 31 | LOGGER.debug("start() - start"); 32 | 33 | screencapture.start(); 34 | 35 | LOGGER.debug("start() - end"); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/injector/OutputStreamShellOutputReceiver.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.injector; 2 | 3 | import com.android.ddmlib.IShellOutputReceiver; 4 | 5 | import java.io.IOException; 6 | import java.io.OutputStream; 7 | 8 | public class OutputStreamShellOutputReceiver implements IShellOutputReceiver { 9 | 10 | private OutputStream os; 11 | 12 | public OutputStreamShellOutputReceiver(OutputStream os) { 13 | this.os = os; 14 | } 15 | 16 | @Override 17 | public void addOutput(byte[] buf, int off, int len) { 18 | try { 19 | os.write(buf, off, len); 20 | } catch (IOException ex) { 21 | throw new RuntimeException(ex); 22 | } 23 | } 24 | 25 | @Override 26 | public void flush() { 27 | try { 28 | os.flush(); 29 | } catch (IOException e) { 30 | e.printStackTrace(); 31 | } 32 | } 33 | 34 | @Override 35 | public boolean isCancelled() { 36 | return false; 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/spring/config/ApplicationContextProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.spring.config; 2 | 3 | import org.springframework.beans.BeansException; 4 | import org.springframework.context.ApplicationContext; 5 | import org.springframework.context.ApplicationContextAware; 6 | import org.springframework.context.annotation.AnnotationConfigApplicationContext; 7 | 8 | public class ApplicationContextProvider implements ApplicationContextAware { 9 | private static ApplicationContext applicationContext; 10 | 11 | private ApplicationContextProvider() { 12 | // 13 | } 14 | 15 | public static ApplicationContext getApplicationContext() { 16 | if (applicationContext == null) 17 | applicationContext = new AnnotationConfigApplicationContext(ApplicationConfiguration.class); 18 | return applicationContext; 19 | } 20 | 21 | @Override 22 | public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { 23 | ApplicationContextProvider.applicationContext = applicationContext; 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/command/SwipeCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.command; 2 | 3 | public class SwipeCommand extends InputCommand { 4 | private int x1; 5 | private int y1; 6 | private int x2; 7 | private int y2; 8 | private long duration; 9 | 10 | public SwipeCommand(int x1, int y1, int x2, int y2) { 11 | this.x1 = x1; 12 | this.y1 = y1; 13 | this.x2 = x2; 14 | this.y2 = y2; 15 | } 16 | 17 | public SwipeCommand(int x1, int y1, int x2, int y2, long duration) { 18 | this(x1, y1, x2, y2); 19 | this.duration = duration; 20 | } 21 | 22 | public void setDuration(int duration) { 23 | this.duration = duration; 24 | } 25 | 26 | @Override 27 | protected String getCommandPart() { 28 | StringBuilder stringBuilder = new StringBuilder("swipe "); 29 | stringBuilder.append(x1).append(' ').append(y1).append(' ').append(x2).append(' ').append(y2); 30 | if (duration > 0) { 31 | stringBuilder.append(' ').append(duration); 32 | } 33 | return stringBuilder.toString(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/explorer/LazyMutableTreeNode.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui.explorer; 2 | 3 | import javax.swing.tree.DefaultMutableTreeNode; 4 | 5 | public abstract class LazyMutableTreeNode extends DefaultMutableTreeNode { 6 | 7 | private static final long serialVersionUID = -6383034137965603498L; 8 | protected boolean _loaded = false; 9 | 10 | public LazyMutableTreeNode() { 11 | super(); 12 | } 13 | 14 | public LazyMutableTreeNode(Object userObject) { 15 | super(userObject); 16 | } 17 | 18 | public LazyMutableTreeNode(Object userObject, boolean allowsChildren) { 19 | super(userObject, allowsChildren); 20 | } 21 | 22 | public void clear() { 23 | removeAllChildren(); 24 | _loaded = false; 25 | } 26 | 27 | @Override 28 | public int getChildCount() { 29 | synchronized (this) { 30 | if (!_loaded) { 31 | _loaded = true; 32 | initChildren(); 33 | } 34 | } 35 | return super.getChildCount(); 36 | } 37 | 38 | protected abstract void initChildren(); 39 | 40 | public boolean isLoaded() { 41 | return _loaded; 42 | } 43 | 44 | } -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/command/KeyCommand.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.command; 2 | 3 | import com.github.xsavikx.androidscreencast.api.injector.InputKeyEvent; 4 | 5 | public class KeyCommand extends InputCommand { 6 | private int code; 7 | private boolean longpress; 8 | 9 | public KeyCommand(int keyCode) { 10 | this.code = keyCode; 11 | } 12 | 13 | public KeyCommand(InputKeyEvent inputKeyEvent) { 14 | code = inputKeyEvent.getCode(); 15 | } 16 | 17 | public KeyCommand(int keyCode, boolean longpress) { 18 | this(keyCode); 19 | this.longpress = longpress; 20 | } 21 | 22 | public KeyCommand(InputKeyEvent inputKeyEvent, boolean longpress) { 23 | this(inputKeyEvent); 24 | this.longpress = longpress; 25 | } 26 | 27 | public void setLongPress(boolean longpress) { 28 | this.longpress = longpress; 29 | } 30 | 31 | @Override 32 | protected String getCommandPart() { 33 | StringBuilder stringBuilder = new StringBuilder("keyevent"); 34 | if (longpress) { 35 | stringBuilder.append(" --longpress"); 36 | } 37 | stringBuilder.append(' ').append(code); 38 | return stringBuilder.toString(); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyboardActionListener.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui.interaction; 2 | 3 | import com.github.xsavikx.androidscreencast.api.command.executor.CommandExecutor; 4 | import com.github.xsavikx.androidscreencast.api.command.factory.AdbInputCommandFactory; 5 | import com.github.xsavikx.androidscreencast.spring.config.ApplicationContextProvider; 6 | 7 | import javax.swing.*; 8 | import java.awt.event.ActionEvent; 9 | import java.awt.event.ActionListener; 10 | 11 | public class KeyboardActionListener implements ActionListener { 12 | private CommandExecutor commandExecutor; 13 | private int key; 14 | 15 | public KeyboardActionListener(int key) { 16 | this.key = key; 17 | } 18 | 19 | @Override 20 | public void actionPerformed(ActionEvent e) { 21 | SwingUtilities.invokeLater(new Runnable() { 22 | @Override 23 | public void run() { 24 | getCommandExecutor().execute(AdbInputCommandFactory.getKeyCommand(key)); 25 | } 26 | }); 27 | 28 | } 29 | 30 | private CommandExecutor getCommandExecutor() { 31 | if (commandExecutor == null) { 32 | commandExecutor = ApplicationContextProvider.getApplicationContext().getBean(CommandExecutor.class); 33 | } 34 | return commandExecutor; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/app/SwingApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.app; 2 | 3 | import com.github.xsavikx.androidscreencast.ui.JDialogError; 4 | 5 | import javax.swing.*; 6 | import java.io.PrintWriter; 7 | import java.io.StringWriter; 8 | 9 | public abstract class SwingApplication extends GUIApplication { 10 | private JDialogError jd = null; 11 | 12 | protected abstract boolean isNativeLook(); 13 | 14 | @Override 15 | public void init() { 16 | try { 17 | if (isNativeLook()) 18 | UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 19 | } catch (Exception ex) { 20 | throw new RuntimeException(ex); 21 | } 22 | } 23 | 24 | @Override 25 | public void handleException(Thread thread, Throwable ex) { 26 | try { 27 | StringWriter sw = new StringWriter(); 28 | ex.printStackTrace(new PrintWriter(sw)); 29 | if (sw.toString().contains("SynthTreeUI")) 30 | return; 31 | ex.printStackTrace(System.err); 32 | if (jd != null && jd.isVisible()) 33 | return; 34 | jd = new JDialogError(ex); 35 | SwingUtilities.invokeLater(new Runnable() { 36 | 37 | @Override 38 | public void run() { 39 | jd.setVisible(true); 40 | } 41 | }); 42 | } catch (Exception ex2) { 43 | // ignored 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogUrl.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.event.ActionEvent; 6 | import java.awt.event.ActionListener; 7 | 8 | public class JDialogUrl extends JDialog { 9 | 10 | private static final long serialVersionUID = -331017582679776599L; 11 | private JTextField jtfUrl = new JTextField(); 12 | private JButton jbOk = new JButton("Ok"); 13 | private boolean result = false; 14 | 15 | public JDialogUrl() { 16 | setModal(true); 17 | setTitle("Open url"); 18 | 19 | setLayout(new BorderLayout()); 20 | add(jbOk, BorderLayout.SOUTH); 21 | add(jtfUrl, BorderLayout.CENTER); 22 | jtfUrl.setColumns(50); 23 | 24 | jbOk.addActionListener(new ActionListener() { 25 | 26 | @Override 27 | public void actionPerformed(ActionEvent arg0) { 28 | setResult(true); 29 | JDialogUrl.this.setVisible(false); 30 | } 31 | }); 32 | 33 | jbOk.setDefaultCapable(true); 34 | getRootPane().setDefaultButton(jbOk); 35 | pack(); 36 | setLocationRelativeTo(null); 37 | 38 | } 39 | 40 | public JTextField getJtfUrl() { 41 | return jtfUrl; 42 | } 43 | 44 | public void setJtfUrl(JTextField jtfUrl) { 45 | this.jtfUrl = jtfUrl; 46 | } 47 | 48 | public boolean isResult() { 49 | return result; 50 | } 51 | 52 | public void setResult(boolean result) { 53 | this.result = result; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/JPanelScreen.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.image.BufferedImage; 6 | 7 | public class JPanelScreen extends JPanel { 8 | 9 | private static final long serialVersionUID = -2034873107028503004L; 10 | private float coef = 1; 11 | private double origX; 12 | private double origY; 13 | private Dimension size = null; 14 | private BufferedImage image = null; 15 | 16 | public JPanelScreen() { 17 | this.setFocusable(true); 18 | } 19 | 20 | public Point getRawPoint(Point p1) { 21 | Point p2 = new Point(); 22 | p2.x = (int) ((p1.x - origX) / coef); 23 | p2.y = (int) ((p1.y - origY) / coef); 24 | return p2; 25 | } 26 | 27 | public void handleNewImage(Dimension size, BufferedImage image) { 28 | this.size = size; 29 | this.image = image; 30 | repaint(); 31 | } 32 | 33 | @Override 34 | protected void paintComponent(Graphics g) { 35 | if (size == null) 36 | return; 37 | if (size.height == 0) 38 | return; 39 | Graphics2D g2 = (Graphics2D) g; 40 | g2.clearRect(0, 0, getWidth(), getHeight()); 41 | double width = Math.min(getWidth(), size.width * getHeight() / size.height); 42 | coef = (float) width / size.width; 43 | double height = width * size.height / size.width; 44 | origX = (getWidth() - width) / 2; 45 | origY = (getHeight() - height) / 2; 46 | g2.drawImage(image, (int) origX, (int) origY, (int) width, (int) height, this); 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | AndroidScreencast 2 | ================= 3 | [![Build Status Travis-CI](https://travis-ci.org/xSAVIKx/AndroidScreencast.svg?branch=master)](https://travis-ci.org/xSAVIKx/AndroidScreencast) 4 | #Description 5 | 6 | AndroidScreencast application was developed to view and control your android iDevice on PC. 7 | 8 | This project gives you opportunity to use your phone even with broken screen. 9 | 10 | ##Features 11 | - No client needed 12 | - Support for Tap and Swipe gestures 13 | - Write messages using PC keyboard 14 | - Support for landscape mode 15 | - Browse your phone files on PC 16 | 17 | 18 | [Small wiki of project](https://github.com/xSAVIKx/AndroidScreencast/wiki) 19 | 20 | ## JNLP 21 | 22 | Application is available using Java web start technology via [androidscreencast.jnlp](http://xsavikx.github.io/AndroidScreencast/jnlp/androidscreencast.jnlp). 23 | 24 | ### Java security configuration 25 | 26 | Due to Java security restriction policy, that was updated in java 7 and 27 | is restricted even more in java 8, we're now not able to run JNLP without some security "hacks". 28 | 29 | To use JNLP, please follow this article: [How can I configure the Exception Site List?](https://www.java.com/en/download/faq/exception_sitelist.xml) 30 | 31 | ##Building and running from source 32 | 33 | This project requires at least Java 7 and Maven 3.2.5 34 | 35 | After cloning the project, run `mvn package` 36 | 37 | The resulting artifacts will be created in the `target` subdirectory 38 | 39 | You can run the executable jar via `java -jar target/androidscreencast-VERSION-executable.jar`, replacing VERSION with the current version. 40 | 41 | For example, `java -jar target/androidscreencast-0.0.7s-executable.jar` 42 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyEventDispatcherImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui.interaction; 2 | 3 | import com.github.xsavikx.androidscreencast.api.command.executor.CommandExecutor; 4 | import com.github.xsavikx.androidscreencast.api.command.factory.AdbInputCommandFactory; 5 | import com.github.xsavikx.androidscreencast.api.injector.KeyCodeConverter; 6 | import com.github.xsavikx.androidscreencast.spring.config.ApplicationContextProvider; 7 | 8 | import javax.swing.*; 9 | import java.awt.*; 10 | import java.awt.event.KeyEvent; 11 | 12 | public class KeyEventDispatcherImpl implements KeyEventDispatcher { 13 | private CommandExecutor commandExecutor; 14 | private Window window; 15 | 16 | public KeyEventDispatcherImpl(Window frame) { 17 | this.window = frame; 18 | } 19 | 20 | @Override 21 | public boolean dispatchKeyEvent(KeyEvent e) { 22 | if (!window.isActive()) 23 | return false; 24 | if (e.getID() == KeyEvent.KEY_TYPED) { 25 | final int code = KeyCodeConverter.getKeyCode(e); 26 | SwingUtilities.invokeLater(new Runnable() { 27 | 28 | @Override 29 | public void run() { 30 | getCommandExecutor().execute(AdbInputCommandFactory.getKeyCommand(code)); 31 | 32 | } 33 | }); 34 | 35 | } 36 | return false; 37 | } 38 | 39 | private CommandExecutor getCommandExecutor() { 40 | if (commandExecutor == null) { 41 | commandExecutor = ApplicationContextProvider.getApplicationContext().getBean(CommandExecutor.class); 42 | } 43 | return commandExecutor; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/spring/config/ApplicationConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.spring.config; 2 | 3 | import com.android.ddmlib.AndroidDebugBridge; 4 | import com.android.ddmlib.IDevice; 5 | import com.github.xsavikx.androidscreencast.app.DeviceChooserApplication; 6 | import com.github.xsavikx.androidscreencast.constant.Constants; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.beans.factory.support.DefaultListableBeanFactory; 9 | import org.springframework.context.annotation.*; 10 | import org.springframework.core.env.Environment; 11 | 12 | @Configuration 13 | @ComponentScan(basePackages = "com.github.xsavikx.androidscreencast") 14 | @PropertySources(value = { 15 | @PropertySource(value = "file:${user.dir}/app.properties", ignoreResourceNotFound = true) 16 | }) 17 | 18 | public class ApplicationConfiguration { 19 | @Autowired 20 | private Environment env; 21 | 22 | @Bean 23 | public AndroidDebugBridge initAndroidDebugBridge() { 24 | AndroidDebugBridge.initIfNeeded(false); 25 | if (env.containsProperty(Constants.ADB_PATH_PROPERTY)) { 26 | return AndroidDebugBridge.createBridge(env.getProperty(Constants.ADB_PATH_PROPERTY), false); 27 | } 28 | return AndroidDebugBridge.createBridge(); 29 | } 30 | 31 | @Bean 32 | public DefaultListableBeanFactory initBeanFactory() { 33 | return new DefaultListableBeanFactory(); 34 | } 35 | 36 | @Bean 37 | @Autowired 38 | public IDevice initDevice(DeviceChooserApplication application) { 39 | application.init(); 40 | application.start(); 41 | application.close(); 42 | return application.getDevice(); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/model/InputKeyEventTableModel.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui.model; 2 | 3 | import com.github.xsavikx.androidscreencast.api.injector.InputKeyEvent; 4 | 5 | import javax.swing.table.AbstractTableModel; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | public class InputKeyEventTableModel extends AbstractTableModel { 10 | private static final long serialVersionUID = 1553313932570896541L; 11 | private final static String INDEX_COLUMN_NAME = "#"; 12 | private final static String TITLE_COLUMN_NAME = "title"; 13 | private final static String DESCRIPTION_COLUMN_NAME = "description"; 14 | public final String[] columnNames = { 15 | INDEX_COLUMN_NAME, TITLE_COLUMN_NAME, DESCRIPTION_COLUMN_NAME 16 | }; 17 | private List> data = new ArrayList<>(); 18 | private int rowCount = 0; 19 | 20 | public InputKeyEventTableModel(InputKeyEvent[] initialData) { 21 | initData(initialData); 22 | } 23 | 24 | private void initData(InputKeyEvent[] inputKeyEvents) { 25 | for (InputKeyEvent e : inputKeyEvents) { 26 | data.add(getDataRow(e)); 27 | } 28 | } 29 | 30 | private List getDataRow(InputKeyEvent e) { 31 | List row = new ArrayList<>(); 32 | row.add(rowCount++); 33 | row.add(e.name()); 34 | row.add(e.getDescription()); 35 | return row; 36 | } 37 | 38 | @Override 39 | public int getRowCount() { 40 | return data.size(); 41 | } 42 | 43 | @Override 44 | public int getColumnCount() { 45 | return columnNames.length; 46 | } 47 | 48 | @Override 49 | public Object getValueAt(int rowIndex, int columnIndex) { 50 | return data.get(rowIndex).get(columnIndex); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/command/executor/ShellCommandExecutor.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.command.executor; 2 | 3 | import com.android.ddmlib.AdbCommandRejectedException; 4 | import com.android.ddmlib.IDevice; 5 | import com.android.ddmlib.ShellCommandUnresponsiveException; 6 | import com.android.ddmlib.TimeoutException; 7 | import com.github.xsavikx.androidscreencast.api.command.Command; 8 | import com.github.xsavikx.androidscreencast.api.command.exception.AdbShellCommandExecutionException; 9 | import com.github.xsavikx.androidscreencast.api.injector.MultiLineReceiverPrinter; 10 | import org.apache.log4j.Logger; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Service; 13 | 14 | import java.io.IOException; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | @Service 18 | public class ShellCommandExecutor implements CommandExecutor { 19 | private static final Logger LOGGER = Logger.getLogger(ShellCommandExecutor.class); 20 | private static final int MAX_TIME_TO_WAIT_RESPONSE = 5; 21 | @Autowired 22 | private IDevice device; 23 | 24 | @Override 25 | public void execute(Command command) { 26 | LOGGER.debug("execute(Command command=" + command + ") - start"); 27 | 28 | try { 29 | device.executeShellCommand(command.getFormattedCommand(), new MultiLineReceiverPrinter(), 30 | MAX_TIME_TO_WAIT_RESPONSE, TimeUnit.SECONDS); 31 | } catch (TimeoutException | AdbCommandRejectedException | ShellCommandUnresponsiveException | IOException e) { 32 | LOGGER.error("execute(Command)", e); 33 | throw new AdbShellCommandExecutionException(command, e); 34 | } 35 | 36 | LOGGER.debug("execute(Command command=" + command + ") - end"); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/model/InputKeyEventTable.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui.model; 2 | 3 | import javax.swing.*; 4 | import javax.swing.table.TableCellRenderer; 5 | import javax.swing.table.TableColumn; 6 | import javax.swing.table.TableColumnModel; 7 | import java.awt.*; 8 | 9 | public class InputKeyEventTable extends JTable { 10 | private static final long serialVersionUID = 3978642864003531967L; 11 | 12 | private final static int MIN_COLUMN_WIDTH = 20; 13 | 14 | public InputKeyEventTable(InputKeyEventTableModel tableModel) { 15 | super(tableModel); 16 | setTableColumnsNames(tableModel.columnNames); 17 | setAutoResizeMode(JTable.AUTO_RESIZE_OFF); 18 | setTableColumnsPreferredSize(); 19 | setSelectionMode(ListSelectionModel.SINGLE_SELECTION); 20 | } 21 | 22 | private void setTableColumnsNames(String[] columnNames) { 23 | TableColumnModel columnModel = getColumnModel(); 24 | for (int i = 0; i < columnNames.length; i++) { 25 | TableColumn column = columnModel.getColumn(i); 26 | column.setHeaderValue(columnNames[i]); 27 | } 28 | } 29 | 30 | private void setTableColumnsPreferredSize() { 31 | final TableColumnModel columnModel = getColumnModel(); 32 | for (int column = 0; column < getColumnCount(); column++) { 33 | int width = MIN_COLUMN_WIDTH; // Min width 34 | for (int row = 0; row < getRowCount(); row++) { 35 | TableCellRenderer renderer = getCellRenderer(row, column); 36 | Component comp = prepareRenderer(renderer, row, column); 37 | width = Math.max(comp.getPreferredSize().width + 5, width); 38 | } 39 | columnModel.getColumn(column).setPreferredWidth(width); 40 | } 41 | } 42 | 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/command/factory/AdbInputCommandFactory.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.command.factory; 2 | 3 | import com.github.xsavikx.androidscreencast.api.command.KeyCommand; 4 | import com.github.xsavikx.androidscreencast.api.command.SwipeCommand; 5 | import com.github.xsavikx.androidscreencast.api.command.TapCommand; 6 | import com.github.xsavikx.androidscreencast.api.injector.InputKeyEvent; 7 | import org.apache.log4j.Logger; 8 | import org.springframework.stereotype.Service; 9 | 10 | @Service 11 | public final class AdbInputCommandFactory { 12 | private static final Logger LOGGER = Logger.getLogger(AdbInputCommandFactory.class); 13 | 14 | public static KeyCommand getKeyCommand(int keyCode) { 15 | KeyCommand returnKeyCommand = new KeyCommand(keyCode); 16 | LOGGER.debug(returnKeyCommand); 17 | return returnKeyCommand; 18 | } 19 | 20 | public static KeyCommand getKeyCommand(InputKeyEvent inputKeyEvent) { 21 | KeyCommand returnKeyCommand = new KeyCommand(inputKeyEvent); 22 | LOGGER.debug(returnKeyCommand); 23 | return returnKeyCommand; 24 | } 25 | 26 | 27 | public static KeyCommand getKeyCommand(int keyCode, boolean longpress) { 28 | KeyCommand returnKeyCommand = new KeyCommand(keyCode, longpress); 29 | LOGGER.debug(returnKeyCommand); 30 | return returnKeyCommand; 31 | } 32 | 33 | public static KeyCommand getKeyCommand(InputKeyEvent inputKeyEvent, boolean longpress) { 34 | KeyCommand returnKeyCommand = new KeyCommand(inputKeyEvent, longpress); 35 | LOGGER.debug(returnKeyCommand); 36 | return returnKeyCommand; 37 | } 38 | 39 | public static SwipeCommand getSwipeCommand(int x1, int y1, int x2, int y2, long duration) { 40 | SwipeCommand returnSwipeCommand = new SwipeCommand(x1, y1, x2, y2, duration); 41 | LOGGER.debug(returnSwipeCommand); 42 | return returnSwipeCommand; 43 | } 44 | 45 | public static SwipeCommand getSwipeCommand(int x1, int y1, int x2, int y2) { 46 | SwipeCommand returnSwipeCommand = new SwipeCommand(x1, y1, x2, y2); 47 | LOGGER.debug(returnSwipeCommand); 48 | return returnSwipeCommand; 49 | } 50 | 51 | public static TapCommand getTapCommand(int x, int y) { 52 | TapCommand returnTapCommand = new TapCommand(x, y); 53 | LOGGER.debug(returnTapCommand); 54 | return returnTapCommand; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/app/AndroidScreencastApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.app; 2 | 3 | import com.android.ddmlib.AndroidDebugBridge; 4 | import com.android.ddmlib.IDevice; 5 | import com.github.xsavikx.androidscreencast.api.injector.Injector; 6 | import com.github.xsavikx.androidscreencast.constant.Constants; 7 | import com.github.xsavikx.androidscreencast.ui.JFrameMain; 8 | import org.apache.log4j.Logger; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.core.env.Environment; 11 | import org.springframework.stereotype.Component; 12 | 13 | import javax.swing.*; 14 | 15 | @Component 16 | public class AndroidScreencastApplication extends SwingApplication { 17 | private static final Logger LOGGER = Logger.getLogger(AndroidScreencastApplication.class); 18 | 19 | private final Environment environment; 20 | private final JFrameMain jFrameMain; 21 | private final Injector injector; 22 | private final IDevice iDevice; 23 | 24 | @Autowired 25 | public AndroidScreencastApplication(Injector injector, IDevice iDevice, JFrameMain jFrameMain, Environment environment) { 26 | this.injector = injector; 27 | this.iDevice = iDevice; 28 | this.jFrameMain = jFrameMain; 29 | this.environment = environment; 30 | } 31 | 32 | @Override 33 | public void close() { 34 | LOGGER.debug("close() - start"); 35 | 36 | if (injector != null) 37 | injector.close(); 38 | 39 | if (iDevice != null) { 40 | synchronized (iDevice) { 41 | if (hasFilledAdbPath()) 42 | AndroidDebugBridge.disconnectBridge(); 43 | AndroidDebugBridge.terminate(); 44 | } 45 | } 46 | 47 | LOGGER.debug("close() - end"); 48 | } 49 | 50 | @Override 51 | public void start() { 52 | LOGGER.debug("start() - start"); 53 | SwingUtilities.invokeLater(new Runnable() { 54 | @Override 55 | public void run() { 56 | // Start showing the iDevice screen 57 | jFrameMain.setTitle("" + iDevice); 58 | 59 | // Show window 60 | jFrameMain.setVisible(true); 61 | 62 | jFrameMain.launchInjector(); 63 | } 64 | }); 65 | LOGGER.debug("start() - end"); 66 | } 67 | 68 | @SuppressWarnings("boxing") 69 | @Override 70 | protected boolean isNativeLook() { 71 | LOGGER.debug("isNativeLook() - start"); 72 | 73 | boolean useNativeLook = environment.getProperty(Constants.APP_NATIVE_LOOK_PROPERTY, Boolean.class, 74 | Constants.DEFAULT_APP_NATIVE_LOOK); 75 | LOGGER.debug("isNativeLook() - end"); 76 | return useNativeLook; 77 | } 78 | 79 | private boolean hasFilledAdbPath() { 80 | LOGGER.debug("hasFilledAdbPath() - start"); 81 | 82 | boolean hasAdbPath = environment.getProperty(Constants.ADB_PATH_PROPERTY) != null; 83 | LOGGER.debug("hasFilledAdbPath() - end"); 84 | return hasAdbPath; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/app/DeviceChooserApplication.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.app; 2 | 3 | import com.android.ddmlib.AndroidDebugBridge; 4 | import com.android.ddmlib.IDevice; 5 | import com.github.xsavikx.androidscreencast.constant.Constants; 6 | import com.github.xsavikx.androidscreencast.ui.JDialogDeviceList; 7 | import org.apache.log4j.Logger; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.core.env.Environment; 10 | import org.springframework.stereotype.Component; 11 | 12 | @Component 13 | public class DeviceChooserApplication extends SwingApplication { 14 | private static final Logger LOGGER = Logger.getLogger(DeviceChooserApplication.class); 15 | 16 | @Autowired 17 | private Environment env; 18 | @Autowired 19 | private AndroidDebugBridge bridge; 20 | 21 | private IDevice device; 22 | 23 | @Override 24 | public void close() { 25 | // ignore 26 | } 27 | 28 | @Override 29 | public void start() { 30 | LOGGER.debug("start() - start"); 31 | initialize(); 32 | 33 | LOGGER.debug("start() - end"); 34 | } 35 | 36 | @SuppressWarnings("boxing") 37 | @Override 38 | protected boolean isNativeLook() { 39 | LOGGER.debug("isNativeLook() - start"); 40 | 41 | boolean returnboolean = env.getProperty(Constants.APP_NATIVE_LOOK_PROPERTY, Boolean.class, 42 | Constants.DEFAULT_APP_NATIVE_LOOK); 43 | LOGGER.debug("isNativeLook() - end"); 44 | return returnboolean; 45 | } 46 | 47 | private void waitDeviceList(AndroidDebugBridge bridge) { 48 | LOGGER.debug("waitDeviceList(AndroidDebugBridge bridge=" + bridge + ") - start"); 49 | 50 | int count = 0; 51 | while (!bridge.hasInitialDeviceList()) { 52 | try { 53 | Thread.sleep(100); 54 | count++; 55 | } catch (InterruptedException e) { 56 | LOGGER.warn("waitDeviceList(AndroidDebugBridge) - exception ignored", e); 57 | 58 | } 59 | // let's not wait > 10 sec. 60 | if (count > 300) { 61 | throw new RuntimeException("Timeout getting device list!"); 62 | } 63 | } 64 | 65 | LOGGER.debug("waitDeviceList(AndroidDebugBridge bridge=" + bridge + ") - end"); 66 | } 67 | 68 | private void initialize() { 69 | LOGGER.debug("initialize() - start"); 70 | 71 | waitDeviceList(bridge); 72 | 73 | IDevice devices[] = bridge.getDevices(); 74 | // Let the user choose the device 75 | if (devices.length == 1) { 76 | device = devices[0]; 77 | } else { 78 | JDialogDeviceList jd = new JDialogDeviceList(devices); 79 | jd.setVisible(true); 80 | 81 | device = jd.getDevice(); 82 | } 83 | if (device == null) { 84 | System.exit(0); 85 | 86 | LOGGER.debug("initialize() - end"); 87 | return; 88 | } 89 | 90 | LOGGER.debug("initialize() - end"); 91 | } 92 | 93 | public IDevice getDevice() { 94 | return device; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/recording/FilterImageOutputStream.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.recording; 2 | 3 | import javax.imageio.stream.ImageOutputStream; 4 | import java.io.FilterOutputStream; 5 | import java.io.IOException; 6 | 7 | public class FilterImageOutputStream extends FilterOutputStream { 8 | private ImageOutputStream imgOut; 9 | 10 | public FilterImageOutputStream(ImageOutputStream iOut) { 11 | super(null); 12 | this.imgOut = iOut; 13 | } 14 | 15 | /** 16 | * Closes this output stream and releases any system resources associated with the stream. 17 | *

18 | * The close method of FilterOutputStream calls its flush method, and then calls the close 19 | * method of its underlying output stream. 20 | * 21 | * @throws IOException if an I/O error occurs. 22 | * @see java.io.FilterOutputStream#flush() 23 | * @see java.io.FilterOutputStream#out 24 | */ 25 | @Override 26 | public void close() throws IOException { 27 | flush(); 28 | imgOut.close(); 29 | } 30 | 31 | /** 32 | * Flushes this output stream and forces any buffered output bytes to be written out to the stream. 33 | *

34 | * The flush method of FilterOutputStream calls the flush method of its underlying output stream. 35 | * 36 | * @throws IOException if an I/O error occurs. 37 | * @see java.io.FilterOutputStream#out 38 | */ 39 | @Override 40 | public void flush() { 41 | // System.err.println(this+" discarded flush"); 42 | // imgOut.flush(); 43 | } 44 | 45 | /** 46 | * Writes len bytes from the specified byte array starting at offset off to this output stream. 47 | *

48 | * The write method of FilterOutputStream calls the write method of one argument on each byte to 49 | * output. 50 | *

51 | * Note that this method does not call the write method of its underlying input stream with the same arguments. Subclasses of 52 | * FilterOutputStream should provide a more efficient implementation of this method. 53 | * 54 | * @param b the data. 55 | * @param off the start offset in the data. 56 | * @param len the number of bytes to write. 57 | * @throws IOException if an I/O error occurs. 58 | * @see java.io.FilterOutputStream#write(int) 59 | */ 60 | @Override 61 | public void write(byte b[], int off, int len) throws IOException { 62 | imgOut.write(b, off, len); 63 | } 64 | 65 | /** 66 | * Writes the specified byte to this output stream. 67 | *

68 | * The write method of FilterOutputStream calls the write method of its underlying output stream, that is, it 69 | * performs out.write(b). 70 | *

71 | * Implements the abstract write method of OutputStream. 72 | * 73 | * @param b the byte. 74 | * @throws IOException if an I/O error occurs. 75 | */ 76 | @Override 77 | public void write(int b) throws IOException { 78 | imgOut.write(b); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogDeviceList.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui; 2 | 3 | import com.android.ddmlib.IDevice; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | import java.awt.event.ActionEvent; 8 | import java.awt.event.ActionListener; 9 | import java.awt.event.MouseAdapter; 10 | import java.awt.event.MouseEvent; 11 | 12 | public class JDialogDeviceList extends JDialog implements ActionListener { 13 | 14 | private static final long serialVersionUID = -3719844308147203239L; 15 | private static final String DEFAULT_HOST = "127.0.0.1"; 16 | private static final int DEFAULT_PORT = 1324; 17 | 18 | private JTextField jtfHost = new JTextField(DEFAULT_HOST); 19 | private JFormattedTextField jftfPort = new JFormattedTextField(DEFAULT_PORT); 20 | private JList jlDevices = new JList(); 21 | private JPanel jpAgent = new JPanel(); 22 | private JPanel jpButtons = new JPanel(); 23 | private JButton jbOk = new JButton("OK"); 24 | private JButton jbQuit = new JButton("Quit"); 25 | 26 | private boolean cancelled = false; 27 | private IDevice[] devices; 28 | 29 | public JDialogDeviceList(IDevice[] devices) { 30 | super(); 31 | setModal(true); 32 | this.devices = devices; 33 | initialize(); 34 | } 35 | 36 | @Override 37 | public void actionPerformed(ActionEvent arg0) { 38 | cancelled = arg0.getSource() == jbQuit; 39 | 40 | setVisible(false); 41 | } 42 | 43 | public IDevice getDevice() { 44 | if (cancelled) 45 | return null; 46 | return jlDevices.getSelectedValue(); 47 | } 48 | 49 | private void initialize() { 50 | setTitle("Please select a device"); 51 | jlDevices.setListData(devices); 52 | jlDevices.setPreferredSize(new Dimension(400, 300)); 53 | if (devices.length != 0) 54 | jlDevices.setSelectedIndex(0); 55 | jbOk.setEnabled(!jlDevices.isSelectionEmpty()); 56 | 57 | jpAgent.setBorder(BorderFactory.createTitledBorder("Agent")); 58 | jpAgent.setLayout(new BorderLayout(10, 10)); 59 | jpAgent.add(jtfHost, BorderLayout.CENTER); 60 | jpAgent.add(jftfPort, BorderLayout.EAST); 61 | 62 | jpButtons.setLayout(new FlowLayout(FlowLayout.RIGHT)); 63 | jpButtons.add(jbOk, BorderLayout.CENTER); 64 | jpButtons.add(jbQuit, BorderLayout.SOUTH); 65 | 66 | JPanel jpBottom = new JPanel(); 67 | jpBottom.setLayout(new BorderLayout()); 68 | jpBottom.add(jpAgent, BorderLayout.CENTER); 69 | jpBottom.add(jpButtons, BorderLayout.SOUTH); 70 | 71 | setLayout(new BorderLayout()); 72 | add(jlDevices, BorderLayout.CENTER); 73 | add(jpBottom, BorderLayout.SOUTH); 74 | 75 | pack(); 76 | setLocationRelativeTo(null); 77 | 78 | jbOk.addActionListener(this); 79 | jbQuit.addActionListener(this); 80 | jlDevices.addMouseListener(new MouseAdapter() { 81 | 82 | @Override 83 | public void mouseClicked(MouseEvent e) { 84 | if (e.getClickCount() == 2) { 85 | int index = jlDevices.locationToIndex(e.getPoint()); 86 | jlDevices.ensureIndexIsVisible(index); 87 | cancelled = false; 88 | setVisible(false); 89 | } 90 | } 91 | 92 | }); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### JetBrains template 3 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 4 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 5 | .idea 6 | # User-specific stuff: 7 | .idea/workspace.xml 8 | .idea/tasks.xml 9 | .idea/dictionaries 10 | .idea/vcs.xml 11 | .idea/jsLibraryMappings.xml 12 | 13 | # Sensitive or high-churn files: 14 | .idea/dataSources.ids 15 | .idea/dataSources.xml 16 | .idea/dataSources.local.xml 17 | .idea/sqlDataSources.xml 18 | .idea/dynamic.xml 19 | .idea/uiDesigner.xml 20 | 21 | # Gradle: 22 | .idea/gradle.xml 23 | .idea/libraries 24 | 25 | # Mongo Explorer plugin: 26 | .idea/mongoSettings.xml 27 | 28 | ## File-based project format: 29 | *.iws 30 | 31 | ## Plugin-specific files: 32 | 33 | # IntelliJ 34 | /out/ 35 | 36 | # mpeltonen/sbt-idea plugin 37 | .idea_modules/ 38 | 39 | # JIRA plugin 40 | atlassian-ide-plugin.xml 41 | 42 | # Crashlytics plugin (for Android Studio and IntelliJ) 43 | com_crashlytics_export_strings.xml 44 | crashlytics.properties 45 | crashlytics-build.properties 46 | fabric.properties 47 | ### Java template 48 | *.class 49 | 50 | # Mobile Tools for Java (J2ME) 51 | .mtj.tmp/ 52 | 53 | # Package Files # 54 | *.jar 55 | *.war 56 | *.ear 57 | 58 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 59 | hs_err_pid* 60 | ### NotepadPP template 61 | # Notepad++ backups # 62 | *.bak 63 | ### NetBeans template 64 | nbproject/private/ 65 | build/ 66 | nbbuild/ 67 | dist/ 68 | nbdist/ 69 | nbactions.xml 70 | .nb-gradle/ 71 | ### Eclipse template 72 | 73 | .metadata 74 | bin/ 75 | tmp/ 76 | *.tmp 77 | *.swp 78 | *~.nib 79 | local.properties 80 | .settings/ 81 | .loadpath 82 | .recommenders 83 | 84 | # Eclipse Core 85 | .project 86 | 87 | # External tool builders 88 | .externalToolBuilders/ 89 | 90 | # Locally stored "Eclipse launch configurations" 91 | *.launch 92 | 93 | # PyDev specific (Python IDE for Eclipse) 94 | *.pydevproject 95 | 96 | # CDT-specific (C/C++ Development Tooling) 97 | .cproject 98 | 99 | # JDT-specific (Eclipse Java Development Tools) 100 | .classpath 101 | 102 | # Java annotation processor (APT) 103 | .factorypath 104 | 105 | # PDT-specific (PHP Development Tools) 106 | .buildpath 107 | 108 | # sbteclipse plugin 109 | .target 110 | 111 | # Tern plugin 112 | .tern-project 113 | 114 | # TeXlipse plugin 115 | .texlipse 116 | 117 | # STS (Spring Tool Suite) 118 | .springBeans 119 | 120 | # Code Recommenders 121 | .recommenders/ 122 | ### Android template 123 | # Built application files 124 | *.apk 125 | *.ap_ 126 | 127 | # Files for the ART/Dalvik VM 128 | *.dex 129 | 130 | # Java class files 131 | 132 | # Generated files 133 | gen/ 134 | out/ 135 | 136 | # Gradle files 137 | .gradle/ 138 | 139 | # Local configuration file (sdk path, etc) 140 | 141 | # Proguard folder generated by Eclipse 142 | proguard/ 143 | 144 | # Log Files 145 | *.log 146 | 147 | # Android Studio Navigation editor temp files 148 | .navigation/ 149 | 150 | # Android Studio captures folder 151 | captures/ 152 | 153 | # Intellij 154 | *.iml 155 | 156 | # Keystore files 157 | *.jks 158 | ### Maven template 159 | target/ 160 | pom.xml.tag 161 | pom.xml.releaseBackup 162 | pom.xml.versionsBackup 163 | pom.xml.next 164 | release.properties 165 | dependency-reduced-pom.xml 166 | buildNumber.properties 167 | .mvn/timing.properties 168 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/MouseActionAdapter.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui.interaction; 2 | 3 | import com.github.xsavikx.androidscreencast.api.command.executor.CommandExecutor; 4 | import com.github.xsavikx.androidscreencast.api.command.factory.AdbInputCommandFactory; 5 | import com.github.xsavikx.androidscreencast.api.injector.Injector; 6 | import com.github.xsavikx.androidscreencast.spring.config.ApplicationContextProvider; 7 | import com.github.xsavikx.androidscreencast.ui.JPanelScreen; 8 | 9 | import javax.swing.*; 10 | import java.awt.*; 11 | import java.awt.event.MouseAdapter; 12 | import java.awt.event.MouseEvent; 13 | import java.awt.event.MouseWheelEvent; 14 | 15 | public class MouseActionAdapter extends MouseAdapter { 16 | private final static long ONE_SECOND = 1000L; 17 | private final JPanelScreen jp; 18 | private CommandExecutor commandExecutor; 19 | private Injector injector; 20 | private int dragFromX = -1; 21 | private int dragFromY = -1; 22 | private long timeFromPress = -1; 23 | 24 | public MouseActionAdapter(JPanelScreen jPanelScreen) { 25 | this.jp = jPanelScreen; 26 | } 27 | 28 | public MouseActionAdapter(JPanelScreen jPanelScreen, Injector injector) { 29 | this(jPanelScreen); 30 | this.injector = injector; 31 | } 32 | 33 | @Override 34 | public void mouseClicked(MouseEvent e) { 35 | if (injector != null && e.getButton() == MouseEvent.BUTTON3) { 36 | injector.screencapture.toogleOrientation(); 37 | e.consume(); 38 | return; 39 | } 40 | final Point p2 = jp.getRawPoint(e.getPoint()); 41 | if (p2.x > 0 && p2.y > 0) { 42 | SwingUtilities.invokeLater(new Runnable() { 43 | @Override 44 | public void run() { 45 | getCommandExecutor().execute(AdbInputCommandFactory.getTapCommand(p2.x, p2.y)); 46 | 47 | } 48 | }); 49 | } 50 | } 51 | 52 | @Override 53 | public void mouseDragged(MouseEvent e) { 54 | if (dragFromX == -1 && dragFromY == -1) { 55 | Point p2 = jp.getRawPoint(e.getPoint()); 56 | dragFromX = p2.x; 57 | dragFromY = p2.y; 58 | timeFromPress = System.currentTimeMillis(); 59 | } 60 | } 61 | 62 | @Override 63 | public void mouseReleased(MouseEvent e) { 64 | if (timeFromPress >= ONE_SECOND) { 65 | final Point p2 = jp.getRawPoint(e.getPoint()); 66 | final int xFrom = dragFromX; 67 | final int yFrom = dragFromY; 68 | final int xTo = p2.x; 69 | final int yTo = p2.y; 70 | SwingUtilities.invokeLater(new Runnable() { 71 | 72 | @Override 73 | public void run() { 74 | getCommandExecutor().execute(AdbInputCommandFactory.getSwipeCommand(xFrom, yFrom, xTo, yTo, timeFromPress)); 75 | } 76 | }); 77 | dragFromX = -1; 78 | dragFromY = -1; 79 | timeFromPress = -1; 80 | } 81 | } 82 | 83 | @Override 84 | public void mouseWheelMoved(MouseWheelEvent arg0) { 85 | // if (JFrameMain.this.injector == null) 86 | // return; 87 | // JFrameMain.this.injector.injectTrackball(arg0.getWheelRotation() < 0 ? 88 | // -1f : 1f); 89 | } 90 | 91 | private CommandExecutor getCommandExecutor() { 92 | if (commandExecutor == null) { 93 | commandExecutor = ApplicationContextProvider.getApplicationContext().getBean(CommandExecutor.class); 94 | } 95 | return commandExecutor; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/explorer/JFrameExplorer.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui.explorer; 2 | 3 | import com.github.xsavikx.androidscreencast.api.AndroidDevice; 4 | import com.github.xsavikx.androidscreencast.api.file.FileInfo; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Component; 7 | 8 | import javax.swing.*; 9 | import javax.swing.event.TreeSelectionEvent; 10 | import javax.swing.event.TreeSelectionListener; 11 | import javax.swing.tree.DefaultMutableTreeNode; 12 | import javax.swing.tree.DefaultTreeModel; 13 | import javax.swing.tree.TreePath; 14 | import java.awt.*; 15 | import java.awt.event.MouseAdapter; 16 | import java.awt.event.MouseEvent; 17 | import java.io.File; 18 | import java.util.LinkedHashMap; 19 | import java.util.List; 20 | import java.util.Map; 21 | import java.util.Vector; 22 | 23 | @Component 24 | public class JFrameExplorer extends JFrame { 25 | 26 | private static final long serialVersionUID = -5209265873286028854L; 27 | private JTree jt; 28 | private JSplitPane jSplitPane; 29 | @Autowired 30 | private AndroidDevice androidDevice; 31 | private JList jListFichiers; 32 | private Map> cache = new LinkedHashMap<>(); 33 | 34 | public JFrameExplorer() { 35 | 36 | setTitle("Explorer"); 37 | setLayout(new BorderLayout()); 38 | 39 | jt = new JTree(new DefaultMutableTreeNode("Test")); 40 | } 41 | 42 | public void launch() { 43 | 44 | jt.setModel(new DefaultTreeModel(new FolderTreeNode("Device", "/"))); 45 | jt.setRootVisible(true); 46 | jt.addTreeSelectionListener(new TreeSelectionListener() { 47 | 48 | @Override 49 | public void valueChanged(TreeSelectionEvent e) { 50 | TreePath tp = e.getPath(); 51 | if (tp == null) 52 | return; 53 | if (!(tp.getLastPathComponent() instanceof FolderTreeNode)) 54 | return; 55 | FolderTreeNode node = (FolderTreeNode) tp.getLastPathComponent(); 56 | displayFolder(node.path); 57 | } 58 | }); 59 | 60 | JScrollPane jsp = new JScrollPane(jt); 61 | 62 | jListFichiers = new JList<>(); 63 | jListFichiers.setListData(new Object[]{}); 64 | 65 | jSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, jsp, new JScrollPane(jListFichiers)); 66 | 67 | add(jSplitPane, BorderLayout.CENTER); 68 | setSize(640, 480); 69 | setLocationRelativeTo(null); 70 | 71 | jListFichiers.addMouseListener(new MouseAdapter() { 72 | 73 | @Override 74 | public void mouseClicked(MouseEvent e) { 75 | if (e.getClickCount() == 2) { 76 | int index = jListFichiers.locationToIndex(e.getPoint()); 77 | ListModel dlm = jListFichiers.getModel(); 78 | FileInfo item = (FileInfo) dlm.getElementAt(index); 79 | launchFile(item); 80 | } 81 | } 82 | 83 | }); 84 | } 85 | 86 | private void displayFolder(String path) { 87 | List fileInfos = cache.get(path); 88 | if (fileInfos == null) 89 | fileInfos = androidDevice.list(path); 90 | 91 | List files = new Vector<>(); 92 | for (FileInfo fi2 : fileInfos) { 93 | if (fi2.directory) 94 | continue; 95 | files.add(fi2); 96 | } 97 | jListFichiers.setListData(files.toArray()); 98 | 99 | } 100 | 101 | private void launchFile(FileInfo node) { 102 | try { 103 | File tempFile = node.downloadTemporary(); 104 | Desktop.getDesktop().open(tempFile); 105 | } catch (Exception ex) { 106 | throw new RuntimeException(ex); 107 | } 108 | } 109 | 110 | private class FolderTreeNode extends LazyMutableTreeNode { 111 | private static final long serialVersionUID = 9131974430354670263L; 112 | String name; 113 | String path; 114 | 115 | public FolderTreeNode(String name, String path) { 116 | this.name = name; 117 | this.path = path; 118 | } 119 | 120 | @Override 121 | public void initChildren() { 122 | List fileInfos = cache.get(path); 123 | if (fileInfos == null) 124 | fileInfos = androidDevice.list(path); 125 | for (FileInfo fi : fileInfos) { 126 | if (fi.directory) 127 | add(new FolderTreeNode(fi.name, path + fi.name + "/")); 128 | // else 129 | // add(new FileTreeNode(fi)); 130 | } 131 | } 132 | 133 | @Override 134 | public String toString() { 135 | return name; 136 | } 137 | 138 | } 139 | 140 | } 141 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogExecuteKeyEvent.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui; 2 | 3 | import com.github.xsavikx.androidscreencast.api.command.executor.CommandExecutor; 4 | import com.github.xsavikx.androidscreencast.api.command.factory.AdbInputCommandFactory; 5 | import com.github.xsavikx.androidscreencast.api.injector.InputKeyEvent; 6 | import com.github.xsavikx.androidscreencast.ui.model.InputKeyEventTable; 7 | import com.github.xsavikx.androidscreencast.ui.model.InputKeyEventTableModel; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | 11 | import javax.swing.*; 12 | import javax.swing.border.TitledBorder; 13 | import java.awt.*; 14 | import java.awt.event.ActionEvent; 15 | import java.awt.event.ActionListener; 16 | 17 | @Component 18 | public class JDialogExecuteKeyEvent extends JDialog { 19 | private static final long serialVersionUID = -4152020879675916776L; 20 | private static final int HEIGHT = 600; 21 | private static final int WIDTH = 800; 22 | private static final int BUTTON_HEIGHT = 20; 23 | private static final int BUTTON_WIDTH = WIDTH >> 1 - 5; 24 | 25 | private static final int TITLE_COLUMN_INDEX = 1; 26 | 27 | private static final String EXECUTE_BUTTON_TEXT = "Execute"; 28 | private static final String USE_LONG_PRESS_BUTTON_TEXT = "Long press"; 29 | private static final String CANCEL_BUTTON_TEXT = "Cancel"; 30 | private static final String COMMAND_LIST_TITLE_TEXT = "Commands to execute"; 31 | private static final String NO_COMMAND_CHOSEN_WARNING_MESSAGE = "Please, select command from the list"; 32 | private static final String NO_COMMAND_CHOSEN_WARNING_DIALOG_TITLE = "Warning"; 33 | 34 | @Autowired 35 | private CommandExecutor commandExecutor; 36 | 37 | /** 38 | * Create the dialog. 39 | */ 40 | public JDialogExecuteKeyEvent() { 41 | setResizable(false); 42 | setTitle("Execute key event"); 43 | setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); 44 | final InputKeyEventTableModel commandList = new InputKeyEventTableModel(InputKeyEvent.values()); 45 | final InputKeyEventTable commandListTable = new InputKeyEventTable(commandList); 46 | final JCheckBoxMenuItem useLongPress = new JCheckBoxMenuItem(USE_LONG_PRESS_BUTTON_TEXT, false); 47 | 48 | JButton executeCommandButton = new JButton(EXECUTE_BUTTON_TEXT); 49 | executeCommandButton.setSize(new Dimension(BUTTON_WIDTH, BUTTON_HEIGHT)); 50 | executeCommandButton.addActionListener(new ActionListener() { 51 | @Override 52 | public void actionPerformed(ActionEvent e) { 53 | int rowIndex = commandListTable.getSelectedRow(); 54 | if (rowIndex > 0) { 55 | final String title = (String) commandList.getValueAt(rowIndex, TITLE_COLUMN_INDEX); 56 | SwingUtilities.invokeLater(new Runnable() { 57 | @Override 58 | public void run() { 59 | commandExecutor.execute(AdbInputCommandFactory.getKeyCommand(InputKeyEvent.valueOf(title), 60 | useLongPress.getState())); 61 | } 62 | }); 63 | closeDialog(); 64 | } else { 65 | JOptionPane.showMessageDialog(null, NO_COMMAND_CHOSEN_WARNING_MESSAGE, NO_COMMAND_CHOSEN_WARNING_DIALOG_TITLE, 66 | JOptionPane.WARNING_MESSAGE); 67 | } 68 | } 69 | }); 70 | JButton cancelButton = new JButton(CANCEL_BUTTON_TEXT); 71 | cancelButton.addActionListener(new ActionListener() { 72 | @Override 73 | public void actionPerformed(ActionEvent e) { 74 | closeDialog(); 75 | } 76 | }); 77 | JScrollPane listScrollPane = new JScrollPane(commandListTable, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, 78 | ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); 79 | listScrollPane.setPreferredSize(new Dimension(WIDTH, HEIGHT)); 80 | listScrollPane.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), 81 | COMMAND_LIST_TITLE_TEXT, TitledBorder.CENTER, TitledBorder.TOP)); 82 | JPanel buttonPane = new JPanel(); 83 | buttonPane.add(executeCommandButton); 84 | buttonPane.add(useLongPress); 85 | buttonPane.add(cancelButton); 86 | buttonPane.setLayout(new GridLayout(1, 2)); 87 | JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, listScrollPane, buttonPane); 88 | splitPane.setEnabled(false); 89 | getContentPane().add(splitPane); 90 | pack(); 91 | setLocationRelativeTo(null); 92 | } 93 | 94 | /** 95 | * Launch the application. 96 | */ 97 | public static void main(String[] args) { 98 | SwingUtilities.invokeLater(new Runnable() { 99 | @Override 100 | public void run() { 101 | new JDialogExecuteKeyEvent().setVisible(true); 102 | } 103 | }); 104 | } 105 | 106 | private void closeDialog() { 107 | setVisible(false); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/worker/AccumulativeRunnable.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui.worker; 2 | 3 | /* 4 | * $Id: AccumulativeRunnable.java,v 1.3 2008/07/25 19:32:29 idk Exp $ 5 | * 6 | * Copyright @ 2005 Sun Microsystems, Inc. All rights 7 | * reserved. Use is subject to license terms. 8 | */ 9 | 10 | import javax.swing.*; 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.Collections; 14 | import java.util.List; 15 | 16 | /** 17 | * An abstract class to be used in the cases where we need {@code Runnable} to perform some actions on an appendable set of data. The set of data 18 | * might be appended after the {@code Runnable} is sent for the execution. Usually such {@code Runnables} are sent to the EDT. 19 | *

20 | *

21 | * Usage example: 22 | *

23 | *

24 | * Say we want to implement JLabel.setText(String text) which sends {@code text} string to the JLabel.setTextImpl(String text) on the EDT. In the 25 | * event JLabel.setText is called rapidly many times off the EDT we will get many updates on the EDT but only the last one is important. (Every next 26 | * updates overrides the previous one.) We might want to implement this {@code setText} in a way that only the last update is delivered. 27 | *

28 | * Here is how one can do this using {@code AccumulativeRunnable}: 29 | *

30 | *

 31 |  * AccumulativeRunnable doSetTextImpl =
 32 |  * new  AccumulativeRunnable() {
 33 |  *     @Override
 34 |  *     protected void run(List<String> args) {
 35 |  *         //set to the last string being passed
 36 |  *         setTextImpl(args.get(args.size() - 1);
 37 |  *     }
 38 |  * }
 39 |  * void setText(String text) {
 40 |  *     //add text and send for the execution if needed.
 41 |  *     doSetTextImpl.add(text);
 42 |  * }
 43 |  * 
44 | *

45 | *

46 | * Say we want want to implement addDirtyRegion(Rectangle rect) which sends this region to the handleDirtyRegions(List regions) on the EDT. 47 | * addDirtyRegions better be accumulated before handling on the EDT. 48 | *

49 | *

50 | * Here is how it can be implemented using AccumulativeRunnable: 51 | *

52 | *

 53 |  * AccumulativeRunnable<Rectangle> doHandleDirtyRegions = new AccumulativeRunnable<Rectangle>() {
 54 |  *   @Override
 55 |  *   protected void run(List<Rectangle> args) {
 56 |  *     handleDirtyRegions(args);
 57 |  *   }
 58 |  * };
 59 |  *
 60 |  * void addDirtyRegion(Rectangle rect) {
 61 |  *   doHandleDirtyRegions.add(rect);
 62 |  * }
 63 |  * 
64 | * 65 | * @param the type this {@code Runnable} accumulates 66 | * @author Igor Kushnirskiy 67 | * @version $Revision: 1.3 $ $Date: 2008/07/25 19:32:29 $ 68 | */ 69 | abstract class AccumulativeRunnable implements Runnable { 70 | private List arguments = null; 71 | 72 | /** 73 | * prepends or appends arguments and sends this {@code Runnable} for the execution if needed. 74 | *

75 | * This implementation uses {@see #submit} to send this {@code Runnable} for execution. 76 | * 77 | * @param isPrepend prepend or append 78 | * @param args the arguments to add 79 | */ 80 | public final synchronized void add(boolean isPrepend, T... args) { 81 | boolean isSubmitted = true; 82 | if (arguments == null) { 83 | isSubmitted = false; 84 | arguments = new ArrayList<>(); 85 | } 86 | if (isPrepend) { 87 | arguments.addAll(0, Arrays.asList(args)); 88 | } else { 89 | Collections.addAll(arguments, args); 90 | } 91 | if (!isSubmitted) { 92 | submit(); 93 | } 94 | } 95 | 96 | /** 97 | * appends arguments and sends this {@code Runnable} for the execution if needed. 98 | *

99 | * This implementation uses {@see #submit} to send this {@code Runnable} for execution. 100 | * 101 | * @param args the arguments to accumulate 102 | */ 103 | public final void add(T... args) { 104 | add(false, args); 105 | } 106 | 107 | /** 108 | * Returns accumulated arguments and flashes the arguments storage. 109 | * 110 | * @return accumulated arguments 111 | */ 112 | private synchronized List flush() { 113 | List list = arguments; 114 | arguments = null; 115 | return list; 116 | } 117 | 118 | /** 119 | * {@inheritDoc} 120 | *

121 | *

122 | * This implementation calls {@code run(List args)} method with the list of accumulated arguments. 123 | */ 124 | @Override 125 | public final void run() { 126 | run(flush()); 127 | } 128 | 129 | /** 130 | * Equivalent to {@code Runnable.run} method with the accumulated arguments to process. 131 | * 132 | * @param args accumulated arguments to process. 133 | */ 134 | protected abstract void run(List args); 135 | 136 | /** 137 | * Sends this {@code Runnable} for the execution 138 | *

139 | *

140 | * This method is to be executed only from {@code add} method. 141 | *

142 | *

143 | * This implementation uses {@code SwingWorker.invokeLater}. 144 | */ 145 | protected void submit() { 146 | SwingUtilities.invokeLater(this); 147 | } 148 | } 149 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/AndroidDeviceImpl.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api; 2 | 3 | import com.android.ddmlib.IDevice; 4 | import com.android.ddmlib.SyncService; 5 | import com.android.ddmlib.SyncService.ISyncProgressMonitor; 6 | import com.github.xsavikx.androidscreencast.api.file.FileInfo; 7 | import com.github.xsavikx.androidscreencast.api.injector.OutputStreamShellOutputReceiver; 8 | import org.apache.log4j.Logger; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Component; 11 | 12 | import java.io.ByteArrayOutputStream; 13 | import java.io.File; 14 | import java.lang.reflect.Method; 15 | import java.util.List; 16 | import java.util.Vector; 17 | 18 | @Component 19 | public class AndroidDeviceImpl implements AndroidDevice { 20 | private static final Logger logger = Logger.getLogger(AndroidDeviceImpl.class); 21 | @Autowired(required = false) 22 | private IDevice device; 23 | 24 | public AndroidDeviceImpl() { 25 | 26 | } 27 | 28 | public AndroidDeviceImpl(IDevice device) { 29 | this.device = device; 30 | } 31 | 32 | @Override 33 | public String executeCommand(String cmd) { 34 | if (logger.isDebugEnabled()) { 35 | logger.debug("executeCommand(String) - start"); 36 | } 37 | 38 | ByteArrayOutputStream bos = new ByteArrayOutputStream(); 39 | try { 40 | device.executeShellCommand(cmd, new OutputStreamShellOutputReceiver(bos)); 41 | String returnString = new String(bos.toByteArray(), "UTF-8"); 42 | if (logger.isDebugEnabled()) { 43 | logger.debug("executeCommand(String) - end"); 44 | } 45 | return returnString; 46 | } catch (Exception ex) { 47 | logger.error("executeCommand(String)", ex); 48 | 49 | throw new RuntimeException(ex); 50 | } 51 | } 52 | 53 | @Override 54 | public List list(String path) { 55 | if (logger.isDebugEnabled()) { 56 | logger.debug("list(String) - start"); 57 | } 58 | 59 | try { 60 | String s = executeCommand("ls -l " + path); 61 | String[] entries = s.split("\r\n"); 62 | Vector liste = new Vector<>(); 63 | for (String entry : entries) { 64 | String[] data = entry.split(" "); 65 | if (data.length < 4) 66 | continue; 67 | /* 68 | * for(int j=0; j 3 | 4.0.0 4 | com.github.xsavikx 5 | androidscreencast 6 | 0.0.7s 7 | Android Screencast 8 | 9 | 4.2.6.RELEASE 10 | 25.1.0 11 | 1.2.17 12 | com.github.xsavikx.androidscreencast.Main 13 | 1.7 14 | UTF-8 15 | 16 | 17 | 18 | com.android.tools.ddms 19 | ddmlib 20 | ${ddmlib-version} 21 | 22 | 23 | log4j 24 | log4j 25 | ${log4j-version} 26 | 27 | 28 | org.springframework 29 | spring-core 30 | ${spring-version} 31 | 32 | 33 | org.springframework 34 | spring-beans 35 | ${spring-version} 36 | 37 | 38 | org.springframework 39 | spring-context 40 | ${spring-version} 41 | 42 | 43 | 44 | 45 | 46 | maven-compiler-plugin 47 | 3.5.1 48 | 49 | ${jdk-version} 50 | ${jdk-version} 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-shade-plugin 57 | 2.4.3 58 | 59 | 60 | package 61 | 62 | shade 63 | 64 | 65 | true 66 | executable 67 | 68 | 70 | ${main-class} 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | org.apache.maven.plugins 80 | maven-jar-plugin 81 | 2.6 82 | 83 | 84 | 85 | ${main-class} 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | release-sign-artifacts 95 | 96 | 97 | performRelease 98 | true 99 | 100 | 101 | 102 | 103 | 104 | org.apache.maven.plugins 105 | maven-jarsigner-plugin 106 | 1.4 107 | 108 | 109 | sign 110 | 111 | sign 112 | 113 | package 114 | 115 | 116 | 117 | ${project.build.directory} 118 | keystore.jks 119 | acast 120 | {FI0aje84ll0HDfvsXBhLQHvBFso9Mam5Ae37ucXm0SU=} 121 | {FI0aje84ll0HDfvsXBhLQHvBFso9Mam5Ae37ucXm0SU=} 122 | 123 | 124 | 125 | 126 | 127 | 128 | http://xsavikx.github.io/AndroidScreencast 129 | 130 | https://travis-ci.org/xSAVIKx/AndroidScreencast 131 | Travis-CI 132 | 133 | 134 | GitHub 135 | https://github.com/xSAVIKx/AndroidScreencast/issues 136 | 137 | AndroidScreencast - View and control your android iDevice on PC. 138 | 139 | This project gives you opportunity to use your phone even with broken screen. 140 | 141 | Features: 142 | No client needed 143 | Support for Tap and Swipe gestures 144 | Write messages using PC keyboard 145 | Support for landscape mode 146 | Browse your phone files on PC 147 | 148 | 149 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/injector/ScreenCaptureThread.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.injector; 2 | 3 | import com.android.ddmlib.AdbCommandRejectedException; 4 | import com.android.ddmlib.IDevice; 5 | import com.android.ddmlib.RawImage; 6 | import com.android.ddmlib.TimeoutException; 7 | import com.github.xsavikx.androidscreencast.api.recording.QuickTimeOutputStream; 8 | import org.apache.log4j.Logger; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Component; 11 | 12 | import javax.swing.*; 13 | import java.awt.*; 14 | import java.awt.image.BufferedImage; 15 | import java.io.File; 16 | import java.io.IOException; 17 | import java.util.concurrent.TimeUnit; 18 | 19 | @Component 20 | public class ScreenCaptureThread extends Thread { 21 | private static final Logger LOGGER = Logger.getLogger(ScreenCaptureThread.class); 22 | private BufferedImage image; 23 | private Dimension size; 24 | @Autowired 25 | private IDevice device; 26 | private QuickTimeOutputStream qos = null; 27 | private boolean landscape = false; 28 | private ScreenCaptureListener listener = null; 29 | 30 | public ScreenCaptureThread() { 31 | super("Screen capture"); 32 | image = null; 33 | size = new Dimension(); 34 | } 35 | 36 | public void display(RawImage rawImage) { 37 | int width2 = landscape ? rawImage.height : rawImage.width; 38 | int height2 = landscape ? rawImage.width : rawImage.height; 39 | if (image == null) { 40 | image = new BufferedImage(width2, height2, BufferedImage.TYPE_INT_RGB); 41 | size.setSize(image.getWidth(), image.getHeight()); 42 | } else { 43 | if (image.getHeight() != height2 || image.getWidth() != width2) { 44 | image = new BufferedImage(width2, height2, BufferedImage.TYPE_INT_RGB); 45 | size.setSize(image.getWidth(), image.getHeight()); 46 | } 47 | } 48 | int index = 0; 49 | int indexInc = rawImage.bpp >> 3; 50 | for (int y = 0; y < rawImage.height; y++) { 51 | for (int x = 0; x < rawImage.width; x++, index += indexInc) { 52 | int value = rawImage.getARGB(index); 53 | if (landscape) 54 | image.setRGB(y, rawImage.width - x - 1, value); 55 | else 56 | image.setRGB(x, y, value); 57 | } 58 | } 59 | 60 | try { 61 | if (qos != null) 62 | qos.writeFrame(image, 10); 63 | } catch (IOException e) { 64 | LOGGER.error("display(RawImage)", e); 65 | 66 | throw new RuntimeException(e); 67 | } 68 | 69 | if (listener != null) { 70 | SwingUtilities.invokeLater(new Runnable() { 71 | @Override 72 | public void run() { 73 | listener.handleNewImage(size, image, landscape); 74 | } 75 | }); 76 | } 77 | 78 | } 79 | 80 | private boolean fetchImage() throws IOException { 81 | 82 | if (device == null) { 83 | // device not ready 84 | try { 85 | Thread.sleep(100); 86 | } catch (InterruptedException e) { 87 | LOGGER.error("fetchImage()", e); 88 | return false; 89 | } 90 | return true; 91 | } 92 | RawImage rawImage = null; 93 | synchronized (device) { 94 | try { 95 | rawImage = device.getScreenshot(5, TimeUnit.SECONDS); 96 | } catch (TimeoutException | AdbCommandRejectedException e) { 97 | LOGGER.error("fetchImage()", e); 98 | } 99 | } 100 | if (rawImage != null) { 101 | display(rawImage); 102 | } else { 103 | LOGGER.info("failed getting screenshot through ADB ok"); 104 | } 105 | try { 106 | Thread.sleep(5); 107 | } catch (InterruptedException e) { 108 | LOGGER.error("fetchImage()", e); 109 | return false; 110 | } 111 | return true; 112 | } 113 | 114 | public ScreenCaptureListener getListener() { 115 | return listener; 116 | } 117 | 118 | public void setListener(ScreenCaptureListener listener) { 119 | this.listener = listener; 120 | } 121 | 122 | public Dimension getPreferredSize() { 123 | return size; 124 | } 125 | 126 | @Override 127 | public void run() { 128 | do { 129 | try { 130 | boolean ok = fetchImage(); 131 | if (!ok) 132 | break; 133 | } catch (java.nio.channels.ClosedByInterruptException ciex) { 134 | LOGGER.error("run()", ciex); 135 | 136 | break; 137 | } catch (IOException e) { 138 | LOGGER.error("run()", e); 139 | LOGGER.error((new StringBuilder()).append("Exception fetching image: ").append(e.toString()).toString()); 140 | } 141 | 142 | } while (true); 143 | } 144 | 145 | public void startRecording(File f) { 146 | LOGGER.debug("startRecording(File f=" + f + ") - start"); 147 | 148 | try { 149 | if (!f.getName().toLowerCase().endsWith(".mov")) 150 | f = new File(f.getAbsolutePath() + ".mov"); 151 | qos = new QuickTimeOutputStream(f, QuickTimeOutputStream.VideoFormat.JPG); 152 | } catch (IOException e) { 153 | LOGGER.error("startRecording(File)", e); 154 | 155 | throw new RuntimeException(e); 156 | } 157 | qos.setVideoCompressionQuality(1f); 158 | qos.setTimeScale(30); // 30 fps 159 | 160 | LOGGER.debug("startRecording(File f=" + f + ") - end"); 161 | } 162 | 163 | public void stopRecording() { 164 | LOGGER.debug("stopRecording() - start"); 165 | 166 | try { 167 | QuickTimeOutputStream o = qos; 168 | qos = null; 169 | o.close(); 170 | } catch (IOException e) { 171 | LOGGER.error("stopRecording()", e); 172 | 173 | throw new RuntimeException(e); 174 | } 175 | 176 | LOGGER.debug("stopRecording() - end"); 177 | } 178 | 179 | public void toogleOrientation() { 180 | LOGGER.debug("toogleOrientation() - start"); 181 | 182 | landscape = !landscape; 183 | 184 | LOGGER.debug("toogleOrientation() - end"); 185 | } 186 | 187 | public interface ScreenCaptureListener { 188 | void handleNewImage(Dimension size, BufferedImage image, boolean landscape); 189 | } 190 | 191 | } 192 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/JFrameMain.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui; 2 | 3 | import com.github.xsavikx.androidscreencast.api.AndroidDevice; 4 | import com.github.xsavikx.androidscreencast.api.injector.Injector; 5 | import com.github.xsavikx.androidscreencast.api.injector.InputKeyEvent; 6 | import com.github.xsavikx.androidscreencast.api.injector.ScreenCaptureThread.ScreenCaptureListener; 7 | import com.github.xsavikx.androidscreencast.constant.Constants; 8 | import com.github.xsavikx.androidscreencast.spring.config.ApplicationContextProvider; 9 | import com.github.xsavikx.androidscreencast.ui.explorer.JFrameExplorer; 10 | import com.github.xsavikx.androidscreencast.ui.interaction.KeyEventDispatcherFactory; 11 | import com.github.xsavikx.androidscreencast.ui.interaction.KeyboardActionListenerFactory; 12 | import com.github.xsavikx.androidscreencast.ui.interaction.MouseActionAdapterFactory; 13 | import org.springframework.beans.factory.annotation.Autowired; 14 | import org.springframework.core.env.Environment; 15 | import org.springframework.stereotype.Component; 16 | 17 | import javax.swing.*; 18 | import javax.swing.filechooser.FileNameExtensionFilter; 19 | import java.awt.*; 20 | import java.awt.event.ActionEvent; 21 | import java.awt.event.ActionListener; 22 | import java.awt.event.MouseAdapter; 23 | import java.awt.image.BufferedImage; 24 | 25 | @Component 26 | public class JFrameMain extends JFrame { 27 | 28 | private static final long serialVersionUID = -2085909236767692371L; 29 | private JPanelScreen jp = new JPanelScreen(); 30 | private JToolBar jtb = new JToolBar(); 31 | private JToolBar jtbHardkeys = new JToolBar(); 32 | // private JToggleButton jtbRecord = new JToggleButton("Record"); 33 | 34 | // private JButton jbOpenUrl = new JButton("Open Url"); 35 | private JScrollPane jsp; 36 | private JButton jbExplorer = new JButton("Explore"); 37 | private JButton jbRestartClient = new JButton("Restart client"); 38 | private JButton jbExecuteKeyEvent = new JButton("Execute keycode"); 39 | 40 | private JButton jbKbHome = new JButton("Home"); 41 | private JButton jbKbMenu = new JButton("Menu"); 42 | private JButton jbKbBack = new JButton("Back"); 43 | private JButton jbKbSearch = new JButton("Search"); 44 | 45 | private JButton jbKbPhoneOn = new JButton("Call"); 46 | 47 | private JButton jbKbPhoneOff = new JButton("End call"); 48 | private AndroidDevice androidDevice; 49 | private Injector injector; 50 | private Environment env; 51 | private Dimension oldImageDimension; 52 | 53 | @Autowired 54 | public JFrameMain(Environment env, Injector injector, AndroidDevice androidDevice) { 55 | this.injector = injector; 56 | this.env = env; 57 | this.androidDevice = androidDevice; 58 | initialize(); 59 | KeyboardFocusManager.getCurrentKeyboardFocusManager() 60 | .addKeyEventDispatcher(KeyEventDispatcherFactory.getKeyEventDispatcher(this)); 61 | } 62 | 63 | private void setPrefferedWindowSize() { 64 | if (env.containsProperty(Constants.DEFAULT_WINDOW_HEIGHT) && env.containsProperty(Constants.DEFAULT_WINDOW_WIDTH)) { 65 | Integer height = env.getProperty(Constants.DEFAULT_WINDOW_HEIGHT, Integer.class); 66 | Integer width = env.getProperty(Constants.DEFAULT_WINDOW_WIDTH, Integer.class); 67 | if (height != null && width != null) 68 | getContentPane().setPreferredSize(new Dimension(width, height)); 69 | } 70 | pack(); 71 | } 72 | 73 | public void initialize() { 74 | 75 | jtb.setFocusable(false); 76 | jbExplorer.setFocusable(false); 77 | // jtbRecord.setFocusable(false); 78 | // jbOpenUrl.setFocusable(false); 79 | jbKbHome.setFocusable(false); 80 | jbKbMenu.setFocusable(false); 81 | jbKbBack.setFocusable(false); 82 | jbKbSearch.setFocusable(false); 83 | jbKbPhoneOn.setFocusable(false); 84 | jbKbPhoneOff.setFocusable(false); 85 | jbRestartClient.setFocusable(false); 86 | jbExecuteKeyEvent.setFocusable(false); 87 | 88 | jbKbHome.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_HOME)); 89 | jbKbMenu.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_MENU)); 90 | jbKbBack.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_BACK)); 91 | jbKbSearch.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_SEARCH)); 92 | jbKbPhoneOn.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_CALL)); 93 | jbKbPhoneOff.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_ENDCALL)); 94 | 95 | jtbHardkeys.add(jbKbHome); 96 | jtbHardkeys.add(jbKbMenu); 97 | jtbHardkeys.add(jbKbBack); 98 | jtbHardkeys.add(jbKbSearch); 99 | jtbHardkeys.add(jbKbPhoneOn); 100 | jtbHardkeys.add(jbKbPhoneOff); 101 | 102 | // setIconImage(Toolkit.getDefaultToolkit().getImage( 103 | // getClass().getResource("icon.png"))); 104 | setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); 105 | setLayout(new BorderLayout()); 106 | add(jtb, BorderLayout.NORTH); 107 | add(jtbHardkeys, BorderLayout.SOUTH); 108 | jsp = new JScrollPane(jp); 109 | add(jsp, BorderLayout.CENTER); 110 | jsp.setPreferredSize(new Dimension(100, 100)); 111 | pack(); 112 | setLocationRelativeTo(null); 113 | setPrefferedWindowSize(); 114 | MouseAdapter ma = MouseActionAdapterFactory.getInstance(jp, injector); 115 | 116 | jp.addMouseMotionListener(ma); 117 | jp.addMouseListener(ma); 118 | jp.addMouseWheelListener(ma); 119 | 120 | // jtbRecord.addActionListener(new ActionListener() { 121 | // 122 | // @Override 123 | // public void actionPerformed(ActionEvent arg0) { 124 | // if (jtbRecord.isSelected()) { 125 | // startRecording(); 126 | // } else { 127 | // stopRecording(); 128 | // } 129 | // } 130 | // 131 | // }); 132 | // jtb.add(jtbRecord); 133 | 134 | jbExplorer.addActionListener(new ActionListener() { 135 | 136 | @Override 137 | public void actionPerformed(ActionEvent arg0) { 138 | JFrameExplorer jf = ApplicationContextProvider.getApplicationContext().getBean(JFrameExplorer.class); 139 | jf.setIconImage(getIconImage()); 140 | jf.launch(); 141 | jf.setVisible(true); 142 | } 143 | }); 144 | jtb.add(jbExplorer); 145 | 146 | jtb.add(jbRestartClient); 147 | 148 | jbExecuteKeyEvent.addActionListener(new ActionListener() { 149 | 150 | @Override 151 | public void actionPerformed(ActionEvent e) { 152 | JDialogExecuteKeyEvent jdExecuteKeyEvent = ApplicationContextProvider.getApplicationContext() 153 | .getBean(JDialogExecuteKeyEvent.class); 154 | jdExecuteKeyEvent.setVisible(true); 155 | } 156 | }); 157 | 158 | jtb.add(jbExecuteKeyEvent); 159 | 160 | // jbOpenUrl.addActionListener(new ActionListener() { 161 | // @Override 162 | // public void actionPerformed(ActionEvent arg0) { 163 | // JDialogUrl jdUrl = new JDialogUrl(); 164 | // jdUrl.setVisible(true); 165 | // if (!jdUrl.isResult()) 166 | // return; 167 | // String url = jdUrl.getJtfUrl().getText(); 168 | // androidDevice.openUrl(url); 169 | // } 170 | // }); 171 | // jtb.add(jbOpenUrl); 172 | 173 | } 174 | 175 | public void launchInjector() { 176 | injector.screencapture.setListener(new ScreenCaptureListener() { 177 | 178 | @Override 179 | public void handleNewImage(Dimension size, BufferedImage image, boolean landscape) { 180 | if (oldImageDimension == null || !size.equals(oldImageDimension)) { 181 | jsp.setPreferredSize(size); 182 | JFrameMain.this.pack(); 183 | oldImageDimension = size; 184 | } 185 | jp.handleNewImage(size, image); 186 | } 187 | }); 188 | injector.start(); 189 | } 190 | 191 | private void startRecording() { 192 | JFileChooser jFileChooser = new JFileChooser(); 193 | FileNameExtensionFilter filter = new FileNameExtensionFilter("Video file", "mov"); 194 | jFileChooser.setFileFilter(filter); 195 | int returnVal = jFileChooser.showSaveDialog(this); 196 | if (returnVal == JFileChooser.APPROVE_OPTION) { 197 | injector.screencapture.startRecording(jFileChooser.getSelectedFile()); 198 | } 199 | } 200 | 201 | private void stopRecording() { 202 | injector.screencapture.stopRecording(); 203 | } 204 | 205 | } 206 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/explorer/LazyLoadingTreeNode.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui.explorer; 2 | 3 | import javax.swing.*; 4 | import javax.swing.event.TreeExpansionEvent; 5 | import javax.swing.event.TreeWillExpandListener; 6 | import javax.swing.tree.DefaultMutableTreeNode; 7 | import javax.swing.tree.DefaultTreeModel; 8 | import javax.swing.tree.MutableTreeNode; 9 | import javax.swing.tree.TreeModel; 10 | import java.awt.event.ActionEvent; 11 | import java.awt.event.KeyEvent; 12 | import java.util.Vector; 13 | import java.util.concurrent.ExecutionException; 14 | 15 | public abstract class LazyLoadingTreeNode extends DefaultMutableTreeNode implements TreeWillExpandListener { 16 | 17 | /** 18 | * 19 | */ 20 | private static final long serialVersionUID = -4981073521761764327L; 21 | private static final String ESCAPE_ACTION_NAME = "escape"; 22 | private static final KeyStroke ESCAPE_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); 23 | /** 24 | * The JTree containing this Node 25 | */ 26 | private JTree tree; 27 | /** 28 | * Can the worker be Canceled ? 29 | */ 30 | private boolean cancelable; 31 | 32 | /** 33 | * Default Constructor 34 | * 35 | * @param userObject an Object provided by the user that constitutes the node's data 36 | * @param tree the JTree containing this Node 37 | * @param cancelable 38 | */ 39 | public LazyLoadingTreeNode(Object userObject, JTree tree, boolean cancelable) { 40 | super(userObject); 41 | tree.addTreeWillExpandListener(this); 42 | this.tree = tree; 43 | this.cancelable = cancelable; 44 | setAllowsChildren(true); 45 | } 46 | 47 | /** 48 | * @return true if there are some childrens 49 | */ 50 | protected boolean areChildrenLoaded() { 51 | return getChildCount() > 0 && getAllowsChildren(); 52 | } 53 | 54 | /** 55 | * @return a new Loading please wait node 56 | */ 57 | protected MutableTreeNode createLoadingNode() { 58 | return new DefaultMutableTreeNode("Loading Please Wait ...", false); 59 | } 60 | 61 | /** 62 | * Create worker that will load the nodes 63 | * 64 | * @param tree the tree 65 | * @return the newly created SwingWorker 66 | */ 67 | protected com.github.xsavikx.androidscreencast.ui.worker.SwingWorker createSwingWorker(final JTree tree) { 68 | 69 | com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker = new com.github.xsavikx.androidscreencast.ui.worker.SwingWorker() { 70 | 71 | @Override 72 | protected MutableTreeNode[] doInBackground() { 73 | return loadChildren(tree); 74 | } 75 | 76 | @Override 77 | protected void done() { 78 | try { 79 | if (!isCancelled()) { 80 | MutableTreeNode[] nodes = get(); 81 | setAllowsChildren(nodes.length > 0); 82 | setChildren(nodes); 83 | unRegisterSwingWorkerForCancel(tree, this); 84 | } else { 85 | reset(); 86 | } 87 | } catch (InterruptedException | ExecutionException e) { 88 | e.printStackTrace(); 89 | } 90 | 91 | } 92 | 93 | }; 94 | registerSwingWorkerForCancel(tree, worker); 95 | return worker; 96 | } 97 | 98 | /** 99 | * If the 100 | * 101 | * @return false, this node can't be a leaf 102 | * @see #getAllowsChildren() 103 | */ 104 | @Override 105 | public boolean isLeaf() { 106 | return !getAllowsChildren(); 107 | } 108 | 109 | /** 110 | * This method will be executed in a background thread. If you have to do some GUI stuff use {@link SwingUtilities#invokeLater(Runnable)} 111 | * 112 | * @param tree the tree 113 | * @return the Created nodes 114 | */ 115 | public abstract MutableTreeNode[] loadChildren(JTree tree); 116 | 117 | /** 118 | * If the node is cancelable an escape Action is registered in the tree's InputMap and ActionMap that will cancel the execution 119 | * 120 | * @param tree the tree 121 | * @param worker the worker to cancel 122 | */ 123 | protected void registerSwingWorkerForCancel(JTree tree, com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker) { 124 | if (!cancelable) { 125 | return; 126 | } 127 | tree.getInputMap().put(ESCAPE_KEY, ESCAPE_ACTION_NAME); 128 | Action action = tree.getActionMap().get(ESCAPE_ACTION_NAME); 129 | if (action == null) { 130 | CancelWorkersAction cancelWorkerAction = new CancelWorkersAction(); 131 | cancelWorkerAction.addSwingWorker(worker); 132 | tree.getActionMap().put(ESCAPE_ACTION_NAME, cancelWorkerAction); 133 | } else { 134 | if (action instanceof CancelWorkersAction) { 135 | CancelWorkersAction cancelAction = (CancelWorkersAction) action; 136 | cancelAction.addSwingWorker(worker); 137 | } 138 | } 139 | } 140 | 141 | /** 142 | * Need some improvement ... This method should restore the Node initial state if the worker if canceled 143 | */ 144 | protected void reset() { 145 | DefaultTreeModel defaultModel = (DefaultTreeModel) tree.getModel(); 146 | int childCount = getChildCount(); 147 | if (childCount > 0) { 148 | for (int i = 0; i < childCount; i++) { 149 | defaultModel.removeNodeFromParent((MutableTreeNode) getChildAt(0)); 150 | } 151 | } 152 | setAllowsChildren(true); 153 | } 154 | 155 | /** 156 | * Define nodes children 157 | * 158 | * @param nodes new nodes 159 | */ 160 | protected void setChildren(MutableTreeNode... nodes) { 161 | TreeModel model = tree.getModel(); 162 | if (model instanceof DefaultTreeModel) { 163 | DefaultTreeModel defaultModel = (DefaultTreeModel) model; 164 | int childCount = getChildCount(); 165 | if (childCount > 0) { 166 | for (int i = 0; i < childCount; i++) { 167 | defaultModel.removeNodeFromParent((MutableTreeNode) getChildAt(0)); 168 | } 169 | } 170 | for (int i = 0; i < nodes.length; i++) { 171 | defaultModel.insertNodeInto(nodes[i], this, i); 172 | } 173 | } 174 | } 175 | 176 | /** 177 | * set the loading state 178 | */ 179 | private void setLoading() { 180 | setChildren(createLoadingNode()); 181 | TreeModel model = tree.getModel(); 182 | if (model instanceof DefaultTreeModel) { 183 | DefaultTreeModel defaultModel = (DefaultTreeModel) model; 184 | int[] indices = new int[getChildCount()]; 185 | for (int i = 0; i < indices.length; i++) { 186 | indices[i] = i; 187 | } 188 | defaultModel.nodesWereInserted(LazyLoadingTreeNode.this, indices); 189 | } 190 | } 191 | 192 | /** 193 | * Default empty implementation, do nothing on collapse event. 194 | */ 195 | @Override 196 | public void treeWillCollapse(TreeExpansionEvent event) { 197 | // ignore 198 | } 199 | 200 | /** 201 | * Node will expand, it's time to retrieve nodes 202 | */ 203 | @Override 204 | public void treeWillExpand(TreeExpansionEvent event) { 205 | if (this.equals(event.getPath().getLastPathComponent())) { 206 | if (areChildrenLoaded()) { 207 | return; 208 | } 209 | setLoading(); 210 | com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker = createSwingWorker(tree); 211 | worker.execute(); 212 | } 213 | } 214 | 215 | /** 216 | * Remove the swingWorker from the cancellable task of the tree 217 | * 218 | * @param tree 219 | * @param worker 220 | */ 221 | protected void unRegisterSwingWorkerForCancel(JTree tree, com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker) { 222 | if (!cancelable) { 223 | return; 224 | } 225 | Action action = tree.getActionMap().get(ESCAPE_ACTION_NAME); 226 | if (action != null && action instanceof CancelWorkersAction) { 227 | CancelWorkersAction cancelWorkerAction = new CancelWorkersAction(); 228 | cancelWorkerAction.removeSwingWorker(worker); 229 | } 230 | } 231 | 232 | /** 233 | * ActionMap can only store one Action for the same key, This Action Stores the list of SwingWorker to be canceled if the escape key is pressed. 234 | * 235 | * @author Thierry LEFORT 3 mars 08 236 | */ 237 | protected static class CancelWorkersAction extends AbstractAction { 238 | /** 239 | * 240 | */ 241 | private static final long serialVersionUID = 3173288834368915117L; 242 | /** 243 | * the SwingWorkers 244 | */ 245 | private Vector> workers = new Vector<>(); 246 | 247 | /** 248 | * Default constructor 249 | */ 250 | private CancelWorkersAction() { 251 | super(ESCAPE_ACTION_NAME); 252 | } 253 | 254 | /** 255 | * Do the Cancel 256 | */ 257 | @Override 258 | public void actionPerformed(ActionEvent e) { 259 | for (com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker : workers) { 260 | worker.cancel(true); 261 | } 262 | 263 | } 264 | 265 | /** 266 | * Add a Cancelable SwingWorker 267 | */ 268 | public void addSwingWorker(com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker) { 269 | workers.add(worker); 270 | } 271 | 272 | /** 273 | * Remove a SwingWorker 274 | */ 275 | public void removeSwingWorker(com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker) { 276 | workers.remove(worker); 277 | } 278 | 279 | } 280 | } 281 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/recording/DataAtomOutputStream.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.api.recording; 2 | 3 | import java.io.FilterOutputStream; 4 | import java.io.IOException; 5 | import java.io.OutputStream; 6 | import java.io.UnsupportedEncodingException; 7 | import java.util.Calendar; 8 | import java.util.Date; 9 | import java.util.GregorianCalendar; 10 | 11 | public class DataAtomOutputStream extends FilterOutputStream { 12 | 13 | protected static final long MAC_TIMESTAMP_EPOCH = new GregorianCalendar(1904, Calendar.JANUARY, 1).getTimeInMillis(); 14 | /** 15 | * The number of bytes written to the data output stream so far. If this counter overflows, it will be wrapped to Integer.MAX_VALUE. 16 | */ 17 | protected long written; 18 | 19 | public DataAtomOutputStream(OutputStream out) { 20 | super(out); 21 | } 22 | 23 | /** 24 | * Increases the written counter by the specified value until it reaches Long.MAX_VALUE. 25 | */ 26 | protected void incCount(int value) { 27 | long temp = written + value; 28 | if (temp < 0) { 29 | temp = Long.MAX_VALUE; 30 | } 31 | written = temp; 32 | } 33 | 34 | /** 35 | * Returns the current value of the counter written, the number of bytes written to this data output stream so far. If the counter 36 | * overflows, it will be wrapped to Integer.MAX_VALUE. 37 | * 38 | * @return the value of the written field. 39 | * @see java.io.DataOutputStream#written 40 | */ 41 | public final long size() { 42 | return written; 43 | } 44 | 45 | /** 46 | * Writes len bytes from the specified byte array starting at offset off to the underlying output stream. If no exception 47 | * is thrown, the counter written is incremented by len . 48 | * 49 | * @param b the data. 50 | * @param off the start offset in the data. 51 | * @param len the number of bytes to write. 52 | * @throws IOException if an I/O error occurs. 53 | * @see java.io.FilterOutputStream#out 54 | */ 55 | @Override 56 | public synchronized void write(byte b[], int off, int len) throws IOException { 57 | out.write(b, off, len); 58 | incCount(len); 59 | } 60 | 61 | /** 62 | * Writes the specified byte (the low eight bits of the argument b) to the underlying output stream. If no exception is thrown, the 63 | * counter written is incremented by 1 . 64 | *

65 | * Implements the write method of OutputStream. 66 | * 67 | * @param b the byte to be written. 68 | * @throws IOException if an I/O error occurs. 69 | * @see java.io.FilterOutputStream#out 70 | */ 71 | @Override 72 | public synchronized void write(int b) throws IOException { 73 | out.write(b); 74 | incCount(1); 75 | } 76 | 77 | /** 78 | * Writes a BCD2 to the underlying output stream. 79 | * 80 | * @param v an int to be written. 81 | * @throws IOException if an I/O error occurs. 82 | * @see java.io.FilterOutputStream#out 83 | */ 84 | public void writeBCD2(int v) throws IOException { 85 | out.write(((v % 100 / 10) << 4) | (v % 10)); 86 | incCount(1); 87 | } 88 | 89 | /** 90 | * Writes a BCD4 to the underlying output stream. 91 | * 92 | * @param v an int to be written. 93 | * @throws IOException if an I/O error occurs. 94 | * @see java.io.FilterOutputStream#out 95 | */ 96 | public void writeBCD4(int v) throws IOException { 97 | out.write(((v % 10000 / 1000) << 4) | (v % 1000 / 100)); 98 | out.write(((v % 100 / 10) << 4) | (v % 10)); 99 | incCount(2); 100 | } 101 | 102 | /** 103 | * Writes out a byte to the underlying output stream as a 1-byte value. If no exception is thrown, the counter written is 104 | * incremented by 1. 105 | * 106 | * @param v a byte value to be written. 107 | * @throws IOException if an I/O error occurs. 108 | * @see java.io.FilterOutputStream#out 109 | */ 110 | public final void writeByte(int v) throws IOException { 111 | out.write(v); 112 | incCount(1); 113 | } 114 | 115 | /** 116 | * Writes 32-bit fixed-point number divided as 16.16. 117 | * 118 | * @param f an int to be written. 119 | * @throws IOException if an I/O error occurs. 120 | * @see java.io.FilterOutputStream#out 121 | */ 122 | public void writeFixed16D16(double f) throws IOException { 123 | double v = (f >= 0) ? f : -f; 124 | 125 | int wholePart = (int) v; 126 | int fractionPart = (int) ((v - wholePart) * 65536); 127 | int t = (wholePart << 16) + fractionPart; 128 | 129 | if (f < 0) { 130 | t = t - 1; 131 | } 132 | writeInt(t); 133 | } 134 | 135 | /** 136 | * Writes 32-bit fixed-point number divided as 2.30. 137 | * 138 | * @param f an int to be written. 139 | * @throws IOException if an I/O error occurs. 140 | * @see java.io.FilterOutputStream#out 141 | */ 142 | public void writeFixed2D30(double f) throws IOException { 143 | double v = (f >= 0) ? f : -f; 144 | 145 | int wholePart = (int) v; 146 | int fractionPart = (int) ((v - wholePart) * 1073741824); 147 | int t = (wholePart << 30) + fractionPart; 148 | 149 | if (f < 0) { 150 | t = t - 1; 151 | } 152 | writeInt(t); 153 | } 154 | 155 | /** 156 | * Writes 16-bit fixed-point number divided as 8.8. 157 | * 158 | * @param f an int to be written. 159 | * @throws IOException if an I/O error occurs. 160 | * @see java.io.FilterOutputStream#out 161 | */ 162 | public void writeFixed8D8(float f) throws IOException { 163 | float v = (f >= 0) ? f : -f; 164 | 165 | int wholePart = (int) v; 166 | int fractionPart = (int) ((v - wholePart) * 256); 167 | int t = (wholePart << 8) + fractionPart; 168 | 169 | if (f < 0) { 170 | t = t - 1; 171 | } 172 | writeUShort(t); 173 | } 174 | 175 | /** 176 | * Writes an int to the underlying output stream as four bytes, high byte first. If no exception is thrown, the counter 177 | * written is incremented by 4. 178 | * 179 | * @param v an int to be written. 180 | * @throws IOException if an I/O error occurs. 181 | * @see java.io.FilterOutputStream#out 182 | */ 183 | public void writeInt(int v) throws IOException { 184 | out.write((v >>> 24) & 0xff); 185 | out.write((v >>> 16) & 0xff); 186 | out.write((v >>> 8) & 0xff); 187 | out.write((v >>> 0) & 0xff); 188 | incCount(4); 189 | } 190 | 191 | public void writeLong(long v) throws IOException { 192 | out.write((int) (v >>> 56) & 0xff); 193 | out.write((int) (v >>> 48) & 0xff); 194 | out.write((int) (v >>> 40) & 0xff); 195 | out.write((int) (v >>> 32) & 0xff); 196 | out.write((int) (v >>> 24) & 0xff); 197 | out.write((int) (v >>> 16) & 0xff); 198 | out.write((int) (v >>> 8) & 0xff); 199 | out.write((int) (v >>> 0) & 0xff); 200 | incCount(8); 201 | } 202 | 203 | /** 204 | * Writes a 32-bit Mac timestamp (seconds since 1902). 205 | * 206 | * @param date 207 | * @throws java.io.IOException 208 | */ 209 | public void writeMacTimestamp(Date date) throws IOException { 210 | long millis = date.getTime(); 211 | long qtMillis = millis - MAC_TIMESTAMP_EPOCH; 212 | long qtSeconds = qtMillis / 1000; 213 | writeUInt(qtSeconds); 214 | } 215 | 216 | /** 217 | * Writes a Pascal String. 218 | * 219 | * @param s 220 | * @throws java.io.IOException 221 | */ 222 | public void writePString(String s) throws IOException { 223 | if (s.length() > 0xffff) { 224 | throw new IllegalArgumentException("String too long for PString"); 225 | } 226 | if (s.length() < 256) { 227 | out.write(s.length()); 228 | } else { 229 | out.write(0); 230 | writeShort(s.length()); // increments +2 231 | } 232 | for (int i = 0; i < s.length(); i++) { 233 | out.write(s.charAt(i)); 234 | } 235 | incCount(1 + s.length()); 236 | } 237 | 238 | /** 239 | * Writes a Pascal String padded to the specified fixed size in bytes 240 | * 241 | * @param s 242 | * @param length the fixed size in bytes 243 | * @throws java.io.IOException 244 | */ 245 | public void writePString(String s, int length) throws IOException { 246 | if (s.length() > length) { 247 | throw new IllegalArgumentException("String too long for PString of length " + length); 248 | } 249 | if (s.length() < 256) { 250 | out.write(s.length()); 251 | } else { 252 | out.write(0); 253 | writeShort(s.length()); // increments +2 254 | } 255 | for (int i = 0; i < s.length(); i++) { 256 | out.write(s.charAt(i)); 257 | } 258 | 259 | // write pad bytes 260 | for (int i = 1 + s.length(); i < length; i++) { 261 | out.write(0); 262 | } 263 | 264 | incCount(length); 265 | } 266 | 267 | /** 268 | * Writes a signed 16 bit integer value. 269 | * 270 | * @param v The value 271 | * @throws java.io.IOException 272 | */ 273 | public void writeShort(int v) throws IOException { 274 | out.write((v >> 8) & 0xff); 275 | out.write((v >>> 0) & 0xff); 276 | incCount(2); 277 | } 278 | 279 | /** 280 | * Writes an Atom Type identifier (4 bytes). 281 | * 282 | * @param s A string with a length of 4 characters. 283 | */ 284 | public void writeType(String s) throws IOException { 285 | if (s.length() != 4) { 286 | throw new IllegalArgumentException("type string must have 4 characters"); 287 | } 288 | 289 | try { 290 | out.write(s.getBytes("ASCII"), 0, 4); 291 | incCount(4); 292 | } catch (UnsupportedEncodingException e) { 293 | throw new InternalError(e.toString()); 294 | } 295 | } 296 | 297 | /** 298 | * Writes an unsigned 32 bit integer value. 299 | * 300 | * @param v The value 301 | * @throws java.io.IOException 302 | */ 303 | public void writeUInt(long v) throws IOException { 304 | out.write((int) ((v >>> 24) & 0xff)); 305 | out.write((int) ((v >>> 16) & 0xff)); 306 | out.write((int) ((v >>> 8) & 0xff)); 307 | out.write((int) ((v >>> 0) & 0xff)); 308 | incCount(4); 309 | } 310 | 311 | public void writeUShort(int v) throws IOException { 312 | out.write((v >> 8) & 0xff); 313 | out.write((v >>> 0) & 0xff); 314 | incCount(2); 315 | } 316 | 317 | } 318 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2014-2016 Iurii Sergiichuk 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/MultiLineLabelUI.java: -------------------------------------------------------------------------------- 1 | package com.github.xsavikx.androidscreencast.ui; 2 | 3 | import javax.swing.*; 4 | import javax.swing.plaf.basic.BasicGraphicsUtils; 5 | import javax.swing.plaf.basic.BasicLabelUI; 6 | import java.awt.*; 7 | import java.util.StringTokenizer; 8 | 9 | public class MultiLineLabelUI extends BasicLabelUI { 10 | static final int LEADING = SwingConstants.LEADING; 11 | static final int TRAILING = SwingConstants.TRAILING; 12 | static final int LEFT = SwingConstants.LEFT; 13 | static final int RIGHT = SwingConstants.RIGHT; 14 | static final int TOP = SwingConstants.TOP; 15 | static final int CENTER = SwingConstants.CENTER; 16 | 17 | static { 18 | labelUI = new MultiLineLabelUI(); 19 | } 20 | 21 | protected String str; 22 | protected String[] strs; 23 | 24 | public static Dimension computeMultiLineDimension(FontMetrics fm, String[] strs) { 25 | int i, c, width = 0; 26 | for (i = 0, c = strs.length; i < c; i++) 27 | width = Math.max(width, SwingUtilities.computeStringWidth(fm, strs[i])); 28 | return new Dimension(width, fm.getHeight() * strs.length); 29 | } 30 | 31 | /** 32 | * Compute and return the location of the icons origin, the location of origin of the text baseline, and a possibly clipped version of the compound 33 | * labels string. Locations are computed relative to the viewR rectangle. This layoutCompoundLabel() does not know how to handle LEADING/TRAILING 34 | * values in horizontalTextPosition (they will default to RIGHT) and in horizontalAlignment (they will default to CENTER). Use the other version of 35 | * layoutCompoundLabel() instead. 36 | */ 37 | public static String layoutCompoundLabel(FontMetrics fm, String[] text, Icon icon, int verticalAlignment, 38 | int horizontalAlignment, int verticalTextPosition, int horizontalTextPosition, Rectangle viewR, Rectangle iconR, 39 | Rectangle textR, int textIconGap) { 40 | /* 41 | * Initialize the icon bounds rectangle iconR. 42 | */ 43 | 44 | if (icon != null) { 45 | iconR.width = icon.getIconWidth(); 46 | iconR.height = icon.getIconHeight(); 47 | } else { 48 | iconR.width = iconR.height = 0; 49 | } 50 | 51 | /* 52 | * Initialize the text bounds rectangle textR. If a null or and empty String was specified we substitute "" here and use 0,0,0,0 for textR. 53 | */ 54 | 55 | // Fix for textIsEmpty sent by Paulo Santos 56 | boolean textIsEmpty = (text == null) || (text.length == 0) 57 | || (text.length == 1 && ((text[0] == null) || text[0].equals(""))); 58 | 59 | String rettext = ""; 60 | if (textIsEmpty) { 61 | textR.width = textR.height = 0; 62 | } else { 63 | Dimension dim = computeMultiLineDimension(fm, text); 64 | textR.width = dim.width; 65 | textR.height = dim.height; 66 | } 67 | 68 | /* 69 | * Unless both text and icon are non-null, we effectively ignore the value of textIconGap. The code that follows uses the value of gap instead of 70 | * textIconGap. 71 | */ 72 | 73 | int gap = (textIsEmpty || (icon == null)) ? 0 : textIconGap; 74 | 75 | if (!textIsEmpty) { 76 | 77 | /* 78 | * If the label text string is too wide to fit within the available space "..." and as many characters as will fit will be displayed instead. 79 | */ 80 | 81 | int availTextWidth; 82 | 83 | if (horizontalTextPosition == CENTER) { 84 | availTextWidth = viewR.width; 85 | } else { 86 | availTextWidth = viewR.width - (iconR.width + gap); 87 | } 88 | 89 | if (textR.width > availTextWidth && text.length == 1) { 90 | String clipString = "..."; 91 | int totalWidth = SwingUtilities.computeStringWidth(fm, clipString); 92 | int nChars; 93 | for (nChars = 0; nChars < text[0].length(); nChars++) { 94 | totalWidth += fm.charWidth(text[0].charAt(nChars)); 95 | if (totalWidth > availTextWidth) { 96 | break; 97 | } 98 | } 99 | rettext = text[0].substring(0, nChars) + clipString; 100 | textR.width = SwingUtilities.computeStringWidth(fm, rettext); 101 | } 102 | } 103 | 104 | /* 105 | * Compute textR.x,y given the verticalTextPosition and horizontalTextPosition properties 106 | */ 107 | 108 | if (verticalTextPosition == TOP) { 109 | if (horizontalTextPosition != CENTER) { 110 | textR.y = 0; 111 | } else { 112 | textR.y = -(textR.height + gap); 113 | } 114 | } else if (verticalTextPosition == CENTER) { 115 | textR.y = (iconR.height / 2) - (textR.height / 2); 116 | } else { // (verticalTextPosition == BOTTOM) 117 | if (horizontalTextPosition != CENTER) { 118 | textR.y = iconR.height - textR.height; 119 | } else { 120 | textR.y = (iconR.height + gap); 121 | } 122 | } 123 | 124 | if (horizontalTextPosition == LEFT) { 125 | textR.x = -(textR.width + gap); 126 | } else if (horizontalTextPosition == CENTER) { 127 | textR.x = (iconR.width / 2) - (textR.width / 2); 128 | } else { // (horizontalTextPosition == RIGHT) 129 | textR.x = (iconR.width + gap); 130 | } 131 | 132 | /* 133 | * labelR is the rectangle that contains iconR and textR. Move it to its proper position given the labelAlignment properties. 134 | * 135 | * To avoid actually allocating a Rectangle, Rectangle.union has been inlined below. 136 | */ 137 | int labelR_x = Math.min(iconR.x, textR.x); 138 | int labelR_width = Math.max(iconR.x + iconR.width, textR.x + textR.width) - labelR_x; 139 | int labelR_y = Math.min(iconR.y, textR.y); 140 | int labelR_height = Math.max(iconR.y + iconR.height, textR.y + textR.height) - labelR_y; 141 | 142 | int dx, dy; 143 | 144 | if (verticalAlignment == TOP) { 145 | dy = viewR.y - labelR_y; 146 | } else if (verticalAlignment == CENTER) { 147 | dy = (viewR.y + (viewR.height / 2)) - (labelR_y + (labelR_height / 2)); 148 | } else { // (verticalAlignment == BOTTOM) 149 | dy = (viewR.y + viewR.height) - (labelR_y + labelR_height); 150 | } 151 | 152 | if (horizontalAlignment == LEFT) { 153 | dx = viewR.x - labelR_x; 154 | } else if (horizontalAlignment == RIGHT) { 155 | dx = (viewR.x + viewR.width) - (labelR_x + labelR_width); 156 | } else { // (horizontalAlignment == CENTER) 157 | dx = (viewR.x + (viewR.width / 2)) - (labelR_x + (labelR_width / 2)); 158 | } 159 | 160 | /* 161 | * Translate textR and glypyR by dx,dy. 162 | */ 163 | 164 | textR.x += dx; 165 | textR.y += dy; 166 | 167 | iconR.x += dx; 168 | iconR.y += dy; 169 | 170 | return rettext; 171 | } 172 | 173 | /** 174 | * Compute and return the location of the icons origin, the location of origin of the text baseline, and a possibly clipped version of the compound 175 | * labels string. Locations are computed relative to the viewR rectangle. The JComponents orientation (LEADING/TRAILING) will also be taken into 176 | * account and translated into LEFT/RIGHT values accordingly. 177 | */ 178 | public static String layoutCompoundLabel(JComponent c, FontMetrics fm, String[] text, Icon icon, 179 | int verticalAlignment, int horizontalAlignment, int verticalTextPosition, int horizontalTextPosition, 180 | Rectangle viewR, Rectangle iconR, Rectangle textR, int textIconGap) { 181 | boolean orientationIsLeftToRight = true; 182 | int hAlign = horizontalAlignment; 183 | int hTextPos = horizontalTextPosition; 184 | 185 | if (c != null) { 186 | if (!(c.getComponentOrientation().isLeftToRight())) { 187 | orientationIsLeftToRight = false; 188 | } 189 | } 190 | 191 | // Translate LEADING/TRAILING values in horizontalAlignment 192 | // to LEFT/RIGHT values depending on the components orientation 193 | switch (horizontalAlignment) { 194 | case LEADING: 195 | hAlign = (orientationIsLeftToRight) ? LEFT : RIGHT; 196 | break; 197 | case TRAILING: 198 | hAlign = (orientationIsLeftToRight) ? RIGHT : LEFT; 199 | break; 200 | } 201 | 202 | // Translate LEADING/TRAILING values in horizontalTextPosition 203 | // to LEFT/RIGHT values depending on the components orientation 204 | switch (horizontalTextPosition) { 205 | case LEADING: 206 | hTextPos = (orientationIsLeftToRight) ? LEFT : RIGHT; 207 | break; 208 | case TRAILING: 209 | hTextPos = (orientationIsLeftToRight) ? RIGHT : LEFT; 210 | break; 211 | } 212 | 213 | return layoutCompoundLabel(fm, text, icon, verticalAlignment, hAlign, verticalTextPosition, hTextPos, viewR, iconR, 214 | textR, textIconGap); 215 | } 216 | 217 | protected void drawString(Graphics g, String s, int accChar, int textX, int textY) { 218 | if (s.indexOf('\n') == -1) 219 | BasicGraphicsUtils.drawString(g, s, accChar, textX, textY); 220 | else { 221 | String[] strs = splitStringByLines(s); 222 | int height = g.getFontMetrics().getHeight(); 223 | // Only the first line can have the accel char 224 | BasicGraphicsUtils.drawString(g, strs[0], accChar, textX, textY); 225 | for (int i = 1; i < strs.length; i++) 226 | g.drawString(strs[i], textX, textY + (height * i)); 227 | } 228 | } 229 | 230 | @Override 231 | protected String layoutCL(JLabel label, FontMetrics fontMetrics, String text, Icon icon, Rectangle viewR, 232 | Rectangle iconR, Rectangle textR) { 233 | String s = layoutCompoundLabel(label, fontMetrics, splitStringByLines(text), icon, label.getVerticalAlignment(), 234 | label.getHorizontalAlignment(), label.getVerticalTextPosition(), label.getHorizontalTextPosition(), viewR, 235 | iconR, textR, label.getIconTextGap()); 236 | 237 | if (s.equals("")) 238 | return text; 239 | return s; 240 | } 241 | 242 | @Override 243 | protected void paintDisabledText(JLabel l, Graphics g, String s, int textX, int textY) { 244 | int accChar = l.getDisplayedMnemonic(); 245 | g.setColor(l.getBackground()); 246 | drawString(g, s, accChar, textX, textY); 247 | } 248 | 249 | @Override 250 | protected void paintEnabledText(JLabel l, Graphics g, String s, int textX, int textY) { 251 | int accChar = l.getDisplayedMnemonic(); 252 | g.setColor(l.getForeground()); 253 | drawString(g, s, accChar, textX, textY); 254 | } 255 | 256 | public String[] splitStringByLines(String str) { 257 | if (str.equals(this.str)) 258 | return strs; 259 | 260 | this.str = str; 261 | 262 | int lines = 1; 263 | int i, c; 264 | for (i = 0, c = str.length(); i < c; i++) { 265 | if (str.charAt(i) == '\n') 266 | lines++; 267 | } 268 | strs = new String[lines]; 269 | StringTokenizer st = new StringTokenizer(str, "\n"); 270 | 271 | int line = 0; 272 | while (st.hasMoreTokens()) 273 | strs[line++] = st.nextToken(); 274 | 275 | return strs; 276 | } 277 | } 278 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/api/injector/InputKeyEvent.java: -------------------------------------------------------------------------------- 1 | //@formatter:off 2 | package com.github.xsavikx.androidscreencast.api.injector; 3 | 4 | import javax.annotation.Resource; 5 | import java.awt.event.KeyEvent; 6 | import java.util.EnumSet; 7 | import java.util.Set; 8 | 9 | @Resource 10 | public enum InputKeyEvent { 11 | KEYCODE_UNKNOWN(0, "Key code constant: Unknown key code."), 12 | KEYCODE_SOFT_LEFT(1, "Key code constant: Soft Left key. Usually situated below the display on phones and used as a multi-function feature key for selecting a software defined function shown on the bottom left of the display."), 13 | KEYCODE_SOFT_RIGHT(2, "Key code constant: Soft Right key. Usually situated below the display on phones and used as a multi-function feature key for selecting a software defined function shown on the bottom right of the display."), 14 | KEYCODE_HOME(3, "Key code constant: Home key. This key is handled by the framework and is never delivered to applications.", KeyEvent.VK_HOME), 15 | KEYCODE_BACK(4, "Key code constant: Back key. ", KeyEvent.VK_ESCAPE), 16 | KEYCODE_CALL(5, "Key code constant: Call key. "), 17 | KEYCODE_ENDCALL(6, "Key code constant: End Call key. "), 18 | KEYCODE_0(7, "Key code constant: '0' key. ", '0'), 19 | KEYCODE_1(8, "Key code constant: '1' key. ", '1'), 20 | KEYCODE_2(9, "Key code constant: '2' key. ", '2'), 21 | KEYCODE_3(10, "Key code constant: '3' key. ", '3'), 22 | KEYCODE_4(11, "Key code constant: '4' key. ", '4'), 23 | KEYCODE_5(12, "Key code constant: '5' key. ", '5'), 24 | KEYCODE_6(13, "Key code constant: '6' key. ", '6'), 25 | KEYCODE_7(14, "Key code constant: '7' key. ", '7'), 26 | KEYCODE_8(15, "Key code constant: '8' key. ", '8'), 27 | KEYCODE_9(16, "Key code constant: '9' key. ", '9'), 28 | KEYCODE_STAR(17, "Key code constant: '*' key. ", '*'), 29 | KEYCODE_POUND(18, "Key code constant: '#' key. ", '#'), 30 | KEYCODE_DPAD_UP(19, "Key code constant: Directional Pad Up key. May also be synthesized from trackball motions.", KeyEvent.VK_UP), 31 | KEYCODE_DPAD_DOWN(20, "Key code constant: Directional Pad Down key. May also be synthesized from trackball motions.", KeyEvent.VK_DOWN), 32 | KEYCODE_DPAD_LEFT(21, "Key code constant: Directional Pad Left key. May also be synthesized from trackball motions.", KeyEvent.VK_LEFT), 33 | KEYCODE_DPAD_RIGHT(22, "Key code constant: Directional Pad Right key. May also be synthesized from trackball motions.", KeyEvent.VK_RIGHT), 34 | KEYCODE_DPAD_CENTER(23, "Key code constant: Directional Pad Center key. May also be synthesized from trackball motions."), 35 | KEYCODE_VOLUME_UP(24, "Key code constant: Volume Up key. Adjusts the speaker volume up."), 36 | KEYCODE_VOLUME_DOWN(25, "Key code constant: Volume Down key. Adjusts the speaker volume down."), 37 | KEYCODE_POWER(26, "Key code constant: Power key. "), 38 | KEYCODE_CAMERA(27, "Key code constant: Camera key. Used to launch a camera application or take pictures."), 39 | KEYCODE_CLEAR(28, "Key code constant: Clear key. "), 40 | KEYCODE_A(29, "Key code constant: 'A' key. ", 'a'), 41 | KEYCODE_B(30, "Key code constant: 'B' key. ", 'b'), 42 | KEYCODE_C(31, "Key code constant: 'C' key. ", 'c'), 43 | KEYCODE_D(32, "Key code constant: 'D' key. ", 'd'), 44 | KEYCODE_E(33, "Key code constant: 'E' key. ", 'e'), 45 | KEYCODE_F(34, "Key code constant: 'F' key. ", 'f'), 46 | KEYCODE_G(35, "Key code constant: 'G' key. ", 'g'), 47 | KEYCODE_H(36, "Key code constant: 'H' key. ", 'h'), 48 | KEYCODE_I(37, "Key code constant: 'I' key. ", 'i'), 49 | KEYCODE_J(38, "Key code constant: 'J' key. ", 'j'), 50 | KEYCODE_K(39, "Key code constant: 'K' key. ", 'k'), 51 | KEYCODE_L(40, "Key code constant: 'L' key. ", 'l'), 52 | KEYCODE_M(41, "Key code constant: 'M' key. ", 'm'), 53 | KEYCODE_N(42, "Key code constant: 'N' key. ", 'n'), 54 | KEYCODE_O(43, "Key code constant: 'O' key. ", 'o'), 55 | KEYCODE_P(44, "Key code constant: 'P' key. ", 'p'), 56 | KEYCODE_Q(45, "Key code constant: 'Q' key. ", 'q'), 57 | KEYCODE_R(46, "Key code constant: 'R' key. ", 'r'), 58 | KEYCODE_S(47, "Key code constant: 'S' key. ", 's'), 59 | KEYCODE_T(48, "Key code constant: 'T' key. ", 't'), 60 | KEYCODE_U(49, "Key code constant: 'U' key. ", 'u'), 61 | KEYCODE_V(50, "Key code constant: 'V' key. ", 'v'), 62 | KEYCODE_W(51, "Key code constant: 'W' key. ", 'w'), 63 | KEYCODE_X(52, "Key code constant: 'X' key. ", 'x'), 64 | KEYCODE_Y(53, "Key code constant: 'Y' key. ", 'y'), 65 | KEYCODE_Z(54, "Key code constant: 'Z' key. ", 'z'), 66 | KEYCODE_COMMA(55, "Key code constant: ',' key. ", ','), 67 | KEYCODE_PERIOD(56, "Key code constant: '.' key. ", '.'), 68 | KEYCODE_ALT_LEFT(57, "Key code constant: Left Alt modifier key. "), 69 | KEYCODE_ALT_RIGHT(58, "Key code constant: Right Alt modifier key. "), 70 | KEYCODE_SHIFT_LEFT(59, "Key code constant: Left Shift modifier key. "), 71 | KEYCODE_SHIFT_RIGHT(60, "Key code constant: Right Shift modifier key. "), 72 | KEYCODE_TAB(61, "Key code constant: Tab key. ", '\t'), 73 | KEYCODE_SPACE(62, "Key code constant: Space key. ", ' '), 74 | KEYCODE_SYM(63, "Key code constant: Symbol modifier key. Used to enter alternate symbols."), 75 | KEYCODE_EXPLORER(64, "Key code constant: Explorer special function key. Used to launch a browser application."), 76 | KEYCODE_ENVELOPE(65, "Key code constant: Envelope special function key. Used to launch a mail application."), 77 | KEYCODE_ENTER(66, "Key code constant: Enter key. ", '\n'), 78 | KEYCODE_DEL(67, "Key code constant: Backspace key. Deletes characters before the insertion point, unlike {@link #KEYCODE_FORWARD_DEL}.", '\b'), 79 | KEYCODE_GRAVE(68, "Key code constant: '`' (backtick) key. ", '`'), 80 | KEYCODE_MINUS(69, "Key code constant: '-'. ", '-'), 81 | KEYCODE_EQUALS(70, "Key code constant: '=' key. ", '='), 82 | KEYCODE_LEFT_BRACKET(71, "Key code constant: '[' key. ", '['), 83 | KEYCODE_RIGHT_BRACKET(72, "Key code constant: ']' key. ", ']'), 84 | KEYCODE_BACKSLASH(73, "Key code constant: '\' key. ", '\\'), 85 | KEYCODE_SEMICOLON(74, "Key code constant: ';' key. ", ';'), 86 | KEYCODE_APOSTROPHE(75, "Key code constant: ''' (apostrophe) key. ", '\''), 87 | KEYCODE_SLASH(76, "Key code constant: '/' key. ", '/'), 88 | KEYCODE_AT(77, "Key code constant: '@' key. ", '@'), 89 | KEYCODE_NUM(78, "Key code constant: Number modifier key. Used to enter numeric symbols. This key is not Num Lock; it is more like {@link #KEYCODE_ALT_LEFT} and is interpreted as an ALT key by {@link android.text.method.MetaKeyKeyListener} ."), 90 | KEYCODE_HEADSETHOOK(79, "Key code constant: Headset Hook key. Used to hang up calls and stop media."), 91 | KEYCODE_FOCUS(80, "Key code constant: Camera Focus key. Used to focus the camera."), // *Camera* focus 92 | KEYCODE_PLUS(81, "Key code constant: '+' key.", '+'), 93 | KEYCODE_MENU(82, "Key code constant: Menu key."), 94 | KEYCODE_NOTIFICATION(83, "Key code constant: Notification key."), 95 | KEYCODE_SEARCH(84, "Key code constant: Search key."), 96 | KEYCODE_MEDIA_PLAY_PAUSE(85, "Key code constant: Play/Pause media key."), 97 | KEYCODE_MEDIA_STOP(86, "Key code constant: Stop media key."), 98 | KEYCODE_MEDIA_NEXT(87, "Key code constant: Play Next media key."), 99 | KEYCODE_MEDIA_PREVIOUS(88, "Key code constant: Play Previous media key."), 100 | KEYCODE_MEDIA_REWIND(89, "Key code constant: Rewind media key."), 101 | KEYCODE_MEDIA_FAST_FORWARD(90, "Key code constant: Fast Forward media key."), 102 | KEYCODE_MUTE(91, "Key code constant: Mute key. Mutes the microphone, unlike {@link #KEYCODE_VOLUME_MUTE}."), 103 | KEYCODE_PAGE_UP(92, "Key code constant: Page Up key. ", KeyEvent.VK_PAGE_UP), 104 | KEYCODE_PAGE_DOWN(93, "Key code constant: Page Down key. ", KeyEvent.VK_PAGE_DOWN), 105 | KEYCODE_PICTSYMBOLS(94, "Key code constant: Picture Symbols modifier key. Used to switch symbol sets (Emoji, Kao-moji)."), // switch symbol-sets (Emoji,Kao-moji) 106 | KEYCODE_SWITCH_CHARSET(95, "Key code constant: Switch Charset modifier key. Used to switch character sets (Kanji, Katakana)."), // switch char-sets (Kanji,Katakana) 107 | KEYCODE_BUTTON_A(96, "Key code constant: A Button key. On a game controller, the A button should be either the button labeled A or the first button on the bottom row of controller buttons."), 108 | KEYCODE_BUTTON_B(97, "Key code constant: B Button key. On a game controller, the B button should be either the button labeled B or the second button on the bottom row of controller buttons."), 109 | KEYCODE_BUTTON_C(98, "Key code constant: C Button key. On a game controller, the C button should be either the button labeled C or the third button on the bottom row of controller buttons."), 110 | KEYCODE_BUTTON_X(99, "Key code constant: X Button key. On a game controller, the X button should be either the button labeled X or the first button on the upper row of controller buttons."), 111 | KEYCODE_BUTTON_Y(100, "Key code constant: Y Button key. On a game controller, the Y button should be either the button labeled Y or the second button on the upper row of controller buttons."), 112 | KEYCODE_BUTTON_Z(101, "Key code constant: Z Button key. On a game controller, the Z button should be either the button labeled Z or the third button on the upper row of controller buttons."), 113 | KEYCODE_BUTTON_L1(102, "Key code constant: L1 Button key. On a game controller, the L1 button should be either the button labeled L1 (or L) or the top left trigger button."), 114 | KEYCODE_BUTTON_R1(103, "Key code constant: R1 Button key. On a game controller, the R1 button should be either the button labeled R1 (or R) or the top right trigger button."), 115 | KEYCODE_BUTTON_L2(104, "Key code constant: L2 Button key. On a game controller, the L2 button should be either the button labeled L2 or the bottom left trigger button."), 116 | KEYCODE_BUTTON_R2(105, "Key code constant: R2 Button key. On a game controller, the R2 button should be either the button labeled R2 or the bottom right trigger button."), 117 | KEYCODE_BUTTON_THUMBL(106, "Key code constant: Left Thumb Button key. On a game controller, the left thumb button indicates that the left (or only) joystick is pressed."), 118 | KEYCODE_BUTTON_THUMBR(107, "Key code constant: Right Thumb Button key. On a game controller, the right thumb button indicates that the right joystick is pressed."), 119 | KEYCODE_BUTTON_START(108, "Key code constant: Start Button key. On a game controller, the button labeled Start."), 120 | KEYCODE_BUTTON_SELECT(109, "Key code constant: Select Button key. On a game controller, the button labeled Select."), 121 | KEYCODE_BUTTON_MODE(110, "Key code constant: Mode Button key. On a game controller, the button labeled Mode."), 122 | KEYCODE_ESCAPE(111, "Key code constant: Escape key."), 123 | KEYCODE_FORWARD_DEL(112, "Key code constant: Forward Delete key. Deletes characters ahead of the insertion point, unlike {@link #KEYCODE_DEL}."), 124 | KEYCODE_CTRL_LEFT(113, "Key code constant: Left Control modifier key."), 125 | KEYCODE_CTRL_RIGHT(114, "Key code constant: Right Control modifier key."), 126 | KEYCODE_CAPS_LOCK(115, "Key code constant: Caps Lock key."), 127 | KEYCODE_SCROLL_LOCK(116, "Key code constant: Scroll Lock key."), 128 | KEYCODE_META_LEFT(117, "Key code constant: Left Meta modifier key."), 129 | KEYCODE_META_RIGHT(118, "Key code constant: Right Meta modifier key."), 130 | KEYCODE_FUNCTION(119, "Key code constant: Function modifier key."), 131 | KEYCODE_SYSRQ(120, "Key code constant: System Request / Print Screen key.", KeyEvent.VK_PRINTSCREEN), 132 | KEYCODE_BREAK(121, "Key code constant: Break / Pause key. ", KeyEvent.VK_PAUSE), 133 | KEYCODE_MOVE_HOME(122, "Key code constant: Home Movement key. Used for scrolling or moving the cursor around to the start of a line or to the top of a list."), 134 | KEYCODE_MOVE_END(123, "Key code constant: End Movement key. Used for scrolling or moving the cursor around to the end of a line or to the bottom of a list."), 135 | KEYCODE_INSERT(124, "Key code constant: Insert key. Toggles insert / overwrite edit mode.", KeyEvent.VK_INSERT), 136 | KEYCODE_FORWARD(125, "Key code constant: Forward key. Navigates forward in the history stack. Complement of {@link #KEYCODE_BACK}."), 137 | KEYCODE_MEDIA_PLAY(126, "Key code constant: Play media key. "), 138 | KEYCODE_MEDIA_PAUSE(127, "Key code constant: Pause media key. "), 139 | KEYCODE_MEDIA_CLOSE(128, "Key code constant: Close media key. May be used to close a CD tray, for example."), 140 | KEYCODE_MEDIA_EJECT(129, "Key code constant: Eject media key. May be used to eject a CD tray, for example."), 141 | KEYCODE_MEDIA_RECORD(130, "Key code constant: Record media key. "), 142 | KEYCODE_F1(131, "Key code constant: F1 key. "), 143 | KEYCODE_F2(132, "Key code constant: F2 key. "), 144 | KEYCODE_F3(133, "Key code constant: F3 key. "), 145 | KEYCODE_F4(134, "Key code constant: F4 key. "), 146 | KEYCODE_F5(135, "Key code constant: F5 key. "), 147 | KEYCODE_F6(136, "Key code constant: F6 key. "), 148 | KEYCODE_F7(137, "Key code constant: F7 key. "), 149 | KEYCODE_F8(138, "Key code constant: F8 key. "), 150 | KEYCODE_F9(139, "Key code constant: F9 key. "), 151 | KEYCODE_F10(140, "Key code constant: F10 key. "), 152 | KEYCODE_F11(141, "Key code constant: F11 key. "), 153 | KEYCODE_F12(142, "Key code constant: F12 key. "), 154 | KEYCODE_NUM_LOCK(143, "Key code constant: Num Lock key. This is the Num Lock key; it is different from {@link #KEYCODE_NUM}. This key alters the behavior of other keys on the numeric keypad."), 155 | KEYCODE_NUMPAD_0(144, "Key code constant: Numeric keypad '0' key. "), 156 | KEYCODE_NUMPAD_1(145, "Key code constant: Numeric keypad '1' key. "), 157 | KEYCODE_NUMPAD_2(146, "Key code constant: Numeric keypad '2' key. "), 158 | KEYCODE_NUMPAD_3(147, "Key code constant: Numeric keypad '3' key. "), 159 | KEYCODE_NUMPAD_4(148, "Key code constant: Numeric keypad '4' key. "), 160 | KEYCODE_NUMPAD_5(149, "Key code constant: Numeric keypad '5' key. "), 161 | KEYCODE_NUMPAD_6(150, "Key code constant: Numeric keypad '6' key. "), 162 | KEYCODE_NUMPAD_7(151, "Key code constant: Numeric keypad '7' key. "), 163 | KEYCODE_NUMPAD_8(152, "Key code constant: Numeric keypad '8' key. "), 164 | KEYCODE_NUMPAD_9(153, "Key code constant: Numeric keypad '9' key. "), 165 | KEYCODE_NUMPAD_DIVIDE(154, "Key code constant: Numeric keypad '/' key (for division). "), 166 | KEYCODE_NUMPAD_MULTIPLY(155, "Key code constant: '*' key (for multiplication). "), 167 | KEYCODE_NUMPAD_SUBTRACT(156, "Key code constant: Numeric keypad '-' key (for subtraction). "), 168 | KEYCODE_NUMPAD_ADD(157, "Key code constant: Numeric keypad '+' key (for addition). "), 169 | KEYCODE_NUMPAD_DOT(158, "Key code constant: Numeric keypad '.' key (for decimals or digit grouping)."), 170 | KEYCODE_NUMPAD_COMMA(159, "Key code constant: Numeric keypad ',' key (for decimals or digit grouping)."), 171 | KEYCODE_NUMPAD_ENTER(160, "Key code constant: Numeric keypad Enter key. "), 172 | KEYCODE_NUMPAD_EQUALS(161, "Key code constant: Numeric keypad '=' key. "), 173 | KEYCODE_NUMPAD_LEFT_PAREN(162, "Key code constant: Numeric keypad '(' key. "), 174 | KEYCODE_NUMPAD_RIGHT_PAREN(163, "Key code constant: Numeric keypad ')' key. "), 175 | KEYCODE_VOLUME_MUTE(164, "Key code constant: Volume Mute key. Mutes the speaker, unlike {@link #KEYCODE_MUTE}. This key should normally be implemented as a toggle such that the first press mutes the speaker and the second press restores the original volume."), 176 | KEYCODE_INFO(165, "Key code constant: Info key. Common on TV remotes to show additional information related to what is currently being viewed."), 177 | KEYCODE_CHANNEL_UP(166, "Key code constant: Channel up key. On TV remotes, increments the television channel."), 178 | KEYCODE_CHANNEL_DOWN(167, "Key code constant: Channel down key. On TV remotes, decrements the television channel."), 179 | KEYCODE_ZOOM_IN(168, "Key code constant: Zoom in key. "), 180 | KEYCODE_ZOOM_OUT(169, "Key code constant: Zoom out key. "), 181 | KEYCODE_TV(170, "Key code constant: TV key. On TV remotes, switches to viewing live TV."), 182 | KEYCODE_WINDOW(171, "Key code constant: Window key. On TV remotes, toggles picture-in-picture mode or other windowing functions."), 183 | KEYCODE_GUIDE(172, "Key code constant: Guide key. On TV remotes, shows a programming guide."), 184 | KEYCODE_DVR(173, "Key code constant: DVR key. On some TV remotes, switches to a DVR mode for recorded shows."), 185 | KEYCODE_BOOKMARK(174, "Key code constant: Bookmark key. On some TV remotes, bookmarks content or web pages."), 186 | KEYCODE_CAPTIONS(175, "Key code constant: Toggle captions key. Switches the mode for closed-captioning text, for example during television shows."), 187 | KEYCODE_SETTINGS(176, "Key code constant: Settings key. Starts the system settings activity."), 188 | KEYCODE_TV_POWER(177, "Key code constant: TV power key. On TV remotes, toggles the power on a television screen."), 189 | KEYCODE_TV_INPUT(178, "Key code constant: TV input key. On TV remotes, switches the input on a television screen."), 190 | KEYCODE_STB_POWER(179, "Key code constant: Set-top-box power key. On TV remotes, toggles the power on an external Set-top-box."), 191 | KEYCODE_STB_INPUT(180, "Key code constant: Set-top-box input key. On TV remotes, switches the input mode on an external Set-top-box."), 192 | KEYCODE_AVR_POWER(181, "Key code constant: A/V Receiver power key. On TV remotes, toggles the power on an external A/V Receiver."), 193 | KEYCODE_AVR_INPUT(182, "Key code constant: A/V Receiver input key. On TV remotes, switches the input mode on an external A/V Receiver."), 194 | KEYCODE_PROG_RED(183, "Key code constant: Red \"programmable\" key. On TV remotes, acts as a contextual/programmable key."), 195 | KEYCODE_PROG_GREEN(184, "Key code constant: Green \"programmable\" key. On TV remotes, actsas a contextual/programmable key."), 196 | KEYCODE_PROG_YELLOW(185, "Key code constant: Yellow \"programmable\" key. On TV remotes, acts as a contextual/programmable key."), 197 | KEYCODE_PROG_BLUE(186, "Key code constant: Blue \"programmable\" key. On TV remotes, acts as a contextual/programmable key."), 198 | KEYCODE_APP_SWITCH(187, "Key code constant: App switch key. Should bring up the application switcher dialog."), 199 | KEYCODE_BUTTON_1(188, "Key code constant: Generic Game Pad Button #1. "), 200 | KEYCODE_BUTTON_2(189, "Key code constant: Generic Game Pad Button #2. "), 201 | KEYCODE_BUTTON_3(190, "Key code constant: Generic Game Pad Button #3. "), 202 | KEYCODE_BUTTON_4(191, "Key code constant: Generic Game Pad Button #4. "), 203 | KEYCODE_BUTTON_5(192, "Key code constant: Generic Game Pad Button #5. "), 204 | KEYCODE_BUTTON_6(193, "Key code constant: Generic Game Pad Button #6. "), 205 | KEYCODE_BUTTON_7(194, "Key code constant: Generic Game Pad Button #7. "), 206 | KEYCODE_BUTTON_8(195, "Key code constant: Generic Game Pad Button #8. "), 207 | KEYCODE_BUTTON_9(196, "Key code constant: Generic Game Pad Button #9. "), 208 | KEYCODE_BUTTON_10(197, "Key code constant: Generic Game Pad Button #10. "), 209 | KEYCODE_BUTTON_11(198, "Key code constant: Generic Game Pad Button #11. "), 210 | KEYCODE_BUTTON_12(199, "Key code constant: Generic Game Pad Button #12. "), 211 | KEYCODE_BUTTON_13(200, "Key code constant: Generic Game Pad Button #13. "), 212 | KEYCODE_BUTTON_14(201, "Key code constant: Generic Game Pad Button #14. "), 213 | KEYCODE_BUTTON_15(202, "Key code constant: Generic Game Pad Button #15. "), 214 | KEYCODE_BUTTON_16(203, "Key code constant: Generic Game Pad Button #16. "), 215 | KEYCODE_LANGUAGE_SWITCH(204, "Key code constant: Language Switch key. Toggles the current input language such as switching between English and Japanese on a QWERTY keyboard. On some devices, the same function may be performed by pressing Shift+Spacebar."), 216 | KEYCODE_MANNER_MODE(205, "Key code constant: Manner Mode key. Toggles silent or vibrate mode on and off to make the device behave more politely in certain settings such as on a crowded train. On some devices, the key may only operate when long-pressed."), 217 | KEYCODE_3D_MODE(206, "Key code constant: 3D Mode key. Toggles the display between 2D and 3D mode."), 218 | KEYCODE_CONTACTS(207, "Key code constant: Contacts special function key. Used to launch an address book application."), 219 | KEYCODE_CALENDAR(208, "Key code constant: Calendar special function key. Used to launch a calendar application."), 220 | KEYCODE_MUSIC(209, "Key code constant: Music special function key. Used to launch a music player application."), 221 | KEYCODE_CALCULATOR(210, "Key code constant: Calculator special function key. Used to launch a calculator application."), 222 | KEYCODE_ZENKAKU_HANKAKU(211, "Key code constant: Japanese full-width / half-width key. "), 223 | KEYCODE_EISU(212, "Key code constant: Japanese alphanumeric key. "), 224 | KEYCODE_MUHENKAN(213, "Key code constant: Japanese non-conversion key. "), 225 | KEYCODE_HENKAN(214, "Key code constant: Japanese conversion key. "), 226 | KEYCODE_KATAKANA_HIRAGANA(215, "Key code constant: Japanese katakana / hiragana key. "), 227 | KEYCODE_YEN(216, "Key code constant: Japanese Yen key. "), 228 | KEYCODE_RO(217, "Key code constant: Japanese Ro key. "), 229 | KEYCODE_KANA(218, "Key code constant: Japanese kana key. "), 230 | KEYCODE_ASSIST(219, "Key code constant: Assist key. Launches the global assist activity. Not delivered to applications."), 231 | KEYCODE_BRIGHTNESS_DOWN(220, "Key code constant: Brightness Down key. Adjusts the screen brighs down."), 232 | KEYCODE_BRIGHTNESS_UP(221, "Key code constant: Brightness Up key. Adjusts the screen brightness up."), 233 | KEYCODE_MEDIA_AUDIO_TRACK(222, "Key code constant: Audio Track key. Switches the audio tracks."), 234 | KEYCODE_SLEEP(223, "Key code constant: Sleep key. Puts the device to sleep. Behaves somewhat like {@link #KEYCODE_POWER} but it has no effect if the device is already asleep."), 235 | KEYCODE_WAKEUP(224, "Key code constant: Wakeup key. Wakes up the device. Behaves somewhat like {@link #KEYCODE_POWER} but it has no effect if the device is already awake."), 236 | KEYCODE_PAIRING(225, "Key code constant: Pairing key. Initiates peripheral pairing mode. Useful for pairing remote control devices or game controllers, especially if no other input mode is available."), 237 | KEYCODE_MEDIA_TOP_MENU(226, "Key code constant: Media Top Menu key. Goes to the top of media menu."), 238 | KEYCODE_11(227, "Key code constant: '11' key. "), 239 | KEYCODE_12(228, "Key code constant: '12' key. "), 240 | KEYCODE_LAST_CHANNEL(229, "Key code constant: Last Channel key. Goes to the last viewed channel."), 241 | KEYCODE_TV_DATA_SERVICE(230, "Key code constant: TV data service key. Displays data services like weather, sports."), 242 | KEYCODE_VOICE_ASSIST(231, "Key code constant: Voice Assist key. Launches the global voice assist activity. Not delivered to applications."), 243 | KEYCODE_TV_RADIO_SERVICE(232, "Key code constant: Radio key. Toggles TV service / Radio service."), 244 | KEYCODE_TV_TELETEXT(233, "Key code constant: Teletext key. Displays Teletext service."), 245 | KEYCODE_TV_NUMBER_ENTRY(234, "Key code constant: Number entry key. Initiates to enter multi-digit channel nubmber when each digit key is assigned for selecting separate channel. Corresponds to Number Entry Mode (0x1D) of CEC User Control Code."), 246 | KEYCODE_TV_TERRESTRIAL_ANALOG(235, "Key code constant: Analog Terrestrial key. Switches to analog terrestrial broadcast service."), 247 | KEYCODE_TV_TERRESTRIAL_DIGITAL(236, "Key code constant: Digital Terrestrial key. Switches to digital terrestrial broadcast service."), 248 | KEYCODE_TV_SATELLITE(237, "Key code constant: Satellite key. Switches to digital satellite broadcast service."), 249 | KEYCODE_TV_SATELLITE_BS(238, "Key code constant: BS key. Switches to BS digital satellite broadcasting service available in Japan."), 250 | KEYCODE_TV_SATELLITE_CS(239, "Key code constant: CS key. Switches to CS digital satellite broadcasting service available in Japan."), 251 | KEYCODE_TV_SATELLITE_SERVICE(240, "Key code constant: BS/CS key. Toggles between BS and CS digital satellite services."), 252 | KEYCODE_TV_NETWORK(241, "Key code constant: Toggle Network key. Toggles selecting broacast services."), 253 | KEYCODE_TV_ANTENNA_CABLE(242, "Key code constant: Antenna/Cable key. Toggles broadcast input source between antenna and cable."), 254 | KEYCODE_TV_INPUT_HDMI_1(243, "Key code constant: HDMI #1 key. Switches to HDMI input #1."), 255 | KEYCODE_TV_INPUT_HDMI_2(244, "Key code constant: HDMI #2 key. Switches to HDMI input #2."), 256 | KEYCODE_TV_INPUT_HDMI_3(245, "Key code constant: HDMI #3 key. Switches to HDMI input #3."), 257 | KEYCODE_TV_INPUT_HDMI_4(246, "Key code constant: HDMI #4 key. Switches to HDMI input #4."), 258 | KEYCODE_TV_INPUT_COMPOSITE_1(247, "Key code constant: Composite #1 key. Switches to composite video input #1."), 259 | KEYCODE_TV_INPUT_COMPOSITE_2(248, "Key code constant: Composite #2 key. Switches to composite video input #2."), 260 | KEYCODE_TV_INPUT_COMPONENT_1(249, "Key code constant: Component #1 key. Switches to component video input #1."), 261 | KEYCODE_TV_INPUT_COMPONENT_2(250, "Key code constant: Component #2 key. Switches to component video input #2."), 262 | KEYCODE_TV_INPUT_VGA_1(251, "Key code constant: VGA #1 key. Switches to VGA (analog RGB) input #1."), 263 | KEYCODE_TV_AUDIO_DESCRIPTION(252, "Key code constant: Audio description key. Toggles audio description off / on."), 264 | KEYCODE_TV_AUDIO_DESCRIPTION_MIX_UP(253, "Key code constant: Audio description mixing volume up key. Louden audio description volume as compared with normal audio volume."), 265 | KEYCODE_TV_AUDIO_DESCRIPTION_MIX_DOWN(254, "Key code constant: Audio description mixing volume down key. Lessen audio description volume as compared with normal audio volume."), 266 | KEYCODE_TV_ZOOM_MODE(255, "Key code constant: Zoom mode key. Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.)"), 267 | KEYCODE_TV_CONTENTS_MENU(256, "Key code constant: Contents menu key. Goes to the title list. Corresponds to Contents Menu (0x0B) of CEC User Control Code"), 268 | KEYCODE_TV_MEDIA_CONTEXT_MENU(257, "Key code constant: Media context menu key. Goes to the context menu of media contents. Corresponds to Media Context-sensitive Menu (0x11) of CEC User Control Code."), 269 | KEYCODE_TV_TIMER_PROGRAMMING(258, "Key code constant: Timer programming key. Goes to the timer recording menu. Corresponds to Timer Programming (0x54) of CEC User Control Code."), 270 | KEYCODE_HELP(259, "Key code constant: Help key. "); 271 | private static final Set eventsWithCharacters = fillEventsWithCharacters(); 272 | private static Set eventsWithKeyCodes = fillEventsWithKeyCodes(); 273 | private int keyCode; 274 | private int code; 275 | private String description; 276 | private char characterToReplace; 277 | 278 | InputKeyEvent(int code, String description) { 279 | this.code = code; 280 | this.description = description; 281 | } 282 | 283 | InputKeyEvent(int code, String description, char character) { 284 | this(code, description); 285 | this.characterToReplace = character; 286 | } 287 | 288 | InputKeyEvent(int code, String description, int keyCode) { 289 | this(code, description); 290 | this.keyCode = keyCode; 291 | } 292 | 293 | public static InputKeyEvent getByCharacter(char c) { 294 | for (InputKeyEvent e : eventsWithCharacters) { 295 | if (e.characterToReplace == c) { 296 | return e; 297 | } 298 | } 299 | return null; 300 | } 301 | 302 | public static InputKeyEvent getByKeyCode(int keyCode) { 303 | for (InputKeyEvent e : eventsWithKeyCodes) { 304 | if (e.keyCode == keyCode) { 305 | return e; 306 | } 307 | } 308 | return null; 309 | } 310 | 311 | public static InputKeyEvent getByCharacterOrKeyCode(char c, int keyCode) { 312 | InputKeyEvent e = getByCharacter(c); 313 | if (e == null) 314 | e = getByKeyCode(keyCode); 315 | return e; 316 | } 317 | 318 | private static Set fillEventsWithCharacters() { 319 | Set eventsWithCharacters = EnumSet.allOf(InputKeyEvent.class); 320 | for (InputKeyEvent e : values()) { 321 | if (e.characterToReplace == '\u0000') { 322 | eventsWithCharacters.remove(e); 323 | } 324 | } 325 | return eventsWithCharacters; 326 | } 327 | 328 | private static Set fillEventsWithKeyCodes() { 329 | Set eventsWithKeyCodes = EnumSet.allOf(InputKeyEvent.class); 330 | for (InputKeyEvent e : values()) { 331 | if (e.keyCode == 0) { 332 | eventsWithKeyCodes.remove(e); 333 | } 334 | } 335 | return eventsWithKeyCodes; 336 | } 337 | 338 | public int getKeyCode() { 339 | return keyCode; 340 | } 341 | 342 | public int getCode() { 343 | return code; 344 | } 345 | 346 | public String getDescription() { 347 | return description; 348 | } 349 | 350 | public char getCharacterToReplace() { 351 | return characterToReplace; 352 | } 353 | 354 | 355 | } 356 | -------------------------------------------------------------------------------- /src/main/java/com/github/xsavikx/androidscreencast/ui/worker/SwingWorker.java: -------------------------------------------------------------------------------- 1 | /* 2 | * $Id: SwingWorker.java,v 1.6 2008/07/25 19:32:29 idk Exp $ 3 | * 4 | * Copyright @ 2005 Sun Microsystems, Inc. All rights 5 | * reserved. Use is subject to license terms. 6 | */ 7 | 8 | package com.github.xsavikx.androidscreencast.ui.worker; 9 | 10 | import javax.swing.*; 11 | import java.awt.event.ActionEvent; 12 | import java.awt.event.ActionListener; 13 | import java.beans.PropertyChangeEvent; 14 | import java.beans.PropertyChangeListener; 15 | import java.beans.PropertyChangeSupport; 16 | import java.util.List; 17 | import java.util.concurrent.*; 18 | import java.util.concurrent.atomic.AtomicInteger; 19 | import java.util.concurrent.locks.Condition; 20 | import java.util.concurrent.locks.ReentrantLock; 21 | 22 | /** 23 | * An abstract class to perform lengthy GUI-interacting tasks in a dedicated thread. 24 | *

25 | *

26 | * When writing a multi-threaded application using Swing, there are two constraints to keep in mind: (refer to 27 | * How to Use Threads for more details): 28 | *

    29 | *
  • Time-consuming tasks should not be run on the Event Dispatch Thread. Otherwise the application becomes unresponsive.
  • 30 | *
  • Swing components should be accessed on the Event Dispatch Thread only.
  • 31 | *
32 | *

33 | *

34 | *

35 | *

36 | * These constraints mean that a GUI application with time intensive computing needs at least two threads: 1) a thread to perform the lengthy task and 37 | * 2) the Event Dispatch Thread (EDT) for all GUI-related activities. This involves inter-thread communication which can be tricky to 38 | * implement. 39 | *

40 | *

41 | * {@code SwingWorker} is designed for situations where you need to have a long running task run in a background thread and provide updates to the UI 42 | * either when done, or while processing. Subclasses of {@code SwingWorker} must implement the {@see #doInBackground} method to perform the background 43 | * computation. 44 | *

45 | *

46 | *

47 | * Workflow 48 | *

49 | * There are three threads involved in the life cycle of a {@code SwingWorker} : 50 | *

    51 | *
  • 52 | *

    53 | * Current thread: The {@link #execute} method is called on this thread. It schedules {@code SwingWorker} for the execution on a worker 54 | * thread and returns immediately. One can wait for the {@code SwingWorker} to complete using the {@link #get get} methods. 55 | *

  • 56 | *

    57 | * Worker thread: The {@link #doInBackground} method is called on this thread. This is where all background activities should happen. To notify 58 | * {@code PropertyChangeListeners} about bound properties changes use the {@link #firePropertyChange firePropertyChange} and 59 | * {@link #getPropertyChangeSupport} methods. By default there are two bound properties available: {@code state} and {@code progress}. 60 | *

  • 61 | *

    62 | * Event Dispatch Thread: All Swing related activities occur on this thread. {@code SwingWorker} invokes the {@link #process process} and 63 | * {@link #done} methods and notifies any {@code PropertyChangeListeners} on this thread. 64 | *

65 | *

66 | *

67 | * Often, the Current thread is the Event Dispatch Thread. 68 | *

69 | *

70 | *

71 | * Before the {@code doInBackground} method is invoked on a worker thread, {@code SwingWorker} notifies any {@code PropertyChangeListeners} 72 | * about the {@code state} property change to {@code StateValue.STARTED}. After the {@code doInBackground} method is finished the {@code done} method 73 | * is executed. Then {@code SwingWorker} notifies any {@code PropertyChangeListeners} about the {@code state} property change to 74 | * {@code StateValue.DONE}. 75 | *

76 | *

77 | * {@code SwingWorker} is only designed to be executed once. Executing a {@code SwingWorker} more than once will not result in invoking the 78 | * {@code doInBackground} method twice. 79 | *

80 | *

81 | * Sample Usage 82 | *

83 | * The following example illustrates the simplest use case. Some processing is done in the background and when done you update a Swing component. 84 | *

85 | *

86 | * Say we want to find the "Meaning of Life" and display the result in a {@code JLabel}. 87 | *

88 | *

 89 |  *   final JLabel label;
 90 |  *   class MeaningOfLifeFinder extends SwingWorker<String, Object> {
 91 |  *       {@code @Override}
 92 |  *       public String doInBackground() {
 93 |  *           return findTheMeaningOfLife();
 94 |  *       }
 95 |  *
 96 |  *       {@code @Override}
 97 |  *       protected void done() {
 98 |  *           try {
 99 |  *               label.setText(get());
100 |  *           } catch (Exception ignore) {
101 |  *           }
102 |  *       }
103 |  *   }
104 |  *
105 |  *   (new MeaningOfLifeFinder()).execute();
106 |  * 
107 | *

108 | *

109 | * The next example is useful in situations where you wish to process data as it is ready on the Event Dispatch Thread. 110 | *

111 | *

112 | * Now we want to find the first N prime numbers and display the results in a {@code JTextArea}. While this is computing, we want to update our 113 | * progress in a {@code JProgressBar}. Finally, we also want to print the prime numbers to {@code System.out}. 114 | *

115 | *

116 |  * class PrimeNumbersTask extends
117 |  *         SwingWorker<List<Integer>, Integer> {
118 |  *     PrimeNumbersTask(JTextArea textArea, int numbersToFind) {
119 |  *         //initialize
120 |  *     }
121 |  *
122 |  *     {@code @Override}
123 |  *     public List<Integer> doInBackground() {
124 |  *         while (! enough && ! isCancelled()) {
125 |  *                 number = nextPrimeNumber();
126 |  *                 publish(number);
127 |  *                 setProgress(100 * numbers.size() / numbersToFind);
128 |  *             }
129 |  *         }
130 |  *         return numbers;
131 |  *     }
132 |  *
133 |  *     {@code @Override}
134 |  *     protected void process(List<Integer> chunks) {
135 |  *         for (int number : chunks) {
136 |  *             textArea.append(number + "\n");
137 |  *         }
138 |  *     }
139 |  * }
140 |  *
141 |  * JTextArea textArea = new JTextArea();
142 |  * final JProgressBar progressBar = new JProgressBar(0, 100);
143 |  * PrimeNumbersTask task = new PrimeNumbersTask(textArea, N);
144 |  * task.addPropertyChangeListener(
145 |  *     new PropertyChangeListener() {
146 |  *         public  void propertyChange(PropertyChangeEvent evt) {
147 |  *             if ("progress".equals(evt.getPropertyName())) {
148 |  *                 progressBar.setValue((Integer)evt.getNewValue());
149 |  *             }
150 |  *         }
151 |  *     });
152 |  *
153 |  * task.execute();
154 |  * System.out.println(task.get()); //prints all prime numbers we have got
155 |  * 
156 | *

157 | *

158 | * Because {@code SwingWorker} implements {@code Runnable}, a {@code SwingWorker} can be submitted to an {@link java.util.concurrent.Executor} for 159 | * execution. 160 | * 161 | * @param the result type returned by this {@code SwingWorker's} {@code doInBackground} and {@code get} methods 162 | * @param the type used for carrying out intermediate results by this {@code SwingWorker's} {@code publish} and {@code process} methods 163 | * @author Igor Kushnirskiy 164 | * @version $Revision: 1.6 $ $Date: 2008/07/25 19:32:29 $ 165 | */ 166 | public abstract class SwingWorker implements Future, Runnable { 167 | /** 168 | * number of worker threads. 169 | */ 170 | private static final int MAX_WORKER_THREADS = 10; 171 | private static final AccumulativeRunnable doSubmit = new DoSubmitAccumulativeRunnable(); 172 | private static ExecutorService executorService = null; 173 | /** 174 | * everything is run inside this FutureTask. Also it is used as a delegatee for the Future API. 175 | */ 176 | private final FutureTask future; 177 | /** 178 | * all propertyChangeSupport goes through this. 179 | */ 180 | private final PropertyChangeSupport propertyChangeSupport; 181 | /** 182 | * current progress. 183 | */ 184 | private volatile int progress; 185 | 186 | /** 187 | * current state. 188 | */ 189 | private volatile StateValue state; 190 | /** 191 | * handler for {@code process} method. 192 | */ 193 | private AccumulativeRunnable doProcess; 194 | /** 195 | * handler for progress property change notifications. 196 | */ 197 | private AccumulativeRunnable doNotifyProgressChange; 198 | 199 | /** 200 | * Constructs this {@code SwingWorker}. 201 | */ 202 | public SwingWorker() { 203 | Callable callable = new Callable() { 204 | @Override 205 | public T call() throws Exception { 206 | setState(StateValue.STARTED); 207 | return doInBackground(); 208 | } 209 | }; 210 | 211 | future = new FutureTask(callable) { 212 | @Override 213 | protected void done() { 214 | doneEDT(); 215 | setState(StateValue.DONE); 216 | } 217 | }; 218 | 219 | state = StateValue.PENDING; 220 | propertyChangeSupport = new SwingWorkerPropertyChangeSupport(this); 221 | doProcess = null; 222 | doNotifyProgressChange = null; 223 | } 224 | 225 | /** 226 | * returns workersExecutorService. 227 | *

228 | * returns the service stored in the appContext or creates it if necessary. If the last one it triggers autoShutdown thread to get started. 229 | * 230 | * @return ExecutorService for the {@code SwingWorkers} 231 | */ 232 | private static synchronized ExecutorService getWorkersExecutorService() { 233 | if (executorService == null) { 234 | // this creates non-daemon threads. 235 | ThreadFactory threadFactory = new ThreadFactory() { 236 | final AtomicInteger threadNumber = new AtomicInteger(1); 237 | 238 | @Override 239 | public Thread newThread(final Runnable r) { 240 | StringBuilder name = new StringBuilder("SwingWorker-pool-"); 241 | name.append(System.identityHashCode(this)); 242 | name.append("-thread-"); 243 | name.append(threadNumber.getAndIncrement()); 244 | 245 | Thread t = new Thread(r, name.toString()); 246 | if (t.isDaemon()) 247 | t.setDaemon(false); 248 | if (t.getPriority() != Thread.NORM_PRIORITY) 249 | t.setPriority(Thread.NORM_PRIORITY); 250 | return t; 251 | } 252 | }; 253 | 254 | /* 255 | * We want a to have no more than MAX_WORKER_THREADS running threads. 256 | * 257 | * We want a worker thread to wait no longer than 1 second for new tasks before terminating. 258 | */ 259 | executorService = new ThreadPoolExecutor(0, MAX_WORKER_THREADS, 5L, TimeUnit.SECONDS, 260 | new LinkedBlockingQueue(), threadFactory) { 261 | 262 | private final ReentrantLock pauseLock = new ReentrantLock(); 263 | private final Condition unpaused = pauseLock.newCondition(); 264 | private final ReentrantLock executeLock = new ReentrantLock(); 265 | private boolean isPaused = false; 266 | 267 | @Override 268 | protected void afterExecute(Runnable r, Throwable t) { 269 | super.afterExecute(r, t); 270 | pauseLock.lock(); 271 | try { 272 | while (isPaused) { 273 | unpaused.await(); 274 | } 275 | } catch (InterruptedException ignore) { 276 | 277 | } finally { 278 | pauseLock.unlock(); 279 | } 280 | } 281 | 282 | @Override 283 | public void execute(Runnable command) { 284 | /* 285 | * ThreadPoolExecutor first tries to run task in a corePool. If all threads are busy it tries to add task to the waiting queue. If it fails 286 | * it run task in maximumPool. 287 | * 288 | * We want corePool to be 0 and maximumPool to be MAX_WORKER_THREADS We need to change the order of the execution. First try corePool then 289 | * try maximumPool pool and only then store to the waiting queue. We can not do that because we would need access to the private methods. 290 | * 291 | * Instead we enlarge corePool to MAX_WORKER_THREADS before the execution and shrink it back to 0 after. It does pretty much what we need. 292 | * 293 | * While we changing the corePoolSize we need to stop running worker threads from accepting new tasks. 294 | */ 295 | 296 | // we need atomicity for the execute method. 297 | executeLock.lock(); 298 | try { 299 | 300 | pauseLock.lock(); 301 | try { 302 | isPaused = true; 303 | } finally { 304 | pauseLock.unlock(); 305 | } 306 | 307 | setCorePoolSize(MAX_WORKER_THREADS); 308 | super.execute(command); 309 | setCorePoolSize(0); 310 | 311 | pauseLock.lock(); 312 | try { 313 | isPaused = false; 314 | unpaused.signalAll(); 315 | } finally { 316 | pauseLock.unlock(); 317 | } 318 | } finally { 319 | executeLock.unlock(); 320 | } 321 | } 322 | }; 323 | } 324 | return executorService; 325 | } 326 | 327 | /** 328 | * Adds a {@code PropertyChangeListener} to the listener list. The listener is registered for all properties. The same listener object may be added 329 | * more than once, and will be called as many times as it is added. If {@code listener} is {@code null}, no exception is thrown and no action is 330 | * taken. 331 | *

332 | *

333 | * Note: This is merely a convenience wrapper. All work is delegated to {@code PropertyChangeSupport} from {@link #getPropertyChangeSupport}. 334 | * 335 | * @param listener the {@code PropertyChangeListener} to be added 336 | */ 337 | public final void addPropertyChangeListener(PropertyChangeListener listener) { 338 | getPropertyChangeSupport().addPropertyChangeListener(listener); 339 | } 340 | 341 | /** 342 | * {@inheritDoc} 343 | */ 344 | @Override 345 | public final boolean cancel(boolean mayInterruptIfRunning) { 346 | return future.cancel(mayInterruptIfRunning); 347 | } 348 | 349 | /** 350 | * Computes a result, or throws an exception if unable to do so. 351 | *

352 | *

353 | * Note that this method is executed only once. 354 | *

355 | *

356 | * Note: this method is executed in a background thread. 357 | * 358 | * @return the computed result 359 | * @throws Exception if unable to compute a result 360 | */ 361 | protected abstract T doInBackground() throws Exception; 362 | 363 | // PropertyChangeSupports methods START 364 | 365 | /** 366 | * Executed on the Event Dispatch Thread after the {@code doInBackground} method is finished. The default implementation does nothing. 367 | * Subclasses may override this method to perform completion actions on the Event Dispatch Thread. Note that you can query status inside the 368 | * implementation of this method to determine the result of this task or whether this task has been cancelled. 369 | * 370 | * @see #doInBackground 371 | * @see #isCancelled() 372 | * @see #get 373 | */ 374 | protected void done() { 375 | } 376 | 377 | // Future methods START 378 | 379 | /** 380 | * Invokes {@code done} on the EDT. 381 | */ 382 | private void doneEDT() { 383 | Runnable doDone = new Runnable() { 384 | @Override 385 | public void run() { 386 | done(); 387 | } 388 | }; 389 | if (SwingUtilities.isEventDispatchThread()) { 390 | doDone.run(); 391 | } else { 392 | doSubmit.add(doDone); 393 | } 394 | } 395 | 396 | /** 397 | * Schedules this {@code SwingWorker} for execution on a worker thread. There are a number of worker threads available. In the event 398 | * all worker threads are busy handling other {@code SwingWorkers} this {@code SwingWorker} is placed in a waiting queue. 399 | *

400 | *

401 | * Note: {@code SwingWorker} is only designed to be executed once. Executing a {@code SwingWorker} more than once will not result in invoking the 402 | * {@code doInBackground} method twice. 403 | */ 404 | public final void execute() { 405 | getWorkersExecutorService().execute(this); 406 | } 407 | 408 | /** 409 | * Reports a bound property update to any registered listeners. No event is fired if {@code old} and {@code new} are equal and non-null. 410 | *

411 | *

412 | * This {@code SwingWorker} will be the source for any generated events. 413 | *

414 | *

415 | * When called off the Event Dispatch Thread {@code PropertyChangeListeners} are notified asynchronously on the Event Dispatch Thread. 416 | *

417 | * Note: This is merely a convenience wrapper. All work is delegated to {@code PropertyChangeSupport} from {@link #getPropertyChangeSupport}. 418 | * 419 | * @param propertyName the programmatic name of the property that was changed 420 | * @param oldValue the old value of the property 421 | * @param newValue the new value of the property 422 | */ 423 | public final void firePropertyChange(String propertyName, Object oldValue, Object newValue) { 424 | getPropertyChangeSupport().firePropertyChange(propertyName, oldValue, newValue); 425 | } 426 | 427 | /** 428 | * {@inheritDoc} 429 | *

430 | * Note: calling {@code get} on the Event Dispatch Thread blocks all events, including repaints, from being processed until this 431 | * {@code SwingWorker} is complete. 432 | *

433 | *

434 | * When you want the {@code SwingWorker} to block on the Event Dispatch Thread we recommend that you use a modal dialog. 435 | *

436 | *

437 | * For example: 438 | *

439 | *

440 |      * class SwingWorkerCompletionWaiter implements PropertyChangeListener {
441 |      *   private JDialog dialog;
442 |      *
443 |      *   public SwingWorkerCompletionWaiter(JDialog dialog) {
444 |      *     this.dialog = dialog;
445 |      *   }
446 |      *
447 |      *   public void propertyChange(PropertyChangeEvent event) {
448 |      *     if ("state".equals(event.getPropertyName()) && SwingWorker.StateValue.DONE == event.getNewValue()) {
449 |      *       dialog.setVisible(false);
450 |      *       dialog.dispose();
451 |      *     }
452 |      *   }
453 |      * }
454 |      * JDialog dialog = new JDialog(owner, true);
455 |      * swingWorker.addPropertyChangeListener(new SwingWorkerCompletionWaiter(dialog));
456 |      * swingWorker.execute();
457 |      * // the dialog will be visible until the SwingWorker is done
458 |      * dialog.setVisible(true);
459 |      * 
460 | */ 461 | @Override 462 | public final T get() throws InterruptedException, ExecutionException { 463 | return future.get(); 464 | } 465 | 466 | /** 467 | * {@inheritDoc} 468 | *

469 | * Please refer to {@link #get} for more details. 470 | */ 471 | @Override 472 | public final T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { 473 | return future.get(timeout, unit); 474 | } 475 | 476 | /** 477 | * Returns the {@code progress} bound property. 478 | * 479 | * @return the progress bound property. 480 | */ 481 | public final int getProgress() { 482 | return progress; 483 | } 484 | 485 | /** 486 | * Sets the {@code progress} bound property. The value should be from 0 to 100. 487 | *

488 | *

489 | * Because {@code PropertyChangeListener}s are notified asynchronously on the Event Dispatch Thread multiple invocations to the 490 | * {@code setProgress} method might occur before any {@code PropertyChangeListeners} are invoked. For performance purposes all these invocations are 491 | * coalesced into one invocation with the last invocation argument only. 492 | *

493 | *

494 | * For example, the following invocations: 495 | *

496 | *

497 |      * setProgress(1);
498 |      * setProgress(2);
499 |      * setProgress(3);
500 |      * 
501 | *

502 | * might result in a single {@code PropertyChangeListener} notification with the value {@code 3}. 503 | * 504 | * @param progress the progress value to set 505 | * @throws IllegalArgumentException is value not from 0 to 100 506 | */ 507 | protected final void setProgress(int progress) { 508 | if (progress < 0 || progress > 100) { 509 | throw new IllegalArgumentException("the value should be from 0 to 100"); 510 | } 511 | if (this.progress == progress) { 512 | return; 513 | } 514 | int oldProgress = this.progress; 515 | this.progress = progress; 516 | if (!getPropertyChangeSupport().hasListeners("progress")) { 517 | return; 518 | } 519 | synchronized (this) { 520 | if (doNotifyProgressChange == null) { 521 | doNotifyProgressChange = new AccumulativeRunnable() { 522 | @Override 523 | public void run(List args) { 524 | firePropertyChange("progress", args.get(0), args.get(args.size() - 1)); 525 | } 526 | 527 | @Override 528 | protected void submit() { 529 | doSubmit.add(this); 530 | } 531 | }; 532 | } 533 | } 534 | doNotifyProgressChange.add(oldProgress, progress); 535 | } 536 | 537 | /** 538 | * Returns the {@code PropertyChangeSupport} for this {@code SwingWorker}. This method is used when flexible access to bound properties support is 539 | * needed. 540 | *

541 | * This {@code SwingWorker} will be the source for any generated events. 542 | *

543 | *

544 | * Note: The returned {@code PropertyChangeSupport} notifies any {@code PropertyChangeListener}s asynchronously on the Event Dispatch Thread 545 | * in the event that {@code firePropertyChange} or {@code fireIndexedPropertyChange} are called off the Event Dispatch Thread. 546 | * 547 | * @return {@code PropertyChangeSupport} for this {@code SwingWorker} 548 | */ 549 | public final PropertyChangeSupport getPropertyChangeSupport() { 550 | return propertyChangeSupport; 551 | } 552 | 553 | /** 554 | * Returns the {@code SwingWorker} state bound property. 555 | * 556 | * @return the current state 557 | */ 558 | public final StateValue getState() { 559 | /* 560 | * DONE is a special case to keep getState and isDone is sync 561 | */ 562 | if (isDone()) { 563 | return StateValue.DONE; 564 | } else { 565 | return state; 566 | } 567 | } 568 | 569 | // Future methods END 570 | 571 | /** 572 | * Sets this {@code SwingWorker} state bound property. 573 | * 574 | * @param the state state to set 575 | */ 576 | private void setState(StateValue state) { 577 | StateValue old = this.state; 578 | this.state = state; 579 | firePropertyChange("state", old, state); 580 | } 581 | 582 | /** 583 | * {@inheritDoc} 584 | */ 585 | @Override 586 | public final boolean isCancelled() { 587 | return future.isCancelled(); 588 | } 589 | 590 | /** 591 | * {@inheritDoc} 592 | */ 593 | @Override 594 | public final boolean isDone() { 595 | return future.isDone(); 596 | } 597 | 598 | /** 599 | * Receives data chunks from the {@code publish} method asynchronously on the Event Dispatch Thread. 600 | *

601 | *

602 | * Please refer to the {@link #publish} method for more details. 603 | * 604 | * @param chunks intermediate results to process 605 | * @see #publish 606 | */ 607 | protected void process(List chunks) { 608 | } 609 | 610 | // PropertyChangeSupports methods END 611 | 612 | /** 613 | * Sends data chunks to the {@link #process} method. This method is to be used from inside the {@code doInBackground} method to deliver intermediate 614 | * results for processing on the Event Dispatch Thread inside the {@code process} method. 615 | *

616 | *

617 | * Because the {@code process} method is invoked asynchronously on the Event Dispatch Thread multiple invocations to the {@code publish} 618 | * method might occur before the {@code process} method is executed. For performance purposes all these invocations are coalesced into one 619 | * invocation with concatenated arguments. 620 | *

621 | *

622 | * For example: 623 | *

624 | *

625 |      * publish("1");
626 |      * publish("2", "3");
627 |      * publish("4", "5", "6");
628 |      * 
629 | *

630 | * might result in: 631 | *

632 | *

633 |      * process("1", "2", "3", "4", "5", "6")
634 |      * 
635 | *

636 | *

637 | * Sample Usage. This code snippet loads some tabular data and updates {@code DefaultTableModel} with it. Note that it safe to mutate the 638 | * tableModel from inside the {@code process} method because it is invoked on the Event Dispatch Thread. 639 | *

640 | *

641 |      * class TableSwingWorker extends
642 |      *         SwingWorker<DefaultTableModel, Object[]> {
643 |      *     private final DefaultTableModel tableModel;
644 |      *
645 |      *     public TableSwingWorker(DefaultTableModel tableModel) {
646 |      *         this.tableModel = tableModel;
647 |      *     }
648 |      *
649 |      *     {@code @Override}
650 |      *     protected DefaultTableModel doInBackground() throws Exception {
651 |      *         for (Object[] row = loadData();
652 |      *                  ! isCancelled() && row != null;
653 |      *                  row = loadData()) {
654 |      *             publish((Object[]) row);
655 |      *         }
656 |      *         return tableModel;
657 |      *     }
658 |      *
659 |      *     {@code @Override}
660 |      *     protected void process(List<Object[]> chunks) {
661 |      *         for (Object[] row : chunks) {
662 |      *             tableModel.addRow(row);
663 |      *         }
664 |      *     }
665 |      * }
666 |      * 
667 | * 668 | * @param chunks intermediate results to process 669 | * @see #process 670 | */ 671 | protected final void publish(V... chunks) { 672 | synchronized (this) { 673 | if (doProcess == null) { 674 | doProcess = new AccumulativeRunnable() { 675 | @Override 676 | public void run(List args) { 677 | process(args); 678 | } 679 | 680 | @Override 681 | protected void submit() { 682 | doSubmit.add(this); 683 | } 684 | }; 685 | } 686 | } 687 | doProcess.add(chunks); 688 | } 689 | 690 | /** 691 | * Removes a {@code PropertyChangeListener} from the listener list. This removes a {@code PropertyChangeListener} that was registered for all 692 | * properties. If {@code listener} was added more than once to the same event source, it will be notified one less time after being removed. If 693 | * {@code listener} is {@code null}, or was never added, no exception is thrown and no action is taken. 694 | *

695 | *

696 | * Note: This is merely a convenience wrapper. All work is delegated to {@code PropertyChangeSupport} from {@link #getPropertyChangeSupport}. 697 | * 698 | * @param listener the {@code PropertyChangeListener} to be removed 699 | */ 700 | public final void removePropertyChangeListener(PropertyChangeListener listener) { 701 | getPropertyChangeSupport().removePropertyChangeListener(listener); 702 | } 703 | 704 | /** 705 | * Sets this {@code Future} to the result of computation unless it has been cancelled. 706 | */ 707 | @Override 708 | public final void run() { 709 | future.run(); 710 | } 711 | 712 | /** 713 | * Values for the {@code state} bound property. 714 | */ 715 | public enum StateValue { 716 | /** 717 | * Initial {@code SwingWorker} state. 718 | */ 719 | PENDING, /** 720 | * {@code SwingWorker} is {@code STARTED} before invoking {@code doInBackground}. 721 | */ 722 | STARTED, 723 | 724 | /** 725 | * {@code SwingWorker} is {@code DONE} after {@code doInBackground} method is finished. 726 | */ 727 | DONE 728 | } 729 | 730 | private static class DoSubmitAccumulativeRunnable extends AccumulativeRunnable implements ActionListener { 731 | private final static int DELAY = 1000 / 30; 732 | 733 | @Override 734 | public void actionPerformed(ActionEvent event) { 735 | run(); 736 | } 737 | 738 | @Override 739 | protected void run(List args) { 740 | int i = 0; 741 | try { 742 | for (Runnable runnable : args) { 743 | i++; 744 | runnable.run(); 745 | } 746 | } finally { 747 | if (i < args.size()) { 748 | /* 749 | * there was an exception schedule all the unhandled items for the next time 750 | */ 751 | Runnable argsTail[] = new Runnable[args.size() - i]; 752 | for (int j = 0; j < argsTail.length; j++) { 753 | argsTail[j] = args.get(i + j); 754 | } 755 | add(true, argsTail); 756 | } 757 | } 758 | } 759 | 760 | @Override 761 | protected void submit() { 762 | Timer timer = new Timer(DELAY, this); 763 | timer.setRepeats(false); 764 | timer.start(); 765 | } 766 | } 767 | 768 | private class SwingWorkerPropertyChangeSupport extends PropertyChangeSupport { 769 | private static final long serialVersionUID = 2409754725172747617L; 770 | 771 | SwingWorkerPropertyChangeSupport(Object source) { 772 | super(source); 773 | } 774 | 775 | @Override 776 | public void firePropertyChange(final PropertyChangeEvent evt) { 777 | if (SwingUtilities.isEventDispatchThread()) { 778 | super.firePropertyChange(evt); 779 | } else { 780 | doSubmit.add(new Runnable() { 781 | @Override 782 | public void run() { 783 | SwingWorkerPropertyChangeSupport.this.firePropertyChange(evt); 784 | } 785 | }); 786 | } 787 | } 788 | } 789 | } 790 | --------------------------------------------------------------------------------