diff --git a/.gitignore b/.gitignore index f99201f..71cf04c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,47 +1,167 @@ -#logs -*.log* +# Created by .ignore support plugin (hsz.mobi) +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 +.idea +# User-specific stuff: +.idea/workspace.xml +.idea/tasks.xml +.idea/dictionaries +.idea/vcs.xml +.idea/jsLibraryMappings.xml -# Bin folder -bin +# Sensitive or high-churn files: +.idea/dataSources.ids +.idea/dataSources.xml +.idea/dataSources.local.xml +.idea/sqlDataSources.xml +.idea/dynamic.xml +.idea/uiDesigner.xml + +# Gradle: +.idea/gradle.xml +.idea/libraries + +# Mongo Explorer plugin: +.idea/mongoSettings.xml + +## File-based project format: +*.iws + +## Plugin-specific files: + +# IntelliJ +/out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties +### Java template *.class # Mobile Tools for Java (J2ME) .mtj.tmp/ -# Log Files -*.log - # Package Files # +*.jar *.war *.ear # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* +### NotepadPP template +# Notepad++ backups # +*.bak +### NetBeans template +nbproject/private/ +build/ +nbbuild/ +dist/ +nbdist/ +nbactions.xml +.nb-gradle/ +### Eclipse template - -# Build folders -dist - -###################### -# Eclipse -###################### -*.pydevproject -.project .metadata -bin/** -tmp/** -tmp/**/* +bin/ +tmp/ *.tmp -*.bak *.swp *~.nib local.properties -.classpath .settings/ .loadpath -/src/main/resources/rebel.xml +.recommenders + +# Eclipse Core +.project + # External tool builders .externalToolBuilders/ + # Locally stored "Eclipse launch configurations" -*.launch -/target/ +*.launch + +# PyDev specific (Python IDE for Eclipse) +*.pydevproject + +# CDT-specific (C/C++ Development Tooling) +.cproject + +# JDT-specific (Eclipse Java Development Tools) +.classpath + +# Java annotation processor (APT) +.factorypath + +# PDT-specific (PHP Development Tools) +.buildpath + +# sbteclipse plugin +.target + +# Tern plugin +.tern-project + +# TeXlipse plugin +.texlipse + +# STS (Spring Tool Suite) +.springBeans + +# Code Recommenders +.recommenders/ +### Android template +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files + +# Generated files +gen/ +out/ + +# Gradle files +.gradle/ + +# Local configuration file (sdk path, etc) + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# Intellij +*.iml + +# Keystore files +*.jks +### Maven template +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties diff --git a/LICENSE b/LICENSE index a3bd98a..54bd737 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2015 Iurii Sergiichuk + Copyright 2014-2016 Iurii Sergiichuk Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 6ac5f22..2374209 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ AndroidScreencast ================= -[![Build Status DroneIO](https://drone.io/github.com/xSAVIKx/AndroidScreencast/status.png)](https://drone.io/github.com/xSAVIKx/AndroidScreencast/latest) - [![Build Status Travis-CI](https://travis-ci.org/xSAVIKx/AndroidScreencast.svg?branch=master)](https://travis-ci.org/xSAVIKx/AndroidScreencast) -AndroidScreencast - View and control your android device on PC. +AndroidScreencast - View and control your android iDevice on PC. This project gives you opportunity to use your phone even with broken screen. diff --git a/app.properties b/app.properties index cab3972..0b4cb24 100644 --- a/app.properties +++ b/app.properties @@ -1,3 +1,3 @@ -adb.path=/usr/bin/adb +adb.path=adb.exe default.window.width=1024 default.window.height=768 \ No newline at end of file diff --git a/lib/AdbWinApi.dll b/lib/AdbWinApi.dll new file mode 100644 index 0000000..b5586eb Binary files /dev/null and b/lib/AdbWinApi.dll differ diff --git a/lib/AdbWinUsbApi.dll b/lib/AdbWinUsbApi.dll new file mode 100644 index 0000000..0c9e00b Binary files /dev/null and b/lib/AdbWinUsbApi.dll differ diff --git a/lib/adb.exe b/lib/adb.exe new file mode 100644 index 0000000..24b5a95 Binary files /dev/null and b/lib/adb.exe differ diff --git a/lib/fastboot.exe b/lib/fastboot.exe new file mode 100644 index 0000000..0e47b3d Binary files /dev/null and b/lib/fastboot.exe differ diff --git a/pom.xml b/pom.xml index 1dc8e0a..c65132b 100644 --- a/pom.xml +++ b/pom.xml @@ -1,114 +1,107 @@ - - 4.0.0 - AndroidScreencast - AndroidScreencast - 0.0.5.1S - Android Screencast - - 4.2.0.RELEASE - 24.3.1 - 1.2.17 - - - - com.android.tools.ddms - ddmlib - ${ddmlib-version} - - - log4j - log4j - ${log4j-version} - - - org.springframework - spring-core - ${spring-version} - - - org.springframework - spring-beans - ${spring-version} - - - org.springframework - spring-context - ${spring-version} - - - - src - - - src - - **/*.java - - - - - - maven-compiler-plugin - 3.3 - - 1.7 - 1.7 - - - - org.apache.maven.plugins - maven-jar-plugin - - - - com.github.xsavikx.android.screencast.Main - - - - - - org.dstovall - onejar-maven-plugin - 1.4.5 - - - - androidscreencast-${project.version}.jar - onejar - - - one-jar - - - - - - + + 4.0.0 + com.github.xsavikx + androidscreencast + 0.0.6 + Android Screencast + + 4.2.6.RELEASE + 25.1.0 + 1.2.17 + com.github.xsavikx.androidscreencast.Main + 1.7 + + + + com.android.tools.ddms + ddmlib + ${ddmlib-version} + + + log4j + log4j + ${log4j-version} + + + org.springframework + spring-core + ${spring-version} + + + org.springframework + spring-beans + ${spring-version} + + + org.springframework + spring-context + ${spring-version} + + + + + + maven-compiler-plugin + 3.5.1 + + ${jdk-version} + ${jdk-version} + + + + org.apache.maven.plugins + maven-jar-plugin + 2.6 + + + + com.github.xsavikx.androidscreencast.Main + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.4.3 + + + + shade + + + true + + + ${main-class} + + + + + + + + + http://xsavikx.github.io/AndroidScreencast + + https://travis-ci.org/xSAVIKx/AndroidScreencast + Travis-CI + + + GitHub + https://github.com/xSAVIKx/AndroidScreencast/issues + + AndroidScreencast - View and control your android iDevice on PC. - - - onejar-maven-plugin.googlecode.com - http://onejar-maven-plugin.googlecode.com/svn/mavenrepo - - - http://xsavikx.github.io/AndroidScreencast - - https://drone.io/github.com/xSAVIKx/AndroidScreencast - DroneIO - - - GitHub - https://github.com/xSAVIKx/AndroidScreencast/issues - - AndroidScreencast - View and control your android device on PC. + This project gives you opportunity to use your phone even with broken screen. -This project gives you opportunity to use your phone even with broken screen. - -Features: - No client needed - Support for Tap and Swipe gestures - Write messages using PC keyboard - Support for landscape mode - Browse your phone files on PC + Features: + No client needed + Support for Tap and Swipe gestures + Write messages using PC keyboard + Support for landscape mode + Browse your phone files on PC + \ No newline at end of file diff --git a/src/com/github/xsavikx/android/screencast/Main.java b/src/com/github/xsavikx/android/screencast/Main.java deleted file mode 100644 index 3b5c449..0000000 --- a/src/com/github/xsavikx/android/screencast/Main.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.github.xsavikx.android.screencast; - -import java.util.Arrays; - -import org.apache.log4j.Logger; - -import com.github.xsavikx.android.screencast.app.AndroidScreencastApplication; -import com.github.xsavikx.android.screencast.app.Application; -import com.github.xsavikx.android.screencast.spring.config.ApplicationContextProvider; - -public class Main { - private static final Logger LOGGER = Logger.getLogger(Main.class); - - public static void main(String args[]) { - LOGGER.debug("main(String[] args=" + Arrays.toString(args) + ") - start"); - Application application = ApplicationContextProvider.getApplicationContext() - .getBean(AndroidScreencastApplication.class); - application.init(); - application.start(); - - LOGGER.debug("main(String[] args=" + Arrays.toString(args) + ") - end"); - } - -} diff --git a/src/com/github/xsavikx/android/screencast/api/AndroidDevice.java b/src/com/github/xsavikx/android/screencast/api/AndroidDevice.java deleted file mode 100644 index 9223d6b..0000000 --- a/src/com/github/xsavikx/android/screencast/api/AndroidDevice.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.github.xsavikx.android.screencast.api; - -import java.io.File; -import java.util.List; - -import com.github.xsavikx.android.screencast.api.file.FileInfo; - -public interface AndroidDevice { - String executeCommand(String command); - - List list(String path); - - void openUrl(String url); - - void pullFile(String remoteFrom, File localTo); - - void pushFile(File localFrom, String remoteTo); -} diff --git a/src/com/github/xsavikx/android/screencast/api/AndroidDeviceImpl.java b/src/com/github/xsavikx/android/screencast/api/AndroidDeviceImpl.java deleted file mode 100644 index 14cfbb5..0000000 --- a/src/com/github/xsavikx/android/screencast/api/AndroidDeviceImpl.java +++ /dev/null @@ -1,161 +0,0 @@ -package com.github.xsavikx.android.screencast.api; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.lang.reflect.Method; -import java.util.List; -import java.util.Vector; - -import org.apache.log4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import com.android.ddmlib.IDevice; -import com.android.ddmlib.SyncService; -import com.android.ddmlib.SyncService.ISyncProgressMonitor; -import com.github.xsavikx.android.screencast.api.file.FileInfo; -import com.github.xsavikx.android.screencast.api.injector.OutputStreamShellOutputReceiver; - -@Component -public class AndroidDeviceImpl implements AndroidDevice { - /** - * Logger for this class - */ - private static final Logger logger = Logger.getLogger(AndroidDeviceImpl.class); - @Autowired(required = false) - private IDevice device; - - public AndroidDeviceImpl() { - - } - - public AndroidDeviceImpl(IDevice device) { - this.device = device; - } - - @Override - public String executeCommand(String cmd) { - if (logger.isDebugEnabled()) { - logger.debug("executeCommand(String) - start"); - } - - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - try { - device.executeShellCommand(cmd, new OutputStreamShellOutputReceiver(bos)); - String returnString = new String(bos.toByteArray(), "UTF-8"); - if (logger.isDebugEnabled()) { - logger.debug("executeCommand(String) - end"); - } - return returnString; - } catch (Exception ex) { - logger.error("executeCommand(String)", ex); - - throw new RuntimeException(ex); - } - } - - @Override - public List list(String path) { - if (logger.isDebugEnabled()) { - logger.debug("list(String) - start"); - } - - try { - String s = executeCommand("ls -l " + path); - String[] entries = s.split("\r\n"); - Vector liste = new Vector(); - for (int i = 0; i < entries.length; i++) { - String[] data = entries[i].split(" "); - if (data.length < 4) - continue; - /* - * for(int j=0; j eventsWithCharacters = fillEventsWithCharacters(); - private static Set eventsWithKeyCodes = fillEventsWithKeyCodes(); - - private InputKeyEvent(int code, String description){ - this.code = code; - this.description = description; - } - - private InputKeyEvent(int code, String description, char character){ - this(code,description); - this.characterToReplace = character; - } - - private InputKeyEvent(int code, String description, int keyCode){ - this(code,description); - this.keyCode = keyCode; - } - - public int getKeyCode() { - return keyCode; - } - - public int getCode(){ - return code; - } - - public String getDescription() { - return description; - } - - public char getCharacterToReplace() { - return characterToReplace; - } - - public static InputKeyEvent getByCharacter(char c){ - for(InputKeyEvent e : eventsWithCharacters){ - if (e.characterToReplace == c){ - return e; - } - } - return null; - } - - public static InputKeyEvent getByKeyCode(int keyCode){ - for(InputKeyEvent e : eventsWithKeyCodes){ - if (e.keyCode == keyCode){ - return e; - } - } - return null; - } - - public static InputKeyEvent getByCharacterOrKeyCode(char c, int keyCode){ - InputKeyEvent e = getByCharacter(c); - if (e==null) - e=getByKeyCode(keyCode); - return e; - } - - private static Set fillEventsWithCharacters(){ - Set eventsWithCharacters = EnumSet.allOf(InputKeyEvent.class); - for(InputKeyEvent e : values()){ - if (e.characterToReplace =='\u0000'){ - eventsWithCharacters.remove(e); - } - } - return eventsWithCharacters; - } - private static Set fillEventsWithKeyCodes(){ - Set eventsWithKeyCodes = EnumSet.allOf(InputKeyEvent.class); - for(InputKeyEvent e : values()){ - if (e.keyCode == 0){ - eventsWithKeyCodes.remove(e); - } - } - return eventsWithKeyCodes; - } - - -} diff --git a/src/com/github/xsavikx/android/screencast/api/injector/KeyCodeConverter.java b/src/com/github/xsavikx/android/screencast/api/injector/KeyCodeConverter.java deleted file mode 100644 index 13d5a00..0000000 --- a/src/com/github/xsavikx/android/screencast/api/injector/KeyCodeConverter.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.github.xsavikx.android.screencast.api.injector; - -import java.awt.event.KeyEvent; - -import org.apache.log4j.Logger; - -public class KeyCodeConverter { - private static final Logger LOGGER = Logger.getLogger(KeyCodeConverter.class); - - public static int getKeyCode(KeyEvent e) { - LOGGER.debug("getKeyCode(KeyEvent e=" + e + ") - start"); - int code = InputKeyEvent.KEYCODE_UNKNOWN.getCode(); - char c = e.getKeyChar(); - int keyCode = e.getKeyCode(); - InputKeyEvent inputKeyEvent = InputKeyEvent.getByCharacterOrKeyCode(Character.toLowerCase(c), keyCode); - if (inputKeyEvent != null) { - code = inputKeyEvent.getCode(); - } - LOGGER.debug("getKeyCode(KeyEvent e=" + e + ") - end"); - return code; - } - -} diff --git a/src/com/github/xsavikx/android/screencast/api/injector/MultiLineReceiverPrinter.java b/src/com/github/xsavikx/android/screencast/api/injector/MultiLineReceiverPrinter.java deleted file mode 100644 index e1d8ee9..0000000 --- a/src/com/github/xsavikx/android/screencast/api/injector/MultiLineReceiverPrinter.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.github.xsavikx.android.screencast.api.injector; - -import org.springframework.stereotype.Component; - -import com.android.ddmlib.MultiLineReceiver; - -@Component -public class MultiLineReceiverPrinter extends MultiLineReceiver { - - @Override - public boolean isCancelled() { - return false; - } - - @Override - public void processNewLines(String[] arg0) { - for (String elem : arg0) { - System.out.println(elem); - } - } - -} diff --git a/src/com/github/xsavikx/android/screencast/api/injector/OutputStreamShellOutputReceiver.java b/src/com/github/xsavikx/android/screencast/api/injector/OutputStreamShellOutputReceiver.java deleted file mode 100644 index 52cdcd8..0000000 --- a/src/com/github/xsavikx/android/screencast/api/injector/OutputStreamShellOutputReceiver.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.github.xsavikx.android.screencast.api.injector; - -import java.io.IOException; -import java.io.OutputStream; - -import com.android.ddmlib.IShellOutputReceiver; - -public class OutputStreamShellOutputReceiver implements IShellOutputReceiver { - - private OutputStream os; - - public OutputStreamShellOutputReceiver(OutputStream os) { - this.os = os; - } - - @Override - public void addOutput(byte[] buf, int off, int len) { - try { - os.write(buf, off, len); - } catch (IOException ex) { - throw new RuntimeException(ex); - } - } - - @Override - public void flush() { - try { - os.flush(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - @Override - public boolean isCancelled() { - return false; - } - -} diff --git a/src/com/github/xsavikx/android/screencast/api/injector/ScreenCaptureThread.java b/src/com/github/xsavikx/android/screencast/api/injector/ScreenCaptureThread.java deleted file mode 100644 index e8e9ac4..0000000 --- a/src/com/github/xsavikx/android/screencast/api/injector/ScreenCaptureThread.java +++ /dev/null @@ -1,195 +0,0 @@ -package com.github.xsavikx.android.screencast.api.injector; - -import java.awt.Dimension; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.IOException; -import java.util.concurrent.TimeUnit; - -import javax.swing.SwingUtilities; - -import org.apache.log4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import com.android.ddmlib.AdbCommandRejectedException; -import com.android.ddmlib.IDevice; -import com.android.ddmlib.RawImage; -import com.android.ddmlib.TimeoutException; -import com.github.xsavikx.android.screencast.api.recording.QuickTimeOutputStream; - -@Component -public class ScreenCaptureThread extends Thread { - private static final Logger LOGGER = Logger.getLogger(ScreenCaptureThread.class); - - public interface ScreenCaptureListener { - public void handleNewImage(Dimension size, BufferedImage image, boolean landscape); - } - - private BufferedImage image; - private Dimension size; - @Autowired - private IDevice device; - private QuickTimeOutputStream qos = null; - private boolean landscape = false; - - private ScreenCaptureListener listener = null; - - public ScreenCaptureThread() { - super("Screen capture"); - image = null; - size = new Dimension(); - } - - public void display(RawImage rawImage) { - int width2 = landscape ? rawImage.height : rawImage.width; - int height2 = landscape ? rawImage.width : rawImage.height; - if (image == null) { - image = new BufferedImage(width2, height2, BufferedImage.TYPE_INT_RGB); - size.setSize(image.getWidth(), image.getHeight()); - } else { - if (image.getHeight() != height2 || image.getWidth() != width2) { - image = new BufferedImage(width2, height2, BufferedImage.TYPE_INT_RGB); - size.setSize(image.getWidth(), image.getHeight()); - } - } - int index = 0; - int indexInc = rawImage.bpp >> 3; - for (int y = 0; y < rawImage.height; y++) { - for (int x = 0; x < rawImage.width; x++, index += indexInc) { - int value = rawImage.getARGB(index); - if (landscape) - image.setRGB(y, rawImage.width - x - 1, value); - else - image.setRGB(x, y, value); - } - } - - try { - if (qos != null) - qos.writeFrame(image, 10); - } catch (IOException e) { - LOGGER.error("display(RawImage)", e); - - throw new RuntimeException(e); - } - - if (listener != null) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - listener.handleNewImage(size, image, landscape); - } - }); - } - - } - - private boolean fetchImage() throws IOException { - - if (device == null) { - // device not ready - try { - Thread.sleep(100); - } catch (InterruptedException e) { - LOGGER.error("fetchImage()", e); - return false; - } - return true; - } - RawImage rawImage = null; - synchronized (device) { - try { - rawImage = device.getScreenshot(5, TimeUnit.SECONDS); - } catch (TimeoutException | AdbCommandRejectedException e) { - LOGGER.error("fetchImage()", e); - } - } - if (rawImage != null) { - display(rawImage); - } else { - LOGGER.info("failed getting screenshot through ADB ok"); - } - try { - Thread.sleep(5); - } catch (InterruptedException e) { - LOGGER.error("fetchImage()", e); - return false; - } - return true; - } - - public ScreenCaptureListener getListener() { - return listener; - } - - public Dimension getPreferredSize() { - return size; - } - - @Override - public void run() { - do { - try { - boolean ok = fetchImage(); - if (!ok) - break; - } catch (java.nio.channels.ClosedByInterruptException ciex) { - LOGGER.error("run()", ciex); - - break; - } catch (IOException e) { - LOGGER.error("run()", e); - LOGGER.error((new StringBuilder()).append("Exception fetching image: ").append(e.toString()).toString()); - } - - } while (true); - } - - public void setListener(ScreenCaptureListener listener) { - this.listener = listener; - } - - public void startRecording(File f) { - LOGGER.debug("startRecording(File f=" + f + ") - start"); - - try { - if (!f.getName().toLowerCase().endsWith(".mov")) - f = new File(f.getAbsolutePath() + ".mov"); - qos = new QuickTimeOutputStream(f, QuickTimeOutputStream.VideoFormat.JPG); - } catch (IOException e) { - LOGGER.error("startRecording(File)", e); - - throw new RuntimeException(e); - } - qos.setVideoCompressionQuality(1f); - qos.setTimeScale(30); // 30 fps - - LOGGER.debug("startRecording(File f=" + f + ") - end"); - } - - public void stopRecording() { - LOGGER.debug("stopRecording() - start"); - - try { - QuickTimeOutputStream o = qos; - qos = null; - o.close(); - } catch (IOException e) { - LOGGER.error("stopRecording()", e); - - throw new RuntimeException(e); - } - - LOGGER.debug("stopRecording() - end"); - } - - public void toogleOrientation() { - LOGGER.debug("toogleOrientation() - start"); - - landscape = !landscape; - - LOGGER.debug("toogleOrientation() - end"); - } - -} diff --git a/src/com/github/xsavikx/android/screencast/api/recording/DataAtomOutputStream.java b/src/com/github/xsavikx/android/screencast/api/recording/DataAtomOutputStream.java deleted file mode 100644 index 5552fef..0000000 --- a/src/com/github/xsavikx/android/screencast/api/recording/DataAtomOutputStream.java +++ /dev/null @@ -1,341 +0,0 @@ -package com.github.xsavikx.android.screencast.api.recording; - -import java.io.FilterOutputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.io.UnsupportedEncodingException; -import java.util.Calendar; -import java.util.Date; -import java.util.GregorianCalendar; - -public class DataAtomOutputStream extends FilterOutputStream { - - protected static final long MAC_TIMESTAMP_EPOCH = new GregorianCalendar(1904, Calendar.JANUARY, 1).getTimeInMillis(); - /** - * The number of bytes written to the data output stream so far. If this counter overflows, it will be wrapped to Integer.MAX_VALUE. - */ - protected long written; - - public DataAtomOutputStream(OutputStream out) { - super(out); - } - - /** - * Increases the written counter by the specified value until it reaches Long.MAX_VALUE. - */ - protected void incCount(int value) { - long temp = written + value; - if (temp < 0) { - temp = Long.MAX_VALUE; - } - written = temp; - } - - /** - * Returns the current value of the counter written, the number of bytes written to this data output stream so far. If the counter - * overflows, it will be wrapped to Integer.MAX_VALUE. - * - * @return the value of the written field. - * @see java.io.DataOutputStream#written - */ - public final long size() { - return written; - } - - /** - * Writes len bytes from the specified byte array starting at offset off to the underlying output stream. If no exception - * is thrown, the counter written is incremented by len . - * - * @param b - * the data. - * @param off - * the start offset in the data. - * @param len - * the number of bytes to write. - * @exception IOException - * if an I/O error occurs. - * @see java.io.FilterOutputStream#out - */ - @Override - public synchronized void write(byte b[], int off, int len) throws IOException { - out.write(b, off, len); - incCount(len); - } - - /** - * Writes the specified byte (the low eight bits of the argument b) to the underlying output stream. If no exception is thrown, the - * counter written is incremented by 1 . - *

- * Implements the write method of OutputStream. - * - * @param b - * the byte to be written. - * @exception IOException - * if an I/O error occurs. - * @see java.io.FilterOutputStream#out - */ - @Override - public synchronized void write(int b) throws IOException { - out.write(b); - incCount(1); - } - - /** - * Writes a BCD2 to the underlying output stream. - * - * @param v - * an int to be written. - * @exception IOException - * if an I/O error occurs. - * @see java.io.FilterOutputStream#out - */ - public void writeBCD2(int v) throws IOException { - out.write(((v % 100 / 10) << 4) | (v % 10)); - incCount(1); - } - - /** - * Writes a BCD4 to the underlying output stream. - * - * @param v - * an int to be written. - * @exception IOException - * if an I/O error occurs. - * @see java.io.FilterOutputStream#out - */ - public void writeBCD4(int v) throws IOException { - out.write(((v % 10000 / 1000) << 4) | (v % 1000 / 100)); - out.write(((v % 100 / 10) << 4) | (v % 10)); - incCount(2); - } - - /** - * Writes out a byte to the underlying output stream as a 1-byte value. If no exception is thrown, the counter written is - * incremented by 1. - * - * @param v - * a byte value to be written. - * @exception IOException - * if an I/O error occurs. - * @see java.io.FilterOutputStream#out - */ - public final void writeByte(int v) throws IOException { - out.write(v); - incCount(1); - } - - /** - * Writes 32-bit fixed-point number divided as 16.16. - * - * @param f - * an int to be written. - * @exception IOException - * if an I/O error occurs. - * @see java.io.FilterOutputStream#out - */ - public void writeFixed16D16(double f) throws IOException { - double v = (f >= 0) ? f : -f; - - int wholePart = (int) v; - int fractionPart = (int) ((v - wholePart) * 65536); - int t = (wholePart << 16) + fractionPart; - - if (f < 0) { - t = t - 1; - } - writeInt(t); - } - - /** - * Writes 32-bit fixed-point number divided as 2.30. - * - * @param f - * an int to be written. - * @exception IOException - * if an I/O error occurs. - * @see java.io.FilterOutputStream#out - */ - public void writeFixed2D30(double f) throws IOException { - double v = (f >= 0) ? f : -f; - - int wholePart = (int) v; - int fractionPart = (int) ((v - wholePart) * 1073741824); - int t = (wholePart << 30) + fractionPart; - - if (f < 0) { - t = t - 1; - } - writeInt(t); - } - - /** - * Writes 16-bit fixed-point number divided as 8.8. - * - * @param f - * an int to be written. - * @exception IOException - * if an I/O error occurs. - * @see java.io.FilterOutputStream#out - */ - public void writeFixed8D8(float f) throws IOException { - float v = (f >= 0) ? f : -f; - - int wholePart = (int) v; - int fractionPart = (int) ((v - wholePart) * 256); - int t = (wholePart << 8) + fractionPart; - - if (f < 0) { - t = t - 1; - } - writeUShort(t); - } - - /** - * Writes an int to the underlying output stream as four bytes, high byte first. If no exception is thrown, the counter - * written is incremented by 4. - * - * @param v - * an int to be written. - * @exception IOException - * if an I/O error occurs. - * @see java.io.FilterOutputStream#out - */ - public void writeInt(int v) throws IOException { - out.write((v >>> 24) & 0xff); - out.write((v >>> 16) & 0xff); - out.write((v >>> 8) & 0xff); - out.write((v >>> 0) & 0xff); - incCount(4); - } - - public void writeLong(long v) throws IOException { - out.write((int) (v >>> 56) & 0xff); - out.write((int) (v >>> 48) & 0xff); - out.write((int) (v >>> 40) & 0xff); - out.write((int) (v >>> 32) & 0xff); - out.write((int) (v >>> 24) & 0xff); - out.write((int) (v >>> 16) & 0xff); - out.write((int) (v >>> 8) & 0xff); - out.write((int) (v >>> 0) & 0xff); - incCount(8); - } - - /** - * Writes a 32-bit Mac timestamp (seconds since 1902). - * - * @param date - * @throws java.io.IOException - */ - public void writeMacTimestamp(Date date) throws IOException { - long millis = date.getTime(); - long qtMillis = millis - MAC_TIMESTAMP_EPOCH; - long qtSeconds = qtMillis / 1000; - writeUInt(qtSeconds); - } - - /** - * Writes a Pascal String. - * - * @param s - * @throws java.io.IOException - */ - public void writePString(String s) throws IOException { - if (s.length() > 0xffff) { - throw new IllegalArgumentException("String too long for PString"); - } - if (s.length() < 256) { - out.write(s.length()); - } else { - out.write(0); - writeShort(s.length()); // increments +2 - } - for (int i = 0; i < s.length(); i++) { - out.write(s.charAt(i)); - } - incCount(1 + s.length()); - } - - /** - * Writes a Pascal String padded to the specified fixed size in bytes - * - * @param s - * @param length - * the fixed size in bytes - * @throws java.io.IOException - */ - public void writePString(String s, int length) throws IOException { - if (s.length() > length) { - throw new IllegalArgumentException("String too long for PString of length " + length); - } - if (s.length() < 256) { - out.write(s.length()); - } else { - out.write(0); - writeShort(s.length()); // increments +2 - } - for (int i = 0; i < s.length(); i++) { - out.write(s.charAt(i)); - } - - // write pad bytes - for (int i = 1 + s.length(); i < length; i++) { - out.write(0); - } - - incCount(length); - } - - /** - * Writes a signed 16 bit integer value. - * - * @param v - * The value - * @throws java.io.IOException - */ - public void writeShort(int v) throws IOException { - out.write((v >> 8) & 0xff); - out.write((v >>> 0) & 0xff); - incCount(2); - } - - /** - * Writes an Atom Type identifier (4 bytes). - * - * @param s - * A string with a length of 4 characters. - */ - public void writeType(String s) throws IOException { - if (s.length() != 4) { - throw new IllegalArgumentException("type string must have 4 characters"); - } - - try { - out.write(s.getBytes("ASCII"), 0, 4); - incCount(4); - } catch (UnsupportedEncodingException e) { - throw new InternalError(e.toString()); - } - } - - /** - * Writes an unsigned 32 bit integer value. - * - * @param v - * The value - * @throws java.io.IOException - */ - public void writeUInt(long v) throws IOException { - out.write((int) ((v >>> 24) & 0xff)); - out.write((int) ((v >>> 16) & 0xff)); - out.write((int) ((v >>> 8) & 0xff)); - out.write((int) ((v >>> 0) & 0xff)); - incCount(4); - } - - public void writeUShort(int v) throws IOException { - out.write((v >> 8) & 0xff); - out.write((v >>> 0) & 0xff); - incCount(2); - } - -} diff --git a/src/com/github/xsavikx/android/screencast/api/recording/FilterImageOutputStream.java b/src/com/github/xsavikx/android/screencast/api/recording/FilterImageOutputStream.java deleted file mode 100644 index 7baaaac..0000000 --- a/src/com/github/xsavikx/android/screencast/api/recording/FilterImageOutputStream.java +++ /dev/null @@ -1,89 +0,0 @@ -package com.github.xsavikx.android.screencast.api.recording; - -import java.io.FilterOutputStream; -import java.io.IOException; - -import javax.imageio.stream.ImageOutputStream; - -public class FilterImageOutputStream extends FilterOutputStream { - private ImageOutputStream imgOut; - - public FilterImageOutputStream(ImageOutputStream iOut) { - super(null); - this.imgOut = iOut; - } - - /** - * Closes this output stream and releases any system resources associated with the stream. - *

- * The close method of FilterOutputStream calls its flush method, and then calls the close - * method of its underlying output stream. - * - * @exception IOException - * if an I/O error occurs. - * @see java.io.FilterOutputStream#flush() - * @see java.io.FilterOutputStream#out - */ - @Override - public void close() throws IOException { - flush(); - imgOut.close(); - } - - /** - * Flushes this output stream and forces any buffered output bytes to be written out to the stream. - *

- * The flush method of FilterOutputStream calls the flush method of its underlying output stream. - * - * @exception IOException - * if an I/O error occurs. - * @see java.io.FilterOutputStream#out - */ - @Override - public void flush() { - // System.err.println(this+" discarded flush"); - // imgOut.flush(); - } - - /** - * Writes len bytes from the specified byte array starting at offset off to this output stream. - *

- * The write method of FilterOutputStream calls the write method of one argument on each byte to - * output. - *

- * Note that this method does not call the write method of its underlying input stream with the same arguments. Subclasses of - * FilterOutputStream should provide a more efficient implementation of this method. - * - * @param b - * the data. - * @param off - * the start offset in the data. - * @param len - * the number of bytes to write. - * @exception IOException - * if an I/O error occurs. - * @see java.io.FilterOutputStream#write(int) - */ - @Override - public void write(byte b[], int off, int len) throws IOException { - imgOut.write(b, off, len); - } - - /** - * Writes the specified byte to this output stream. - *

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

- * Implements the abstract write method of OutputStream. - * - * @param b - * the byte. - * @exception IOException - * if an I/O error occurs. - */ - @Override - public void write(int b) throws IOException { - imgOut.write(b); - } -} diff --git a/src/com/github/xsavikx/android/screencast/api/recording/QuickTimeOutputStream.java b/src/com/github/xsavikx/android/screencast/api/recording/QuickTimeOutputStream.java deleted file mode 100644 index 3da662d..0000000 --- a/src/com/github/xsavikx/android/screencast/api/recording/QuickTimeOutputStream.java +++ /dev/null @@ -1,1632 +0,0 @@ -package com.github.xsavikx.android.screencast.api.recording; - -import java.awt.image.BufferedImage; -import java.awt.image.WritableRaster; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.Date; -import java.util.LinkedList; - -import javax.imageio.IIOImage; -import javax.imageio.ImageIO; -import javax.imageio.ImageWriteParam; -import javax.imageio.ImageWriter; -import javax.imageio.stream.FileImageOutputStream; -import javax.imageio.stream.ImageOutputStream; -import javax.imageio.stream.MemoryCacheImageOutputStream; - -public class QuickTimeOutputStream { - - /** - * Atom base class. - */ - private abstract class Atom { - - /** - * The type of the atom. A String with the length of 4 characters. - */ - protected String type; - /** - * The offset of the atom relative to the start of the ImageOutputStream. - */ - protected long offset; - - /** - * Creates a new Atom at the current position of the ImageOutputStream. - * - * @param type - * The type of the atom. A string with a length of 4 characters. - */ - public Atom(String type) throws IOException { - this.type = type; - offset = out.getStreamPosition(); - } - - /** - * Writes the atom to the ImageOutputStream and disposes it. - */ - public abstract void finish() throws IOException; - - /** - * Returns the size of the atom including the size of the atom header. - * - * @return The size of the atom. - */ - public abstract long size(); - } - - /** - * A CompositeAtom contains an ordered list of Atoms. - */ - private class CompositeAtom extends Atom { - - private LinkedList children; - private boolean finished; - - /** - * Creates a new CompositeAtom at the current position of the ImageOutputStream. - * - * @param type - * The type of the atom. - */ - public CompositeAtom(String type) throws IOException { - super(type); - out.writeLong(0); // make room for the atom header - children = new LinkedList(); - } - - public void add(Atom child) throws IOException { - if (children.size() > 0) { - children.getLast().finish(); - } - children.add(child); - } - - /** - * Writes the atom and all its children to the ImageOutputStream and disposes of all resources held by the atom. - * - * @throws java.io.IOException - */ - @Override - public void finish() throws IOException { - if (!finished) { - if (size() > 0xffffffffL) { - throw new IOException("CompositeAtom \"" + type + "\" is too large: " + size()); - } - - long pointer = out.getStreamPosition(); - out.seek(offset); - try (DataAtomOutputStream headerData = new DataAtomOutputStream(new FilterImageOutputStream(out));) { - - headerData.writeInt((int) size()); - headerData.writeType(type); - for (Atom child : children) { - child.finish(); - } - out.seek(pointer); - finished = true; - } - } - } - - @Override - public long size() { - long length = 8; - for (Atom child : children) { - length += child.size(); - } - return length; - } - } - - /** - * Data Atom. - */ - protected class DataAtom extends Atom { - - private DataAtomOutputStream data; - private boolean finished; - - /** - * Creates a new DataAtom at the current position of the ImageOutputStream. - * - * @param type - * The type of the atom. - */ - public DataAtom(String name) throws IOException { - super(name); - out.writeLong(0); // make room for the atom header - data = new DataAtomOutputStream(new FilterImageOutputStream(out)); - } - - @Override - public void finish() throws IOException { - if (!finished) { - long sizeBefore = size(); - - if (size() > 0xffffffffL) { - throw new IOException("DataAtom \"" + type + "\" is too large: " + size()); - } - - long pointer = out.getStreamPosition(); - out.seek(offset); - try (DataAtomOutputStream headerData = new DataAtomOutputStream(new FilterImageOutputStream(out));) { - headerData.writeUInt(size()); - headerData.writeType(type); - out.seek(pointer); - finished = true; - long sizeAfter = size(); - if (sizeBefore != sizeAfter) { - System.err.println("size mismatch " + sizeBefore + ".." + sizeAfter); - } - } - } - } - - /** - * Returns the offset of this atom to the beginning of the random access file - * - * @return - */ - public long getOffset() { - return offset; - } - - public DataAtomOutputStream getOutputStream() { - if (finished) { - throw new IllegalStateException("DataAtom is finished"); - } - return data; - } - - @Override - public long size() { - return 8 + data.size(); - } - } - - /** - * QuickTime stores media data in samples. A sample is a single element in a sequence of time-ordered data. Samples are stored in the mdat atom. - */ - private static class Sample { - - /** - * Offset of the sample relative to the start of the QuickTime file. - */ - long offset; - /** Data length of the sample. */ - long length; - /** - * The duration of the sample in time scale units. - */ - int duration; - - /** - * Creates a new sample. - * - * @param duration - * @param offset - * @param length - */ - public Sample(int duration, long offset, long length) { - this.duration = duration; - this.offset = offset; - this.length = length; - } - } - - /** - * The states of the movie output stream. - */ - private static enum States { - - STARTED, FINISHED, CLOSED; - } - - /** - * Supported video formats. - */ - public static enum VideoFormat { - - RAW, JPG, PNG; - } - - /** - * WideDataAtom can grow larger then 4 gigabytes. - */ - protected class WideDataAtom extends Atom { - - private DataAtomOutputStream data; - private boolean finished; - - /** - * Creates a new DataAtom at the current position of the ImageOutputStream. - * - * @param type - * The type of the atom. - */ - public WideDataAtom(String type) throws IOException { - super(type); - out.writeLong(0); // make room for the atom header - out.writeLong(0); // make room for the atom header - data = new DataAtomOutputStream(new FilterImageOutputStream(out)); - } - - @Override - public void finish() throws IOException { - if (!finished) { - long pointer = out.getStreamPosition(); - out.seek(offset); - try (DataAtomOutputStream headerData = new DataAtomOutputStream(new FilterImageOutputStream(out));) { - - if (size() <= 0xffffffffL) { - headerData.writeUInt(8); - headerData.writeType("wide"); - headerData.writeUInt(size()); - headerData.writeType(type); - } else { - headerData.writeInt(1); // special value for extended - // size - // atoms - headerData.writeType(type); - headerData.writeLong(size()); - } - - out.seek(pointer); - finished = true; - } - } - } - - /** - * Returns the offset of this atom to the beginning of the random access file - * - * @return - */ - public long getOffset() { - return offset; - } - - public DataAtomOutputStream getOutputStream() { - if (finished) { - throw new IllegalStateException("Atom is finished"); - } - return data; - } - - @Override - public long size() { - long size = 8 + data.size(); - return (size > 0xffffffffL) ? size + 8 : size; - } - } - - /** - * Output stream of the QuickTimeOutputStream. - */ - private ImageOutputStream out; - - /** - * Current video format. - */ - private VideoFormat videoFormat; - - /** - * Quality of JPEG encoded video frames. - */ - private float quality = 0.9f; - - /** - * Creation time of the movie output stream. - */ - private Date creationTime; - - /** - * Width of the video frames. All frames must have the same width. The value -1 is used to mark unspecified width. - */ - private int imgWidth = -1; - /** - * Height of the video frames. All frames must have the same height. The value -1 is used to mark unspecified height. - */ - private int imgHeight = -1; - - /** - * The timeScale of the movie. A time value that indicates the time scale for this media-that is, the number of time units that pass per second in - * its time coordinate system. - */ - private int timeScale = 600; - - /** - * The current state of the movie output stream. - */ - private States state = States.FINISHED; - - /** - * List of video frames. - */ - private LinkedList videoFrames; - - /** - * This atom holds the movie frames. - */ - private WideDataAtom mdatAtom; - - /** - * Creates a new output stream with the specified image videoFormat and framerate. - * - * @param file - * the output file - * @param format - * Selects an encoder for the video format "JPG" or "PNG". - * @exception IllegalArgumentException - * if videoFormat is null or if framerate is <= 0 - */ - public QuickTimeOutputStream(File file, VideoFormat format) throws IOException { - if (file.exists()) { - file.delete(); - } - out = new FileImageOutputStream(file); - - if (format == null) { - throw new IllegalArgumentException("format must not be null"); - } - - this.videoFormat = format; - - this.videoFrames = new LinkedList(); - } - - /** - * Closes the movie file as well as the stream being filtered. - * - * @exception IOException - * if an I/O error has occurred - */ - public void close() throws IOException { - if (state == States.STARTED) { - finish(); - } - if (state != States.CLOSED) { - out.close(); - state = States.CLOSED; - } - } - - /** - * Check to make sure that this stream has not been closed - */ - private void ensureOpen() throws IOException { - if (state == States.CLOSED) { - throw new IOException("Stream closed"); - } - } - - /** - * Sets the state of the QuickTimeOutpuStream to started. - *

- * If the state is changed by this method, the prolog is written. - */ - private void ensureStarted() throws IOException { - if (state != States.STARTED) { - creationTime = new Date(); - writeProlog(); - mdatAtom = new WideDataAtom("mdat"); - state = States.STARTED; - } - } - - /** - * Finishes writing the contents of the QuickTime output stream without closing the underlying stream. Use this method when applying multiple - * filters in succession to the same output stream. - * - * @exception IllegalStateException - * if the dimension of the video track has not been specified or determined yet. - * @exception IOException - * if an I/O exception has occurred - */ - public void finish() throws IOException { - ensureOpen(); - if (state != States.FINISHED) { - if (imgWidth == -1 || imgHeight == -1) { - throw new IllegalStateException("image width and height must be specified"); - } - mdatAtom.finish(); - writeEpilog(); - state = States.FINISHED; - imgWidth = imgHeight = -1; - } - } - - /** - * Returns the time scale of this media. - * - * @return time scale - */ - public int getTimeScale() { - return timeScale; - } - - /** - * Returns the video compression quality. - * - * @return video compression quality - */ - public float getVideoCompressionQuality() { - return quality; - } - - /** - * Sets the time scale for this media, that is, the number of time units that pass per second in its time coordinate system. - *

- * The default value is 600. - * - * @param newValue - */ - public void setTimeScale(int newValue) { - if (newValue <= 0) { - throw new IllegalArgumentException("timeScale must be greater 0"); - } - this.timeScale = newValue; - } - - /** - * Sets the compression quality of the video track. A value of 0 stands for "high compression is important" a value of 1 for - * "high image quality is important". - *

- * Changing this value affects frames which are subsequently written to the QuickTimeOutputStream. Frames which have already been written are not - * changed. - *

- * This value has no effect on videos encoded with the PNG format. - *

- * The default value is 0.9. - * - * @param newValue - */ - public void setVideoCompressionQuality(float newValue) { - this.quality = newValue; - } - - /** - * Sets the dimension of the video track. - *

- * You need to explicitly set the dimension, if you add all frames from files or input streams. - *

- * If you add frames from buffered images, then QuickTimeOutputStream can determine the video dimension from the image width and height. - * - * @param width - * @param height - */ - public void setVideoDimension(int width, int height) { - if (width < 1 || height < 1) { - throw new IllegalArgumentException("width and height must be greater zero."); - } - this.imgWidth = width; - this.imgHeight = height; - } - - private void writeEpilog() throws IOException { - Date modificationTime = new Date(); - int duration = 0; - for (Sample s : videoFrames) { - duration += s.duration; - } - - DataAtom leaf; - - /* Movie Atom ========= */ - CompositeAtom moovAtom = new CompositeAtom("moov"); - - /* - * Movie Header Atom ------------- The data contained in this atom defines characteristics of the entire QuickTime movie, such as time scale and - * duration. It has an atom type value of 'mvhd'. - * - * typedef struct { byte version; byte[3] flags; mactimestamp creationTime; mactimestamp modificationTime; int timeScale; int duration; int - * preferredRate; short preferredVolume; byte[10] reserved; int[9] matrix; int previewTime; int previewDuration; int posterTime; int - * selectionTime; int selectionDuration; int currentTime; int nextTrackId; } movieHeaderAtom; - */ - leaf = new DataAtom("mvhd"); - moovAtom.add(leaf); - DataAtomOutputStream d = leaf.getOutputStream(); - d.writeByte(0); // version - // A 1-byte specification of the version of this movie header atom. - - d.writeByte(0); // flags[0] - d.writeByte(0); // flags[1] - d.writeByte(0); // flags[2] - // Three bytes of space for future movie header flags. - - d.writeMacTimestamp(creationTime); // creationTime - // A 32-bit integer that specifies the calendar date and time (in - // seconds since midnight, January 1, 1904) when the movie atom was - // created. It is strongly recommended that this value should be - // specified using coordinated universal time (UTC). - - d.writeMacTimestamp(modificationTime); // modificationTime - // A 32-bit integer that specifies the calendar date and time (in - // seconds since midnight, January 1, 1904) when the movie atom was - // changed. BooleanIt is strongly recommended that this value should be - // specified using coordinated universal time (UTC). - - d.writeInt(timeScale); // timeScale - // A time value that indicates the time scale for this movie-that is, - // the number of time units that pass per second in its time coordinate - // system. A time coordinate system that measures time in sixtieths of a - // second, for example, has a time scale of 60. - - d.writeInt(duration); // duration - // A time value that indicates the duration of the movie in time scale - // units. Note that this property is derived from the movie's tracks. - // The value of this field corresponds to the duration of the longest - // track in the movie. - - d.writeFixed16D16(1d); // preferredRate - // A 32-bit fixed-point number that specifies the rate at which to play - // this movie. A value of 1.0 indicates normal rate. - - d.writeShort(256); // preferredVolume - // A 16-bit fixed-point number that specifies how loud to play this - // movie's sound. A value of 1.0 indicates full volume. - - d.write(new byte[10]); // reserved; - // Ten bytes reserved for use by Apple. Set to 0. - - d.writeFixed16D16(1f); // matrix[0] - d.writeFixed16D16(0f); // matrix[1] - d.writeFixed2D30(0f); // matrix[2] - d.writeFixed16D16(0f); // matrix[3] - d.writeFixed16D16(1f); // matrix[4] - d.writeFixed2D30(0); // matrix[5] - d.writeFixed16D16(0); // matrix[6] - d.writeFixed16D16(0); // matrix[7] - d.writeFixed2D30(1f); // matrix[8] - // The matrix structure associated with this movie. A matrix shows how - // to map points from one coordinate space into another. See "Matrices" - // for a discussion of how display matrices are used in QuickTime: - // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap4/chapter_5_section_4.html#//apple_ref/doc/uid/TP40000939-CH206-18737 - - d.writeInt(0); // previewTime - // The time value in the movie at which the preview begins. - - d.writeInt(0); // previewDuration - // The duration of the movie preview in movie time scale units. - - d.writeInt(0); // posterTime - // The time value of the time of the movie poster. - - d.writeInt(0); // selectionTime - // The time value for the start time of the current selection. - - d.writeInt(0); // selectionDuration - // The duration of the current selection in movie time scale units. - - d.writeInt(0); // currentTime; - // The time value for current time position within the movie. - - d.writeInt(2); // nextTrackId - // A 32-bit integer that indicates a value to use for the track ID - // number of the next track added to this movie. Note that 0 is not a - // valid track ID value. - - /* Track Atom ======== */ - CompositeAtom trakAtom = new CompositeAtom("trak"); - moovAtom.add(trakAtom); - - /* - * Track Header Atom ----------- The track header atom specifies the characteristics of a single track within a movie. A track header atom - * contains a size field that specifies the number of bytes and a type field that indicates the format of the data (defined by the atom type - * 'tkhd'). - * - * typedef struct { byte version; byte flag0; byte flag1; byte set TrackHeaderFlags flag2; mactimestamp creationTime; mactimestamp - * modificationTime; int trackId; byte[4] reserved; int duration; byte[8] reserved; short layer; short alternateGroup; short volume; byte[2] - * reserved; int[9] matrix; int trackWidth; int trackHeight; } trackHeaderAtom; - */ - leaf = new DataAtom("tkhd"); - trakAtom.add(leaf); - d = leaf.getOutputStream(); - d.write(0); // version - // A 1-byte specification of the version of this track header. - - d.write(0); // flag[0] - d.write(0); // flag[1] - d.write(0xf); // flag[2] - // Three bytes that are reserved for the track header flags. These flags - // indicate how the track is used in the movie. The following flags are - // valid (all flags are enabled when set to 1): - // - // Track enabled - // Indicates that the track is enabled. Flag value is 0x0001. - // Track in movie - // Indicates that the track is used in the movie. Flag value is - // 0x0002. - // Track in preview - // Indicates that the track is used in the movie's preview. Flag - // value is 0x0004. - // Track in poster - // Indicates that the track is used in the movie's poster. Flag - // value is 0x0008. - - d.writeMacTimestamp(creationTime); // creationTime - // A 32-bit integer that indicates the calendar date and time (expressed - // in seconds since midnight, January 1, 1904) when the track header was - // created. It is strongly recommended that this value should be - // specified using coordinated universal time (UTC). - - d.writeMacTimestamp(modificationTime); // modificationTime - // A 32-bit integer that indicates the calendar date and time (expressed - // in seconds since midnight, January 1, 1904) when the track header was - // changed. It is strongly recommended that this value should be - // specified using coordinated universal time (UTC). - - d.writeInt(1); // trackId - // A 32-bit integer that uniquely identifies the track. The value 0 - // cannot be used. - - d.writeInt(0); // reserved; - // A 32-bit integer that is reserved for use by Apple. Set this field to - // 0. - - d.writeInt(duration); // duration - // A time value that indicates the duration of this track (in the - // movie's time coordinate system). Note that this property is derived - // from the track's edits. The value of this field is equal to the sum - // of the durations of all of the track's edits. If there is no edit - // list, then the duration is the sum of the sample durations, converted - // into the movie timescale. - - d.writeLong(0); // reserved - // An 8-byte value that is reserved for use by Apple. Set this field to - // 0. - - d.writeShort(0); // layer; - // A 16-bit integer that indicates this track's spatial priority in its - // movie. The QuickTime Movie Toolbox uses this value to determine how - // tracks overlay one another. Tracks with lower layer values are - // displayed in front of tracks with higher layer values. - - d.writeShort(0); // alternate group - // A 16-bit integer that specifies a collection of movie tracks that - // contain alternate data for one another. QuickTime chooses one track - // from the group to be used when the movie is played. The choice may be - // based on such considerations as playback quality, language, or the - // capabilities of the computer. - - d.writeShort(0); // volume - // A 16-bit fixed-point value that indicates how loudly this track's - // sound is to be played. A value of 1.0 indicates normal volume. - - d.writeShort(0); // reserved - // A 16-bit integer that is reserved for use by Apple. Set this field to - // 0. - - d.writeFixed16D16(1f); // matrix[0] - d.writeFixed16D16(0f); // matrix[1] - d.writeFixed2D30(0f); // matrix[2] - d.writeFixed16D16(0f); // matrix[3] - d.writeFixed16D16(1f); // matrix[4] - d.writeFixed2D30(0); // matrix[5] - d.writeFixed16D16(0); // matrix[6] - d.writeFixed16D16(0); // matrix[7] - d.writeFixed2D30(1f); // matrix[8] - // The matrix structure associated with this track. - // See Figure 2-8 for an illustration of a matrix structure: - // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_3.html#//apple_ref/doc/uid/TP40000939-CH204-32967 - - d.writeFixed16D16(imgWidth); // width - // A 32-bit fixed-point number that specifies the width of this track in - // pixels. - - d.writeFixed16D16(imgHeight); // height - // A 32-bit fixed-point number that indicates the height of this track - // in pixels. - - /* Media Atom ========= */ - CompositeAtom mdiaAtom = new CompositeAtom("mdia"); - trakAtom.add(mdiaAtom); - - /* - * Media Header atom ------- typedef struct { byte version; byte[3] flags; mactimestamp creationTime; mactimestamp modificationTime; int - * timeScale; int duration; short language; short quality; } mediaHeaderAtom; - */ - leaf = new DataAtom("mdhd"); - mdiaAtom.add(leaf); - d = leaf.getOutputStream(); - d.write(0); // version - // One byte that specifies the version of this header atom. - - d.write(0); // flag[0] - d.write(0); // flag[1] - d.write(0); // flag[2] - // Three bytes of space for media header flags. Set this field to 0. - - d.writeMacTimestamp(creationTime); // creationTime - // A 32-bit integer that specifies (in seconds since midnight, January - // 1, 1904) when the media atom was created. It is strongly recommended - // that this value should be specified using coordinated universal time - // (UTC). - - d.writeMacTimestamp(modificationTime); // modificationTime - // A 32-bit integer that specifies (in seconds since midnight, January - // 1, 1904) when the media atom was changed. It is strongly recommended - // that this value should be specified using coordinated universal time - // (UTC). - - d.writeInt(timeScale); // timeScale - // A time value that indicates the time scale for this media-that is, - // the number of time units that pass per second in its time coordinate - // system. - - d.writeInt(duration); // duration - // The duration of this media in units of its time scale. - - d.writeShort(0); // language; - // A 16-bit integer that specifies the language code for this media. - // See "Language Code Values" for valid language codes: - // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap4/chapter_5_section_2.html#//apple_ref/doc/uid/TP40000939-CH206-27005 - - d.writeShort(0); // quality - // A 16-bit integer that specifies the media's playback quality-that is, - // its suitability for playback in a given environment. - - /** Media Handler Atom ------- */ - leaf = new DataAtom("hdlr"); - mdiaAtom.add(leaf); - /* - * typedef struct { byte version; byte[3] flags; magic componentType; magic componentSubtype; magic componentManufacturer; int componentFlags; int - * componentFlagsMask; cstring componentName; } handlerReferenceAtom; - */ - d = leaf.getOutputStream(); - d.write(0); // version - // A 1-byte specification of the version of this handler information. - - d.write(0); // flag[0] - d.write(0); // flag[1] - d.write(0); // flag[2] - // A 3-byte space for handler information flags. Set this field to 0. - - d.writeType("mhlr"); // componentType - // A four-character code that identifies the type of the handler. Only - // two values are valid for this field: 'mhlr' for media handlers and - // 'dhlr' for data handlers. - - d.writeType("vide"); // componentSubtype - // A four-character code that identifies the type of the media handler - // or data handler. For media handlers, this field defines the type of - // data-for example, 'vide' for video data or 'soun' for sound data. - // - // For data handlers, this field defines the data reference type-for - // example, a component subtype value of 'alis' identifies a file alias. - - d.writeInt(0); // componentManufacturer - // Reserved. Set to 0. - - d.writeInt(0); // componentFlags - // Reserved. Set to 0. - - d.writeInt(0); // componentFlagsMask - // Reserved. Set to 0. - - d.write(0); // componentName (empty string) - // A (counted) string that specifies the name of the component-that is, - // the media handler used when this media was created. This field may - // contain a zero-length (empty) string. - - /* Media Information atom ========= */ - CompositeAtom minfAtom = new CompositeAtom("minf"); - mdiaAtom.add(minfAtom); - - /* Video media information atom -------- */ - leaf = new DataAtom("vmhd"); - minfAtom.add(leaf); - /* - * typedef struct { byte version; byte flag1; byte flag2; byte set vmhdFlags flag3; short graphicsMode; ushort[3] opcolor; } - * videoMediaInformationHeaderAtom; - */ - d = leaf.getOutputStream(); - d.write(0); // version - // One byte that specifies the version of this header atom. - - d.write(0); // flag[0] - d.write(0); // flag[1] - d.write(0x1); // flag[2] - // Three bytes of space for media header flags. - // This is a compatibility flag that allows QuickTime to distinguish - // between movies created with QuickTime 1.0 and newer movies. You - // should always set this flag to 1, unless you are creating a movie - // intended for playback using version 1.0 of QuickTime. This flag's - // value is 0x0001. - - d.writeShort(0x40); // graphicsMode (0x40 = ditherCopy) - // A 16-bit integer that specifies the transfer mode. The transfer mode - // specifies which Boolean operation QuickDraw should perform when - // drawing or transferring an image from one location to another. - // See "Graphics Modes" for a list of graphics modes supported by - // QuickTime: - // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap4/chapter_5_section_5.html#//apple_ref/doc/uid/TP40000939-CH206-18741 - - d.writeUShort(0); // opcolor[0] - d.writeUShort(0); // opcolor[1] - d.writeUShort(0); // opcolor[2] - // Three 16-bit values that specify the red, green, and blue colors for - // the transfer mode operation indicated in the graphics mode field. - - /* Handle reference atom -------- */ - // The handler reference atom specifies the media handler component that - // is to be used to interpret the media's data. The handler reference - // atom has an atom type value of 'hdlr'. - leaf = new DataAtom("hdlr"); - minfAtom.add(leaf); - /* - * typedef struct { byte version; byte[3] flags; magic componentType; magic componentSubtype; magic componentManufacturer; int componentFlags; int - * componentFlagsMask; cstring componentName; } handlerReferenceAtom; - */ - d = leaf.getOutputStream(); - d.write(0); // version - // A 1-byte specification of the version of this handler information. - - d.write(0); // flag[0] - d.write(0); // flag[1] - d.write(0); // flag[2] - // A 3-byte space for handler information flags. Set this field to 0. - - d.writeType("dhlr"); // componentType - // A four-character code that identifies the type of the handler. Only - // two values are valid for this field: 'mhlr' for media handlers and - // 'dhlr' for data handlers. - - d.writeType("alis"); // componentSubtype - // A four-character code that identifies the type of the media handler - // or data handler. For media handlers, this field defines the type of - // data-for example, 'vide' for video data or 'soun' for sound data. - // For data handlers, this field defines the data reference type-for - // example, a component subtype value of 'alis' identifies a file alias. - - d.writeInt(0); // componentManufacturer - // Reserved. Set to 0. - - d.writeInt(0); // componentFlags - // Reserved. Set to 0. - - d.writeInt(0); // componentFlagsMask - // Reserved. Set to 0. - - d.write(0); // componentName (empty string) - // A (counted) string that specifies the name of the component-that is, - // the media handler used when this media was created. This field may - // contain a zero-length (empty) string. - - /* Data information atom ===== */ - CompositeAtom dinfAtom = new CompositeAtom("dinf"); - minfAtom.add(dinfAtom); - - /* Data reference atom ----- */ - // Data reference atoms contain tabular data that instructs the data - // handler component how to access the media's data. - leaf = new DataAtom("dref"); - dinfAtom.add(leaf); - /* - * typedef struct { ubyte version; ubyte[3] flags; int numberOfEntries; dataReferenceEntry dataReference[numberOfEntries]; } dataReferenceAtom; - * - * set { dataRefSelfReference=1 // I am not shure if this is the correct value for this flag } drefEntryFlags; - * - * typedef struct { int size; magic type; byte version; ubyte flag1; ubyte flag2; ubyte set drefEntryFlags flag3; byte[size - 12] data; } - * dataReferenceEntry; - */ - d = leaf.getOutputStream(); - d.write(0); // version - // A 1-byte specification of the version of this data reference atom. - - d.write(0); // flag[0] - d.write(0); // flag[1] - d.write(0); // flag[2] - // A 3-byte space for data reference flags. Set this field to 0. - - d.writeInt(1); // numberOfEntries - // A 32-bit integer containing the count of data references that follow. - - d.writeInt(12); // dataReference.size - // A 32-bit integer that specifies the number of bytes in the data - // reference. - - d.writeType("alis"); // dataReference.type - // A 32-bit integer that specifies the type of the data in the data - // reference. Table 2-4 lists valid type values: - // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_4.html#//apple_ref/doc/uid/TP40000939-CH204-38840 - - d.write(0); // dataReference.version - // A 1-byte specification of the version of the data reference. - - d.write(0); // dataReference.flag1 - d.write(0); // dataReference.flag2 - d.write(0x1); // dataReference.flag3 - // A 3-byte space for data reference flags. There is one defined flag. - // - // Self reference - // This flag indicates that the media's data is in the same file as - // the movie atom. On the Macintosh, and other file systems with - // multifork files, set this flag to 1 even if the data resides in - // a different fork from the movie atom. This flag's value is - // 0x0001. - - /* Sample Table atom ========= */ - CompositeAtom stblAtom = new CompositeAtom("stbl"); - minfAtom.add(stblAtom); - - /* Sample Description atom ------- */ - // The sample description atom stores information that allows you to - // decode samples in the media. The data stored in the sample - // description varies, depending on the media type. For example, in the - // case of video media, the sample descriptions are image description - // structures. The sample description information for each media type is - // explained in "Media Data Atom Types": - // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_1.html#//apple_ref/doc/uid/TP40000939-CH205-SW1 - leaf = new DataAtom("stsd"); - stblAtom.add(leaf); - /* - * typedef struct { byte version; byte[3] flags; int numberOfEntries; sampleDescriptionEntry sampleDescriptionTable[numberOfEntries]; } - * sampleDescriptionAtom; - * - * typedef struct { int size; magic type; byte[6] reserved; // six bytes that must be zero short dataReferenceIndex; // A 16-bit integer that - * contains the index of the data reference to use to retrieve data associated with samples that use this sample description. Data references are - * stored in data reference atoms. byte[size - 16] data; } sampleDescriptionEntry; - */ - d = leaf.getOutputStream(); - d.write(0); // version - // A 1-byte specification of the version of this sample description - // atom. - - d.write(0); // flag[0] - d.write(0); // flag[1] - d.write(0); // flag[2] - // A 3-byte space for sample description flags. Set this field to 0. - - d.writeInt(1); // number of Entries - // A 32-bit integer containing the number of sample descriptions that - // follow. - - // A 32-bit integer indicating the number of bytes in the sample - // description. - switch (videoFormat) { - case RAW: { - d.writeInt(86); // sampleDescriptionTable[0].size - d.writeType("raw "); // sampleDescriptionTable[0].type - - // A 32-bit integer indicating the format of the stored data. - // This depends on the media type, but is usually either the - // compression format or the media type. - - d.write(new byte[6]); // sampleDescriptionTable[0].reserved - // Six bytes that must be set to 0. - - d.writeShort(1); // sampleDescriptionTable[0].dataReferenceIndex - // A 16-bit integer that contains the index of the data - // reference to use to retrieve data associated with samples - // that use this sample description. Data references are stored - // in data reference atoms. - - // Video Sample Description - // ------------------------ - // The format of the following fields is described here: - // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html#//apple_ref/doc/uid/TP40000939-CH205-BBCGICBJ - - d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.version - // A 16-bit integer indicating the version number of the - // compressed data. This is set to 0, unless a compressor has - // changed its data format. - - d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.revisionLevel - // A 16-bit integer that must be set to 0. - - d.writeType("java"); // sampleDescriptionTable.videoSampleDescription.manufacturer - // A 32-bit integer that specifies the developer of the - // compressor that generated the compressed data. Often this - // field contains 'appl' to indicate Apple Computer, Inc. - - d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.temporalQuality - // A 32-bit integer containing a value from 0 to 1023 indicating - // the degree of temporal compression. - - d.writeInt(512); // sampleDescriptionTable.videoSampleDescription.spatialQuality - // A 32-bit integer containing a value from 0 to 1024 indicating - // the degree of spatial compression. - - d.writeUShort(imgWidth); // sampleDescriptionTable.videoSampleDescription.width - // A 16-bit integer that specifies the width of the source image - // in pixels. - - d.writeUShort(imgHeight); // sampleDescriptionTable.videoSampleDescription.height - // A 16-bit integer that specifies the height of the source image in - // pixels. - - d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.horizontalResolution - // A 32-bit fixed-point number containing the horizontal - // resolution of the image in pixels per inch. - - d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.verticalResolution - // A 32-bit fixed-point number containing the vertical - // resolution of the image in pixels per inch. - - d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.dataSize - // A 32-bit integer that must be set to 0. - - d.writeShort(1); // sampleDescriptionTable.videoSampleDescription.frameCount - // A 16-bit integer that indicates how many frames of compressed - // data are stored in each sample. Usually set to 1. - - d.writePString("None", 32); // sampleDescriptionTable.videoSampleDescription.compressorName - // A 32-byte Pascal string containing the name of the compressor - // that created the image, such as "jpeg". - - d.writeShort(24); // sampleDescriptionTable.videoSampleDescription.depth - // A 16-bit integer that indicates the pixel depth of the - // compressed image. Values of 1, 2, 4, 8 ,16, 24, and 32 - // indicate the depth of color images. The value 32 should be - // used only if the image contains an alpha channel. Values of - // 34, 36, and 40 indicate 2-, 4-, and 8-bit grayscale, - // respectively, for grayscale images. - - d.writeShort(-1); // sampleDescriptionTable.videoSampleDescription.colorTableID - // A 16-bit integer that identifies which color table to use. - // If this field is set to -1, the default color table should be - // used for the specified depth. For all depths below 16 bits - // per pixel, this indicates a standard Macintosh color table - // for the specified depth. Depths of 16, 24, and 32 have no - // color table. - - break; - } - case JPG: { - d.writeInt(86); // sampleDescriptionTable[0].size - d.writeType("jpeg"); // sampleDescriptionTable[0].type - - // A 32-bit integer indicating the format of the stored data. - // This depends on the media type, but is usually either the - // compression format or the media type. - - d.write(new byte[6]); // sampleDescriptionTable[0].reserved - // Six bytes that must be set to 0. - - d.writeShort(1); // sampleDescriptionTable[0].dataReferenceIndex - // A 16-bit integer that contains the index of the data - // reference to use to retrieve data associated with samples - // that use this sample description. Data references are stored - // in data reference atoms. - - // Video Sample Description - // ------------------------ - // The format of the following fields is described here: - // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html#//apple_ref/doc/uid/TP40000939-CH205-BBCGICBJ - - d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.version - // A 16-bit integer indicating the version number of the - // compressed data. This is set to 0, unless a compressor has - // changed its data format. - - d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.revisionLevel - // A 16-bit integer that must be set to 0. - - d.writeType("java"); // sampleDescriptionTable.videoSampleDescription.manufacturer - // A 32-bit integer that specifies the developer of the - // compressor that generated the compressed data. Often this - // field contains 'appl' to indicate Apple Computer, Inc. - - d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.temporalQuality - // A 32-bit integer containing a value from 0 to 1023 indicating - // the degree of temporal compression. - - d.writeInt(512); // sampleDescriptionTable.videoSampleDescription.spatialQuality - // A 32-bit integer containing a value from 0 to 1024 indicating - // the degree of spatial compression. - - d.writeUShort(imgWidth); // sampleDescriptionTable.videoSampleDescription.width - // A 16-bit integer that specifies the width of the source image - // in pixels. - - d.writeUShort(imgHeight); // sampleDescriptionTable.videoSampleDescription.height - // A 16-bit integer that specifies the height of the source image in - // pixels. - - d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.horizontalResolution - // A 32-bit fixed-point number containing the horizontal - // resolution of the image in pixels per inch. - - d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.verticalResolution - // A 32-bit fixed-point number containing the vertical - // resolution of the image in pixels per inch. - - d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.dataSize - // A 32-bit integer that must be set to 0. - - d.writeShort(1); // sampleDescriptionTable.videoSampleDescription.frameCount - // A 16-bit integer that indicates how many frames of compressed - // data are stored in each sample. Usually set to 1. - - d.writePString("Photo - JPEG", 32); // sampleDescriptionTable.videoSampleDescription.compressorName - // A 32-byte Pascal string containing the name of the compressor - // that created the image, such as "jpeg". - - d.writeShort(24); // sampleDescriptionTable.videoSampleDescription.depth - // A 16-bit integer that indicates the pixel depth of the - // compressed image. Values of 1, 2, 4, 8 ,16, 24, and 32 - // indicate the depth of color images. The value 32 should be - // used only if the image contains an alpha channel. Values of - // 34, 36, and 40 indicate 2-, 4-, and 8-bit grayscale, - // respectively, for grayscale images. - - d.writeShort(-1); // sampleDescriptionTable.videoSampleDescription.colorTableID - // A 16-bit integer that identifies which color table to use. - // If this field is set to -1, the default color table should be - // used for the specified depth. For all depths below 16 bits - // per pixel, this indicates a standard Macintosh color table - // for the specified depth. Depths of 16, 24, and 32 have no - // color table. - - break; - } - case PNG: { - d.writeInt(86); // sampleDescriptionTable[0].size - d.writeType("png "); // sampleDescriptionTable[0].type - // A 32-bit integer indicating the format of the stored data. - // This depends on the media type, but is usually either the - // compression format or the media type. - - d.write(new byte[6]); // sampleDescriptionTable[0].reserved - // Six bytes that must be set to 0. - - d.writeShort(1); // sampleDescriptionTable[0].dataReferenceIndex - // A 16-bit integer that contains the index of the data - // reference to use to retrieve data associated with samples - // that use this sample description. Data references are stored - // in data reference atoms. - - // Video Sample Description - // ------------------------ - // The format of the following fields is described here: - // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html#//apple_ref/doc/uid/TP40000939-CH205-BBCGICBJ - - d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.version - // A 16-bit integer indicating the version number of the - // compressed data. This is set to 0, unless a compressor has - // changed its data format. - - d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.revisionLevel - // A 16-bit integer that must be set to 0. - - d.writeType("java"); // sampleDescriptionTable.videoSampleDescription.manufacturer - // A 32-bit integer that specifies the developer of the - // compressor that generated the compressed data. Often this - // field contains 'appl' to indicate Apple Computer, Inc. - - d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.temporalQuality - // A 32-bit integer containing a value from 0 to 1023 indicating - // the degree of temporal compression. - - d.writeInt(512); // sampleDescriptionTable.videoSampleDescription.spatialQuality - // A 32-bit integer containing a value from 0 to 1024 indicating - // the degree of spatial compression. - - d.writeUShort(imgWidth); // sampleDescriptionTable.videoSampleDescription.width - // A 16-bit integer that specifies the width of the source image - // in pixels. - - d.writeUShort(imgHeight); // sampleDescriptionTable.videoSampleDescription.height - // A 16-bit integer that specifies the height of the source image in - // pixels. - - d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.horizontalResolution - // A 32-bit fixed-point number containing the horizontal - // resolution of the image in pixels per inch. - - d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.verticalResolution - // A 32-bit fixed-point number containing the vertical - // resolution of the image in pixels per inch. - - d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.dataSize - // A 32-bit integer that must be set to 0. - - d.writeShort(1); // sampleDescriptionTable.videoSampleDescription.frameCount - // A 16-bit integer that indicates how many frames of compressed - // data are stored in each sample. Usually set to 1. - - d.writePString("PNG", 32); // sampleDescriptionTable.videoSampleDescription.compressorName - // A 32-byte Pascal string containing the name of the compressor - // that created the image, such as "jpeg". - - d.writeShort(24); // sampleDescriptionTable.videoSampleDescription.depth - // A 16-bit integer that indicates the pixel depth of the - // compressed image. Values of 1, 2, 4, 8 ,16, 24, and 32 - // indicate the depth of color images. The value 32 should be - // used only if the image contains an alpha channel. Values of - // 34, 36, and 40 indicate 2-, 4-, and 8-bit grayscale, - // respectively, for grayscale images. - - d.writeShort(-1); // sampleDescriptionTable.videoSampleDescription.colorTableID - // A 16-bit integer that identifies which color table to use. - // If this field is set to -1, the default color table should be - // used for the specified depth. For all depths below 16 bits - // per pixel, this indicates a standard Macintosh color table - // for the specified depth. Depths of 16, 24, and 32 have no - // color table. - - break; - } - } - - /* Time to Sample atom ---- */ - // Time-to-sample atoms store duration information for a media's - // samples, providing a mapping from a time in a media to the - // corresponding data sample. The time-to-sample atom has an atom type - // of 'stts'. - leaf = new DataAtom("stts"); - stblAtom.add(leaf); - /* - * typedef struct { byte version; byte[3] flags; int numberOfEntries; timeToSampleTable timeToSampleTable[numberOfEntries]; } timeToSampleAtom; - * - * typedef struct { int sampleCount; int sampleDuration; } timeToSampleTable; - */ - d = leaf.getOutputStream(); - d.write(0); // version - // A 1-byte specification of the version of this time-to-sample atom. - - d.write(0); // flag[0] - d.write(0); // flag[1] - d.write(0); // flag[2] - // A 3-byte space for time-to-sample flags. Set this field to 0. - - // count runs of video frame durations - int runCount = 1; - int prevDuration = videoFrames.size() == 0 ? 0 : videoFrames.get(0).duration; - for (Sample s : videoFrames) { - if (s.duration != prevDuration) { - runCount++; - prevDuration = s.duration; - } - } - d.writeInt(runCount); // numberOfEntries - // A 32-bit integer containing the count of entries in the - // time-to-sample table. - - int runLength = 0; - prevDuration = videoFrames.size() == 0 ? 0 : videoFrames.get(0).duration; - for (Sample s : videoFrames) { - if (s.duration != prevDuration) { - if (runLength > 0) { - d.writeInt(runLength); // timeToSampleTable[0].sampleCount - // A 32-bit integer that specifies the number of consecutive - // samples that have the same duration. - - d.writeInt(prevDuration); // timeToSampleTable[0].sampleDuration - // A 32-bit integer that specifies the duration of each - // sample. - } - prevDuration = s.duration; - runLength = 1; - } else { - runLength++; - } - } - if (runLength > 0) { - d.writeInt(runLength); // timeToSampleTable[0].sampleCount - // A 32-bit integer that specifies the number of consecutive - // samples that have the same duration. - - d.writeInt(prevDuration); // timeToSampleTable[0].sampleDuration - // A 32-bit integer that specifies the duration of each - // sample. - } - /* sample to chunk atom -------- */ - // The sample-to-chunk atom contains a table that maps samples to chunks - // in the media data stream. By examining the sample-to-chunk atom, you - // can determine the chunk that contains a specific sample. - leaf = new DataAtom("stsc"); - stblAtom.add(leaf); - /* - * typedef struct { byte version; byte[3] flags; int numberOfEntries; sampleToChunkTable sampleToChunkTable[numberOfEntries]; } sampleToChunkAtom; - * - * typedef struct { int firstChunk; int samplesPerChunk; int sampleDescription; } sampleToChunkTable; - */ - d = leaf.getOutputStream(); - d.write(0); // version - // A 1-byte specification of the version of this time-to-sample atom. - - d.write(0); // flag[0] - d.write(0); // flag[1] - d.write(0); // flag[2] - // A 3-byte space for time-to-sample flags. Set this field to 0. - - d.writeInt(1); // number of entries - // A 32-bit integer containing the count of entries in the - // sample-to-chunk table. - - d.writeInt(1); // first chunk - // The first chunk number using this table entry. - - d.writeInt(1); // samples per chunk - // The number of samples in each chunk. - - d.writeInt(1); // sample description - // The identification number associated with the sample description for - // the sample. For details on sample description atoms, see "Sample - // Description Atoms.": - // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_5.html#//apple_ref/doc/uid/TP40000939-CH204-25691 - - /* sample size atom -------- */ - // The sample size atom contains the sample count and a table giving the - // size of each sample. This allows the media data itself to be - // unframed. The total number of samples in the media is always - // indicated in the sample count. If the default size is indicated, then - // no table follows. - leaf = new DataAtom("stsz"); - stblAtom.add(leaf); - /* - * typedef struct { byte version; byte[3] flags; int sampleSize; int numberOfEntries; sampleSizeTable sampleSizeTable[numberOfEntries]; } - * sampleSizeAtom; - * - * typedef struct { int size; } sampleSizeTable; - */ - d = leaf.getOutputStream(); - d.write(0); // version - // A 1-byte specification of the version of this time-to-sample atom. - - d.write(0); // flag[0] - d.write(0); // flag[1] - d.write(0); // flag[2] - // A 3-byte space for time-to-sample flags. Set this field to 0. - - d.writeUInt(0); // sample size - // A 32-bit integer specifying the sample size. If all the samples are - // the same size, this field contains that size value. If this field is - // set to 0, then the samples have different sizes, and those sizes are - // stored in the sample size table. - - d.writeUInt(videoFrames.size()); // number of entries - // A 32-bit integer containing the count of entries in the sample size - // table. - - for (Sample s : videoFrames) { - d.writeUInt(s.length); // sample size - // The size field contains the size, in bytes, of the sample in - // question. The table is indexed by sample number-the first entry - // corresponds to the first sample, the second entry is for the - // second sample, and so on. - } - // - /* chunk offset atom -------- */ - // The chunk-offset table gives the index of each chunk into the - // containing file. There are two variants, permitting the use of - // 32-bit or 64-bit offsets. The latter is useful when managing very - // large movies. Only one of these variants occurs in any single - // instance of a sample table atom. - if (videoFrames.size() == 0 || videoFrames.getLast().offset <= 0xffffffffL) { - /* 32-bit chunk offset atom -------- */ - leaf = new DataAtom("stco"); - stblAtom.add(leaf); - /* - * typedef struct { byte version; byte[3] flags; int numberOfEntries; chunkOffsetTable chunkOffsetTable[numberOfEntries]; } chunkOffsetAtom; - * - * typedef struct { int offset; } chunkOffsetTable; - */ - d = leaf.getOutputStream(); - d.write(0); // version - // A 1-byte specification of the version of this time-to-sample - // atom. - - d.write(0); // flag[0] - d.write(0); // flag[1] - d.write(0); // flag[2] - // A 3-byte space for time-to-sample flags. Set this field to 0. - - d.writeUInt(videoFrames.size()); // number of entries - // A 32-bit integer containing the count of entries in the chunk - // offset table. - - for (Sample s : videoFrames) { - d.writeUInt(s.offset); // offset - // The offset contains the byte offset from the beginning of the - // data stream to the chunk. The table is indexed by chunk - // number-the first table entry corresponds to the first chunk, - // the second table entry is for the second chunk, and so on. - } - } else { - /* 64-bit chunk offset atom -------- */ - leaf = new DataAtom("co64"); - stblAtom.add(leaf); - /* - * typedef struct { byte version; byte[3] flags; int numberOfEntries; chunkOffsetTable chunkOffset64Table[numberOfEntries]; } chunkOffset64Atom; - * - * typedef struct { long offset; } chunkOffset64Table; - */ - d = leaf.getOutputStream(); - d.write(0); // version - // A 1-byte specification of the version of this time-to-sample - // atom. - - d.write(0); // flag[0] - d.write(0); // flag[1] - d.write(0); // flag[2] - // A 3-byte space for time-to-sample flags. Set this field to 0. - - d.writeUInt(videoFrames.size()); // number of entries - // A 32-bit integer containing the count of entries in the chunk - // offset table. - - for (Sample s : videoFrames) { - d.writeLong(s.offset); // offset - // The offset contains the byte offset from the beginning of the - // data stream to the chunk. The table is indexed by chunk - // number-the first table entry corresponds to the first chunk, - // the second table entry is for the second chunk, and so on. - } - } - // - moovAtom.finish(); - } - - /** - * Writes a frame to the video track. - *

- * If the dimension of the video track has not been specified yet, it is derived from the first buffered image added to the QuickTimeOutputStream. - * - * @param image - * The frame image. - * @param duration - * The duration of the frame in time scale units. - * - * @throws IllegalArgumentException - * if the duration is less than 1, or if the dimension of the frame does not match the dimension of the video track. - * @throws IOException - * if writing the image failed. - */ - public void writeFrame(BufferedImage image, int duration) throws IOException { - if (duration <= 0) { - throw new IllegalArgumentException("duration must be greater 0"); - } - ensureOpen(); - ensureStarted(); - - // Get the dimensions of the first image - if (imgWidth == -1) { - imgWidth = image.getWidth(); - imgHeight = image.getHeight(); - } else { - // The dimension of the image must match the dimension of the video - // track - if (imgWidth != image.getWidth() || imgHeight != image.getHeight()) { - throw new IllegalArgumentException("Dimensions of image[" + videoFrames.size() + "] (width=" + image.getWidth() - + ", height=" + image.getHeight() + ") differs from image[0] (width=" + imgWidth + ", height=" + imgHeight); - } - } - - long offset = out.getStreamPosition(); - - switch (videoFormat) { - case RAW: { - WritableRaster raster = image.getRaster(); - int[] raw = new int[imgWidth * 3]; // holds a scanline of raw image - // data with 3 channels of 32 - // bit data - byte[] bytes = new byte[imgWidth * 3]; // holds a scanline of raw - // image data with 3 - // channels of 8 bit data - for (int y = 0; y < imgHeight; y++) { - raster.getPixels(0, y, imgWidth, 1, raw); - for (int k = 0, n = imgWidth * 3; k < n; k++) { - bytes[k] = (byte) raw[k]; - } - mdatAtom.getOutputStream().write(bytes); - } - break; - } - case JPG: { - ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/jpeg").next(); - ImageWriteParam iwParam = iw.getDefaultWriteParam(); - iwParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); - iwParam.setCompressionQuality(quality); - MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(mdatAtom.getOutputStream()); - iw.setOutput(imgOut); - IIOImage img = new IIOImage(image, null, null); - iw.write(null, img, iwParam); - iw.dispose(); - break; - } - case PNG: - default: { - ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/png").next(); - ImageWriteParam iwParam = iw.getDefaultWriteParam(); - MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(mdatAtom.getOutputStream()); - iw.setOutput(imgOut); - IIOImage img = new IIOImage(image, null, null); - iw.write(null, img, iwParam); - iw.dispose(); - break; - } - } - long length = out.getStreamPosition() - offset; - videoFrames.add(new Sample(duration, offset, length)); - } - - /** - * Writes a frame from a file to the video track. - *

- * This method does not inspect the contents of the file. The contents has to match the video format. For example, it is your responsibility to only - * add JPG files if you have chosen the JPEG video format. - *

- * If you add all frames from files or from input streams, then you have to explicitly set the dimension of the video track before you call finish() - * or close(). - * - * @param file - * The file which holds the image data. - * @param duration - * The duration of the frame in time scale units. - * - * @throws IllegalStateException - * if the duration is less than 1. - * @throws IOException - * if writing the image failed. - */ - public void writeFrame(File file, int duration) throws IOException { - - try (FileInputStream in = new FileInputStream(file);) { - writeFrame(in, duration); - } - } - - /** - * Writes a frame to the video track. - *

- * This method does not inspect the contents of the input stream. The contents has to match the video format. For example, it is your responsibility - * to only add JPG files if you have chosen the JPEG video format. - *

- * If you add all frames from files or from input streams, then you have to explicitly set the dimension of the video track before you call finish() - * or close(). - * - * @param in - * The input stream which holds the image data. - * @param duration - * The duration of the frame in time scale units. - * - * @throws IllegalArgumentException - * if the duration is less than 1. - * @throws IOException - * if writing the image failed. - */ - public void writeFrame(InputStream in, int duration) throws IOException { - if (duration <= 0) { - throw new IllegalArgumentException("duration must be greater 0"); - } - ensureOpen(); - ensureStarted(); - - long offset = out.getStreamPosition(); - try (OutputStream mdatOut = mdatAtom.getOutputStream();) { - byte[] buf = new byte[512]; - int len; - while ((len = in.read(buf)) != -1) { - mdatOut.write(buf, 0, len); - } - long length = out.getStreamPosition() - offset; - videoFrames.add(new Sample(duration, offset, length)); - } - } - - private void writeProlog() throws IOException { - /* - * File type atom - * - * typedef struct { magic brand; bcd4 versionYear; bcd2 versionMonth; bcd2 versionMinor; magic[4] compatibleBrands; } ftypAtom; - */ - DataAtom ftypAtom = new DataAtom("ftyp"); - DataAtomOutputStream d = ftypAtom.getOutputStream(); - d.writeType("qt "); // brand - d.writeBCD4(2005); // versionYear - d.writeBCD2(3); // versionMonth - d.writeBCD2(0); // versionMinor - d.writeType("qt "); // compatibleBrands - d.writeInt(0); // compatibleBrands (0 is used to denote no value) - d.writeInt(0); // compatibleBrands (0 is used to denote no value) - d.writeInt(0); // compatibleBrands (0 is used to denote no value) - ftypAtom.finish(); - } -} diff --git a/src/com/github/xsavikx/android/screencast/app/AndroidScreencastApplication.java b/src/com/github/xsavikx/android/screencast/app/AndroidScreencastApplication.java deleted file mode 100644 index 426499a..0000000 --- a/src/com/github/xsavikx/android/screencast/app/AndroidScreencastApplication.java +++ /dev/null @@ -1,84 +0,0 @@ -package com.github.xsavikx.android.screencast.app; - -import javax.swing.SwingUtilities; - -import org.apache.log4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; -import org.springframework.stereotype.Component; - -import com.android.ddmlib.AndroidDebugBridge; -import com.android.ddmlib.IDevice; -import com.github.xsavikx.android.screencast.api.injector.Injector; -import com.github.xsavikx.android.screencast.constant.Constants; -import com.github.xsavikx.android.screencast.ui.JFrameMain; - -@Component -public class AndroidScreencastApplication extends SwingApplication { - - @Autowired - private Environment env; - - private static final Logger LOGGER = Logger.getLogger(AndroidScreencastApplication.class); - @Autowired - private JFrameMain jf; - @Autowired - private Injector injector; - @Autowired - private IDevice device; - - @Override - public void close() { - LOGGER.debug("close() - start"); - - if (injector != null) - injector.close(); - - if (device != null) { - synchronized (device) { - if (hasFilledAdbPath()) - AndroidDebugBridge.disconnectBridge(); - AndroidDebugBridge.terminate(); - } - } - - LOGGER.debug("close() - end"); - } - - @Override - public void start() { - LOGGER.debug("start() - start"); - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - // Start showing the device screen - jf.setTitle("" + device); - - // Show window - jf.setVisible(true); - - jf.launchInjector(); - } - }); - LOGGER.debug("start() - end"); - } - - @SuppressWarnings("boxing") - @Override - protected boolean isNativeLook() { - LOGGER.debug("isNativeLook() - start"); - - boolean returnboolean = env.getProperty(Constants.APP_NATIVE_LOOK_PROPERTY, Boolean.class, - Constants.DEFAULT_APP_NATIVE_LOOK); - LOGGER.debug("isNativeLook() - end"); - return returnboolean; - } - - private boolean hasFilledAdbPath() { - LOGGER.debug("hasFilledAdbPath() - start"); - - boolean returnboolean = env.getProperty(Constants.ADB_PATH_PROPERTY) != null; - LOGGER.debug("hasFilledAdbPath() - end"); - return returnboolean; - } -} diff --git a/src/com/github/xsavikx/android/screencast/app/Application.java b/src/com/github/xsavikx/android/screencast/app/Application.java deleted file mode 100644 index 8feedd6..0000000 --- a/src/com/github/xsavikx/android/screencast/app/Application.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.github.xsavikx.android.screencast.app; - -public interface Application { - - public void close(); - - public void handleException(Thread thread, Throwable ex); - - public void start(); - - public void init(); -} diff --git a/src/com/github/xsavikx/android/screencast/app/DeviceChooserApplication.java b/src/com/github/xsavikx/android/screencast/app/DeviceChooserApplication.java deleted file mode 100644 index 8d5e60d..0000000 --- a/src/com/github/xsavikx/android/screencast/app/DeviceChooserApplication.java +++ /dev/null @@ -1,97 +0,0 @@ -package com.github.xsavikx.android.screencast.app; - -import org.apache.log4j.Logger; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; -import org.springframework.stereotype.Component; - -import com.android.ddmlib.AndroidDebugBridge; -import com.android.ddmlib.IDevice; -import com.github.xsavikx.android.screencast.constant.Constants; -import com.github.xsavikx.android.screencast.ui.JDialogDeviceList; - -@Component -public class DeviceChooserApplication extends SwingApplication { - private static final Logger LOGGER = Logger.getLogger(DeviceChooserApplication.class); - - @Autowired - private Environment env; - @Autowired - private AndroidDebugBridge bridge; - - private IDevice device; - - @Override - public void close() { - // ignore - } - - @Override - public void start() { - LOGGER.debug("start() - start"); - initialize(); - - LOGGER.debug("start() - end"); - } - - @SuppressWarnings("boxing") - @Override - protected boolean isNativeLook() { - LOGGER.debug("isNativeLook() - start"); - - boolean returnboolean = env.getProperty(Constants.APP_NATIVE_LOOK_PROPERTY, Boolean.class, - Constants.DEFAULT_APP_NATIVE_LOOK); - LOGGER.debug("isNativeLook() - end"); - return returnboolean; - } - - private void waitDeviceList(AndroidDebugBridge bridge) { - LOGGER.debug("waitDeviceList(AndroidDebugBridge bridge=" + bridge + ") - start"); - - int count = 0; - while (bridge.hasInitialDeviceList() == false) { - try { - Thread.sleep(100); - count++; - } catch (InterruptedException e) { - LOGGER.warn("waitDeviceList(AndroidDebugBridge) - exception ignored", e); - - } - // let's not wait > 10 sec. - if (count > 300) { - throw new RuntimeException("Timeout getting device list!"); - } - } - - LOGGER.debug("waitDeviceList(AndroidDebugBridge bridge=" + bridge + ") - end"); - } - - private void initialize() { - LOGGER.debug("initialize() - start"); - - waitDeviceList(bridge); - - IDevice devices[] = bridge.getDevices(); - // Let the user choose the device - if (devices.length == 1) { - device = devices[0]; - } else { - JDialogDeviceList jd = new JDialogDeviceList(devices); - jd.setVisible(true); - - device = jd.getDevice(); - } - if (device == null) { - System.exit(0); - - LOGGER.debug("initialize() - end"); - return; - } - - LOGGER.debug("initialize() - end"); - } - - public IDevice getDevice() { - return device; - } -} diff --git a/src/com/github/xsavikx/android/screencast/app/GUIApplication.java b/src/com/github/xsavikx/android/screencast/app/GUIApplication.java deleted file mode 100644 index 3473def..0000000 --- a/src/com/github/xsavikx/android/screencast/app/GUIApplication.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.github.xsavikx.android.screencast.app; - -import java.lang.Thread.UncaughtExceptionHandler; - -public abstract class GUIApplication implements Application { - - public GUIApplication() { - Runtime.getRuntime().addShutdownHook(new Thread() { - @Override - public void run() { - close(); - } - }); - Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { - @Override - public void uncaughtException(Thread arg0, Throwable ex) { - try { - handleException(arg0, ex); - } catch (Exception ex2) { - // ignored - ex2.printStackTrace(); - } - } - }); - } - -} diff --git a/src/com/github/xsavikx/android/screencast/app/SwingApplication.java b/src/com/github/xsavikx/android/screencast/app/SwingApplication.java deleted file mode 100644 index c354b3f..0000000 --- a/src/com/github/xsavikx/android/screencast/app/SwingApplication.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.github.xsavikx.android.screencast.app; - -import java.io.PrintWriter; -import java.io.StringWriter; - -import javax.swing.SwingUtilities; -import javax.swing.UIManager; - -import com.github.xsavikx.android.screencast.ui.JDialogError; - -public abstract class SwingApplication extends GUIApplication { - private JDialogError jd = null; - - protected abstract boolean isNativeLook(); - - @Override - public void init() { - try { - if (isNativeLook()) - UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - - @Override - public void handleException(Thread thread, Throwable ex) { - try { - StringWriter sw = new StringWriter(); - ex.printStackTrace(new PrintWriter(sw)); - if (sw.toString().contains("SynthTreeUI")) - return; - ex.printStackTrace(System.err); - if (jd != null && jd.isVisible()) - return; - jd = new JDialogError(ex); - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - jd.setVisible(true); - } - }); - } catch (Exception ex2) { - // ignored - } - } - -} diff --git a/src/com/github/xsavikx/android/screencast/constant/Constants.java b/src/com/github/xsavikx/android/screencast/constant/Constants.java deleted file mode 100644 index 7fa2373..0000000 --- a/src/com/github/xsavikx/android/screencast/constant/Constants.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.xsavikx.android.screencast.constant; - -import javax.annotation.Resource; - -@Resource -public final class Constants { - public static final String APP_NATIVE_LOOK_PROPERTY = "app.nativeLook"; - public static final String ADB_PATH_PROPERTY = "adb.path"; - public static final String DEFAULT_WINDOW_WIDTH = "default.window.width"; - public static final String DEFAULT_WINDOW_HEIGHT = "default.window.height"; - - public static final boolean DEFAULT_APP_NATIVE_LOOK = true; - public static final int DEFAULT_ADB_PORT = 2345; -} diff --git a/src/com/github/xsavikx/android/screencast/spring/config/ApplicationConfiguration.java b/src/com/github/xsavikx/android/screencast/spring/config/ApplicationConfiguration.java deleted file mode 100644 index 01d9007..0000000 --- a/src/com/github/xsavikx/android/screencast/spring/config/ApplicationConfiguration.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.github.xsavikx.android.screencast.spring.config; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.PropertySource; -import org.springframework.core.env.Environment; - -import com.android.ddmlib.AndroidDebugBridge; -import com.android.ddmlib.IDevice; -import com.github.xsavikx.android.screencast.app.DeviceChooserApplication; -import com.github.xsavikx.android.screencast.constant.Constants; - -@Configuration -@ComponentScan(basePackages = "com.github.xsavikx.android.screencast") -@PropertySource(value = "file:${user.dir}/app.properties", ignoreResourceNotFound = true) -public class ApplicationConfiguration { - @Autowired - private Environment env; - - @Bean - public AndroidDebugBridge initAndroidDebugBridge() { - AndroidDebugBridge.initIfNeeded(false); - if (env.containsProperty(Constants.ADB_PATH_PROPERTY)) { - return AndroidDebugBridge.createBridge(env.getProperty(Constants.ADB_PATH_PROPERTY), false); - } - return AndroidDebugBridge.createBridge(); - } - - @Bean - public DefaultListableBeanFactory initBeanFactory() { - DefaultListableBeanFactory factory = new DefaultListableBeanFactory(); - return factory; - } - - @Bean - @Autowired - public IDevice initDevice(DeviceChooserApplication application) { - application.init(); - application.start(); - application.close(); - return application.getDevice(); - } - -} diff --git a/src/com/github/xsavikx/android/screencast/spring/config/ApplicationContextProvider.java b/src/com/github/xsavikx/android/screencast/spring/config/ApplicationContextProvider.java deleted file mode 100644 index 5ca5974..0000000 --- a/src/com/github/xsavikx/android/screencast/spring/config/ApplicationContextProvider.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.github.xsavikx.android.screencast.spring.config; - -import org.springframework.beans.BeansException; -import org.springframework.context.ApplicationContext; -import org.springframework.context.ApplicationContextAware; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; - -public class ApplicationContextProvider implements ApplicationContextAware { - private static ApplicationContext applicationContext; - - private ApplicationContextProvider() { - // - } - - public static ApplicationContext getApplicationContext() { - if (applicationContext == null) - applicationContext = new AnnotationConfigApplicationContext(ApplicationConfiguration.class); - return applicationContext; - } - - @Override - public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { - ApplicationContextProvider.applicationContext = applicationContext; - } - -} diff --git a/src/com/github/xsavikx/android/screencast/ui/JDialogDeviceList.java b/src/com/github/xsavikx/android/screencast/ui/JDialogDeviceList.java deleted file mode 100644 index ded501f..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/JDialogDeviceList.java +++ /dev/null @@ -1,103 +0,0 @@ -package com.github.xsavikx.android.screencast.ui; - -import java.awt.BorderLayout; -import java.awt.Dimension; -import java.awt.FlowLayout; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JFormattedTextField; -import javax.swing.JList; -import javax.swing.JPanel; -import javax.swing.JTextField; - -import com.android.ddmlib.IDevice; - -public class JDialogDeviceList extends JDialog implements ActionListener { - - private static final long serialVersionUID = -3719844308147203239L; - private static final String DEFAULT_HOST = "127.0.0.1"; - private static final int DEFAULT_PORT = 1324; - - private JTextField jtfHost = new JTextField(DEFAULT_HOST); - private JFormattedTextField jftfPort = new JFormattedTextField(DEFAULT_PORT); - private JList jlDevices = new JList(); - private JPanel jpAgent = new JPanel(); - private JPanel jpButtons = new JPanel(); - private JButton jbOk = new JButton("OK"); - private JButton jbQuit = new JButton("Quit"); - - private boolean cancelled = false; - private IDevice[] devices; - - public JDialogDeviceList(IDevice[] devices) { - super(); - setModal(true); - this.devices = devices; - initialize(); - } - - @Override - public void actionPerformed(ActionEvent arg0) { - cancelled = arg0.getSource() == jbQuit; - - setVisible(false); - } - - public IDevice getDevice() { - if (cancelled) - return null; - return jlDevices.getSelectedValue(); - } - - private void initialize() { - setTitle("Please select a device"); - jlDevices.setListData(devices); - jlDevices.setPreferredSize(new Dimension(400, 300)); - if (devices.length != 0) - jlDevices.setSelectedIndex(0); - jbOk.setEnabled(!jlDevices.isSelectionEmpty()); - - jpAgent.setBorder(BorderFactory.createTitledBorder("Agent")); - jpAgent.setLayout(new BorderLayout(10, 10)); - jpAgent.add(jtfHost, BorderLayout.CENTER); - jpAgent.add(jftfPort, BorderLayout.EAST); - - jpButtons.setLayout(new FlowLayout(FlowLayout.RIGHT)); - jpButtons.add(jbOk, BorderLayout.CENTER); - jpButtons.add(jbQuit, BorderLayout.SOUTH); - - JPanel jpBottom = new JPanel(); - jpBottom.setLayout(new BorderLayout()); - jpBottom.add(jpAgent, BorderLayout.CENTER); - jpBottom.add(jpButtons, BorderLayout.SOUTH); - - setLayout(new BorderLayout()); - add(jlDevices, BorderLayout.CENTER); - add(jpBottom, BorderLayout.SOUTH); - - pack(); - setLocationRelativeTo(null); - - jbOk.addActionListener(this); - jbQuit.addActionListener(this); - jlDevices.addMouseListener(new MouseAdapter() { - - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - int index = jlDevices.locationToIndex(e.getPoint()); - jlDevices.ensureIndexIsVisible(index); - cancelled = false; - setVisible(false); - } - } - - }); - } -} diff --git a/src/com/github/xsavikx/android/screencast/ui/JDialogError.java b/src/com/github/xsavikx/android/screencast/ui/JDialogError.java deleted file mode 100644 index cb84114..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/JDialogError.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.github.xsavikx.android.screencast.ui; - -import java.awt.BorderLayout; -import java.io.PrintWriter; -import java.io.StringWriter; - -import javax.swing.JDialog; -import javax.swing.JTextArea; - -public class JDialogError extends JDialog { - - private static final long serialVersionUID = -2562084286663149628L; - - public JDialogError(Throwable ex) { - getRootPane().setLayout(new BorderLayout()); - JTextArea l = new JTextArea(); - StringWriter w = new StringWriter(); - if (ex.getClass() == RuntimeException.class && ex.getCause() != null) - ex = ex.getCause(); - ex.printStackTrace(new PrintWriter(w)); - l.setText(w.toString()); - getRootPane().add(l, BorderLayout.CENTER); - pack(); - setLocationRelativeTo(null); - setAlwaysOnTop(true); - } - -} diff --git a/src/com/github/xsavikx/android/screencast/ui/JDialogExecuteKeyEvent.java b/src/com/github/xsavikx/android/screencast/ui/JDialogExecuteKeyEvent.java deleted file mode 100644 index 7bfd7cf..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/JDialogExecuteKeyEvent.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.github.xsavikx.android.screencast.ui; - -import java.awt.Dimension; -import java.awt.GridLayout; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.BorderFactory; -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JOptionPane; -import javax.swing.JPanel; -import javax.swing.JScrollPane; -import javax.swing.JSplitPane; -import javax.swing.ScrollPaneConstants; -import javax.swing.SwingUtilities; -import javax.swing.WindowConstants; -import javax.swing.border.TitledBorder; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import com.github.xsavikx.android.screencast.api.command.executor.CommandExecutor; -import com.github.xsavikx.android.screencast.api.command.factory.AdbInputCommandFactory; -import com.github.xsavikx.android.screencast.api.injector.InputKeyEvent; -import com.github.xsavikx.android.screencast.ui.model.InputKeyEventTable; -import com.github.xsavikx.android.screencast.ui.model.InputKeyEventTableModel; - -@Component -public class JDialogExecuteKeyEvent extends JDialog { - private static final long serialVersionUID = -4152020879675916776L; - private static final int HEIGHT = 600; - private static final int WIDTH = 800; - private static final int BUTTON_HEIGHT = 20; - private static final int BUTTON_WIDTH = WIDTH >> 1 - 5; - - private static final int TITLE_COLUMN_INDEX = 1; - - private static final String EXECUTE_BUTTON_TEXT = "Execute"; - private static final String CANCEL_BUTTON_TEXT = "Cancel"; - private static final String COMMAND_LIST_TITLE_TEXT = "Commands to execute"; - private static final String NO_COMMAND_CHOSEN_WARNING_MESSAGE = "Please, select command from the list"; - private static final String NO_COMMAND_CHOSEN_WARNING_DIALOG_TITLE = "Warning"; - - @Autowired - private CommandExecutor commandExecutor; - - /** - * Launch the application. - */ - public static void main(String[] args) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - new JDialogExecuteKeyEvent().setVisible(true); - } - }); - } - - /** - * Create the dialog. - */ - public JDialogExecuteKeyEvent() { - setResizable(false); - setTitle("Execute key event"); - setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); - final InputKeyEventTableModel commandList = new InputKeyEventTableModel(InputKeyEvent.values()); - final InputKeyEventTable commandListTable = new InputKeyEventTable(commandList); - - JButton executeCommandButton = new JButton(EXECUTE_BUTTON_TEXT); - executeCommandButton.setSize(new Dimension(BUTTON_WIDTH, BUTTON_HEIGHT)); - executeCommandButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - int rowIndex = commandListTable.getSelectedRow(); - if (rowIndex > 0) { - final String title = (String) commandList.getValueAt(rowIndex, TITLE_COLUMN_INDEX); - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - commandExecutor.execute(AdbInputCommandFactory.getKeyCommand(InputKeyEvent.valueOf(title))); - } - }); - closeDialog(); - } else { - JOptionPane.showMessageDialog(null, NO_COMMAND_CHOSEN_WARNING_MESSAGE, NO_COMMAND_CHOSEN_WARNING_DIALOG_TITLE, - JOptionPane.WARNING_MESSAGE); - } - } - }); - JButton cancelButton = new JButton(CANCEL_BUTTON_TEXT); - cancelButton.addActionListener(new ActionListener() { - @Override - public void actionPerformed(ActionEvent e) { - closeDialog(); - } - }); - JScrollPane listScrollPane = new JScrollPane(commandListTable, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, - ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); - listScrollPane.setPreferredSize(new Dimension(WIDTH, HEIGHT)); - listScrollPane.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), - COMMAND_LIST_TITLE_TEXT, TitledBorder.CENTER, TitledBorder.TOP)); - JPanel buttonPane = new JPanel(); - buttonPane.add(executeCommandButton); - buttonPane.add(cancelButton); - buttonPane.setLayout(new GridLayout(1, 2)); - JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, listScrollPane, buttonPane); - splitPane.setEnabled(false); - getContentPane().add(splitPane); - pack(); - setLocationRelativeTo(null); - } - - private void closeDialog() { - setVisible(false); - } -} diff --git a/src/com/github/xsavikx/android/screencast/ui/JDialogUrl.java b/src/com/github/xsavikx/android/screencast/ui/JDialogUrl.java deleted file mode 100644 index a1b0198..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/JDialogUrl.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.github.xsavikx.android.screencast.ui; - -import java.awt.BorderLayout; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.JButton; -import javax.swing.JDialog; -import javax.swing.JTextField; - -public class JDialogUrl extends JDialog { - - private static final long serialVersionUID = -331017582679776599L; - private JTextField jtfUrl = new JTextField(); - - public JTextField getJtfUrl() { - return jtfUrl; - } - - public void setJtfUrl(JTextField jtfUrl) { - this.jtfUrl = jtfUrl; - } - - private JButton jbOk = new JButton("Ok"); - private boolean result = false; - - public JDialogUrl() { - setModal(true); - setTitle("Open url"); - - setLayout(new BorderLayout()); - add(jbOk, BorderLayout.SOUTH); - add(jtfUrl, BorderLayout.CENTER); - jtfUrl.setColumns(50); - - jbOk.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent arg0) { - setResult(true); - JDialogUrl.this.setVisible(false); - } - }); - - jbOk.setDefaultCapable(true); - getRootPane().setDefaultButton(jbOk); - pack(); - setLocationRelativeTo(null); - - } - - public boolean isResult() { - return result; - } - - public void setResult(boolean result) { - this.result = result; - } -} diff --git a/src/com/github/xsavikx/android/screencast/ui/JFrameMain.java b/src/com/github/xsavikx/android/screencast/ui/JFrameMain.java deleted file mode 100644 index 2e10acd..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/JFrameMain.java +++ /dev/null @@ -1,213 +0,0 @@ -package com.github.xsavikx.android.screencast.ui; - -import java.awt.BorderLayout; -import java.awt.Dimension; -import java.awt.KeyboardFocusManager; -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.awt.event.MouseAdapter; -import java.awt.image.BufferedImage; - -import javax.swing.JButton; -import javax.swing.JFileChooser; -import javax.swing.JFrame; -import javax.swing.JScrollPane; -import javax.swing.JToolBar; -import javax.swing.filechooser.FileNameExtensionFilter; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.core.env.Environment; -import org.springframework.stereotype.Component; - -import com.github.xsavikx.android.screencast.api.AndroidDevice; -import com.github.xsavikx.android.screencast.api.injector.Injector; -import com.github.xsavikx.android.screencast.api.injector.InputKeyEvent; -import com.github.xsavikx.android.screencast.api.injector.ScreenCaptureThread.ScreenCaptureListener; -import com.github.xsavikx.android.screencast.constant.Constants; -import com.github.xsavikx.android.screencast.spring.config.ApplicationContextProvider; -import com.github.xsavikx.android.screencast.ui.explorer.JFrameExplorer; -import com.github.xsavikx.android.screencast.ui.interaction.KeyEventDispatcherFactory; -import com.github.xsavikx.android.screencast.ui.interaction.KeyboardActionListenerFactory; -import com.github.xsavikx.android.screencast.ui.interaction.MouseActionAdapterFactory; - -@Component -public class JFrameMain extends JFrame { - - private static final long serialVersionUID = -2085909236767692371L; - private JPanelScreen jp = new JPanelScreen(); - private JToolBar jtb = new JToolBar(); - private JToolBar jtbHardkeys = new JToolBar(); - // private JToggleButton jtbRecord = new JToggleButton("Record"); - - // private JButton jbOpenUrl = new JButton("Open Url"); - private JScrollPane jsp; - private JButton jbExplorer = new JButton("Explore"); - private JButton jbRestartClient = new JButton("Restart client"); - private JButton jbExecuteKeyEvent = new JButton("Execute keycode"); - - private JButton jbKbHome = new JButton("Home"); - private JButton jbKbMenu = new JButton("Menu"); - private JButton jbKbBack = new JButton("Back"); - private JButton jbKbSearch = new JButton("Search"); - - private JButton jbKbPhoneOn = new JButton("Call"); - - private JButton jbKbPhoneOff = new JButton("End call"); - private AndroidDevice androidDevice; - private Injector injector; - private Environment env; - private Dimension oldImageDimension; - - @Autowired - public JFrameMain(Environment env, Injector injector, AndroidDevice androidDevice) { - this.injector = injector; - this.env = env; - this.androidDevice = androidDevice; - initialize(); - KeyboardFocusManager.getCurrentKeyboardFocusManager() - .addKeyEventDispatcher(KeyEventDispatcherFactory.getKeyEventDispatcher(this)); - } - - private void setPrefferedWindowSize() { - if (env.containsProperty(Constants.DEFAULT_WINDOW_HEIGHT) && env.containsProperty(Constants.DEFAULT_WINDOW_WIDTH)) { - Integer height = env.getProperty(Constants.DEFAULT_WINDOW_HEIGHT, Integer.class); - Integer width = env.getProperty(Constants.DEFAULT_WINDOW_WIDTH, Integer.class); - if (height != null && width != null) - getContentPane().setPreferredSize(new Dimension(width.intValue(), height.intValue())); - } - pack(); - } - - public void initialize() { - - jtb.setFocusable(false); - jbExplorer.setFocusable(false); - // jtbRecord.setFocusable(false); - // jbOpenUrl.setFocusable(false); - jbKbHome.setFocusable(false); - jbKbMenu.setFocusable(false); - jbKbBack.setFocusable(false); - jbKbSearch.setFocusable(false); - jbKbPhoneOn.setFocusable(false); - jbKbPhoneOff.setFocusable(false); - jbRestartClient.setFocusable(false); - jbExecuteKeyEvent.setFocusable(false); - - jbKbHome.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_HOME)); - jbKbMenu.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_MENU)); - jbKbBack.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_BACK)); - jbKbSearch.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_SEARCH)); - jbKbPhoneOn.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_CALL)); - jbKbPhoneOff.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_ENDCALL)); - - jtbHardkeys.add(jbKbHome); - jtbHardkeys.add(jbKbMenu); - jtbHardkeys.add(jbKbBack); - jtbHardkeys.add(jbKbSearch); - jtbHardkeys.add(jbKbPhoneOn); - jtbHardkeys.add(jbKbPhoneOff); - - // setIconImage(Toolkit.getDefaultToolkit().getImage( - // getClass().getResource("icon.png"))); - setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); - setLayout(new BorderLayout()); - add(jtb, BorderLayout.NORTH); - add(jtbHardkeys, BorderLayout.SOUTH); - jsp = new JScrollPane(jp); - add(jsp, BorderLayout.CENTER); - jsp.setPreferredSize(new Dimension(100, 100)); - pack(); - setLocationRelativeTo(null); - setPrefferedWindowSize(); - MouseAdapter ma = MouseActionAdapterFactory.getInstance(jp, injector); - - jp.addMouseMotionListener(ma); - jp.addMouseListener(ma); - jp.addMouseWheelListener(ma); - - // jtbRecord.addActionListener(new ActionListener() { - // - // @Override - // public void actionPerformed(ActionEvent arg0) { - // if (jtbRecord.isSelected()) { - // startRecording(); - // } else { - // stopRecording(); - // } - // } - // - // }); - // jtb.add(jtbRecord); - - jbExplorer.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent arg0) { - JFrameExplorer jf = ApplicationContextProvider.getApplicationContext().getBean(JFrameExplorer.class); - jf.setIconImage(getIconImage()); - jf.launch(); - jf.setVisible(true); - } - }); - jtb.add(jbExplorer); - - jtb.add(jbRestartClient); - - jbExecuteKeyEvent.addActionListener(new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - JDialogExecuteKeyEvent jdExecuteKeyEvent = ApplicationContextProvider.getApplicationContext() - .getBean(JDialogExecuteKeyEvent.class); - jdExecuteKeyEvent.setVisible(true); - } - }); - - jtb.add(jbExecuteKeyEvent); - - // jbOpenUrl.addActionListener(new ActionListener() { - // @Override - // public void actionPerformed(ActionEvent arg0) { - // JDialogUrl jdUrl = new JDialogUrl(); - // jdUrl.setVisible(true); - // if (!jdUrl.isResult()) - // return; - // String url = jdUrl.getJtfUrl().getText(); - // androidDevice.openUrl(url); - // } - // }); - // jtb.add(jbOpenUrl); - - } - - public void launchInjector() { - injector.screencapture.setListener(new ScreenCaptureListener() { - - @Override - public void handleNewImage(Dimension size, BufferedImage image, boolean landscape) { - if (oldImageDimension == null || !size.equals(oldImageDimension)) { - jsp.setPreferredSize(size); - JFrameMain.this.pack(); - oldImageDimension = size; - } - jp.handleNewImage(size, image); - } - }); - injector.start(); - } - - private void startRecording() { - JFileChooser jFileChooser = new JFileChooser(); - FileNameExtensionFilter filter = new FileNameExtensionFilter("Video file", "mov"); - jFileChooser.setFileFilter(filter); - int returnVal = jFileChooser.showSaveDialog(this); - if (returnVal == JFileChooser.APPROVE_OPTION) { - injector.screencapture.startRecording(jFileChooser.getSelectedFile()); - } - } - - private void stopRecording() { - injector.screencapture.stopRecording(); - } - -} diff --git a/src/com/github/xsavikx/android/screencast/ui/JPanelScreen.java b/src/com/github/xsavikx/android/screencast/ui/JPanelScreen.java deleted file mode 100644 index 81533a2..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/JPanelScreen.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.github.xsavikx.android.screencast.ui; - -import java.awt.Dimension; -import java.awt.Graphics; -import java.awt.Graphics2D; -import java.awt.Point; -import java.awt.image.BufferedImage; - -import javax.swing.JPanel; - -public class JPanelScreen extends JPanel { - - private static final long serialVersionUID = -2034873107028503004L; - private float coef = 1; - private double origX; - private double origY; - private Dimension size = null; - private BufferedImage image = null; - - public JPanelScreen() { - this.setFocusable(true); - } - - public Point getRawPoint(Point p1) { - Point p2 = new Point(); - p2.x = (int) ((p1.x - origX) / coef); - p2.y = (int) ((p1.y - origY) / coef); - return p2; - } - - public void handleNewImage(Dimension size, BufferedImage image) { - this.size = size; - this.image = image; - repaint(); - } - - @Override - protected void paintComponent(Graphics g) { - if (size == null) - return; - if (size.height == 0) - return; - Graphics2D g2 = (Graphics2D) g; - g2.clearRect(0, 0, getWidth(), getHeight()); - double width = Math.min(getWidth(), size.width * getHeight() / size.height); - coef = (float) width / size.width; - double height = width * size.height / size.width; - origX = (getWidth() - width) / 2; - origY = (getHeight() - height) / 2; - g2.drawImage(image, (int) origX, (int) origY, (int) width, (int) height, this); - } - -} diff --git a/src/com/github/xsavikx/android/screencast/ui/JSplashScreen.java b/src/com/github/xsavikx/android/screencast/ui/JSplashScreen.java deleted file mode 100644 index ef4620d..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/JSplashScreen.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.github.xsavikx.android.screencast.ui; - -import java.awt.BorderLayout; -import java.awt.Component; - -import javax.swing.BorderFactory; -import javax.swing.JLabel; -import javax.swing.JWindow; - -public class JSplashScreen extends JWindow { - - private static final long serialVersionUID = -4537199368044671301L; - private JLabel label; - - public JSplashScreen(String text) { - label = new JLabel("Loading...", (int) Component.CENTER_ALIGNMENT); - initialize(); - setText(text); - } - - private void initialize() { - setLayout(new BorderLayout()); - label.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); - // createLineBorder(Color.BLACK)); - add(label, BorderLayout.CENTER); - } - - public void setText(String text) { - label.setText(text); - pack(); - setLocationRelativeTo(null); - } - -} diff --git a/src/com/github/xsavikx/android/screencast/ui/MultiLineLabelUI.java b/src/com/github/xsavikx/android/screencast/ui/MultiLineLabelUI.java deleted file mode 100644 index 861ae32..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/MultiLineLabelUI.java +++ /dev/null @@ -1,288 +0,0 @@ -package com.github.xsavikx.android.screencast.ui; - -import java.awt.Dimension; -import java.awt.FontMetrics; -import java.awt.Graphics; -import java.awt.Rectangle; -import java.util.StringTokenizer; - -import javax.swing.Icon; -import javax.swing.JComponent; -import javax.swing.JLabel; -import javax.swing.SwingConstants; -import javax.swing.SwingUtilities; -import javax.swing.plaf.basic.BasicGraphicsUtils; -import javax.swing.plaf.basic.BasicLabelUI; - -public class MultiLineLabelUI extends BasicLabelUI { - public static Dimension computeMultiLineDimension(FontMetrics fm, String[] strs) { - int i, c, width = 0; - for (i = 0, c = strs.length; i < c; i++) - width = Math.max(width, SwingUtilities.computeStringWidth(fm, strs[i])); - return new Dimension(width, fm.getHeight() * strs.length); - } - - /** - * 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 - * labels string. Locations are computed relative to the viewR rectangle. This layoutCompoundLabel() does not know how to handle LEADING/TRAILING - * values in horizontalTextPosition (they will default to RIGHT) and in horizontalAlignment (they will default to CENTER). Use the other version of - * layoutCompoundLabel() instead. - */ - public static String layoutCompoundLabel(FontMetrics fm, String[] text, Icon icon, int verticalAlignment, - int horizontalAlignment, int verticalTextPosition, int horizontalTextPosition, Rectangle viewR, Rectangle iconR, - Rectangle textR, int textIconGap) { - /* - * Initialize the icon bounds rectangle iconR. - */ - - if (icon != null) { - iconR.width = icon.getIconWidth(); - iconR.height = icon.getIconHeight(); - } else { - iconR.width = iconR.height = 0; - } - - /* - * 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. - */ - - // Fix for textIsEmpty sent by Paulo Santos - boolean textIsEmpty = (text == null) || (text.length == 0) - || (text.length == 1 && ((text[0] == null) || text[0].equals(""))); - - String rettext = ""; - if (textIsEmpty) { - textR.width = textR.height = 0; - } else { - Dimension dim = computeMultiLineDimension(fm, text); - textR.width = dim.width; - textR.height = dim.height; - } - - /* - * 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 - * textIconGap. - */ - - int gap = (textIsEmpty || (icon == null)) ? 0 : textIconGap; - - if (!textIsEmpty) { - - /* - * 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. - */ - - int availTextWidth; - - if (horizontalTextPosition == CENTER) { - availTextWidth = viewR.width; - } else { - availTextWidth = viewR.width - (iconR.width + gap); - } - - if (textR.width > availTextWidth && text.length == 1) { - String clipString = "..."; - int totalWidth = SwingUtilities.computeStringWidth(fm, clipString); - int nChars; - for (nChars = 0; nChars < text[0].length(); nChars++) { - totalWidth += fm.charWidth(text[0].charAt(nChars)); - if (totalWidth > availTextWidth) { - break; - } - } - rettext = text[0].substring(0, nChars) + clipString; - textR.width = SwingUtilities.computeStringWidth(fm, rettext); - } - } - - /* - * Compute textR.x,y given the verticalTextPosition and horizontalTextPosition properties - */ - - if (verticalTextPosition == TOP) { - if (horizontalTextPosition != CENTER) { - textR.y = 0; - } else { - textR.y = -(textR.height + gap); - } - } else if (verticalTextPosition == CENTER) { - textR.y = (iconR.height / 2) - (textR.height / 2); - } else { // (verticalTextPosition == BOTTOM) - if (horizontalTextPosition != CENTER) { - textR.y = iconR.height - textR.height; - } else { - textR.y = (iconR.height + gap); - } - } - - if (horizontalTextPosition == LEFT) { - textR.x = -(textR.width + gap); - } else if (horizontalTextPosition == CENTER) { - textR.x = (iconR.width / 2) - (textR.width / 2); - } else { // (horizontalTextPosition == RIGHT) - textR.x = (iconR.width + gap); - } - - /* - * labelR is the rectangle that contains iconR and textR. Move it to its proper position given the labelAlignment properties. - * - * To avoid actually allocating a Rectangle, Rectangle.union has been inlined below. - */ - int labelR_x = Math.min(iconR.x, textR.x); - int labelR_width = Math.max(iconR.x + iconR.width, textR.x + textR.width) - labelR_x; - int labelR_y = Math.min(iconR.y, textR.y); - int labelR_height = Math.max(iconR.y + iconR.height, textR.y + textR.height) - labelR_y; - - int dx, dy; - - if (verticalAlignment == TOP) { - dy = viewR.y - labelR_y; - } else if (verticalAlignment == CENTER) { - dy = (viewR.y + (viewR.height / 2)) - (labelR_y + (labelR_height / 2)); - } else { // (verticalAlignment == BOTTOM) - dy = (viewR.y + viewR.height) - (labelR_y + labelR_height); - } - - if (horizontalAlignment == LEFT) { - dx = viewR.x - labelR_x; - } else if (horizontalAlignment == RIGHT) { - dx = (viewR.x + viewR.width) - (labelR_x + labelR_width); - } else { // (horizontalAlignment == CENTER) - dx = (viewR.x + (viewR.width / 2)) - (labelR_x + (labelR_width / 2)); - } - - /* - * Translate textR and glypyR by dx,dy. - */ - - textR.x += dx; - textR.y += dy; - - iconR.x += dx; - iconR.y += dy; - - return rettext; - } - - /** - * 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 - * labels string. Locations are computed relative to the viewR rectangle. The JComponents orientation (LEADING/TRAILING) will also be taken into - * account and translated into LEFT/RIGHT values accordingly. - */ - public static String layoutCompoundLabel(JComponent c, FontMetrics fm, String[] text, Icon icon, - int verticalAlignment, int horizontalAlignment, int verticalTextPosition, int horizontalTextPosition, - Rectangle viewR, Rectangle iconR, Rectangle textR, int textIconGap) { - boolean orientationIsLeftToRight = true; - int hAlign = horizontalAlignment; - int hTextPos = horizontalTextPosition; - - if (c != null) { - if (!(c.getComponentOrientation().isLeftToRight())) { - orientationIsLeftToRight = false; - } - } - - // Translate LEADING/TRAILING values in horizontalAlignment - // to LEFT/RIGHT values depending on the components orientation - switch (horizontalAlignment) { - case LEADING: - hAlign = (orientationIsLeftToRight) ? LEFT : RIGHT; - break; - case TRAILING: - hAlign = (orientationIsLeftToRight) ? RIGHT : LEFT; - break; - } - - // Translate LEADING/TRAILING values in horizontalTextPosition - // to LEFT/RIGHT values depending on the components orientation - switch (horizontalTextPosition) { - case LEADING: - hTextPos = (orientationIsLeftToRight) ? LEFT : RIGHT; - break; - case TRAILING: - hTextPos = (orientationIsLeftToRight) ? RIGHT : LEFT; - break; - } - - return layoutCompoundLabel(fm, text, icon, verticalAlignment, hAlign, verticalTextPosition, hTextPos, viewR, iconR, - textR, textIconGap); - } - - static { - labelUI = new MultiLineLabelUI(); - } - - static final int LEADING = SwingConstants.LEADING; - static final int TRAILING = SwingConstants.TRAILING; - static final int LEFT = SwingConstants.LEFT; - static final int RIGHT = SwingConstants.RIGHT; - - static final int TOP = SwingConstants.TOP; - - static final int CENTER = SwingConstants.CENTER; - - protected String str; - - protected String[] strs; - - protected void drawString(Graphics g, String s, int accChar, int textX, int textY) { - if (s.indexOf('\n') == -1) - BasicGraphicsUtils.drawString(g, s, accChar, textX, textY); - else { - String[] strs = splitStringByLines(s); - int height = g.getFontMetrics().getHeight(); - // Only the first line can have the accel char - BasicGraphicsUtils.drawString(g, strs[0], accChar, textX, textY); - for (int i = 1; i < strs.length; i++) - g.drawString(strs[i], textX, textY + (height * i)); - } - } - - @Override - protected String layoutCL(JLabel label, FontMetrics fontMetrics, String text, Icon icon, Rectangle viewR, - Rectangle iconR, Rectangle textR) { - String s = layoutCompoundLabel(label, fontMetrics, splitStringByLines(text), icon, label.getVerticalAlignment(), - label.getHorizontalAlignment(), label.getVerticalTextPosition(), label.getHorizontalTextPosition(), viewR, - iconR, textR, label.getIconTextGap()); - - if (s.equals("")) - return text; - return s; - } - - @Override - protected void paintDisabledText(JLabel l, Graphics g, String s, int textX, int textY) { - int accChar = l.getDisplayedMnemonic(); - g.setColor(l.getBackground()); - drawString(g, s, accChar, textX, textY); - } - - @Override - protected void paintEnabledText(JLabel l, Graphics g, String s, int textX, int textY) { - int accChar = l.getDisplayedMnemonic(); - g.setColor(l.getForeground()); - drawString(g, s, accChar, textX, textY); - } - - public String[] splitStringByLines(String str) { - if (str.equals(this.str)) - return strs; - - this.str = str; - - int lines = 1; - int i, c; - for (i = 0, c = str.length(); i < c; i++) { - if (str.charAt(i) == '\n') - lines++; - } - strs = new String[lines]; - StringTokenizer st = new StringTokenizer(str, "\n"); - - int line = 0; - while (st.hasMoreTokens()) - strs[line++] = st.nextToken(); - - return strs; - } -} diff --git a/src/com/github/xsavikx/android/screencast/ui/explorer/JFrameExplorer.java b/src/com/github/xsavikx/android/screencast/ui/explorer/JFrameExplorer.java deleted file mode 100644 index b24c569..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/explorer/JFrameExplorer.java +++ /dev/null @@ -1,150 +0,0 @@ -package com.github.xsavikx.android.screencast.ui.explorer; - -import java.awt.BorderLayout; -import java.awt.Desktop; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.io.File; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Vector; - -import javax.swing.JFrame; -import javax.swing.JList; -import javax.swing.JScrollPane; -import javax.swing.JSplitPane; -import javax.swing.JTree; -import javax.swing.ListModel; -import javax.swing.event.TreeSelectionEvent; -import javax.swing.event.TreeSelectionListener; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.TreePath; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import com.github.xsavikx.android.screencast.api.AndroidDevice; -import com.github.xsavikx.android.screencast.api.file.FileInfo; - -@Component -public class JFrameExplorer extends JFrame { - - private class FolderTreeNode extends LazyMutableTreeNode { - private static final long serialVersionUID = 9131974430354670263L; - String name; - String path; - - public FolderTreeNode(String name, String path) { - this.name = name; - this.path = path; - } - - @Override - public void initChildren() { - List fileInfos = cache.get(path); - if (fileInfos == null) - fileInfos = androidDevice.list(path); - for (FileInfo fi : fileInfos) { - if (fi.directory) - add(new FolderTreeNode(fi.name, path + fi.name + "/")); - // else - // add(new FileTreeNode(fi)); - } - } - - @Override - public String toString() { - return name; - } - - } - - private static final long serialVersionUID = -5209265873286028854L; - private JTree jt; - private JSplitPane jSplitPane; - @Autowired - private AndroidDevice androidDevice; - - private JList jListFichiers; - - private Map> cache = new LinkedHashMap>(); - - public JFrameExplorer() { - - setTitle("Explorer"); - setLayout(new BorderLayout()); - - jt = new JTree(new DefaultMutableTreeNode("Test")); - } - - public void launch() { - - jt.setModel(new DefaultTreeModel(new FolderTreeNode("Device", "/"))); - jt.setRootVisible(true); - jt.addTreeSelectionListener(new TreeSelectionListener() { - - @Override - public void valueChanged(TreeSelectionEvent e) { - TreePath tp = e.getPath(); - if (tp == null) - return; - if (!(tp.getLastPathComponent() instanceof FolderTreeNode)) - return; - FolderTreeNode node = (FolderTreeNode) tp.getLastPathComponent(); - displayFolder(node.path); - } - }); - - JScrollPane jsp = new JScrollPane(jt); - - jListFichiers = new JList(); - jListFichiers.setListData(new Object[] {}); - - jSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, jsp, new JScrollPane(jListFichiers)); - - add(jSplitPane, BorderLayout.CENTER); - setSize(640, 480); - setLocationRelativeTo(null); - - jListFichiers.addMouseListener(new MouseAdapter() { - - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - int index = jListFichiers.locationToIndex(e.getPoint()); - ListModel dlm = jListFichiers.getModel(); - FileInfo item = (FileInfo) dlm.getElementAt(index); - launchFile(item); - } - } - - }); - } - - private void displayFolder(String path) { - List fileInfos = cache.get(path); - if (fileInfos == null) - fileInfos = androidDevice.list(path); - - List files = new Vector(); - for (FileInfo fi2 : fileInfos) { - if (fi2.directory) - continue; - files.add(fi2); - } - jListFichiers.setListData(files.toArray()); - - } - - private void launchFile(FileInfo node) { - try { - File tempFile = node.downloadTemporary(); - Desktop.getDesktop().open(tempFile); - } catch (Exception ex) { - throw new RuntimeException(ex); - } - } - -} diff --git a/src/com/github/xsavikx/android/screencast/ui/explorer/LazyLoadingTreeNode.java b/src/com/github/xsavikx/android/screencast/ui/explorer/LazyLoadingTreeNode.java deleted file mode 100644 index bd886dc..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/explorer/LazyLoadingTreeNode.java +++ /dev/null @@ -1,291 +0,0 @@ -package com.github.xsavikx.android.screencast.ui.explorer; - -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.util.Iterator; -import java.util.Vector; -import java.util.concurrent.ExecutionException; - -import javax.swing.AbstractAction; -import javax.swing.Action; -import javax.swing.JTree; -import javax.swing.KeyStroke; -import javax.swing.SwingUtilities; -import javax.swing.event.TreeExpansionEvent; -import javax.swing.event.TreeWillExpandListener; -import javax.swing.tree.DefaultMutableTreeNode; -import javax.swing.tree.DefaultTreeModel; -import javax.swing.tree.MutableTreeNode; -import javax.swing.tree.TreeModel; - -import com.github.xsavikx.android.screencast.ui.worker.SwingWorker; - -public abstract class LazyLoadingTreeNode extends DefaultMutableTreeNode implements TreeWillExpandListener { - - /** - * 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. - * - * @author Thierry LEFORT 3 mars 08 - * - */ - protected static class CancelWorkersAction extends AbstractAction { - /** - * - */ - private static final long serialVersionUID = 3173288834368915117L; - /** the SwingWorkers */ - private Vector> workers = new Vector>(); - - /** Default constructor */ - private CancelWorkersAction() { - super(ESCAPE_ACTION_NAME); - } - - /** Do the Cancel */ - @Override - public void actionPerformed(ActionEvent e) { - Iterator> it = workers.iterator(); - while (it.hasNext()) { - SwingWorker worker = it.next(); - worker.cancel(true); - } - - } - - /** Add a Cancelable SwingWorker */ - public void addSwingWorker(SwingWorker worker) { - workers.add(worker); - } - - /** Remove a SwingWorker */ - public void removeSwingWorker(SwingWorker worker) { - workers.remove(worker); - } - - } - - /** - * - */ - private static final long serialVersionUID = -4981073521761764327L; - - private static final String ESCAPE_ACTION_NAME = "escape"; - - private static final KeyStroke ESCAPE_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); - - /** The JTree containing this Node */ - private JTree tree; - /** Can the worker be Canceled ? */ - private boolean cancelable; - - /** - * Default Constructor - * - * @param userObject - * an Object provided by the user that constitutes the node's data - * @param tree - * the JTree containing this Node - * @param cancelable - */ - public LazyLoadingTreeNode(Object userObject, JTree tree, boolean cancelable) { - super(userObject); - tree.addTreeWillExpandListener(this); - this.tree = tree; - this.cancelable = cancelable; - setAllowsChildren(true); - } - - /** - * - * @return true if there are some childrens - */ - protected boolean areChildrenLoaded() { - return getChildCount() > 0 && getAllowsChildren(); - } - - /** - * - * @return a new Loading please wait node - */ - protected MutableTreeNode createLoadingNode() { - return new DefaultMutableTreeNode("Loading Please Wait ...", false); - } - - /** - * Create worker that will load the nodes - * - * @param tree - * the tree - * @return the newly created SwingWorker - */ - protected SwingWorker createSwingWorker(final JTree tree) { - - SwingWorker worker = new SwingWorker() { - - @Override - protected MutableTreeNode[] doInBackground() { - return loadChildren(tree); - } - - @Override - protected void done() { - try { - if (!isCancelled()) { - MutableTreeNode[] nodes = get(); - setAllowsChildren(nodes.length > 0); - setChildren(nodes); - unRegisterSwingWorkerForCancel(tree, this); - } else { - reset(); - } - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (ExecutionException e) { - e.printStackTrace(); - } - - } - - }; - registerSwingWorkerForCancel(tree, worker); - return worker; - } - - /** - * If the - * - * @see #getAllowsChildren() - * @return false, this node can't be a leaf - */ - @Override - public boolean isLeaf() { - return !getAllowsChildren(); - } - - /** - * This method will be executed in a background thread. If you have to do some GUI stuff use {@link SwingUtilities#invokeLater(Runnable)} - * - * @param tree - * the tree - * @return the Created nodes - */ - public abstract MutableTreeNode[] loadChildren(JTree tree); - - /** - * If the node is cancelable an escape Action is registered in the tree's InputMap and ActionMap that will cancel the execution - * - * @param tree - * the tree - * @param worker - * the worker to cancel - */ - protected void registerSwingWorkerForCancel(JTree tree, SwingWorker worker) { - if (!cancelable) { - return; - } - tree.getInputMap().put(ESCAPE_KEY, ESCAPE_ACTION_NAME); - Action action = tree.getActionMap().get(ESCAPE_ACTION_NAME); - if (action == null) { - CancelWorkersAction cancelWorkerAction = new CancelWorkersAction(); - cancelWorkerAction.addSwingWorker(worker); - tree.getActionMap().put(ESCAPE_ACTION_NAME, cancelWorkerAction); - } else { - if (action instanceof CancelWorkersAction) { - CancelWorkersAction cancelAction = (CancelWorkersAction) action; - cancelAction.addSwingWorker(worker); - } - } - } - - /** - * Need some improvement ... This method should restore the Node initial state if the worker if canceled - */ - protected void reset() { - DefaultTreeModel defaultModel = (DefaultTreeModel) tree.getModel(); - int childCount = getChildCount(); - if (childCount > 0) { - for (int i = 0; i < childCount; i++) { - defaultModel.removeNodeFromParent((MutableTreeNode) getChildAt(0)); - } - } - setAllowsChildren(true); - } - - /** - * Define nodes children - * - * @param nodes - * new nodes - */ - protected void setChildren(MutableTreeNode... nodes) { - TreeModel model = tree.getModel(); - if (model instanceof DefaultTreeModel) { - DefaultTreeModel defaultModel = (DefaultTreeModel) model; - int childCount = getChildCount(); - if (childCount > 0) { - for (int i = 0; i < childCount; i++) { - defaultModel.removeNodeFromParent((MutableTreeNode) getChildAt(0)); - } - } - for (int i = 0; i < nodes.length; i++) { - defaultModel.insertNodeInto(nodes[i], this, i); - } - } - } - - /** - * set the loading state - */ - private void setLoading() { - setChildren(createLoadingNode()); - TreeModel model = tree.getModel(); - if (model instanceof DefaultTreeModel) { - DefaultTreeModel defaultModel = (DefaultTreeModel) model; - int[] indices = new int[getChildCount()]; - for (int i = 0; i < indices.length; i++) { - indices[i] = i; - } - defaultModel.nodesWereInserted(LazyLoadingTreeNode.this, indices); - } - } - - /** - * Default empty implementation, do nothing on collapse event. - */ - @Override - public void treeWillCollapse(TreeExpansionEvent event) { - // ignore - } - - /** - * Node will expand, it's time to retrieve nodes - */ - @Override - public void treeWillExpand(TreeExpansionEvent event) { - if (this.equals(event.getPath().getLastPathComponent())) { - if (areChildrenLoaded()) { - return; - } - setLoading(); - SwingWorker worker = createSwingWorker(tree); - worker.execute(); - } - } - - /** - * Remove the swingWorker from the cancellable task of the tree - * - * @param tree - * @param worker - */ - protected void unRegisterSwingWorkerForCancel(JTree tree, SwingWorker worker) { - if (!cancelable) { - return; - } - Action action = tree.getActionMap().get(ESCAPE_ACTION_NAME); - if (action != null && action instanceof CancelWorkersAction) { - CancelWorkersAction cancelWorkerAction = new CancelWorkersAction(); - cancelWorkerAction.removeSwingWorker(worker); - } - } -} diff --git a/src/com/github/xsavikx/android/screencast/ui/explorer/LazyMutableTreeNode.java b/src/com/github/xsavikx/android/screencast/ui/explorer/LazyMutableTreeNode.java deleted file mode 100644 index d83c9e3..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/explorer/LazyMutableTreeNode.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.github.xsavikx.android.screencast.ui.explorer; - -import javax.swing.tree.DefaultMutableTreeNode; - -public abstract class LazyMutableTreeNode extends DefaultMutableTreeNode { - - private static final long serialVersionUID = -6383034137965603498L; - protected boolean _loaded = false; - - public LazyMutableTreeNode() { - super(); - } - - public LazyMutableTreeNode(Object userObject) { - super(userObject); - } - - public LazyMutableTreeNode(Object userObject, boolean allowsChildren) { - super(userObject, allowsChildren); - } - - public void clear() { - removeAllChildren(); - _loaded = false; - } - - @Override - public int getChildCount() { - synchronized (this) { - if (!_loaded) { - _loaded = true; - initChildren(); - } - } - return super.getChildCount(); - } - - protected abstract void initChildren(); - - public boolean isLoaded() { - return _loaded; - } - -} \ No newline at end of file diff --git a/src/com/github/xsavikx/android/screencast/ui/interaction/KeyEventDispatcherFactory.java b/src/com/github/xsavikx/android/screencast/ui/interaction/KeyEventDispatcherFactory.java deleted file mode 100644 index 9a111cd..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/interaction/KeyEventDispatcherFactory.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.github.xsavikx.android.screencast.ui.interaction; - -import java.awt.KeyEventDispatcher; -import java.awt.Window; - -public final class KeyEventDispatcherFactory { - public static KeyEventDispatcher getKeyEventDispatcher(Window frame) { - return new KeyEventDispatcherImpl(frame); - } -} diff --git a/src/com/github/xsavikx/android/screencast/ui/interaction/KeyEventDispatcherImpl.java b/src/com/github/xsavikx/android/screencast/ui/interaction/KeyEventDispatcherImpl.java deleted file mode 100644 index d4f3b4c..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/interaction/KeyEventDispatcherImpl.java +++ /dev/null @@ -1,47 +0,0 @@ -package com.github.xsavikx.android.screencast.ui.interaction; - -import java.awt.KeyEventDispatcher; -import java.awt.Window; -import java.awt.event.KeyEvent; - -import javax.swing.SwingUtilities; - -import com.github.xsavikx.android.screencast.api.command.executor.CommandExecutor; -import com.github.xsavikx.android.screencast.api.command.factory.AdbInputCommandFactory; -import com.github.xsavikx.android.screencast.api.injector.KeyCodeConverter; -import com.github.xsavikx.android.screencast.spring.config.ApplicationContextProvider; - -public class KeyEventDispatcherImpl implements KeyEventDispatcher { - private CommandExecutor commandExecutor; - private Window window; - - public KeyEventDispatcherImpl(Window frame) { - this.window = frame; - } - - @Override - public boolean dispatchKeyEvent(KeyEvent e) { - if (!window.isActive()) - return false; - if (e.getID() == KeyEvent.KEY_TYPED) { - final int code = KeyCodeConverter.getKeyCode(e); - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - getCommandExecutor().execute(AdbInputCommandFactory.getKeyCommand(code)); - - } - }); - - } - return false; - } - - private CommandExecutor getCommandExecutor() { - if (commandExecutor == null) { - commandExecutor = ApplicationContextProvider.getApplicationContext().getBean(CommandExecutor.class); - } - return commandExecutor; - } -} diff --git a/src/com/github/xsavikx/android/screencast/ui/interaction/KeyboardActionListener.java b/src/com/github/xsavikx/android/screencast/ui/interaction/KeyboardActionListener.java deleted file mode 100644 index 05db73b..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/interaction/KeyboardActionListener.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.github.xsavikx.android.screencast.ui.interaction; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; - -import javax.swing.SwingUtilities; - -import com.github.xsavikx.android.screencast.api.command.executor.CommandExecutor; -import com.github.xsavikx.android.screencast.api.command.factory.AdbInputCommandFactory; -import com.github.xsavikx.android.screencast.spring.config.ApplicationContextProvider; - -public class KeyboardActionListener implements ActionListener { - private CommandExecutor commandExecutor; - private int key; - - public KeyboardActionListener(int key) { - this.key = key; - } - - @Override - public void actionPerformed(ActionEvent e) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - getCommandExecutor().execute(AdbInputCommandFactory.getKeyCommand(key)); - } - }); - - } - - private CommandExecutor getCommandExecutor() { - if (commandExecutor == null) { - commandExecutor = ApplicationContextProvider.getApplicationContext().getBean(CommandExecutor.class); - } - return commandExecutor; - } - -} diff --git a/src/com/github/xsavikx/android/screencast/ui/interaction/KeyboardActionListenerFactory.java b/src/com/github/xsavikx/android/screencast/ui/interaction/KeyboardActionListenerFactory.java deleted file mode 100644 index a6061fb..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/interaction/KeyboardActionListenerFactory.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.github.xsavikx.android.screencast.ui.interaction; - -import com.github.xsavikx.android.screencast.api.injector.InputKeyEvent; - -public final class KeyboardActionListenerFactory { - public static KeyboardActionListener getInstance(InputKeyEvent inputKeyEvent) { - return new KeyboardActionListener(inputKeyEvent.getCode()); - } -} diff --git a/src/com/github/xsavikx/android/screencast/ui/interaction/MouseActionAdapter.java b/src/com/github/xsavikx/android/screencast/ui/interaction/MouseActionAdapter.java deleted file mode 100644 index 1022adf..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/interaction/MouseActionAdapter.java +++ /dev/null @@ -1,99 +0,0 @@ -package com.github.xsavikx.android.screencast.ui.interaction; - -import java.awt.Point; -import java.awt.event.MouseAdapter; -import java.awt.event.MouseEvent; -import java.awt.event.MouseWheelEvent; - -import javax.swing.SwingUtilities; - -import com.github.xsavikx.android.screencast.api.command.executor.CommandExecutor; -import com.github.xsavikx.android.screencast.api.command.factory.AdbInputCommandFactory; -import com.github.xsavikx.android.screencast.api.injector.Injector; -import com.github.xsavikx.android.screencast.spring.config.ApplicationContextProvider; -import com.github.xsavikx.android.screencast.ui.JPanelScreen; - -public class MouseActionAdapter extends MouseAdapter { - private CommandExecutor commandExecutor; - private final JPanelScreen jp; - private Injector injector; - private int dragFromX = -1; - - private int dragFromY = -1; - private long timeFromPress = -1; - private final static long ONE_SECOND = 1000L; - - public MouseActionAdapter(JPanelScreen jPanelScreen) { - this.jp = jPanelScreen; - } - - public MouseActionAdapter(JPanelScreen jPanelScreen, Injector injector) { - this(jPanelScreen); - this.injector = injector; - } - - @Override - public void mouseClicked(MouseEvent e) { - if (injector != null && e.getButton() == MouseEvent.BUTTON3) { - injector.screencapture.toogleOrientation(); - e.consume(); - return; - } - final Point p2 = jp.getRawPoint(e.getPoint()); - if (p2.x > 0 && p2.y > 0) { - SwingUtilities.invokeLater(new Runnable() { - @Override - public void run() { - getCommandExecutor().execute(AdbInputCommandFactory.getTapCommand(p2.x, p2.y)); - - } - }); - } - } - - @Override - public void mouseDragged(MouseEvent e) { - if (dragFromX == -1 && dragFromY == -1) { - Point p2 = jp.getRawPoint(e.getPoint()); - dragFromX = p2.x; - dragFromY = p2.y; - timeFromPress = System.currentTimeMillis(); - } - } - - @Override - public void mouseReleased(MouseEvent e) { - if (timeFromPress >= ONE_SECOND) { - final Point p2 = jp.getRawPoint(e.getPoint()); - final int xFrom = dragFromX; - final int yFrom = dragFromY; - final int xTo = p2.x; - final int yTo = p2.y; - SwingUtilities.invokeLater(new Runnable() { - - @Override - public void run() { - getCommandExecutor().execute(AdbInputCommandFactory.getSwipeCommand(xFrom, yFrom, xTo, yTo)); - } - }); - dragFromX = -1; - dragFromY = -1; - timeFromPress = -1; - } - } - - @Override - public void mouseWheelMoved(MouseWheelEvent arg0) { - // if (JFrameMain.this.injector == null) - // return; - // JFrameMain.this.injector.injectTrackball(arg0.getWheelRotation() < 0 ? - // -1f : 1f); - } - - private CommandExecutor getCommandExecutor() { - if (commandExecutor == null) { - commandExecutor = ApplicationContextProvider.getApplicationContext().getBean(CommandExecutor.class); - } - return commandExecutor; - } -} diff --git a/src/com/github/xsavikx/android/screencast/ui/interaction/MouseActionAdapterFactory.java b/src/com/github/xsavikx/android/screencast/ui/interaction/MouseActionAdapterFactory.java deleted file mode 100644 index 407526b..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/interaction/MouseActionAdapterFactory.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.github.xsavikx.android.screencast.ui.interaction; - -import com.github.xsavikx.android.screencast.api.injector.Injector; -import com.github.xsavikx.android.screencast.ui.JPanelScreen; - -public final class MouseActionAdapterFactory { - public static MouseActionAdapter getInstance(JPanelScreen jPanelScreen) { - return new MouseActionAdapter(jPanelScreen); - } - - public static MouseActionAdapter getInstance(JPanelScreen jPanelScreen, Injector injector) { - return new MouseActionAdapter(jPanelScreen, injector); - } -} diff --git a/src/com/github/xsavikx/android/screencast/ui/model/InputKeyEventTable.java b/src/com/github/xsavikx/android/screencast/ui/model/InputKeyEventTable.java deleted file mode 100644 index efb0d19..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/model/InputKeyEventTable.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.github.xsavikx.android.screencast.ui.model; - -import java.awt.Component; - -import javax.swing.JTable; -import javax.swing.ListSelectionModel; -import javax.swing.table.TableCellRenderer; -import javax.swing.table.TableColumn; -import javax.swing.table.TableColumnModel; - -public class InputKeyEventTable extends JTable { - private static final long serialVersionUID = 3978642864003531967L; - - private final static int MIN_COLUMN_WIDTH = 20; - - public InputKeyEventTable(InputKeyEventTableModel tableModel) { - super(tableModel); - setTableColumnsNames(tableModel.columnNames); - setAutoResizeMode(JTable.AUTO_RESIZE_OFF); - setTableColumnsPrefferedSize(); - setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - } - - private void setTableColumnsNames(String[] columnNames) { - TableColumnModel columnModel = getColumnModel(); - for (int i = 0; i < columnNames.length; i++) { - TableColumn column = columnModel.getColumn(i); - column.setHeaderValue(columnNames[i]); - } - } - - private void setTableColumnsPrefferedSize() { - final TableColumnModel columnModel = getColumnModel(); - for (int column = 0; column < getColumnCount(); column++) { - int width = MIN_COLUMN_WIDTH; // Min width - for (int row = 0; row < getRowCount(); row++) { - TableCellRenderer renderer = getCellRenderer(row, column); - Component comp = prepareRenderer(renderer, row, column); - width = Math.max(comp.getPreferredSize().width + 5, width); - } - columnModel.getColumn(column).setPreferredWidth(width); - } - } - -} diff --git a/src/com/github/xsavikx/android/screencast/ui/model/InputKeyEventTableModel.java b/src/com/github/xsavikx/android/screencast/ui/model/InputKeyEventTableModel.java deleted file mode 100644 index 3be911f..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/model/InputKeyEventTableModel.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.github.xsavikx.android.screencast.ui.model; - -import java.util.ArrayList; -import java.util.List; - -import javax.swing.table.AbstractTableModel; - -import com.github.xsavikx.android.screencast.api.injector.InputKeyEvent; - -public class InputKeyEventTableModel extends AbstractTableModel { - private static final long serialVersionUID = 1553313932570896541L; - private final static String INDEX_COLUMN_NAME = "#"; - private final static String TITLE_COLUMN_NAME = "title"; - private final static String DESCRIPTION_COLUMN_NAME = "description"; - public final String[] columnNames = { - INDEX_COLUMN_NAME, TITLE_COLUMN_NAME, DESCRIPTION_COLUMN_NAME - }; - private List> data = new ArrayList<>(); - private int rowCount = 1; - - public InputKeyEventTableModel(InputKeyEvent[] initialData) { - initData(initialData); - } - - private void initData(InputKeyEvent[] inputKeyEvents) { - for (InputKeyEvent e : inputKeyEvents) { - data.add(getDataRow(e)); - } - } - - private List getDataRow(InputKeyEvent e) { - List row = new ArrayList<>(); - row.add(rowCount++); - row.add(e.name()); - row.add(e.getDescription()); - return row; - } - - @Override - public int getRowCount() { - return data.size(); - } - - @Override - public int getColumnCount() { - return columnNames.length; - } - - @Override - public Object getValueAt(int rowIndex, int columnIndex) { - return data.get(rowIndex).get(columnIndex); - } - -} diff --git a/src/com/github/xsavikx/android/screencast/ui/worker/AccumulativeRunnable.java b/src/com/github/xsavikx/android/screencast/ui/worker/AccumulativeRunnable.java deleted file mode 100644 index e9654a3..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/worker/AccumulativeRunnable.java +++ /dev/null @@ -1,156 +0,0 @@ -package com.github.xsavikx.android.screencast.ui.worker; - -/* - * $Id: AccumulativeRunnable.java,v 1.3 2008/07/25 19:32:29 idk Exp $ - * - * Copyright @ 2005 Sun Microsystems, Inc. All rights - * reserved. Use is subject to license terms. - */ - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - -import javax.swing.SwingUtilities; - -/** - * 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 - * might be appended after the {@code Runnable} is sent for the execution. Usually such {@code Runnables} are sent to the EDT. - * - *

- * Usage example: - * - *

- * 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 - * 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 - * updates overrides the previous one.) We might want to implement this {@code setText} in a way that only the last update is delivered. - *

- * Here is how one can do this using {@code AccumulativeRunnable}: - * - *

- * AccumulativeRunnable doSetTextImpl = 
- * new  AccumulativeRunnable() {
- *     @Override 
- *     protected void run(List<String> args) {
- *         //set to the last string being passed
- *         setTextImpl(args.get(args.size() - 1);
- *     }
- * }
- * void setText(String text) {
- *     //add text and send for the execution if needed.
- *     doSetTextImpl.add(text);
- * }
- * 
- * - *

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

- * Here is how it can be implemented using AccumulativeRunnable: - * - *

- * AccumulativeRunnable<Rectangle> doHandleDirtyRegions = new AccumulativeRunnable<Rectangle>() {
- *   @Override
- *   protected void run(List<Rectangle> args) {
- *     handleDirtyRegions(args);
- *   }
- * };
- * 
- * void addDirtyRegion(Rectangle rect) {
- *   doHandleDirtyRegions.add(rect);
- * }
- * 
- * - * @author Igor Kushnirskiy - * @version $Revision: 1.3 $ $Date: 2008/07/25 19:32:29 $ - * - * @param - * the type this {@code Runnable} accumulates - * - */ -abstract class AccumulativeRunnable implements Runnable { - private List arguments = null; - - /** - * prepends or appends arguments and sends this {@code Runnable} for the execution if needed. - *

- * This implementation uses {@see #submit} to send this {@code Runnable} for execution. - * - * @param isPrepend - * prepend or append - * @param args - * the arguments to add - */ - public final synchronized void add(boolean isPrepend, T... args) { - boolean isSubmitted = true; - if (arguments == null) { - isSubmitted = false; - arguments = new ArrayList(); - } - if (isPrepend) { - arguments.addAll(0, Arrays.asList(args)); - } else { - Collections.addAll(arguments, args); - } - if (!isSubmitted) { - submit(); - } - } - - /** - * appends arguments and sends this {@code Runnable} for the execution if needed. - *

- * This implementation uses {@see #submit} to send this {@code Runnable} for execution. - * - * @param args - * the arguments to accumulate - */ - public final void add(T... args) { - add(false, args); - } - - /** - * Returns accumulated arguments and flashes the arguments storage. - * - * @return accumulated arguments - */ - private final synchronized List flush() { - List list = arguments; - arguments = null; - return list; - } - - /** - * {@inheritDoc} - * - *

- * This implementation calls {@code run(List args)} method with the list of accumulated arguments. - */ - @Override - public final void run() { - run(flush()); - } - - /** - * Equivalent to {@code Runnable.run} method with the accumulated arguments to process. - * - * @param args - * accumulated arguments to process. - */ - protected abstract void run(List args); - - /** - * Sends this {@code Runnable} for the execution - * - *

- * This method is to be executed only from {@code add} method. - * - *

- * This implementation uses {@code SwingWorker.invokeLater}. - */ - protected void submit() { - SwingUtilities.invokeLater(this); - } -} diff --git a/src/com/github/xsavikx/android/screencast/ui/worker/SwingWorker.java b/src/com/github/xsavikx/android/screencast/ui/worker/SwingWorker.java deleted file mode 100644 index 1260698..0000000 --- a/src/com/github/xsavikx/android/screencast/ui/worker/SwingWorker.java +++ /dev/null @@ -1,829 +0,0 @@ -/* - * $Id: SwingWorker.java,v 1.6 2008/07/25 19:32:29 idk Exp $ - * - * Copyright @ 2005 Sun Microsystems, Inc. All rights - * reserved. Use is subject to license terms. - */ - -package com.github.xsavikx.android.screencast.ui.worker; - -import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; -import java.beans.PropertyChangeEvent; -import java.beans.PropertyChangeListener; -import java.beans.PropertyChangeSupport; -import java.util.List; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Future; -import java.util.concurrent.FutureTask; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; -import java.util.concurrent.atomic.AtomicInteger; -import java.util.concurrent.locks.Condition; -import java.util.concurrent.locks.ReentrantLock; - -import javax.swing.SwingUtilities; -import javax.swing.Timer; - -/** - * An abstract class to perform lengthy GUI-interacting tasks in a dedicated thread. - * - *

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

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

- * - *

- * 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 - * 2) the Event Dispatch Thread (EDT) for all GUI-related activities. This involves inter-thread communication which can be tricky to - * implement. - * - *

- * {@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 - * either when done, or while processing. Subclasses of {@code SwingWorker} must implement the {@see #doInBackground} method to perform the background - * computation. - * - * - *

- * Workflow - *

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

    - *
  • - *

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

  • - *

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

  • - *

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

- * - *

- * Often, the Current thread is the Event Dispatch Thread. - * - * - *

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

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

- * Sample Usage - *

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

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

- *   final JLabel label;
- *   class MeaningOfLifeFinder extends SwingWorker<String, Object> {
- *       {@code @Override}
- *       public String doInBackground() {
- *           return findTheMeaningOfLife();
- *       }
- * 
- *       {@code @Override}
- *       protected void done() {
- *           try { 
- *               label.setText(get());
- *           } catch (Exception ignore) {
- *           }
- *       }
- *   }
- * 
- *   (new MeaningOfLifeFinder()).execute();
- * 
- * - *

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

- * 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 - * progress in a {@code JProgressBar}. Finally, we also want to print the prime numbers to {@code System.out}. - * - *

- * class PrimeNumbersTask extends 
- *         SwingWorker<List<Integer>, Integer> {
- *     PrimeNumbersTask(JTextArea textArea, int numbersToFind) { 
- *         //initialize 
- *     }
- * 
- *     {@code @Override}
- *     public List<Integer> doInBackground() {
- *         while (! enough && ! isCancelled()) {
- *                 number = nextPrimeNumber();
- *                 publish(number);
- *                 setProgress(100 * numbers.size() / numbersToFind);
- *             }
- *         }
- *         return numbers;
- *     }
- * 
- *     {@code @Override}
- *     protected void process(List<Integer> chunks) { 
- *         for (int number : chunks) {
- *             textArea.append(number + "\n");
- *         }
- *     }
- * }
- * 
- * JTextArea textArea = new JTextArea();
- * final JProgressBar progressBar = new JProgressBar(0, 100);
- * PrimeNumbersTask task = new PrimeNumbersTask(textArea, N);
- * task.addPropertyChangeListener(
- *     new PropertyChangeListener() {
- *         public  void propertyChange(PropertyChangeEvent evt) {
- *             if ("progress".equals(evt.getPropertyName())) {
- *                 progressBar.setValue((Integer)evt.getNewValue());
- *             }
- *         }
- *     });
- * 
- * task.execute();
- * System.out.println(task.get()); //prints all prime numbers we have got
- * 
- * - *

- * Because {@code SwingWorker} implements {@code Runnable}, a {@code SwingWorker} can be submitted to an {@link java.util.concurrent.Executor} for - * execution. - * - * @author Igor Kushnirskiy - * @version $Revision: 1.6 $ $Date: 2008/07/25 19:32:29 $ - * - * @param - * the result type returned by this {@code SwingWorker's} {@code doInBackground} and {@code get} methods - * @param - * the type used for carrying out intermediate results by this {@code SwingWorker's} {@code publish} and {@code process} methods - * - */ -public abstract class SwingWorker implements Future, Runnable { - private static class DoSubmitAccumulativeRunnable extends AccumulativeRunnableimplements ActionListener { - private final static int DELAY = 1000 / 30; - - @Override - public void actionPerformed(ActionEvent event) { - run(); - } - - @Override - protected void run(List args) { - int i = 0; - try { - for (Runnable runnable : args) { - i++; - runnable.run(); - } - } finally { - if (i < args.size()) { - /* - * there was an exception schedule all the unhandled items for the next time - */ - Runnable argsTail[] = new Runnable[args.size() - i]; - for (int j = 0; j < argsTail.length; j++) { - argsTail[j] = args.get(i + j); - } - add(true, argsTail); - } - } - } - - @Override - protected void submit() { - Timer timer = new Timer(DELAY, this); - timer.setRepeats(false); - timer.start(); - } - } - - /** - * Values for the {@code state} bound property. - */ - public enum StateValue { - /** - * Initial {@code SwingWorker} state. - */ - PENDING, /** - * {@code SwingWorker} is {@code STARTED} before invoking {@code doInBackground}. - */ - STARTED, - - /** - * {@code SwingWorker} is {@code DONE} after {@code doInBackground} method is finished. - */ - DONE - } - - private class SwingWorkerPropertyChangeSupport extends PropertyChangeSupport { - private static final long serialVersionUID = 2409754725172747617L; - - SwingWorkerPropertyChangeSupport(Object source) { - super(source); - } - - @Override - public void firePropertyChange(final PropertyChangeEvent evt) { - if (SwingUtilities.isEventDispatchThread()) { - super.firePropertyChange(evt); - } else { - doSubmit.add(new Runnable() { - @Override - public void run() { - SwingWorkerPropertyChangeSupport.this.firePropertyChange(evt); - } - }); - } - } - } - - /** - * returns workersExecutorService. - * - * returns the service stored in the appContext or creates it if necessary. If the last one it triggers autoShutdown thread to get started. - * - * @return ExecutorService for the {@code SwingWorkers} - * @see #startAutoShutdownThread - */ - private static synchronized ExecutorService getWorkersExecutorService() { - if (executorService == null) { - // this creates non-daemon threads. - ThreadFactory threadFactory = new ThreadFactory() { - final AtomicInteger threadNumber = new AtomicInteger(1); - - @Override - public Thread newThread(final Runnable r) { - StringBuilder name = new StringBuilder("SwingWorker-pool-"); - name.append(System.identityHashCode(this)); - name.append("-thread-"); - name.append(threadNumber.getAndIncrement()); - - Thread t = new Thread(r, name.toString()); - ; - if (t.isDaemon()) - t.setDaemon(false); - if (t.getPriority() != Thread.NORM_PRIORITY) - t.setPriority(Thread.NORM_PRIORITY); - return t; - } - }; - - /* - * We want a to have no more than MAX_WORKER_THREADS running threads. - * - * We want a worker thread to wait no longer than 1 second for new tasks before terminating. - */ - executorService = new ThreadPoolExecutor(0, MAX_WORKER_THREADS, 5L, TimeUnit.SECONDS, - new LinkedBlockingQueue(), threadFactory) { - - private final ReentrantLock pauseLock = new ReentrantLock(); - private final Condition unpaused = pauseLock.newCondition(); - private boolean isPaused = false; - private final ReentrantLock executeLock = new ReentrantLock(); - - @Override - protected void afterExecute(Runnable r, Throwable t) { - super.afterExecute(r, t); - pauseLock.lock(); - try { - while (isPaused) { - unpaused.await(); - } - } catch (InterruptedException ignore) { - - } finally { - pauseLock.unlock(); - } - } - - @Override - public void execute(Runnable command) { - /* - * 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 - * it run task in maximumPool. - * - * 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 - * 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. - * - * 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. - * - * While we changing the corePoolSize we need to stop running worker threads from accepting new tasks. - */ - - // we need atomicity for the execute method. - executeLock.lock(); - try { - - pauseLock.lock(); - try { - isPaused = true; - } finally { - pauseLock.unlock(); - } - - setCorePoolSize(MAX_WORKER_THREADS); - super.execute(command); - setCorePoolSize(0); - - pauseLock.lock(); - try { - isPaused = false; - unpaused.signalAll(); - } finally { - pauseLock.unlock(); - } - } finally { - executeLock.unlock(); - } - } - }; - } - return executorService; - } - - /** - * number of worker threads. - */ - private static final int MAX_WORKER_THREADS = 10; - - /** - * current progress. - */ - private volatile int progress; - - /** - * current state. - */ - private volatile StateValue state; - - /** - * everything is run inside this FutureTask. Also it is used as a delegatee for the Future API. - */ - private final FutureTask future; - - /** - * all propertyChangeSupport goes through this. - */ - private final PropertyChangeSupport propertyChangeSupport; - - /** - * handler for {@code process} method. - */ - private AccumulativeRunnable doProcess;; - - /** - * handler for progress property change notifications. - */ - private AccumulativeRunnable doNotifyProgressChange; - - private static final AccumulativeRunnable doSubmit = new DoSubmitAccumulativeRunnable(); - - private static ExecutorService executorService = null; - - /** - * Constructs this {@code SwingWorker}. - */ - public SwingWorker() { - Callable callable = new Callable() { - @Override - public T call() throws Exception { - setState(StateValue.STARTED); - return doInBackground(); - } - }; - - future = new FutureTask(callable) { - @Override - protected void done() { - doneEDT(); - setState(StateValue.DONE); - } - }; - - state = StateValue.PENDING; - propertyChangeSupport = new SwingWorkerPropertyChangeSupport(this); - doProcess = null; - doNotifyProgressChange = null; - } - - // PropertyChangeSupports methods START - /** - * Adds a {@code PropertyChangeListener} to the listener list. The listener is registered for all properties. The same listener object may be added - * 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 - * taken. - * - *

- * Note: This is merely a convenience wrapper. All work is delegated to {@code PropertyChangeSupport} from {@link #getPropertyChangeSupport}. - * - * @param listener - * the {@code PropertyChangeListener} to be added - */ - public final void addPropertyChangeListener(PropertyChangeListener listener) { - getPropertyChangeSupport().addPropertyChangeListener(listener); - } - - // Future methods START - /** - * {@inheritDoc} - */ - @Override - public final boolean cancel(boolean mayInterruptIfRunning) { - return future.cancel(mayInterruptIfRunning); - } - - /** - * Computes a result, or throws an exception if unable to do so. - * - *

- * Note that this method is executed only once. - * - *

- * Note: this method is executed in a background thread. - * - * - * @return the computed result - * @throws Exception - * if unable to compute a result - * - */ - protected abstract T doInBackground() throws Exception; - - /** - * Executed on the Event Dispatch Thread after the {@code doInBackground} method is finished. The default implementation does nothing. - * Subclasses may override this method to perform completion actions on the Event Dispatch Thread. Note that you can query status inside the - * implementation of this method to determine the result of this task or whether this task has been cancelled. - * - * @see #doInBackground - * @see #isCancelled() - * @see #get - */ - protected void done() { - } - - /** - * Invokes {@code done} on the EDT. - */ - private void doneEDT() { - Runnable doDone = new Runnable() { - @Override - public void run() { - done(); - } - }; - if (SwingUtilities.isEventDispatchThread()) { - doDone.run(); - } else { - doSubmit.add(doDone); - } - } - - /** - * Schedules this {@code SwingWorker} for execution on a worker thread. There are a number of worker threads available. In the event - * all worker threads are busy handling other {@code SwingWorkers} this {@code SwingWorker} is placed in a waiting queue. - * - *

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

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

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

- * Note: This is merely a convenience wrapper. All work is delegated to {@code PropertyChangeSupport} from {@link #getPropertyChangeSupport}. - * - * - * @param propertyName - * the programmatic name of the property that was changed - * @param oldValue - * the old value of the property - * @param newValue - * the new value of the property - */ - public final void firePropertyChange(String propertyName, Object oldValue, Object newValue) { - getPropertyChangeSupport().firePropertyChange(propertyName, oldValue, newValue); - } - - /** - * {@inheritDoc} - *

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

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

- * For example: - * - *

-   * class SwingWorkerCompletionWaiter implements PropertyChangeListener {
-   *   private JDialog dialog;
-   * 
-   *   public SwingWorkerCompletionWaiter(JDialog dialog) {
-   *     this.dialog = dialog;
-   *   }
-   * 
-   *   public void propertyChange(PropertyChangeEvent event) {
-   *     if ("state".equals(event.getPropertyName()) && SwingWorker.StateValue.DONE == event.getNewValue()) {
-   *       dialog.setVisible(false);
-   *       dialog.dispose();
-   *     }
-   *   }
-   * }
-   * JDialog dialog = new JDialog(owner, true);
-   * swingWorker.addPropertyChangeListener(new SwingWorkerCompletionWaiter(dialog));
-   * swingWorker.execute();
-   * // the dialog will be visible until the SwingWorker is done
-   * dialog.setVisible(true);
-   * 
- */ - @Override - public final T get() throws InterruptedException, ExecutionException { - return future.get(); - } - - /** - * {@inheritDoc} - *

- * Please refer to {@link #get} for more details. - */ - @Override - public final T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - return future.get(timeout, unit); - } - - /** - * Returns the {@code progress} bound property. - * - * @return the progress bound property. - */ - public final int getProgress() { - return progress; - } - - // Future methods END - - /** - * Returns the {@code PropertyChangeSupport} for this {@code SwingWorker}. This method is used when flexible access to bound properties support is - * needed. - *

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

- * Note: The returned {@code PropertyChangeSupport} notifies any {@code PropertyChangeListener}s asynchronously on the Event Dispatch Thread - * in the event that {@code firePropertyChange} or {@code fireIndexedPropertyChange} are called off the Event Dispatch Thread. - * - * @return {@code PropertyChangeSupport} for this {@code SwingWorker} - */ - public final PropertyChangeSupport getPropertyChangeSupport() { - return propertyChangeSupport; - } - - /** - * Returns the {@code SwingWorker} state bound property. - * - * @return the current state - */ - public final StateValue getState() { - /* - * DONE is a special case to keep getState and isDone is sync - */ - if (isDone()) { - return StateValue.DONE; - } else { - return state; - } - } - - /** - * {@inheritDoc} - */ - @Override - public final boolean isCancelled() { - return future.isCancelled(); - } - - /** - * {@inheritDoc} - */ - @Override - public final boolean isDone() { - return future.isDone(); - } - - // PropertyChangeSupports methods END - - /** - * Receives data chunks from the {@code publish} method asynchronously on the Event Dispatch Thread. - * - *

- * Please refer to the {@link #publish} method for more details. - * - * @param chunks - * intermediate results to process - * - * @see #publish - * - */ - protected void process(List chunks) { - } - - /** - * Sends data chunks to the {@link #process} method. This method is to be used from inside the {@code doInBackground} method to deliver intermediate - * results for processing on the Event Dispatch Thread inside the {@code process} method. - * - *

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

- * For example: - * - *

-   * publish("1");
-   * publish("2", "3");
-   * publish("4", "5", "6");
-   * 
- * - * might result in: - * - *
-   * process("1", "2", "3", "4", "5", "6")
-   * 
- * - *

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

-   * class TableSwingWorker extends 
-   *         SwingWorker<DefaultTableModel, Object[]> {
-   *     private final DefaultTableModel tableModel;
-   * 
-   *     public TableSwingWorker(DefaultTableModel tableModel) {
-   *         this.tableModel = tableModel;
-   *     }
-   * 
-   *     {@code @Override}
-   *     protected DefaultTableModel doInBackground() throws Exception {
-   *         for (Object[] row = loadData(); 
-   *                  ! isCancelled() && row != null; 
-   *                  row = loadData()) {
-   *             publish((Object[]) row);
-   *         }
-   *         return tableModel;
-   *     }
-   * 
-   *     {@code @Override}
-   *     protected void process(List<Object[]> chunks) {
-   *         for (Object[] row : chunks) {
-   *             tableModel.addRow(row);
-   *         }
-   *     }
-   * }
-   * 
- * - * @param chunks - * intermediate results to process - * - * @see #process - * - */ - protected final void publish(V... chunks) { - synchronized (this) { - if (doProcess == null) { - doProcess = new AccumulativeRunnable() { - @Override - public void run(List args) { - process(args); - } - - @Override - protected void submit() { - doSubmit.add(this); - } - }; - } - } - doProcess.add(chunks); - } - - /** - * Removes a {@code PropertyChangeListener} from the listener list. This removes a {@code PropertyChangeListener} that was registered for all - * 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 - * {@code listener} is {@code null}, or was never added, no exception is thrown and no action is taken. - * - *

- * Note: This is merely a convenience wrapper. All work is delegated to {@code PropertyChangeSupport} from {@link #getPropertyChangeSupport}. - * - * @param listener - * the {@code PropertyChangeListener} to be removed - */ - public final void removePropertyChangeListener(PropertyChangeListener listener) { - getPropertyChangeSupport().removePropertyChangeListener(listener); - } - - /** - * Sets this {@code Future} to the result of computation unless it has been cancelled. - */ - @Override - public final void run() { - future.run(); - } - - /** - * Sets the {@code progress} bound property. The value should be from 0 to 100. - * - *

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

- * For example, the following invocations: - * - *

-   * setProgress(1);
-   * setProgress(2);
-   * setProgress(3);
-   * 
- * - * might result in a single {@code PropertyChangeListener} notification with the value {@code 3}. - * - * @param progress - * the progress value to set - * @throws IllegalArgumentException - * is value not from 0 to 100 - */ - protected final void setProgress(int progress) { - if (progress < 0 || progress > 100) { - throw new IllegalArgumentException("the value should be from 0 to 100"); - } - if (this.progress == progress) { - return; - } - int oldProgress = this.progress; - this.progress = progress; - if (!getPropertyChangeSupport().hasListeners("progress")) { - return; - } - synchronized (this) { - if (doNotifyProgressChange == null) { - doNotifyProgressChange = new AccumulativeRunnable() { - @Override - public void run(List args) { - firePropertyChange("progress", args.get(0), args.get(args.size() - 1)); - } - - @Override - protected void submit() { - doSubmit.add(this); - } - }; - } - } - doNotifyProgressChange.add(oldProgress, progress); - } - - /** - * Sets this {@code SwingWorker} state bound property. - * - * @param the - * state state to set - */ - private void setState(StateValue state) { - StateValue old = this.state; - this.state = state; - firePropertyChange("state", old, state); - } -} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/Main.java b/src/main/java/com/github/xsavikx/androidscreencast/Main.java new file mode 100644 index 0000000..6b8477a --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/Main.java @@ -0,0 +1,23 @@ +package com.github.xsavikx.androidscreencast; + +import com.github.xsavikx.androidscreencast.app.AndroidScreencastApplication; +import com.github.xsavikx.androidscreencast.app.Application; +import com.github.xsavikx.androidscreencast.spring.config.ApplicationContextProvider; +import org.apache.log4j.Logger; + +import java.util.Arrays; + +public class Main { + private static final Logger LOGGER = Logger.getLogger(Main.class); + + public static void main(String args[]) { + LOGGER.debug("main(String[] args=" + Arrays.toString(args) + ") - start"); + Application application = ApplicationContextProvider.getApplicationContext() + .getBean(AndroidScreencastApplication.class); + application.init(); + application.start(); + + LOGGER.debug("main(String[] args=" + Arrays.toString(args) + ") - end"); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/AndroidDevice.java b/src/main/java/com/github/xsavikx/androidscreencast/api/AndroidDevice.java new file mode 100644 index 0000000..8fb1ab2 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/AndroidDevice.java @@ -0,0 +1,18 @@ +package com.github.xsavikx.androidscreencast.api; + +import com.github.xsavikx.androidscreencast.api.file.FileInfo; + +import java.io.File; +import java.util.List; + +public interface AndroidDevice { + String executeCommand(String command); + + List list(String path); + + void openUrl(String url); + + void pullFile(String remoteFrom, File localTo); + + void pushFile(File localFrom, String remoteTo); +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/AndroidDeviceImpl.java b/src/main/java/com/github/xsavikx/androidscreencast/api/AndroidDeviceImpl.java new file mode 100644 index 0000000..b283333 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/AndroidDeviceImpl.java @@ -0,0 +1,157 @@ +package com.github.xsavikx.androidscreencast.api; + +import com.android.ddmlib.IDevice; +import com.android.ddmlib.SyncService; +import com.android.ddmlib.SyncService.ISyncProgressMonitor; +import com.github.xsavikx.androidscreencast.api.file.FileInfo; +import com.github.xsavikx.androidscreencast.api.injector.OutputStreamShellOutputReceiver; +import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Vector; + +@Component +public class AndroidDeviceImpl implements AndroidDevice { + private static final Logger logger = Logger.getLogger(AndroidDeviceImpl.class); + @Autowired(required = false) + private IDevice device; + + public AndroidDeviceImpl() { + + } + + public AndroidDeviceImpl(IDevice device) { + this.device = device; + } + + @Override + public String executeCommand(String cmd) { + if (logger.isDebugEnabled()) { + logger.debug("executeCommand(String) - start"); + } + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + try { + device.executeShellCommand(cmd, new OutputStreamShellOutputReceiver(bos)); + String returnString = new String(bos.toByteArray(), "UTF-8"); + if (logger.isDebugEnabled()) { + logger.debug("executeCommand(String) - end"); + } + return returnString; + } catch (Exception ex) { + logger.error("executeCommand(String)", ex); + + throw new RuntimeException(ex); + } + } + + @Override + public List list(String path) { + if (logger.isDebugEnabled()) { + logger.debug("list(String) - start"); + } + + try { + String s = executeCommand("ls -l " + path); + String[] entries = s.split("\r\n"); + Vector liste = new Vector<>(); + for (String entry : entries) { + String[] data = entry.split(" "); + if (data.length < 4) + continue; + /* + * for(int j=0; j 0) { + stringBuilder.append(' ').append(duration); + } + return stringBuilder.toString(); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/command/TapCommand.java b/src/main/java/com/github/xsavikx/androidscreencast/api/command/TapCommand.java new file mode 100644 index 0000000..5a19897 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/command/TapCommand.java @@ -0,0 +1,18 @@ +package com.github.xsavikx.androidscreencast.api.command; + +public class TapCommand extends InputCommand { + private int x; + private int y; + + public TapCommand(int x, int y) { + this.x = x; + this.y = y; + } + + @Override + protected String getCommandPart() { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append("tap ").append(x).append(' ').append(y); + return stringBuilder.toString(); + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/command/exception/AdbShellCommandExecutionException.java b/src/main/java/com/github/xsavikx/androidscreencast/api/command/exception/AdbShellCommandExecutionException.java new file mode 100644 index 0000000..b19d6b6 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/command/exception/AdbShellCommandExecutionException.java @@ -0,0 +1,14 @@ +package com.github.xsavikx.androidscreencast.api.command.exception; + +import com.github.xsavikx.androidscreencast.api.command.Command; + +public class AdbShellCommandExecutionException extends CommandExecutionException { + private static final String ERROR_MESSAGE = "Error while executing command: %s"; + + private static final long serialVersionUID = -503890452151627952L; + + public AdbShellCommandExecutionException(Command command, Throwable cause) { + super(String.format(ERROR_MESSAGE, command.getFormattedCommand()), cause); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/command/exception/CommandExecutionException.java b/src/main/java/com/github/xsavikx/androidscreencast/api/command/exception/CommandExecutionException.java new file mode 100644 index 0000000..7eab12c --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/command/exception/CommandExecutionException.java @@ -0,0 +1,28 @@ +package com.github.xsavikx.androidscreencast.api.command.exception; + +public class CommandExecutionException extends RuntimeException { + + private static final long serialVersionUID = 8676432388325401069L; + + public CommandExecutionException() { + super(); + } + + public CommandExecutionException(String message, Throwable cause, boolean enableSuppression, + boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } + + public CommandExecutionException(String message, Throwable cause) { + super(message, cause); + } + + public CommandExecutionException(String message) { + super(message); + } + + public CommandExecutionException(Throwable cause) { + super(cause); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/command/executor/CommandExecutor.java b/src/main/java/com/github/xsavikx/androidscreencast/api/command/executor/CommandExecutor.java new file mode 100644 index 0000000..742521c --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/command/executor/CommandExecutor.java @@ -0,0 +1,7 @@ +package com.github.xsavikx.androidscreencast.api.command.executor; + +import com.github.xsavikx.androidscreencast.api.command.Command; + +public interface CommandExecutor { + void execute(Command command); +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/command/executor/ShellCommandExecutor.java b/src/main/java/com/github/xsavikx/androidscreencast/api/command/executor/ShellCommandExecutor.java new file mode 100644 index 0000000..3cbac0e --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/command/executor/ShellCommandExecutor.java @@ -0,0 +1,39 @@ +package com.github.xsavikx.androidscreencast.api.command.executor; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.ShellCommandUnresponsiveException; +import com.android.ddmlib.TimeoutException; +import com.github.xsavikx.androidscreencast.api.command.Command; +import com.github.xsavikx.androidscreencast.api.command.exception.AdbShellCommandExecutionException; +import com.github.xsavikx.androidscreencast.api.injector.MultiLineReceiverPrinter; +import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +@Service +public class ShellCommandExecutor implements CommandExecutor { + private static final Logger LOGGER = Logger.getLogger(ShellCommandExecutor.class); + private static final int MAX_TIME_TO_WAIT_RESPONSE = 5; + @Autowired + private IDevice device; + + @Override + public void execute(Command command) { + LOGGER.debug("execute(Command command=" + command + ") - start"); + + try { + device.executeShellCommand(command.getFormattedCommand(), new MultiLineReceiverPrinter(), + MAX_TIME_TO_WAIT_RESPONSE, TimeUnit.SECONDS); + } catch (TimeoutException | AdbCommandRejectedException | ShellCommandUnresponsiveException | IOException e) { + LOGGER.error("execute(Command)", e); + throw new AdbShellCommandExecutionException(command, e); + } + + LOGGER.debug("execute(Command command=" + command + ") - end"); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/command/factory/AdbInputCommandFactory.java b/src/main/java/com/github/xsavikx/androidscreencast/api/command/factory/AdbInputCommandFactory.java new file mode 100644 index 0000000..6682e3c --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/command/factory/AdbInputCommandFactory.java @@ -0,0 +1,56 @@ +package com.github.xsavikx.androidscreencast.api.command.factory; + +import com.github.xsavikx.androidscreencast.api.command.KeyCommand; +import com.github.xsavikx.androidscreencast.api.command.SwipeCommand; +import com.github.xsavikx.androidscreencast.api.command.TapCommand; +import com.github.xsavikx.androidscreencast.api.injector.InputKeyEvent; +import org.apache.log4j.Logger; +import org.springframework.stereotype.Service; + +@Service +public final class AdbInputCommandFactory { + private static final Logger LOGGER = Logger.getLogger(AdbInputCommandFactory.class); + + public static KeyCommand getKeyCommand(int keyCode) { + KeyCommand returnKeyCommand = new KeyCommand(keyCode); + LOGGER.debug(returnKeyCommand); + return returnKeyCommand; + } + + public static KeyCommand getKeyCommand(InputKeyEvent inputKeyEvent) { + KeyCommand returnKeyCommand = new KeyCommand(inputKeyEvent); + LOGGER.debug(returnKeyCommand); + return returnKeyCommand; + } + + + public static KeyCommand getKeyCommand(int keyCode, boolean longpress) { + KeyCommand returnKeyCommand = new KeyCommand(keyCode, longpress); + LOGGER.debug(returnKeyCommand); + return returnKeyCommand; + } + + public static KeyCommand getKeyCommand(InputKeyEvent inputKeyEvent, boolean longpress) { + KeyCommand returnKeyCommand = new KeyCommand(inputKeyEvent, longpress); + LOGGER.debug(returnKeyCommand); + return returnKeyCommand; + } + + public static SwipeCommand getSwipeCommand(int x1, int y1, int x2, int y2, long duration) { + SwipeCommand returnSwipeCommand = new SwipeCommand(x1, y1, x2, y2, duration); + LOGGER.debug(returnSwipeCommand); + return returnSwipeCommand; + } + + public static SwipeCommand getSwipeCommand(int x1, int y1, int x2, int y2) { + SwipeCommand returnSwipeCommand = new SwipeCommand(x1, y1, x2, y2); + LOGGER.debug(returnSwipeCommand); + return returnSwipeCommand; + } + + public static TapCommand getTapCommand(int x, int y) { + TapCommand returnTapCommand = new TapCommand(x, y); + LOGGER.debug(returnTapCommand); + return returnTapCommand; + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/file/FileInfo.java b/src/main/java/com/github/xsavikx/androidscreencast/api/file/FileInfo.java new file mode 100644 index 0000000..ce9c284 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/file/FileInfo.java @@ -0,0 +1,32 @@ +package com.github.xsavikx.androidscreencast.api.file; + +import com.github.xsavikx.androidscreencast.api.AndroidDeviceImpl; +import org.springframework.stereotype.Component; + +import java.io.File; + +@Component +public class FileInfo { + public AndroidDeviceImpl device; + public String path; + public String attribs; + public boolean directory; + public String name; + + public File downloadTemporary() { + try { + File tempFile = File.createTempFile("android", name); + device.pullFile(path + name, tempFile); + tempFile.deleteOnExit(); + return tempFile; + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + @Override + public String toString() { + return name; + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/injector/Injector.java b/src/main/java/com/github/xsavikx/androidscreencast/api/injector/Injector.java new file mode 100644 index 0000000..2b6a62e --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/injector/Injector.java @@ -0,0 +1,38 @@ +package com.github.xsavikx.androidscreencast.api.injector; + +import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class Injector { + private static final Logger LOGGER = Logger.getLogger(Injector.class); + @Autowired + public ScreenCaptureThread screencapture; + + public void restart() { + LOGGER.debug("restart() - start"); + + close(); + start(); + + LOGGER.debug("restart() - end"); + } + + public void close() { + LOGGER.debug("close() - start"); + + screencapture.interrupt(); + + LOGGER.debug("close() - end"); + } + + public void start() { + LOGGER.debug("start() - start"); + + screencapture.start(); + + LOGGER.debug("start() - end"); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/injector/InputKeyEvent.java b/src/main/java/com/github/xsavikx/androidscreencast/api/injector/InputKeyEvent.java new file mode 100644 index 0000000..d7fbc8e --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/injector/InputKeyEvent.java @@ -0,0 +1,355 @@ +//@formatter:off +package com.github.xsavikx.androidscreencast.api.injector; + +import javax.annotation.Resource; +import java.awt.event.KeyEvent; +import java.util.EnumSet; +import java.util.Set; + +@Resource +public enum InputKeyEvent { + KEYCODE_UNKNOWN(0, "Key code constant: Unknown key code."), + 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."), + 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."), + KEYCODE_HOME(3, "Key code constant: Home key. This key is handled by the framework and is never delivered to applications.", KeyEvent.VK_HOME), + KEYCODE_BACK(4, "Key code constant: Back key. ", KeyEvent.VK_ESCAPE), + KEYCODE_CALL(5, "Key code constant: Call key. "), + KEYCODE_ENDCALL(6, "Key code constant: End Call key. "), + KEYCODE_0(7, "Key code constant: '0' key. ", '0'), + KEYCODE_1(8, "Key code constant: '1' key. ", '1'), + KEYCODE_2(9, "Key code constant: '2' key. ", '2'), + KEYCODE_3(10, "Key code constant: '3' key. ", '3'), + KEYCODE_4(11, "Key code constant: '4' key. ", '4'), + KEYCODE_5(12, "Key code constant: '5' key. ", '5'), + KEYCODE_6(13, "Key code constant: '6' key. ", '6'), + KEYCODE_7(14, "Key code constant: '7' key. ", '7'), + KEYCODE_8(15, "Key code constant: '8' key. ", '8'), + KEYCODE_9(16, "Key code constant: '9' key. ", '9'), + KEYCODE_STAR(17, "Key code constant: '*' key. ", '*'), + KEYCODE_POUND(18, "Key code constant: '#' key. ", '#'), + KEYCODE_DPAD_UP(19, "Key code constant: Directional Pad Up key. May also be synthesized from trackball motions.", KeyEvent.VK_UP), + KEYCODE_DPAD_DOWN(20, "Key code constant: Directional Pad Down key. May also be synthesized from trackball motions.", KeyEvent.VK_DOWN), + KEYCODE_DPAD_LEFT(21, "Key code constant: Directional Pad Left key. May also be synthesized from trackball motions.", KeyEvent.VK_LEFT), + KEYCODE_DPAD_RIGHT(22, "Key code constant: Directional Pad Right key. May also be synthesized from trackball motions.", KeyEvent.VK_RIGHT), + KEYCODE_DPAD_CENTER(23, "Key code constant: Directional Pad Center key. May also be synthesized from trackball motions."), + KEYCODE_VOLUME_UP(24, "Key code constant: Volume Up key. Adjusts the speaker volume up."), + KEYCODE_VOLUME_DOWN(25, "Key code constant: Volume Down key. Adjusts the speaker volume down."), + KEYCODE_POWER(26, "Key code constant: Power key. "), + KEYCODE_CAMERA(27, "Key code constant: Camera key. Used to launch a camera application or take pictures."), + KEYCODE_CLEAR(28, "Key code constant: Clear key. "), + KEYCODE_A(29, "Key code constant: 'A' key. ", 'a'), + KEYCODE_B(30, "Key code constant: 'B' key. ", 'b'), + KEYCODE_C(31, "Key code constant: 'C' key. ", 'c'), + KEYCODE_D(32, "Key code constant: 'D' key. ", 'd'), + KEYCODE_E(33, "Key code constant: 'E' key. ", 'e'), + KEYCODE_F(34, "Key code constant: 'F' key. ", 'f'), + KEYCODE_G(35, "Key code constant: 'G' key. ", 'g'), + KEYCODE_H(36, "Key code constant: 'H' key. ", 'h'), + KEYCODE_I(37, "Key code constant: 'I' key. ", 'i'), + KEYCODE_J(38, "Key code constant: 'J' key. ", 'j'), + KEYCODE_K(39, "Key code constant: 'K' key. ", 'k'), + KEYCODE_L(40, "Key code constant: 'L' key. ", 'l'), + KEYCODE_M(41, "Key code constant: 'M' key. ", 'm'), + KEYCODE_N(42, "Key code constant: 'N' key. ", 'n'), + KEYCODE_O(43, "Key code constant: 'O' key. ", 'o'), + KEYCODE_P(44, "Key code constant: 'P' key. ", 'p'), + KEYCODE_Q(45, "Key code constant: 'Q' key. ", 'q'), + KEYCODE_R(46, "Key code constant: 'R' key. ", 'r'), + KEYCODE_S(47, "Key code constant: 'S' key. ", 's'), + KEYCODE_T(48, "Key code constant: 'T' key. ", 't'), + KEYCODE_U(49, "Key code constant: 'U' key. ", 'u'), + KEYCODE_V(50, "Key code constant: 'V' key. ", 'v'), + KEYCODE_W(51, "Key code constant: 'W' key. ", 'w'), + KEYCODE_X(52, "Key code constant: 'X' key. ", 'x'), + KEYCODE_Y(53, "Key code constant: 'Y' key. ", 'y'), + KEYCODE_Z(54, "Key code constant: 'Z' key. ", 'z'), + KEYCODE_COMMA(55, "Key code constant: ',' key. ", ','), + KEYCODE_PERIOD(56, "Key code constant: '.' key. ", '.'), + KEYCODE_ALT_LEFT(57, "Key code constant: Left Alt modifier key. "), + KEYCODE_ALT_RIGHT(58, "Key code constant: Right Alt modifier key. "), + KEYCODE_SHIFT_LEFT(59, "Key code constant: Left Shift modifier key. "), + KEYCODE_SHIFT_RIGHT(60, "Key code constant: Right Shift modifier key. "), + KEYCODE_TAB(61, "Key code constant: Tab key. ", '\t'), + KEYCODE_SPACE(62, "Key code constant: Space key. ", ' '), + KEYCODE_SYM(63, "Key code constant: Symbol modifier key. Used to enter alternate symbols."), + KEYCODE_EXPLORER(64, "Key code constant: Explorer special function key. Used to launch a browser application."), + KEYCODE_ENVELOPE(65, "Key code constant: Envelope special function key. Used to launch a mail application."), + KEYCODE_ENTER(66, "Key code constant: Enter key. ", '\n'), + KEYCODE_DEL(67, "Key code constant: Backspace key. Deletes characters before the insertion point, unlike {@link #KEYCODE_FORWARD_DEL}.", '\b'), + KEYCODE_GRAVE(68, "Key code constant: '`' (backtick) key. ", '`'), + KEYCODE_MINUS(69, "Key code constant: '-'. ", '-'), + KEYCODE_EQUALS(70, "Key code constant: '=' key. ", '='), + KEYCODE_LEFT_BRACKET(71, "Key code constant: '[' key. ", '['), + KEYCODE_RIGHT_BRACKET(72, "Key code constant: ']' key. ", ']'), + KEYCODE_BACKSLASH(73, "Key code constant: '\' key. ", '\\'), + KEYCODE_SEMICOLON(74, "Key code constant: ';' key. ", ';'), + KEYCODE_APOSTROPHE(75, "Key code constant: ''' (apostrophe) key. ", '\''), + KEYCODE_SLASH(76, "Key code constant: '/' key. ", '/'), + KEYCODE_AT(77, "Key code constant: '@' key. ", '@'), + 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} ."), + KEYCODE_HEADSETHOOK(79, "Key code constant: Headset Hook key. Used to hang up calls and stop media."), + KEYCODE_FOCUS(80, "Key code constant: Camera Focus key. Used to focus the camera."), // *Camera* focus + KEYCODE_PLUS(81, "Key code constant: '+' key.", '+'), + KEYCODE_MENU(82, "Key code constant: Menu key."), + KEYCODE_NOTIFICATION(83, "Key code constant: Notification key."), + KEYCODE_SEARCH(84, "Key code constant: Search key."), + KEYCODE_MEDIA_PLAY_PAUSE(85, "Key code constant: Play/Pause media key."), + KEYCODE_MEDIA_STOP(86, "Key code constant: Stop media key."), + KEYCODE_MEDIA_NEXT(87, "Key code constant: Play Next media key."), + KEYCODE_MEDIA_PREVIOUS(88, "Key code constant: Play Previous media key."), + KEYCODE_MEDIA_REWIND(89, "Key code constant: Rewind media key."), + KEYCODE_MEDIA_FAST_FORWARD(90, "Key code constant: Fast Forward media key."), + KEYCODE_MUTE(91, "Key code constant: Mute key. Mutes the microphone, unlike {@link #KEYCODE_VOLUME_MUTE}."), + KEYCODE_PAGE_UP(92, "Key code constant: Page Up key. ", KeyEvent.VK_PAGE_UP), + KEYCODE_PAGE_DOWN(93, "Key code constant: Page Down key. ", KeyEvent.VK_PAGE_DOWN), + KEYCODE_PICTSYMBOLS(94, "Key code constant: Picture Symbols modifier key. Used to switch symbol sets (Emoji, Kao-moji)."), // switch symbol-sets (Emoji,Kao-moji) + KEYCODE_SWITCH_CHARSET(95, "Key code constant: Switch Charset modifier key. Used to switch character sets (Kanji, Katakana)."), // switch char-sets (Kanji,Katakana) + 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."), + 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."), + 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."), + 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."), + 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."), + 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."), + 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."), + 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."), + 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."), + 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."), + 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."), + 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."), + KEYCODE_BUTTON_START(108, "Key code constant: Start Button key. On a game controller, the button labeled Start."), + KEYCODE_BUTTON_SELECT(109, "Key code constant: Select Button key. On a game controller, the button labeled Select."), + KEYCODE_BUTTON_MODE(110, "Key code constant: Mode Button key. On a game controller, the button labeled Mode."), + KEYCODE_ESCAPE(111, "Key code constant: Escape key."), + KEYCODE_FORWARD_DEL(112, "Key code constant: Forward Delete key. Deletes characters ahead of the insertion point, unlike {@link #KEYCODE_DEL}."), + KEYCODE_CTRL_LEFT(113, "Key code constant: Left Control modifier key."), + KEYCODE_CTRL_RIGHT(114, "Key code constant: Right Control modifier key."), + KEYCODE_CAPS_LOCK(115, "Key code constant: Caps Lock key."), + KEYCODE_SCROLL_LOCK(116, "Key code constant: Scroll Lock key."), + KEYCODE_META_LEFT(117, "Key code constant: Left Meta modifier key."), + KEYCODE_META_RIGHT(118, "Key code constant: Right Meta modifier key."), + KEYCODE_FUNCTION(119, "Key code constant: Function modifier key."), + KEYCODE_SYSRQ(120, "Key code constant: System Request / Print Screen key.", KeyEvent.VK_PRINTSCREEN), + KEYCODE_BREAK(121, "Key code constant: Break / Pause key. ", KeyEvent.VK_PAUSE), + 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."), + 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."), + KEYCODE_INSERT(124, "Key code constant: Insert key. Toggles insert / overwrite edit mode.", KeyEvent.VK_INSERT), + KEYCODE_FORWARD(125, "Key code constant: Forward key. Navigates forward in the history stack. Complement of {@link #KEYCODE_BACK}."), + KEYCODE_MEDIA_PLAY(126, "Key code constant: Play media key. "), + KEYCODE_MEDIA_PAUSE(127, "Key code constant: Pause media key. "), + KEYCODE_MEDIA_CLOSE(128, "Key code constant: Close media key. May be used to close a CD tray, for example."), + KEYCODE_MEDIA_EJECT(129, "Key code constant: Eject media key. May be used to eject a CD tray, for example."), + KEYCODE_MEDIA_RECORD(130, "Key code constant: Record media key. "), + KEYCODE_F1(131, "Key code constant: F1 key. "), + KEYCODE_F2(132, "Key code constant: F2 key. "), + KEYCODE_F3(133, "Key code constant: F3 key. "), + KEYCODE_F4(134, "Key code constant: F4 key. "), + KEYCODE_F5(135, "Key code constant: F5 key. "), + KEYCODE_F6(136, "Key code constant: F6 key. "), + KEYCODE_F7(137, "Key code constant: F7 key. "), + KEYCODE_F8(138, "Key code constant: F8 key. "), + KEYCODE_F9(139, "Key code constant: F9 key. "), + KEYCODE_F10(140, "Key code constant: F10 key. "), + KEYCODE_F11(141, "Key code constant: F11 key. "), + KEYCODE_F12(142, "Key code constant: F12 key. "), + 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."), + KEYCODE_NUMPAD_0(144, "Key code constant: Numeric keypad '0' key. "), + KEYCODE_NUMPAD_1(145, "Key code constant: Numeric keypad '1' key. "), + KEYCODE_NUMPAD_2(146, "Key code constant: Numeric keypad '2' key. "), + KEYCODE_NUMPAD_3(147, "Key code constant: Numeric keypad '3' key. "), + KEYCODE_NUMPAD_4(148, "Key code constant: Numeric keypad '4' key. "), + KEYCODE_NUMPAD_5(149, "Key code constant: Numeric keypad '5' key. "), + KEYCODE_NUMPAD_6(150, "Key code constant: Numeric keypad '6' key. "), + KEYCODE_NUMPAD_7(151, "Key code constant: Numeric keypad '7' key. "), + KEYCODE_NUMPAD_8(152, "Key code constant: Numeric keypad '8' key. "), + KEYCODE_NUMPAD_9(153, "Key code constant: Numeric keypad '9' key. "), + KEYCODE_NUMPAD_DIVIDE(154, "Key code constant: Numeric keypad '/' key (for division). "), + KEYCODE_NUMPAD_MULTIPLY(155, "Key code constant: '*' key (for multiplication). "), + KEYCODE_NUMPAD_SUBTRACT(156, "Key code constant: Numeric keypad '-' key (for subtraction). "), + KEYCODE_NUMPAD_ADD(157, "Key code constant: Numeric keypad '+' key (for addition). "), + KEYCODE_NUMPAD_DOT(158, "Key code constant: Numeric keypad '.' key (for decimals or digit grouping)."), + KEYCODE_NUMPAD_COMMA(159, "Key code constant: Numeric keypad ',' key (for decimals or digit grouping)."), + KEYCODE_NUMPAD_ENTER(160, "Key code constant: Numeric keypad Enter key. "), + KEYCODE_NUMPAD_EQUALS(161, "Key code constant: Numeric keypad '=' key. "), + KEYCODE_NUMPAD_LEFT_PAREN(162, "Key code constant: Numeric keypad '(' key. "), + KEYCODE_NUMPAD_RIGHT_PAREN(163, "Key code constant: Numeric keypad ')' key. "), + 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."), + KEYCODE_INFO(165, "Key code constant: Info key. Common on TV remotes to show additional information related to what is currently being viewed."), + KEYCODE_CHANNEL_UP(166, "Key code constant: Channel up key. On TV remotes, increments the television channel."), + KEYCODE_CHANNEL_DOWN(167, "Key code constant: Channel down key. On TV remotes, decrements the television channel."), + KEYCODE_ZOOM_IN(168, "Key code constant: Zoom in key. "), + KEYCODE_ZOOM_OUT(169, "Key code constant: Zoom out key. "), + KEYCODE_TV(170, "Key code constant: TV key. On TV remotes, switches to viewing live TV."), + KEYCODE_WINDOW(171, "Key code constant: Window key. On TV remotes, toggles picture-in-picture mode or other windowing functions."), + KEYCODE_GUIDE(172, "Key code constant: Guide key. On TV remotes, shows a programming guide."), + KEYCODE_DVR(173, "Key code constant: DVR key. On some TV remotes, switches to a DVR mode for recorded shows."), + KEYCODE_BOOKMARK(174, "Key code constant: Bookmark key. On some TV remotes, bookmarks content or web pages."), + KEYCODE_CAPTIONS(175, "Key code constant: Toggle captions key. Switches the mode for closed-captioning text, for example during television shows."), + KEYCODE_SETTINGS(176, "Key code constant: Settings key. Starts the system settings activity."), + KEYCODE_TV_POWER(177, "Key code constant: TV power key. On TV remotes, toggles the power on a television screen."), + KEYCODE_TV_INPUT(178, "Key code constant: TV input key. On TV remotes, switches the input on a television screen."), + KEYCODE_STB_POWER(179, "Key code constant: Set-top-box power key. On TV remotes, toggles the power on an external Set-top-box."), + 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."), + KEYCODE_AVR_POWER(181, "Key code constant: A/V Receiver power key. On TV remotes, toggles the power on an external A/V Receiver."), + 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."), + KEYCODE_PROG_RED(183, "Key code constant: Red \"programmable\" key. On TV remotes, acts as a contextual/programmable key."), + KEYCODE_PROG_GREEN(184, "Key code constant: Green \"programmable\" key. On TV remotes, actsas a contextual/programmable key."), + KEYCODE_PROG_YELLOW(185, "Key code constant: Yellow \"programmable\" key. On TV remotes, acts as a contextual/programmable key."), + KEYCODE_PROG_BLUE(186, "Key code constant: Blue \"programmable\" key. On TV remotes, acts as a contextual/programmable key."), + KEYCODE_APP_SWITCH(187, "Key code constant: App switch key. Should bring up the application switcher dialog."), + KEYCODE_BUTTON_1(188, "Key code constant: Generic Game Pad Button #1. "), + KEYCODE_BUTTON_2(189, "Key code constant: Generic Game Pad Button #2. "), + KEYCODE_BUTTON_3(190, "Key code constant: Generic Game Pad Button #3. "), + KEYCODE_BUTTON_4(191, "Key code constant: Generic Game Pad Button #4. "), + KEYCODE_BUTTON_5(192, "Key code constant: Generic Game Pad Button #5. "), + KEYCODE_BUTTON_6(193, "Key code constant: Generic Game Pad Button #6. "), + KEYCODE_BUTTON_7(194, "Key code constant: Generic Game Pad Button #7. "), + KEYCODE_BUTTON_8(195, "Key code constant: Generic Game Pad Button #8. "), + KEYCODE_BUTTON_9(196, "Key code constant: Generic Game Pad Button #9. "), + KEYCODE_BUTTON_10(197, "Key code constant: Generic Game Pad Button #10. "), + KEYCODE_BUTTON_11(198, "Key code constant: Generic Game Pad Button #11. "), + KEYCODE_BUTTON_12(199, "Key code constant: Generic Game Pad Button #12. "), + KEYCODE_BUTTON_13(200, "Key code constant: Generic Game Pad Button #13. "), + KEYCODE_BUTTON_14(201, "Key code constant: Generic Game Pad Button #14. "), + KEYCODE_BUTTON_15(202, "Key code constant: Generic Game Pad Button #15. "), + KEYCODE_BUTTON_16(203, "Key code constant: Generic Game Pad Button #16. "), + 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."), + 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."), + KEYCODE_3D_MODE(206, "Key code constant: 3D Mode key. Toggles the display between 2D and 3D mode."), + KEYCODE_CONTACTS(207, "Key code constant: Contacts special function key. Used to launch an address book application."), + KEYCODE_CALENDAR(208, "Key code constant: Calendar special function key. Used to launch a calendar application."), + KEYCODE_MUSIC(209, "Key code constant: Music special function key. Used to launch a music player application."), + KEYCODE_CALCULATOR(210, "Key code constant: Calculator special function key. Used to launch a calculator application."), + KEYCODE_ZENKAKU_HANKAKU(211, "Key code constant: Japanese full-width / half-width key. "), + KEYCODE_EISU(212, "Key code constant: Japanese alphanumeric key. "), + KEYCODE_MUHENKAN(213, "Key code constant: Japanese non-conversion key. "), + KEYCODE_HENKAN(214, "Key code constant: Japanese conversion key. "), + KEYCODE_KATAKANA_HIRAGANA(215, "Key code constant: Japanese katakana / hiragana key. "), + KEYCODE_YEN(216, "Key code constant: Japanese Yen key. "), + KEYCODE_RO(217, "Key code constant: Japanese Ro key. "), + KEYCODE_KANA(218, "Key code constant: Japanese kana key. "), + KEYCODE_ASSIST(219, "Key code constant: Assist key. Launches the global assist activity. Not delivered to applications."), + KEYCODE_BRIGHTNESS_DOWN(220, "Key code constant: Brightness Down key. Adjusts the screen brighs down."), + KEYCODE_BRIGHTNESS_UP(221, "Key code constant: Brightness Up key. Adjusts the screen brightness up."), + KEYCODE_MEDIA_AUDIO_TRACK(222, "Key code constant: Audio Track key. Switches the audio tracks."), + 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."), + 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."), + 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."), + KEYCODE_MEDIA_TOP_MENU(226, "Key code constant: Media Top Menu key. Goes to the top of media menu."), + KEYCODE_11(227, "Key code constant: '11' key. "), + KEYCODE_12(228, "Key code constant: '12' key. "), + KEYCODE_LAST_CHANNEL(229, "Key code constant: Last Channel key. Goes to the last viewed channel."), + KEYCODE_TV_DATA_SERVICE(230, "Key code constant: TV data service key. Displays data services like weather, sports."), + KEYCODE_VOICE_ASSIST(231, "Key code constant: Voice Assist key. Launches the global voice assist activity. Not delivered to applications."), + KEYCODE_TV_RADIO_SERVICE(232, "Key code constant: Radio key. Toggles TV service / Radio service."), + KEYCODE_TV_TELETEXT(233, "Key code constant: Teletext key. Displays Teletext service."), + 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."), + KEYCODE_TV_TERRESTRIAL_ANALOG(235, "Key code constant: Analog Terrestrial key. Switches to analog terrestrial broadcast service."), + KEYCODE_TV_TERRESTRIAL_DIGITAL(236, "Key code constant: Digital Terrestrial key. Switches to digital terrestrial broadcast service."), + KEYCODE_TV_SATELLITE(237, "Key code constant: Satellite key. Switches to digital satellite broadcast service."), + KEYCODE_TV_SATELLITE_BS(238, "Key code constant: BS key. Switches to BS digital satellite broadcasting service available in Japan."), + KEYCODE_TV_SATELLITE_CS(239, "Key code constant: CS key. Switches to CS digital satellite broadcasting service available in Japan."), + KEYCODE_TV_SATELLITE_SERVICE(240, "Key code constant: BS/CS key. Toggles between BS and CS digital satellite services."), + KEYCODE_TV_NETWORK(241, "Key code constant: Toggle Network key. Toggles selecting broacast services."), + KEYCODE_TV_ANTENNA_CABLE(242, "Key code constant: Antenna/Cable key. Toggles broadcast input source between antenna and cable."), + KEYCODE_TV_INPUT_HDMI_1(243, "Key code constant: HDMI #1 key. Switches to HDMI input #1."), + KEYCODE_TV_INPUT_HDMI_2(244, "Key code constant: HDMI #2 key. Switches to HDMI input #2."), + KEYCODE_TV_INPUT_HDMI_3(245, "Key code constant: HDMI #3 key. Switches to HDMI input #3."), + KEYCODE_TV_INPUT_HDMI_4(246, "Key code constant: HDMI #4 key. Switches to HDMI input #4."), + KEYCODE_TV_INPUT_COMPOSITE_1(247, "Key code constant: Composite #1 key. Switches to composite video input #1."), + KEYCODE_TV_INPUT_COMPOSITE_2(248, "Key code constant: Composite #2 key. Switches to composite video input #2."), + KEYCODE_TV_INPUT_COMPONENT_1(249, "Key code constant: Component #1 key. Switches to component video input #1."), + KEYCODE_TV_INPUT_COMPONENT_2(250, "Key code constant: Component #2 key. Switches to component video input #2."), + KEYCODE_TV_INPUT_VGA_1(251, "Key code constant: VGA #1 key. Switches to VGA (analog RGB) input #1."), + KEYCODE_TV_AUDIO_DESCRIPTION(252, "Key code constant: Audio description key. Toggles audio description off / on."), + 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."), + 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."), + KEYCODE_TV_ZOOM_MODE(255, "Key code constant: Zoom mode key. Changes Zoom mode (Normal, Full, Zoom, Wide-zoom, etc.)"), + 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"), + 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."), + 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."), + KEYCODE_HELP(259, "Key code constant: Help key. "); + private static final Set eventsWithCharacters = fillEventsWithCharacters(); + private static Set eventsWithKeyCodes = fillEventsWithKeyCodes(); + private int keyCode; + private int code; + private String description; + private char characterToReplace; + + InputKeyEvent(int code, String description) { + this.code = code; + this.description = description; + } + + InputKeyEvent(int code, String description, char character) { + this(code, description); + this.characterToReplace = character; + } + + InputKeyEvent(int code, String description, int keyCode) { + this(code, description); + this.keyCode = keyCode; + } + + public static InputKeyEvent getByCharacter(char c) { + for (InputKeyEvent e : eventsWithCharacters) { + if (e.characterToReplace == c) { + return e; + } + } + return null; + } + + public static InputKeyEvent getByKeyCode(int keyCode) { + for (InputKeyEvent e : eventsWithKeyCodes) { + if (e.keyCode == keyCode) { + return e; + } + } + return null; + } + + public static InputKeyEvent getByCharacterOrKeyCode(char c, int keyCode) { + InputKeyEvent e = getByCharacter(c); + if (e == null) + e = getByKeyCode(keyCode); + return e; + } + + private static Set fillEventsWithCharacters() { + Set eventsWithCharacters = EnumSet.allOf(InputKeyEvent.class); + for (InputKeyEvent e : values()) { + if (e.characterToReplace == '\u0000') { + eventsWithCharacters.remove(e); + } + } + return eventsWithCharacters; + } + + private static Set fillEventsWithKeyCodes() { + Set eventsWithKeyCodes = EnumSet.allOf(InputKeyEvent.class); + for (InputKeyEvent e : values()) { + if (e.keyCode == 0) { + eventsWithKeyCodes.remove(e); + } + } + return eventsWithKeyCodes; + } + + public int getKeyCode() { + return keyCode; + } + + public int getCode() { + return code; + } + + public String getDescription() { + return description; + } + + public char getCharacterToReplace() { + return characterToReplace; + } + + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/injector/KeyCodeConverter.java b/src/main/java/com/github/xsavikx/androidscreencast/api/injector/KeyCodeConverter.java new file mode 100644 index 0000000..89c05b8 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/injector/KeyCodeConverter.java @@ -0,0 +1,23 @@ +package com.github.xsavikx.androidscreencast.api.injector; + +import org.apache.log4j.Logger; + +import java.awt.event.KeyEvent; + +public class KeyCodeConverter { + private static final Logger LOGGER = Logger.getLogger(KeyCodeConverter.class); + + public static int getKeyCode(KeyEvent e) { + LOGGER.debug("getKeyCode(KeyEvent e=" + e + ") - start"); + int code = InputKeyEvent.KEYCODE_UNKNOWN.getCode(); + char c = e.getKeyChar(); + int keyCode = e.getKeyCode(); + InputKeyEvent inputKeyEvent = InputKeyEvent.getByCharacterOrKeyCode(Character.toLowerCase(c), keyCode); + if (inputKeyEvent != null) { + code = inputKeyEvent.getCode(); + } + LOGGER.debug("getKeyCode(KeyEvent e=" + e + ") - end"); + return code; + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/injector/MultiLineReceiverPrinter.java b/src/main/java/com/github/xsavikx/androidscreencast/api/injector/MultiLineReceiverPrinter.java new file mode 100644 index 0000000..d33a9ce --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/injector/MultiLineReceiverPrinter.java @@ -0,0 +1,21 @@ +package com.github.xsavikx.androidscreencast.api.injector; + +import com.android.ddmlib.MultiLineReceiver; +import org.springframework.stereotype.Component; + +@Component +public class MultiLineReceiverPrinter extends MultiLineReceiver { + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public void processNewLines(String[] arg0) { + for (String elem : arg0) { + System.out.println(elem); + } + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/injector/OutputStreamShellOutputReceiver.java b/src/main/java/com/github/xsavikx/androidscreencast/api/injector/OutputStreamShellOutputReceiver.java new file mode 100644 index 0000000..be0a981 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/injector/OutputStreamShellOutputReceiver.java @@ -0,0 +1,39 @@ +package com.github.xsavikx.androidscreencast.api.injector; + +import com.android.ddmlib.IShellOutputReceiver; + +import java.io.IOException; +import java.io.OutputStream; + +public class OutputStreamShellOutputReceiver implements IShellOutputReceiver { + + private OutputStream os; + + public OutputStreamShellOutputReceiver(OutputStream os) { + this.os = os; + } + + @Override + public void addOutput(byte[] buf, int off, int len) { + try { + os.write(buf, off, len); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + } + + @Override + public void flush() { + try { + os.flush(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public boolean isCancelled() { + return false; + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/injector/ScreenCaptureThread.java b/src/main/java/com/github/xsavikx/androidscreencast/api/injector/ScreenCaptureThread.java new file mode 100644 index 0000000..f7ad07b --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/injector/ScreenCaptureThread.java @@ -0,0 +1,191 @@ +package com.github.xsavikx.androidscreencast.api.injector; + +import com.android.ddmlib.AdbCommandRejectedException; +import com.android.ddmlib.IDevice; +import com.android.ddmlib.RawImage; +import com.android.ddmlib.TimeoutException; +import com.github.xsavikx.androidscreencast.api.recording.QuickTimeOutputStream; +import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; +import java.util.concurrent.TimeUnit; + +@Component +public class ScreenCaptureThread extends Thread { + private static final Logger LOGGER = Logger.getLogger(ScreenCaptureThread.class); + private BufferedImage image; + private Dimension size; + @Autowired + private IDevice device; + private QuickTimeOutputStream qos = null; + private boolean landscape = false; + private ScreenCaptureListener listener = null; + + public ScreenCaptureThread() { + super("Screen capture"); + image = null; + size = new Dimension(); + } + + public void display(RawImage rawImage) { + int width2 = landscape ? rawImage.height : rawImage.width; + int height2 = landscape ? rawImage.width : rawImage.height; + if (image == null) { + image = new BufferedImage(width2, height2, BufferedImage.TYPE_INT_RGB); + size.setSize(image.getWidth(), image.getHeight()); + } else { + if (image.getHeight() != height2 || image.getWidth() != width2) { + image = new BufferedImage(width2, height2, BufferedImage.TYPE_INT_RGB); + size.setSize(image.getWidth(), image.getHeight()); + } + } + int index = 0; + int indexInc = rawImage.bpp >> 3; + for (int y = 0; y < rawImage.height; y++) { + for (int x = 0; x < rawImage.width; x++, index += indexInc) { + int value = rawImage.getARGB(index); + if (landscape) + image.setRGB(y, rawImage.width - x - 1, value); + else + image.setRGB(x, y, value); + } + } + + try { + if (qos != null) + qos.writeFrame(image, 10); + } catch (IOException e) { + LOGGER.error("display(RawImage)", e); + + throw new RuntimeException(e); + } + + if (listener != null) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + listener.handleNewImage(size, image, landscape); + } + }); + } + + } + + private boolean fetchImage() throws IOException { + + if (device == null) { + // device not ready + try { + Thread.sleep(100); + } catch (InterruptedException e) { + LOGGER.error("fetchImage()", e); + return false; + } + return true; + } + RawImage rawImage = null; + synchronized (device) { + try { + rawImage = device.getScreenshot(5, TimeUnit.SECONDS); + } catch (TimeoutException | AdbCommandRejectedException e) { + LOGGER.error("fetchImage()", e); + } + } + if (rawImage != null) { + display(rawImage); + } else { + LOGGER.info("failed getting screenshot through ADB ok"); + } + try { + Thread.sleep(5); + } catch (InterruptedException e) { + LOGGER.error("fetchImage()", e); + return false; + } + return true; + } + + public ScreenCaptureListener getListener() { + return listener; + } + + public void setListener(ScreenCaptureListener listener) { + this.listener = listener; + } + + public Dimension getPreferredSize() { + return size; + } + + @Override + public void run() { + do { + try { + boolean ok = fetchImage(); + if (!ok) + break; + } catch (java.nio.channels.ClosedByInterruptException ciex) { + LOGGER.error("run()", ciex); + + break; + } catch (IOException e) { + LOGGER.error("run()", e); + LOGGER.error((new StringBuilder()).append("Exception fetching image: ").append(e.toString()).toString()); + } + + } while (true); + } + + public void startRecording(File f) { + LOGGER.debug("startRecording(File f=" + f + ") - start"); + + try { + if (!f.getName().toLowerCase().endsWith(".mov")) + f = new File(f.getAbsolutePath() + ".mov"); + qos = new QuickTimeOutputStream(f, QuickTimeOutputStream.VideoFormat.JPG); + } catch (IOException e) { + LOGGER.error("startRecording(File)", e); + + throw new RuntimeException(e); + } + qos.setVideoCompressionQuality(1f); + qos.setTimeScale(30); // 30 fps + + LOGGER.debug("startRecording(File f=" + f + ") - end"); + } + + public void stopRecording() { + LOGGER.debug("stopRecording() - start"); + + try { + QuickTimeOutputStream o = qos; + qos = null; + o.close(); + } catch (IOException e) { + LOGGER.error("stopRecording()", e); + + throw new RuntimeException(e); + } + + LOGGER.debug("stopRecording() - end"); + } + + public void toogleOrientation() { + LOGGER.debug("toogleOrientation() - start"); + + landscape = !landscape; + + LOGGER.debug("toogleOrientation() - end"); + } + + public interface ScreenCaptureListener { + void handleNewImage(Dimension size, BufferedImage image, boolean landscape); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/recording/DataAtomOutputStream.java b/src/main/java/com/github/xsavikx/androidscreencast/api/recording/DataAtomOutputStream.java new file mode 100644 index 0000000..fc214d1 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/recording/DataAtomOutputStream.java @@ -0,0 +1,317 @@ +package com.github.xsavikx.androidscreencast.api.recording; + +import java.io.FilterOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; + +public class DataAtomOutputStream extends FilterOutputStream { + + protected static final long MAC_TIMESTAMP_EPOCH = new GregorianCalendar(1904, Calendar.JANUARY, 1).getTimeInMillis(); + /** + * The number of bytes written to the data output stream so far. If this counter overflows, it will be wrapped to Integer.MAX_VALUE. + */ + protected long written; + + public DataAtomOutputStream(OutputStream out) { + super(out); + } + + /** + * Increases the written counter by the specified value until it reaches Long.MAX_VALUE. + */ + protected void incCount(int value) { + long temp = written + value; + if (temp < 0) { + temp = Long.MAX_VALUE; + } + written = temp; + } + + /** + * Returns the current value of the counter written, the number of bytes written to this data output stream so far. If the counter + * overflows, it will be wrapped to Integer.MAX_VALUE. + * + * @return the value of the written field. + * @see java.io.DataOutputStream#written + */ + public final long size() { + return written; + } + + /** + * Writes len bytes from the specified byte array starting at offset off to the underlying output stream. If no exception + * is thrown, the counter written is incremented by len . + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterOutputStream#out + */ + @Override + public synchronized void write(byte b[], int off, int len) throws IOException { + out.write(b, off, len); + incCount(len); + } + + /** + * Writes the specified byte (the low eight bits of the argument b) to the underlying output stream. If no exception is thrown, the + * counter written is incremented by 1 . + *

+ * Implements the write method of OutputStream. + * + * @param b the byte to be written. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterOutputStream#out + */ + @Override + public synchronized void write(int b) throws IOException { + out.write(b); + incCount(1); + } + + /** + * Writes a BCD2 to the underlying output stream. + * + * @param v an int to be written. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterOutputStream#out + */ + public void writeBCD2(int v) throws IOException { + out.write(((v % 100 / 10) << 4) | (v % 10)); + incCount(1); + } + + /** + * Writes a BCD4 to the underlying output stream. + * + * @param v an int to be written. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterOutputStream#out + */ + public void writeBCD4(int v) throws IOException { + out.write(((v % 10000 / 1000) << 4) | (v % 1000 / 100)); + out.write(((v % 100 / 10) << 4) | (v % 10)); + incCount(2); + } + + /** + * Writes out a byte to the underlying output stream as a 1-byte value. If no exception is thrown, the counter written is + * incremented by 1. + * + * @param v a byte value to be written. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterOutputStream#out + */ + public final void writeByte(int v) throws IOException { + out.write(v); + incCount(1); + } + + /** + * Writes 32-bit fixed-point number divided as 16.16. + * + * @param f an int to be written. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterOutputStream#out + */ + public void writeFixed16D16(double f) throws IOException { + double v = (f >= 0) ? f : -f; + + int wholePart = (int) v; + int fractionPart = (int) ((v - wholePart) * 65536); + int t = (wholePart << 16) + fractionPart; + + if (f < 0) { + t = t - 1; + } + writeInt(t); + } + + /** + * Writes 32-bit fixed-point number divided as 2.30. + * + * @param f an int to be written. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterOutputStream#out + */ + public void writeFixed2D30(double f) throws IOException { + double v = (f >= 0) ? f : -f; + + int wholePart = (int) v; + int fractionPart = (int) ((v - wholePart) * 1073741824); + int t = (wholePart << 30) + fractionPart; + + if (f < 0) { + t = t - 1; + } + writeInt(t); + } + + /** + * Writes 16-bit fixed-point number divided as 8.8. + * + * @param f an int to be written. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterOutputStream#out + */ + public void writeFixed8D8(float f) throws IOException { + float v = (f >= 0) ? f : -f; + + int wholePart = (int) v; + int fractionPart = (int) ((v - wholePart) * 256); + int t = (wholePart << 8) + fractionPart; + + if (f < 0) { + t = t - 1; + } + writeUShort(t); + } + + /** + * Writes an int to the underlying output stream as four bytes, high byte first. If no exception is thrown, the counter + * written is incremented by 4. + * + * @param v an int to be written. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterOutputStream#out + */ + public void writeInt(int v) throws IOException { + out.write((v >>> 24) & 0xff); + out.write((v >>> 16) & 0xff); + out.write((v >>> 8) & 0xff); + out.write((v >>> 0) & 0xff); + incCount(4); + } + + public void writeLong(long v) throws IOException { + out.write((int) (v >>> 56) & 0xff); + out.write((int) (v >>> 48) & 0xff); + out.write((int) (v >>> 40) & 0xff); + out.write((int) (v >>> 32) & 0xff); + out.write((int) (v >>> 24) & 0xff); + out.write((int) (v >>> 16) & 0xff); + out.write((int) (v >>> 8) & 0xff); + out.write((int) (v >>> 0) & 0xff); + incCount(8); + } + + /** + * Writes a 32-bit Mac timestamp (seconds since 1902). + * + * @param date + * @throws java.io.IOException + */ + public void writeMacTimestamp(Date date) throws IOException { + long millis = date.getTime(); + long qtMillis = millis - MAC_TIMESTAMP_EPOCH; + long qtSeconds = qtMillis / 1000; + writeUInt(qtSeconds); + } + + /** + * Writes a Pascal String. + * + * @param s + * @throws java.io.IOException + */ + public void writePString(String s) throws IOException { + if (s.length() > 0xffff) { + throw new IllegalArgumentException("String too long for PString"); + } + if (s.length() < 256) { + out.write(s.length()); + } else { + out.write(0); + writeShort(s.length()); // increments +2 + } + for (int i = 0; i < s.length(); i++) { + out.write(s.charAt(i)); + } + incCount(1 + s.length()); + } + + /** + * Writes a Pascal String padded to the specified fixed size in bytes + * + * @param s + * @param length the fixed size in bytes + * @throws java.io.IOException + */ + public void writePString(String s, int length) throws IOException { + if (s.length() > length) { + throw new IllegalArgumentException("String too long for PString of length " + length); + } + if (s.length() < 256) { + out.write(s.length()); + } else { + out.write(0); + writeShort(s.length()); // increments +2 + } + for (int i = 0; i < s.length(); i++) { + out.write(s.charAt(i)); + } + + // write pad bytes + for (int i = 1 + s.length(); i < length; i++) { + out.write(0); + } + + incCount(length); + } + + /** + * Writes a signed 16 bit integer value. + * + * @param v The value + * @throws java.io.IOException + */ + public void writeShort(int v) throws IOException { + out.write((v >> 8) & 0xff); + out.write((v >>> 0) & 0xff); + incCount(2); + } + + /** + * Writes an Atom Type identifier (4 bytes). + * + * @param s A string with a length of 4 characters. + */ + public void writeType(String s) throws IOException { + if (s.length() != 4) { + throw new IllegalArgumentException("type string must have 4 characters"); + } + + try { + out.write(s.getBytes("ASCII"), 0, 4); + incCount(4); + } catch (UnsupportedEncodingException e) { + throw new InternalError(e.toString()); + } + } + + /** + * Writes an unsigned 32 bit integer value. + * + * @param v The value + * @throws java.io.IOException + */ + public void writeUInt(long v) throws IOException { + out.write((int) ((v >>> 24) & 0xff)); + out.write((int) ((v >>> 16) & 0xff)); + out.write((int) ((v >>> 8) & 0xff)); + out.write((int) ((v >>> 0) & 0xff)); + incCount(4); + } + + public void writeUShort(int v) throws IOException { + out.write((v >> 8) & 0xff); + out.write((v >>> 0) & 0xff); + incCount(2); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/recording/FilterImageOutputStream.java b/src/main/java/com/github/xsavikx/androidscreencast/api/recording/FilterImageOutputStream.java new file mode 100644 index 0000000..199baca --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/recording/FilterImageOutputStream.java @@ -0,0 +1,80 @@ +package com.github.xsavikx.androidscreencast.api.recording; + +import javax.imageio.stream.ImageOutputStream; +import java.io.FilterOutputStream; +import java.io.IOException; + +public class FilterImageOutputStream extends FilterOutputStream { + private ImageOutputStream imgOut; + + public FilterImageOutputStream(ImageOutputStream iOut) { + super(null); + this.imgOut = iOut; + } + + /** + * Closes this output stream and releases any system resources associated with the stream. + *

+ * The close method of FilterOutputStream calls its flush method, and then calls the close + * method of its underlying output stream. + * + * @throws IOException if an I/O error occurs. + * @see java.io.FilterOutputStream#flush() + * @see java.io.FilterOutputStream#out + */ + @Override + public void close() throws IOException { + flush(); + imgOut.close(); + } + + /** + * Flushes this output stream and forces any buffered output bytes to be written out to the stream. + *

+ * The flush method of FilterOutputStream calls the flush method of its underlying output stream. + * + * @throws IOException if an I/O error occurs. + * @see java.io.FilterOutputStream#out + */ + @Override + public void flush() { + // System.err.println(this+" discarded flush"); + // imgOut.flush(); + } + + /** + * Writes len bytes from the specified byte array starting at offset off to this output stream. + *

+ * The write method of FilterOutputStream calls the write method of one argument on each byte to + * output. + *

+ * Note that this method does not call the write method of its underlying input stream with the same arguments. Subclasses of + * FilterOutputStream should provide a more efficient implementation of this method. + * + * @param b the data. + * @param off the start offset in the data. + * @param len the number of bytes to write. + * @throws IOException if an I/O error occurs. + * @see java.io.FilterOutputStream#write(int) + */ + @Override + public void write(byte b[], int off, int len) throws IOException { + imgOut.write(b, off, len); + } + + /** + * Writes the specified byte to this output stream. + *

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

+ * Implements the abstract write method of OutputStream. + * + * @param b the byte. + * @throws IOException if an I/O error occurs. + */ + @Override + public void write(int b) throws IOException { + imgOut.write(b); + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/api/recording/QuickTimeOutputStream.java b/src/main/java/com/github/xsavikx/androidscreencast/api/recording/QuickTimeOutputStream.java new file mode 100644 index 0000000..313833f --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/api/recording/QuickTimeOutputStream.java @@ -0,0 +1,1596 @@ +package com.github.xsavikx.androidscreencast.api.recording; + +import javax.imageio.IIOImage; +import javax.imageio.ImageIO; +import javax.imageio.ImageWriteParam; +import javax.imageio.ImageWriter; +import javax.imageio.stream.FileImageOutputStream; +import javax.imageio.stream.ImageOutputStream; +import javax.imageio.stream.MemoryCacheImageOutputStream; +import java.awt.image.BufferedImage; +import java.awt.image.WritableRaster; +import java.io.*; +import java.util.Date; +import java.util.LinkedList; + +public class QuickTimeOutputStream { + + /** + * Output stream of the QuickTimeOutputStream. + */ + private ImageOutputStream out; + /** + * Current video format. + */ + private VideoFormat videoFormat; + /** + * Quality of JPEG encoded video frames. + */ + private float quality = 0.9f; + /** + * Creation time of the movie output stream. + */ + private Date creationTime; + /** + * Width of the video frames. All frames must have the same width. The value -1 is used to mark unspecified width. + */ + private int imgWidth = -1; + /** + * Height of the video frames. All frames must have the same height. The value -1 is used to mark unspecified height. + */ + private int imgHeight = -1; + /** + * The timeScale of the movie. A time value that indicates the time scale for this media-that is, the number of time units that pass per second in + * its time coordinate system. + */ + private int timeScale = 600; + /** + * The current state of the movie output stream. + */ + private States state = States.FINISHED; + /** + * List of video frames. + */ + private LinkedList videoFrames; + /** + * This atom holds the movie frames. + */ + private WideDataAtom mdatAtom; + + /** + * Creates a new output stream with the specified image videoFormat and framerate. + * + * @param file the output file + * @param format Selects an encoder for the video format "JPG" or "PNG". + * @throws IllegalArgumentException if videoFormat is null or if framerate is <= 0 + */ + public QuickTimeOutputStream(File file, VideoFormat format) throws IOException { + if (file.exists()) { + file.delete(); + } + out = new FileImageOutputStream(file); + + if (format == null) { + throw new IllegalArgumentException("format must not be null"); + } + + this.videoFormat = format; + + this.videoFrames = new LinkedList<>(); + } + + /** + * Closes the movie file as well as the stream being filtered. + * + * @throws IOException if an I/O error has occurred + */ + public void close() throws IOException { + if (state == States.STARTED) { + finish(); + } + if (state != States.CLOSED) { + out.close(); + state = States.CLOSED; + } + } + + /** + * Check to make sure that this stream has not been closed + */ + private void ensureOpen() throws IOException { + if (state == States.CLOSED) { + throw new IOException("Stream closed"); + } + } + + /** + * Sets the state of the QuickTimeOutpuStream to started. + *

+ * If the state is changed by this method, the prolog is written. + */ + private void ensureStarted() throws IOException { + if (state != States.STARTED) { + creationTime = new Date(); + writeProlog(); + mdatAtom = new WideDataAtom("mdat"); + state = States.STARTED; + } + } + + /** + * Finishes writing the contents of the QuickTime output stream without closing the underlying stream. Use this method when applying multiple + * filters in succession to the same output stream. + * + * @throws IllegalStateException if the dimension of the video track has not been specified or determined yet. + * @throws IOException if an I/O exception has occurred + */ + public void finish() throws IOException { + ensureOpen(); + if (state != States.FINISHED) { + if (imgWidth == -1 || imgHeight == -1) { + throw new IllegalStateException("image width and height must be specified"); + } + mdatAtom.finish(); + writeEpilog(); + state = States.FINISHED; + imgWidth = imgHeight = -1; + } + } + + /** + * Returns the time scale of this media. + * + * @return time scale + */ + public int getTimeScale() { + return timeScale; + } + + /** + * Sets the time scale for this media, that is, the number of time units that pass per second in its time coordinate system. + *

+ * The default value is 600. + * + * @param newValue + */ + public void setTimeScale(int newValue) { + if (newValue <= 0) { + throw new IllegalArgumentException("timeScale must be greater 0"); + } + this.timeScale = newValue; + } + + /** + * Returns the video compression quality. + * + * @return video compression quality + */ + public float getVideoCompressionQuality() { + return quality; + } + + /** + * Sets the compression quality of the video track. A value of 0 stands for "high compression is important" a value of 1 for + * "high image quality is important". + *

+ * Changing this value affects frames which are subsequently written to the QuickTimeOutputStream. Frames which have already been written are not + * changed. + *

+ * This value has no effect on videos encoded with the PNG format. + *

+ * The default value is 0.9. + * + * @param newValue + */ + public void setVideoCompressionQuality(float newValue) { + this.quality = newValue; + } + + /** + * Sets the dimension of the video track. + *

+ * You need to explicitly set the dimension, if you add all frames from files or input streams. + *

+ * If you add frames from buffered images, then QuickTimeOutputStream can determine the video dimension from the image width and height. + * + * @param width + * @param height + */ + public void setVideoDimension(int width, int height) { + if (width < 1 || height < 1) { + throw new IllegalArgumentException("width and height must be greater zero."); + } + this.imgWidth = width; + this.imgHeight = height; + } + + private void writeEpilog() throws IOException { + Date modificationTime = new Date(); + int duration = 0; + for (Sample s : videoFrames) { + duration += s.duration; + } + + DataAtom leaf; + + /* Movie Atom ========= */ + CompositeAtom moovAtom = new CompositeAtom("moov"); + + /* + * Movie Header Atom ------------- The data contained in this atom defines characteristics of the entire QuickTime movie, such as time scale and + * duration. It has an atom type value of 'mvhd'. + * + * typedef struct { byte version; byte[3] flags; mactimestamp creationTime; mactimestamp modificationTime; int timeScale; int duration; int + * preferredRate; short preferredVolume; byte[10] reserved; int[9] matrix; int previewTime; int previewDuration; int posterTime; int + * selectionTime; int selectionDuration; int currentTime; int nextTrackId; } movieHeaderAtom; + */ + leaf = new DataAtom("mvhd"); + moovAtom.add(leaf); + DataAtomOutputStream d = leaf.getOutputStream(); + d.writeByte(0); // version + // A 1-byte specification of the version of this movie header atom. + + d.writeByte(0); // flags[0] + d.writeByte(0); // flags[1] + d.writeByte(0); // flags[2] + // Three bytes of space for future movie header flags. + + d.writeMacTimestamp(creationTime); // creationTime + // A 32-bit integer that specifies the calendar date and time (in + // seconds since midnight, January 1, 1904) when the movie atom was + // created. It is strongly recommended that this value should be + // specified using coordinated universal time (UTC). + + d.writeMacTimestamp(modificationTime); // modificationTime + // A 32-bit integer that specifies the calendar date and time (in + // seconds since midnight, January 1, 1904) when the movie atom was + // changed. BooleanIt is strongly recommended that this value should be + // specified using coordinated universal time (UTC). + + d.writeInt(timeScale); // timeScale + // A time value that indicates the time scale for this movie-that is, + // the number of time units that pass per second in its time coordinate + // system. A time coordinate system that measures time in sixtieths of a + // second, for example, has a time scale of 60. + + d.writeInt(duration); // duration + // A time value that indicates the duration of the movie in time scale + // units. Note that this property is derived from the movie's tracks. + // The value of this field corresponds to the duration of the longest + // track in the movie. + + d.writeFixed16D16(1d); // preferredRate + // A 32-bit fixed-point number that specifies the rate at which to play + // this movie. A value of 1.0 indicates normal rate. + + d.writeShort(256); // preferredVolume + // A 16-bit fixed-point number that specifies how loud to play this + // movie's sound. A value of 1.0 indicates full volume. + + d.write(new byte[10]); // reserved; + // Ten bytes reserved for use by Apple. Set to 0. + + d.writeFixed16D16(1f); // matrix[0] + d.writeFixed16D16(0f); // matrix[1] + d.writeFixed2D30(0f); // matrix[2] + d.writeFixed16D16(0f); // matrix[3] + d.writeFixed16D16(1f); // matrix[4] + d.writeFixed2D30(0); // matrix[5] + d.writeFixed16D16(0); // matrix[6] + d.writeFixed16D16(0); // matrix[7] + d.writeFixed2D30(1f); // matrix[8] + // The matrix structure associated with this movie. A matrix shows how + // to map points from one coordinate space into another. See "Matrices" + // for a discussion of how display matrices are used in QuickTime: + // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap4/chapter_5_section_4.html#//apple_ref/doc/uid/TP40000939-CH206-18737 + + d.writeInt(0); // previewTime + // The time value in the movie at which the preview begins. + + d.writeInt(0); // previewDuration + // The duration of the movie preview in movie time scale units. + + d.writeInt(0); // posterTime + // The time value of the time of the movie poster. + + d.writeInt(0); // selectionTime + // The time value for the start time of the current selection. + + d.writeInt(0); // selectionDuration + // The duration of the current selection in movie time scale units. + + d.writeInt(0); // currentTime; + // The time value for current time position within the movie. + + d.writeInt(2); // nextTrackId + // A 32-bit integer that indicates a value to use for the track ID + // number of the next track added to this movie. Note that 0 is not a + // valid track ID value. + + /* Track Atom ======== */ + CompositeAtom trakAtom = new CompositeAtom("trak"); + moovAtom.add(trakAtom); + + /* + * Track Header Atom ----------- The track header atom specifies the characteristics of a single track within a movie. A track header atom + * contains a size field that specifies the number of bytes and a type field that indicates the format of the data (defined by the atom type + * 'tkhd'). + * + * typedef struct { byte version; byte flag0; byte flag1; byte set TrackHeaderFlags flag2; mactimestamp creationTime; mactimestamp + * modificationTime; int trackId; byte[4] reserved; int duration; byte[8] reserved; short layer; short alternateGroup; short volume; byte[2] + * reserved; int[9] matrix; int trackWidth; int trackHeight; } trackHeaderAtom; + */ + leaf = new DataAtom("tkhd"); + trakAtom.add(leaf); + d = leaf.getOutputStream(); + d.write(0); // version + // A 1-byte specification of the version of this track header. + + d.write(0); // flag[0] + d.write(0); // flag[1] + d.write(0xf); // flag[2] + // Three bytes that are reserved for the track header flags. These flags + // indicate how the track is used in the movie. The following flags are + // valid (all flags are enabled when set to 1): + // + // Track enabled + // Indicates that the track is enabled. Flag value is 0x0001. + // Track in movie + // Indicates that the track is used in the movie. Flag value is + // 0x0002. + // Track in preview + // Indicates that the track is used in the movie's preview. Flag + // value is 0x0004. + // Track in poster + // Indicates that the track is used in the movie's poster. Flag + // value is 0x0008. + + d.writeMacTimestamp(creationTime); // creationTime + // A 32-bit integer that indicates the calendar date and time (expressed + // in seconds since midnight, January 1, 1904) when the track header was + // created. It is strongly recommended that this value should be + // specified using coordinated universal time (UTC). + + d.writeMacTimestamp(modificationTime); // modificationTime + // A 32-bit integer that indicates the calendar date and time (expressed + // in seconds since midnight, January 1, 1904) when the track header was + // changed. It is strongly recommended that this value should be + // specified using coordinated universal time (UTC). + + d.writeInt(1); // trackId + // A 32-bit integer that uniquely identifies the track. The value 0 + // cannot be used. + + d.writeInt(0); // reserved; + // A 32-bit integer that is reserved for use by Apple. Set this field to + // 0. + + d.writeInt(duration); // duration + // A time value that indicates the duration of this track (in the + // movie's time coordinate system). Note that this property is derived + // from the track's edits. The value of this field is equal to the sum + // of the durations of all of the track's edits. If there is no edit + // list, then the duration is the sum of the sample durations, converted + // into the movie timescale. + + d.writeLong(0); // reserved + // An 8-byte value that is reserved for use by Apple. Set this field to + // 0. + + d.writeShort(0); // layer; + // A 16-bit integer that indicates this track's spatial priority in its + // movie. The QuickTime Movie Toolbox uses this value to determine how + // tracks overlay one another. Tracks with lower layer values are + // displayed in front of tracks with higher layer values. + + d.writeShort(0); // alternate group + // A 16-bit integer that specifies a collection of movie tracks that + // contain alternate data for one another. QuickTime chooses one track + // from the group to be used when the movie is played. The choice may be + // based on such considerations as playback quality, language, or the + // capabilities of the computer. + + d.writeShort(0); // volume + // A 16-bit fixed-point value that indicates how loudly this track's + // sound is to be played. A value of 1.0 indicates normal volume. + + d.writeShort(0); // reserved + // A 16-bit integer that is reserved for use by Apple. Set this field to + // 0. + + d.writeFixed16D16(1f); // matrix[0] + d.writeFixed16D16(0f); // matrix[1] + d.writeFixed2D30(0f); // matrix[2] + d.writeFixed16D16(0f); // matrix[3] + d.writeFixed16D16(1f); // matrix[4] + d.writeFixed2D30(0); // matrix[5] + d.writeFixed16D16(0); // matrix[6] + d.writeFixed16D16(0); // matrix[7] + d.writeFixed2D30(1f); // matrix[8] + // The matrix structure associated with this track. + // See Figure 2-8 for an illustration of a matrix structure: + // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_3.html#//apple_ref/doc/uid/TP40000939-CH204-32967 + + d.writeFixed16D16(imgWidth); // width + // A 32-bit fixed-point number that specifies the width of this track in + // pixels. + + d.writeFixed16D16(imgHeight); // height + // A 32-bit fixed-point number that indicates the height of this track + // in pixels. + + /* Media Atom ========= */ + CompositeAtom mdiaAtom = new CompositeAtom("mdia"); + trakAtom.add(mdiaAtom); + + /* + * Media Header atom ------- typedef struct { byte version; byte[3] flags; mactimestamp creationTime; mactimestamp modificationTime; int + * timeScale; int duration; short language; short quality; } mediaHeaderAtom; + */ + leaf = new DataAtom("mdhd"); + mdiaAtom.add(leaf); + d = leaf.getOutputStream(); + d.write(0); // version + // One byte that specifies the version of this header atom. + + d.write(0); // flag[0] + d.write(0); // flag[1] + d.write(0); // flag[2] + // Three bytes of space for media header flags. Set this field to 0. + + d.writeMacTimestamp(creationTime); // creationTime + // A 32-bit integer that specifies (in seconds since midnight, January + // 1, 1904) when the media atom was created. It is strongly recommended + // that this value should be specified using coordinated universal time + // (UTC). + + d.writeMacTimestamp(modificationTime); // modificationTime + // A 32-bit integer that specifies (in seconds since midnight, January + // 1, 1904) when the media atom was changed. It is strongly recommended + // that this value should be specified using coordinated universal time + // (UTC). + + d.writeInt(timeScale); // timeScale + // A time value that indicates the time scale for this media-that is, + // the number of time units that pass per second in its time coordinate + // system. + + d.writeInt(duration); // duration + // The duration of this media in units of its time scale. + + d.writeShort(0); // language; + // A 16-bit integer that specifies the language code for this media. + // See "Language Code Values" for valid language codes: + // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap4/chapter_5_section_2.html#//apple_ref/doc/uid/TP40000939-CH206-27005 + + d.writeShort(0); // quality + // A 16-bit integer that specifies the media's playback quality-that is, + // its suitability for playback in a given environment. + + /** Media Handler Atom ------- */ + leaf = new DataAtom("hdlr"); + mdiaAtom.add(leaf); + /* + * typedef struct { byte version; byte[3] flags; magic componentType; magic componentSubtype; magic componentManufacturer; int componentFlags; int + * componentFlagsMask; cstring componentName; } handlerReferenceAtom; + */ + d = leaf.getOutputStream(); + d.write(0); // version + // A 1-byte specification of the version of this handler information. + + d.write(0); // flag[0] + d.write(0); // flag[1] + d.write(0); // flag[2] + // A 3-byte space for handler information flags. Set this field to 0. + + d.writeType("mhlr"); // componentType + // A four-character code that identifies the type of the handler. Only + // two values are valid for this field: 'mhlr' for media handlers and + // 'dhlr' for data handlers. + + d.writeType("vide"); // componentSubtype + // A four-character code that identifies the type of the media handler + // or data handler. For media handlers, this field defines the type of + // data-for example, 'vide' for video data or 'soun' for sound data. + // + // For data handlers, this field defines the data reference type-for + // example, a component subtype value of 'alis' identifies a file alias. + + d.writeInt(0); // componentManufacturer + // Reserved. Set to 0. + + d.writeInt(0); // componentFlags + // Reserved. Set to 0. + + d.writeInt(0); // componentFlagsMask + // Reserved. Set to 0. + + d.write(0); // componentName (empty string) + // A (counted) string that specifies the name of the component-that is, + // the media handler used when this media was created. This field may + // contain a zero-length (empty) string. + + /* Media Information atom ========= */ + CompositeAtom minfAtom = new CompositeAtom("minf"); + mdiaAtom.add(minfAtom); + + /* Video media information atom -------- */ + leaf = new DataAtom("vmhd"); + minfAtom.add(leaf); + /* + * typedef struct { byte version; byte flag1; byte flag2; byte set vmhdFlags flag3; short graphicsMode; ushort[3] opcolor; } + * videoMediaInformationHeaderAtom; + */ + d = leaf.getOutputStream(); + d.write(0); // version + // One byte that specifies the version of this header atom. + + d.write(0); // flag[0] + d.write(0); // flag[1] + d.write(0x1); // flag[2] + // Three bytes of space for media header flags. + // This is a compatibility flag that allows QuickTime to distinguish + // between movies created with QuickTime 1.0 and newer movies. You + // should always set this flag to 1, unless you are creating a movie + // intended for playback using version 1.0 of QuickTime. This flag's + // value is 0x0001. + + d.writeShort(0x40); // graphicsMode (0x40 = ditherCopy) + // A 16-bit integer that specifies the transfer mode. The transfer mode + // specifies which Boolean operation QuickDraw should perform when + // drawing or transferring an image from one location to another. + // See "Graphics Modes" for a list of graphics modes supported by + // QuickTime: + // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap4/chapter_5_section_5.html#//apple_ref/doc/uid/TP40000939-CH206-18741 + + d.writeUShort(0); // opcolor[0] + d.writeUShort(0); // opcolor[1] + d.writeUShort(0); // opcolor[2] + // Three 16-bit values that specify the red, green, and blue colors for + // the transfer mode operation indicated in the graphics mode field. + + /* Handle reference atom -------- */ + // The handler reference atom specifies the media handler component that + // is to be used to interpret the media's data. The handler reference + // atom has an atom type value of 'hdlr'. + leaf = new DataAtom("hdlr"); + minfAtom.add(leaf); + /* + * typedef struct { byte version; byte[3] flags; magic componentType; magic componentSubtype; magic componentManufacturer; int componentFlags; int + * componentFlagsMask; cstring componentName; } handlerReferenceAtom; + */ + d = leaf.getOutputStream(); + d.write(0); // version + // A 1-byte specification of the version of this handler information. + + d.write(0); // flag[0] + d.write(0); // flag[1] + d.write(0); // flag[2] + // A 3-byte space for handler information flags. Set this field to 0. + + d.writeType("dhlr"); // componentType + // A four-character code that identifies the type of the handler. Only + // two values are valid for this field: 'mhlr' for media handlers and + // 'dhlr' for data handlers. + + d.writeType("alis"); // componentSubtype + // A four-character code that identifies the type of the media handler + // or data handler. For media handlers, this field defines the type of + // data-for example, 'vide' for video data or 'soun' for sound data. + // For data handlers, this field defines the data reference type-for + // example, a component subtype value of 'alis' identifies a file alias. + + d.writeInt(0); // componentManufacturer + // Reserved. Set to 0. + + d.writeInt(0); // componentFlags + // Reserved. Set to 0. + + d.writeInt(0); // componentFlagsMask + // Reserved. Set to 0. + + d.write(0); // componentName (empty string) + // A (counted) string that specifies the name of the component-that is, + // the media handler used when this media was created. This field may + // contain a zero-length (empty) string. + + /* Data information atom ===== */ + CompositeAtom dinfAtom = new CompositeAtom("dinf"); + minfAtom.add(dinfAtom); + + /* Data reference atom ----- */ + // Data reference atoms contain tabular data that instructs the data + // handler component how to access the media's data. + leaf = new DataAtom("dref"); + dinfAtom.add(leaf); + /* + * typedef struct { ubyte version; ubyte[3] flags; int numberOfEntries; dataReferenceEntry dataReference[numberOfEntries]; } dataReferenceAtom; + * + * set { dataRefSelfReference=1 // I am not shure if this is the correct value for this flag } drefEntryFlags; + * + * typedef struct { int size; magic type; byte version; ubyte flag1; ubyte flag2; ubyte set drefEntryFlags flag3; byte[size - 12] data; } + * dataReferenceEntry; + */ + d = leaf.getOutputStream(); + d.write(0); // version + // A 1-byte specification of the version of this data reference atom. + + d.write(0); // flag[0] + d.write(0); // flag[1] + d.write(0); // flag[2] + // A 3-byte space for data reference flags. Set this field to 0. + + d.writeInt(1); // numberOfEntries + // A 32-bit integer containing the count of data references that follow. + + d.writeInt(12); // dataReference.size + // A 32-bit integer that specifies the number of bytes in the data + // reference. + + d.writeType("alis"); // dataReference.type + // A 32-bit integer that specifies the type of the data in the data + // reference. Table 2-4 lists valid type values: + // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_4.html#//apple_ref/doc/uid/TP40000939-CH204-38840 + + d.write(0); // dataReference.version + // A 1-byte specification of the version of the data reference. + + d.write(0); // dataReference.flag1 + d.write(0); // dataReference.flag2 + d.write(0x1); // dataReference.flag3 + // A 3-byte space for data reference flags. There is one defined flag. + // + // Self reference + // This flag indicates that the media's data is in the same file as + // the movie atom. On the Macintosh, and other file systems with + // multifork files, set this flag to 1 even if the data resides in + // a different fork from the movie atom. This flag's value is + // 0x0001. + + /* Sample Table atom ========= */ + CompositeAtom stblAtom = new CompositeAtom("stbl"); + minfAtom.add(stblAtom); + + /* Sample Description atom ------- */ + // The sample description atom stores information that allows you to + // decode samples in the media. The data stored in the sample + // description varies, depending on the media type. For example, in the + // case of video media, the sample descriptions are image description + // structures. The sample description information for each media type is + // explained in "Media Data Atom Types": + // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_1.html#//apple_ref/doc/uid/TP40000939-CH205-SW1 + leaf = new DataAtom("stsd"); + stblAtom.add(leaf); + /* + * typedef struct { byte version; byte[3] flags; int numberOfEntries; sampleDescriptionEntry sampleDescriptionTable[numberOfEntries]; } + * sampleDescriptionAtom; + * + * typedef struct { int size; magic type; byte[6] reserved; // six bytes that must be zero short dataReferenceIndex; // A 16-bit integer that + * contains the index of the data reference to use to retrieve data associated with samples that use this sample description. Data references are + * stored in data reference atoms. byte[size - 16] data; } sampleDescriptionEntry; + */ + d = leaf.getOutputStream(); + d.write(0); // version + // A 1-byte specification of the version of this sample description + // atom. + + d.write(0); // flag[0] + d.write(0); // flag[1] + d.write(0); // flag[2] + // A 3-byte space for sample description flags. Set this field to 0. + + d.writeInt(1); // number of Entries + // A 32-bit integer containing the number of sample descriptions that + // follow. + + // A 32-bit integer indicating the number of bytes in the sample + // description. + switch (videoFormat) { + case RAW: { + d.writeInt(86); // sampleDescriptionTable[0].size + d.writeType("raw "); // sampleDescriptionTable[0].type + + // A 32-bit integer indicating the format of the stored data. + // This depends on the media type, but is usually either the + // compression format or the media type. + + d.write(new byte[6]); // sampleDescriptionTable[0].reserved + // Six bytes that must be set to 0. + + d.writeShort(1); // sampleDescriptionTable[0].dataReferenceIndex + // A 16-bit integer that contains the index of the data + // reference to use to retrieve data associated with samples + // that use this sample description. Data references are stored + // in data reference atoms. + + // Video Sample Description + // ------------------------ + // The format of the following fields is described here: + // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html#//apple_ref/doc/uid/TP40000939-CH205-BBCGICBJ + + d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.version + // A 16-bit integer indicating the version number of the + // compressed data. This is set to 0, unless a compressor has + // changed its data format. + + d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.revisionLevel + // A 16-bit integer that must be set to 0. + + d.writeType("java"); // sampleDescriptionTable.videoSampleDescription.manufacturer + // A 32-bit integer that specifies the developer of the + // compressor that generated the compressed data. Often this + // field contains 'appl' to indicate Apple Computer, Inc. + + d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.temporalQuality + // A 32-bit integer containing a value from 0 to 1023 indicating + // the degree of temporal compression. + + d.writeInt(512); // sampleDescriptionTable.videoSampleDescription.spatialQuality + // A 32-bit integer containing a value from 0 to 1024 indicating + // the degree of spatial compression. + + d.writeUShort(imgWidth); // sampleDescriptionTable.videoSampleDescription.width + // A 16-bit integer that specifies the width of the source image + // in pixels. + + d.writeUShort(imgHeight); // sampleDescriptionTable.videoSampleDescription.height + // A 16-bit integer that specifies the height of the source image in + // pixels. + + d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.horizontalResolution + // A 32-bit fixed-point number containing the horizontal + // resolution of the image in pixels per inch. + + d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.verticalResolution + // A 32-bit fixed-point number containing the vertical + // resolution of the image in pixels per inch. + + d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.dataSize + // A 32-bit integer that must be set to 0. + + d.writeShort(1); // sampleDescriptionTable.videoSampleDescription.frameCount + // A 16-bit integer that indicates how many frames of compressed + // data are stored in each sample. Usually set to 1. + + d.writePString("None", 32); // sampleDescriptionTable.videoSampleDescription.compressorName + // A 32-byte Pascal string containing the name of the compressor + // that created the image, such as "jpeg". + + d.writeShort(24); // sampleDescriptionTable.videoSampleDescription.depth + // A 16-bit integer that indicates the pixel depth of the + // compressed image. Values of 1, 2, 4, 8 ,16, 24, and 32 + // indicate the depth of color images. The value 32 should be + // used only if the image contains an alpha channel. Values of + // 34, 36, and 40 indicate 2-, 4-, and 8-bit grayscale, + // respectively, for grayscale images. + + d.writeShort(-1); // sampleDescriptionTable.videoSampleDescription.colorTableID + // A 16-bit integer that identifies which color table to use. + // If this field is set to -1, the default color table should be + // used for the specified depth. For all depths below 16 bits + // per pixel, this indicates a standard Macintosh color table + // for the specified depth. Depths of 16, 24, and 32 have no + // color table. + + break; + } + case JPG: { + d.writeInt(86); // sampleDescriptionTable[0].size + d.writeType("jpeg"); // sampleDescriptionTable[0].type + + // A 32-bit integer indicating the format of the stored data. + // This depends on the media type, but is usually either the + // compression format or the media type. + + d.write(new byte[6]); // sampleDescriptionTable[0].reserved + // Six bytes that must be set to 0. + + d.writeShort(1); // sampleDescriptionTable[0].dataReferenceIndex + // A 16-bit integer that contains the index of the data + // reference to use to retrieve data associated with samples + // that use this sample description. Data references are stored + // in data reference atoms. + + // Video Sample Description + // ------------------------ + // The format of the following fields is described here: + // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html#//apple_ref/doc/uid/TP40000939-CH205-BBCGICBJ + + d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.version + // A 16-bit integer indicating the version number of the + // compressed data. This is set to 0, unless a compressor has + // changed its data format. + + d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.revisionLevel + // A 16-bit integer that must be set to 0. + + d.writeType("java"); // sampleDescriptionTable.videoSampleDescription.manufacturer + // A 32-bit integer that specifies the developer of the + // compressor that generated the compressed data. Often this + // field contains 'appl' to indicate Apple Computer, Inc. + + d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.temporalQuality + // A 32-bit integer containing a value from 0 to 1023 indicating + // the degree of temporal compression. + + d.writeInt(512); // sampleDescriptionTable.videoSampleDescription.spatialQuality + // A 32-bit integer containing a value from 0 to 1024 indicating + // the degree of spatial compression. + + d.writeUShort(imgWidth); // sampleDescriptionTable.videoSampleDescription.width + // A 16-bit integer that specifies the width of the source image + // in pixels. + + d.writeUShort(imgHeight); // sampleDescriptionTable.videoSampleDescription.height + // A 16-bit integer that specifies the height of the source image in + // pixels. + + d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.horizontalResolution + // A 32-bit fixed-point number containing the horizontal + // resolution of the image in pixels per inch. + + d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.verticalResolution + // A 32-bit fixed-point number containing the vertical + // resolution of the image in pixels per inch. + + d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.dataSize + // A 32-bit integer that must be set to 0. + + d.writeShort(1); // sampleDescriptionTable.videoSampleDescription.frameCount + // A 16-bit integer that indicates how many frames of compressed + // data are stored in each sample. Usually set to 1. + + d.writePString("Photo - JPEG", 32); // sampleDescriptionTable.videoSampleDescription.compressorName + // A 32-byte Pascal string containing the name of the compressor + // that created the image, such as "jpeg". + + d.writeShort(24); // sampleDescriptionTable.videoSampleDescription.depth + // A 16-bit integer that indicates the pixel depth of the + // compressed image. Values of 1, 2, 4, 8 ,16, 24, and 32 + // indicate the depth of color images. The value 32 should be + // used only if the image contains an alpha channel. Values of + // 34, 36, and 40 indicate 2-, 4-, and 8-bit grayscale, + // respectively, for grayscale images. + + d.writeShort(-1); // sampleDescriptionTable.videoSampleDescription.colorTableID + // A 16-bit integer that identifies which color table to use. + // If this field is set to -1, the default color table should be + // used for the specified depth. For all depths below 16 bits + // per pixel, this indicates a standard Macintosh color table + // for the specified depth. Depths of 16, 24, and 32 have no + // color table. + + break; + } + case PNG: { + d.writeInt(86); // sampleDescriptionTable[0].size + d.writeType("png "); // sampleDescriptionTable[0].type + // A 32-bit integer indicating the format of the stored data. + // This depends on the media type, but is usually either the + // compression format or the media type. + + d.write(new byte[6]); // sampleDescriptionTable[0].reserved + // Six bytes that must be set to 0. + + d.writeShort(1); // sampleDescriptionTable[0].dataReferenceIndex + // A 16-bit integer that contains the index of the data + // reference to use to retrieve data associated with samples + // that use this sample description. Data references are stored + // in data reference atoms. + + // Video Sample Description + // ------------------------ + // The format of the following fields is described here: + // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html#//apple_ref/doc/uid/TP40000939-CH205-BBCGICBJ + + d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.version + // A 16-bit integer indicating the version number of the + // compressed data. This is set to 0, unless a compressor has + // changed its data format. + + d.writeShort(0); // sampleDescriptionTable.videoSampleDescription.revisionLevel + // A 16-bit integer that must be set to 0. + + d.writeType("java"); // sampleDescriptionTable.videoSampleDescription.manufacturer + // A 32-bit integer that specifies the developer of the + // compressor that generated the compressed data. Often this + // field contains 'appl' to indicate Apple Computer, Inc. + + d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.temporalQuality + // A 32-bit integer containing a value from 0 to 1023 indicating + // the degree of temporal compression. + + d.writeInt(512); // sampleDescriptionTable.videoSampleDescription.spatialQuality + // A 32-bit integer containing a value from 0 to 1024 indicating + // the degree of spatial compression. + + d.writeUShort(imgWidth); // sampleDescriptionTable.videoSampleDescription.width + // A 16-bit integer that specifies the width of the source image + // in pixels. + + d.writeUShort(imgHeight); // sampleDescriptionTable.videoSampleDescription.height + // A 16-bit integer that specifies the height of the source image in + // pixels. + + d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.horizontalResolution + // A 32-bit fixed-point number containing the horizontal + // resolution of the image in pixels per inch. + + d.writeFixed16D16(72.0); // sampleDescriptionTable.videoSampleDescription.verticalResolution + // A 32-bit fixed-point number containing the vertical + // resolution of the image in pixels per inch. + + d.writeInt(0); // sampleDescriptionTable.videoSampleDescription.dataSize + // A 32-bit integer that must be set to 0. + + d.writeShort(1); // sampleDescriptionTable.videoSampleDescription.frameCount + // A 16-bit integer that indicates how many frames of compressed + // data are stored in each sample. Usually set to 1. + + d.writePString("PNG", 32); // sampleDescriptionTable.videoSampleDescription.compressorName + // A 32-byte Pascal string containing the name of the compressor + // that created the image, such as "jpeg". + + d.writeShort(24); // sampleDescriptionTable.videoSampleDescription.depth + // A 16-bit integer that indicates the pixel depth of the + // compressed image. Values of 1, 2, 4, 8 ,16, 24, and 32 + // indicate the depth of color images. The value 32 should be + // used only if the image contains an alpha channel. Values of + // 34, 36, and 40 indicate 2-, 4-, and 8-bit grayscale, + // respectively, for grayscale images. + + d.writeShort(-1); // sampleDescriptionTable.videoSampleDescription.colorTableID + // A 16-bit integer that identifies which color table to use. + // If this field is set to -1, the default color table should be + // used for the specified depth. For all depths below 16 bits + // per pixel, this indicates a standard Macintosh color table + // for the specified depth. Depths of 16, 24, and 32 have no + // color table. + + break; + } + } + + /* Time to Sample atom ---- */ + // Time-to-sample atoms store duration information for a media's + // samples, providing a mapping from a time in a media to the + // corresponding data sample. The time-to-sample atom has an atom type + // of 'stts'. + leaf = new DataAtom("stts"); + stblAtom.add(leaf); + /* + * typedef struct { byte version; byte[3] flags; int numberOfEntries; timeToSampleTable timeToSampleTable[numberOfEntries]; } timeToSampleAtom; + * + * typedef struct { int sampleCount; int sampleDuration; } timeToSampleTable; + */ + d = leaf.getOutputStream(); + d.write(0); // version + // A 1-byte specification of the version of this time-to-sample atom. + + d.write(0); // flag[0] + d.write(0); // flag[1] + d.write(0); // flag[2] + // A 3-byte space for time-to-sample flags. Set this field to 0. + + // count runs of video frame durations + int runCount = 1; + int prevDuration = videoFrames.size() == 0 ? 0 : videoFrames.get(0).duration; + for (Sample s : videoFrames) { + if (s.duration != prevDuration) { + runCount++; + prevDuration = s.duration; + } + } + d.writeInt(runCount); // numberOfEntries + // A 32-bit integer containing the count of entries in the + // time-to-sample table. + + int runLength = 0; + prevDuration = videoFrames.size() == 0 ? 0 : videoFrames.get(0).duration; + for (Sample s : videoFrames) { + if (s.duration != prevDuration) { + if (runLength > 0) { + d.writeInt(runLength); // timeToSampleTable[0].sampleCount + // A 32-bit integer that specifies the number of consecutive + // samples that have the same duration. + + d.writeInt(prevDuration); // timeToSampleTable[0].sampleDuration + // A 32-bit integer that specifies the duration of each + // sample. + } + prevDuration = s.duration; + runLength = 1; + } else { + runLength++; + } + } + if (runLength > 0) { + d.writeInt(runLength); // timeToSampleTable[0].sampleCount + // A 32-bit integer that specifies the number of consecutive + // samples that have the same duration. + + d.writeInt(prevDuration); // timeToSampleTable[0].sampleDuration + // A 32-bit integer that specifies the duration of each + // sample. + } + /* sample to chunk atom -------- */ + // The sample-to-chunk atom contains a table that maps samples to chunks + // in the media data stream. By examining the sample-to-chunk atom, you + // can determine the chunk that contains a specific sample. + leaf = new DataAtom("stsc"); + stblAtom.add(leaf); + /* + * typedef struct { byte version; byte[3] flags; int numberOfEntries; sampleToChunkTable sampleToChunkTable[numberOfEntries]; } sampleToChunkAtom; + * + * typedef struct { int firstChunk; int samplesPerChunk; int sampleDescription; } sampleToChunkTable; + */ + d = leaf.getOutputStream(); + d.write(0); // version + // A 1-byte specification of the version of this time-to-sample atom. + + d.write(0); // flag[0] + d.write(0); // flag[1] + d.write(0); // flag[2] + // A 3-byte space for time-to-sample flags. Set this field to 0. + + d.writeInt(1); // number of entries + // A 32-bit integer containing the count of entries in the + // sample-to-chunk table. + + d.writeInt(1); // first chunk + // The first chunk number using this table entry. + + d.writeInt(1); // samples per chunk + // The number of samples in each chunk. + + d.writeInt(1); // sample description + // The identification number associated with the sample description for + // the sample. For details on sample description atoms, see "Sample + // Description Atoms.": + // http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap2/chapter_3_section_5.html#//apple_ref/doc/uid/TP40000939-CH204-25691 + + /* sample size atom -------- */ + // The sample size atom contains the sample count and a table giving the + // size of each sample. This allows the media data itself to be + // unframed. The total number of samples in the media is always + // indicated in the sample count. If the default size is indicated, then + // no table follows. + leaf = new DataAtom("stsz"); + stblAtom.add(leaf); + /* + * typedef struct { byte version; byte[3] flags; int sampleSize; int numberOfEntries; sampleSizeTable sampleSizeTable[numberOfEntries]; } + * sampleSizeAtom; + * + * typedef struct { int size; } sampleSizeTable; + */ + d = leaf.getOutputStream(); + d.write(0); // version + // A 1-byte specification of the version of this time-to-sample atom. + + d.write(0); // flag[0] + d.write(0); // flag[1] + d.write(0); // flag[2] + // A 3-byte space for time-to-sample flags. Set this field to 0. + + d.writeUInt(0); // sample size + // A 32-bit integer specifying the sample size. If all the samples are + // the same size, this field contains that size value. If this field is + // set to 0, then the samples have different sizes, and those sizes are + // stored in the sample size table. + + d.writeUInt(videoFrames.size()); // number of entries + // A 32-bit integer containing the count of entries in the sample size + // table. + + for (Sample s : videoFrames) { + d.writeUInt(s.length); // sample size + // The size field contains the size, in bytes, of the sample in + // question. The table is indexed by sample number-the first entry + // corresponds to the first sample, the second entry is for the + // second sample, and so on. + } + // + /* chunk offset atom -------- */ + // The chunk-offset table gives the index of each chunk into the + // containing file. There are two variants, permitting the use of + // 32-bit or 64-bit offsets. The latter is useful when managing very + // large movies. Only one of these variants occurs in any single + // instance of a sample table atom. + if (videoFrames.size() == 0 || videoFrames.getLast().offset <= 0xffffffffL) { + /* 32-bit chunk offset atom -------- */ + leaf = new DataAtom("stco"); + stblAtom.add(leaf); + /* + * typedef struct { byte version; byte[3] flags; int numberOfEntries; chunkOffsetTable chunkOffsetTable[numberOfEntries]; } chunkOffsetAtom; + * + * typedef struct { int offset; } chunkOffsetTable; + */ + d = leaf.getOutputStream(); + d.write(0); // version + // A 1-byte specification of the version of this time-to-sample + // atom. + + d.write(0); // flag[0] + d.write(0); // flag[1] + d.write(0); // flag[2] + // A 3-byte space for time-to-sample flags. Set this field to 0. + + d.writeUInt(videoFrames.size()); // number of entries + // A 32-bit integer containing the count of entries in the chunk + // offset table. + + for (Sample s : videoFrames) { + d.writeUInt(s.offset); // offset + // The offset contains the byte offset from the beginning of the + // data stream to the chunk. The table is indexed by chunk + // number-the first table entry corresponds to the first chunk, + // the second table entry is for the second chunk, and so on. + } + } else { + /* 64-bit chunk offset atom -------- */ + leaf = new DataAtom("co64"); + stblAtom.add(leaf); + /* + * typedef struct { byte version; byte[3] flags; int numberOfEntries; chunkOffsetTable chunkOffset64Table[numberOfEntries]; } chunkOffset64Atom; + * + * typedef struct { long offset; } chunkOffset64Table; + */ + d = leaf.getOutputStream(); + d.write(0); // version + // A 1-byte specification of the version of this time-to-sample + // atom. + + d.write(0); // flag[0] + d.write(0); // flag[1] + d.write(0); // flag[2] + // A 3-byte space for time-to-sample flags. Set this field to 0. + + d.writeUInt(videoFrames.size()); // number of entries + // A 32-bit integer containing the count of entries in the chunk + // offset table. + + for (Sample s : videoFrames) { + d.writeLong(s.offset); // offset + // The offset contains the byte offset from the beginning of the + // data stream to the chunk. The table is indexed by chunk + // number-the first table entry corresponds to the first chunk, + // the second table entry is for the second chunk, and so on. + } + } + // + moovAtom.finish(); + } + + /** + * Writes a frame to the video track. + *

+ * If the dimension of the video track has not been specified yet, it is derived from the first buffered image added to the QuickTimeOutputStream. + * + * @param image The frame image. + * @param duration The duration of the frame in time scale units. + * @throws IllegalArgumentException if the duration is less than 1, or if the dimension of the frame does not match the dimension of the video track. + * @throws IOException if writing the image failed. + */ + public void writeFrame(BufferedImage image, int duration) throws IOException { + if (duration <= 0) { + throw new IllegalArgumentException("duration must be greater 0"); + } + ensureOpen(); + ensureStarted(); + + // Get the dimensions of the first image + if (imgWidth == -1) { + imgWidth = image.getWidth(); + imgHeight = image.getHeight(); + } else { + // The dimension of the image must match the dimension of the video + // track + if (imgWidth != image.getWidth() || imgHeight != image.getHeight()) { + throw new IllegalArgumentException("Dimensions of image[" + videoFrames.size() + "] (width=" + image.getWidth() + + ", height=" + image.getHeight() + ") differs from image[0] (width=" + imgWidth + ", height=" + imgHeight); + } + } + + long offset = out.getStreamPosition(); + + switch (videoFormat) { + case RAW: { + WritableRaster raster = image.getRaster(); + int[] raw = new int[imgWidth * 3]; // holds a scanline of raw image + // data with 3 channels of 32 + // bit data + byte[] bytes = new byte[imgWidth * 3]; // holds a scanline of raw + // image data with 3 + // channels of 8 bit data + for (int y = 0; y < imgHeight; y++) { + raster.getPixels(0, y, imgWidth, 1, raw); + for (int k = 0, n = imgWidth * 3; k < n; k++) { + bytes[k] = (byte) raw[k]; + } + mdatAtom.getOutputStream().write(bytes); + } + break; + } + case JPG: { + ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/jpeg").next(); + ImageWriteParam iwParam = iw.getDefaultWriteParam(); + iwParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + iwParam.setCompressionQuality(quality); + MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(mdatAtom.getOutputStream()); + iw.setOutput(imgOut); + IIOImage img = new IIOImage(image, null, null); + iw.write(null, img, iwParam); + iw.dispose(); + break; + } + case PNG: + default: { + ImageWriter iw = ImageIO.getImageWritersByMIMEType("image/png").next(); + ImageWriteParam iwParam = iw.getDefaultWriteParam(); + MemoryCacheImageOutputStream imgOut = new MemoryCacheImageOutputStream(mdatAtom.getOutputStream()); + iw.setOutput(imgOut); + IIOImage img = new IIOImage(image, null, null); + iw.write(null, img, iwParam); + iw.dispose(); + break; + } + } + long length = out.getStreamPosition() - offset; + videoFrames.add(new Sample(duration, offset, length)); + } + + /** + * Writes a frame from a file to the video track. + *

+ * This method does not inspect the contents of the file. The contents has to match the video format. For example, it is your responsibility to only + * add JPG files if you have chosen the JPEG video format. + *

+ * If you add all frames from files or from input streams, then you have to explicitly set the dimension of the video track before you call finish() + * or close(). + * + * @param file The file which holds the image data. + * @param duration The duration of the frame in time scale units. + * @throws IllegalStateException if the duration is less than 1. + * @throws IOException if writing the image failed. + */ + public void writeFrame(File file, int duration) throws IOException { + + try (FileInputStream in = new FileInputStream(file)) { + writeFrame(in, duration); + } + } + + /** + * Writes a frame to the video track. + *

+ * This method does not inspect the contents of the input stream. The contents has to match the video format. For example, it is your responsibility + * to only add JPG files if you have chosen the JPEG video format. + *

+ * If you add all frames from files or from input streams, then you have to explicitly set the dimension of the video track before you call finish() + * or close(). + * + * @param in The input stream which holds the image data. + * @param duration The duration of the frame in time scale units. + * @throws IllegalArgumentException if the duration is less than 1. + * @throws IOException if writing the image failed. + */ + public void writeFrame(InputStream in, int duration) throws IOException { + if (duration <= 0) { + throw new IllegalArgumentException("duration must be greater 0"); + } + ensureOpen(); + ensureStarted(); + + long offset = out.getStreamPosition(); + try (OutputStream mdatOut = mdatAtom.getOutputStream()) { + byte[] buf = new byte[512]; + int len; + while ((len = in.read(buf)) != -1) { + mdatOut.write(buf, 0, len); + } + long length = out.getStreamPosition() - offset; + videoFrames.add(new Sample(duration, offset, length)); + } + } + + private void writeProlog() throws IOException { + /* + * File type atom + * + * typedef struct { magic brand; bcd4 versionYear; bcd2 versionMonth; bcd2 versionMinor; magic[4] compatibleBrands; } ftypAtom; + */ + DataAtom ftypAtom = new DataAtom("ftyp"); + DataAtomOutputStream d = ftypAtom.getOutputStream(); + d.writeType("qt "); // brand + d.writeBCD4(2005); // versionYear + d.writeBCD2(3); // versionMonth + d.writeBCD2(0); // versionMinor + d.writeType("qt "); // compatibleBrands + d.writeInt(0); // compatibleBrands (0 is used to denote no value) + d.writeInt(0); // compatibleBrands (0 is used to denote no value) + d.writeInt(0); // compatibleBrands (0 is used to denote no value) + ftypAtom.finish(); + } + + /** + * The states of the movie output stream. + */ + private enum States { + + STARTED, FINISHED, CLOSED + } + + /** + * Supported video formats. + */ + public enum VideoFormat { + + RAW, JPG, PNG + } + + /** + * QuickTime stores media data in samples. A sample is a single element in a sequence of time-ordered data. Samples are stored in the mdat atom. + */ + private static class Sample { + + /** + * Offset of the sample relative to the start of the QuickTime file. + */ + long offset; + /** + * Data length of the sample. + */ + long length; + /** + * The duration of the sample in time scale units. + */ + int duration; + + /** + * Creates a new sample. + * + * @param duration + * @param offset + * @param length + */ + public Sample(int duration, long offset, long length) { + this.duration = duration; + this.offset = offset; + this.length = length; + } + } + + /** + * Atom base class. + */ + private abstract class Atom { + + /** + * The type of the atom. A String with the length of 4 characters. + */ + protected String type; + /** + * The offset of the atom relative to the start of the ImageOutputStream. + */ + protected long offset; + + /** + * Creates a new Atom at the current position of the ImageOutputStream. + * + * @param type The type of the atom. A string with a length of 4 characters. + */ + public Atom(String type) throws IOException { + this.type = type; + offset = out.getStreamPosition(); + } + + /** + * Writes the atom to the ImageOutputStream and disposes it. + */ + public abstract void finish() throws IOException; + + /** + * Returns the size of the atom including the size of the atom header. + * + * @return The size of the atom. + */ + public abstract long size(); + } + + /** + * A CompositeAtom contains an ordered list of Atoms. + */ + private class CompositeAtom extends Atom { + + private LinkedList children; + private boolean finished; + + /** + * Creates a new CompositeAtom at the current position of the ImageOutputStream. + * + * @param type The type of the atom. + */ + public CompositeAtom(String type) throws IOException { + super(type); + out.writeLong(0); // make room for the atom header + children = new LinkedList<>(); + } + + public void add(Atom child) throws IOException { + if (children.size() > 0) { + children.getLast().finish(); + } + children.add(child); + } + + /** + * Writes the atom and all its children to the ImageOutputStream and disposes of all resources held by the atom. + * + * @throws java.io.IOException + */ + @Override + public void finish() throws IOException { + if (!finished) { + if (size() > 0xffffffffL) { + throw new IOException("CompositeAtom \"" + type + "\" is too large: " + size()); + } + + long pointer = out.getStreamPosition(); + out.seek(offset); + try (DataAtomOutputStream headerData = new DataAtomOutputStream(new FilterImageOutputStream(out))) { + + headerData.writeInt((int) size()); + headerData.writeType(type); + for (Atom child : children) { + child.finish(); + } + out.seek(pointer); + finished = true; + } + } + } + + @Override + public long size() { + long length = 8; + for (Atom child : children) { + length += child.size(); + } + return length; + } + } + + /** + * Data Atom. + */ + protected class DataAtom extends Atom { + + private DataAtomOutputStream data; + private boolean finished; + + /** + * Creates a new DataAtom at the current position of the ImageOutputStream. + * + * @param name The type of the atom. + */ + public DataAtom(String name) throws IOException { + super(name); + out.writeLong(0); // make room for the atom header + data = new DataAtomOutputStream(new FilterImageOutputStream(out)); + } + + @Override + public void finish() throws IOException { + if (!finished) { + long sizeBefore = size(); + + if (size() > 0xffffffffL) { + throw new IOException("DataAtom \"" + type + "\" is too large: " + size()); + } + + long pointer = out.getStreamPosition(); + out.seek(offset); + try (DataAtomOutputStream headerData = new DataAtomOutputStream(new FilterImageOutputStream(out))) { + headerData.writeUInt(size()); + headerData.writeType(type); + out.seek(pointer); + finished = true; + long sizeAfter = size(); + if (sizeBefore != sizeAfter) { + System.err.println("size mismatch " + sizeBefore + ".." + sizeAfter); + } + } + } + } + + /** + * Returns the offset of this atom to the beginning of the random access file + * + * @return + */ + public long getOffset() { + return offset; + } + + public DataAtomOutputStream getOutputStream() { + if (finished) { + throw new IllegalStateException("DataAtom is finished"); + } + return data; + } + + @Override + public long size() { + return 8 + data.size(); + } + } + + /** + * WideDataAtom can grow larger then 4 gigabytes. + */ + protected class WideDataAtom extends Atom { + + private DataAtomOutputStream data; + private boolean finished; + + /** + * Creates a new DataAtom at the current position of the ImageOutputStream. + * + * @param type The type of the atom. + */ + public WideDataAtom(String type) throws IOException { + super(type); + out.writeLong(0); // make room for the atom header + out.writeLong(0); // make room for the atom header + data = new DataAtomOutputStream(new FilterImageOutputStream(out)); + } + + @Override + public void finish() throws IOException { + if (!finished) { + long pointer = out.getStreamPosition(); + out.seek(offset); + try (DataAtomOutputStream headerData = new DataAtomOutputStream(new FilterImageOutputStream(out))) { + + if (size() <= 0xffffffffL) { + headerData.writeUInt(8); + headerData.writeType("wide"); + headerData.writeUInt(size()); + headerData.writeType(type); + } else { + headerData.writeInt(1); // special value for extended + // size + // atoms + headerData.writeType(type); + headerData.writeLong(size()); + } + + out.seek(pointer); + finished = true; + } + } + } + + /** + * Returns the offset of this atom to the beginning of the random access file + * + * @return + */ + public long getOffset() { + return offset; + } + + public DataAtomOutputStream getOutputStream() { + if (finished) { + throw new IllegalStateException("Atom is finished"); + } + return data; + } + + @Override + public long size() { + long size = 8 + data.size(); + return (size > 0xffffffffL) ? size + 8 : size; + } + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/app/AndroidScreencastApplication.java b/src/main/java/com/github/xsavikx/androidscreencast/app/AndroidScreencastApplication.java new file mode 100644 index 0000000..bd9dfdc --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/app/AndroidScreencastApplication.java @@ -0,0 +1,86 @@ +package com.github.xsavikx.androidscreencast.app; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; +import com.github.xsavikx.androidscreencast.api.injector.Injector; +import com.github.xsavikx.androidscreencast.constant.Constants; +import com.github.xsavikx.androidscreencast.ui.JFrameMain; +import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import javax.swing.*; + +@Component +public class AndroidScreencastApplication extends SwingApplication { + private static final Logger LOGGER = Logger.getLogger(AndroidScreencastApplication.class); + + private final Environment environment; + private final JFrameMain jFrameMain; + private final Injector injector; + private final IDevice iDevice; + + @Autowired + public AndroidScreencastApplication(Injector injector, IDevice iDevice, JFrameMain jFrameMain, Environment environment) { + this.injector = injector; + this.iDevice = iDevice; + this.jFrameMain = jFrameMain; + this.environment = environment; + } + + @Override + public void close() { + LOGGER.debug("close() - start"); + + if (injector != null) + injector.close(); + + if (iDevice != null) { + synchronized (iDevice) { + if (hasFilledAdbPath()) + AndroidDebugBridge.disconnectBridge(); + AndroidDebugBridge.terminate(); + } + } + + LOGGER.debug("close() - end"); + } + + @Override + public void start() { + LOGGER.debug("start() - start"); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + // Start showing the iDevice screen + jFrameMain.setTitle("" + iDevice); + + // Show window + jFrameMain.setVisible(true); + + jFrameMain.launchInjector(); + } + }); + LOGGER.debug("start() - end"); + } + + @SuppressWarnings("boxing") + @Override + protected boolean isNativeLook() { + LOGGER.debug("isNativeLook() - start"); + + boolean useNativeLook = environment.getProperty(Constants.APP_NATIVE_LOOK_PROPERTY, Boolean.class, + Constants.DEFAULT_APP_NATIVE_LOOK); + LOGGER.debug("isNativeLook() - end"); + return useNativeLook; + } + + private boolean hasFilledAdbPath() { + LOGGER.debug("hasFilledAdbPath() - start"); + + boolean hasAdbPath = environment.getProperty(Constants.ADB_PATH_PROPERTY) != null; + LOGGER.debug("hasFilledAdbPath() - end"); + return hasAdbPath; + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/app/Application.java b/src/main/java/com/github/xsavikx/androidscreencast/app/Application.java new file mode 100644 index 0000000..b82c8d9 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/app/Application.java @@ -0,0 +1,12 @@ +package com.github.xsavikx.androidscreencast.app; + +public interface Application { + + void close(); + + void handleException(Thread thread, Throwable ex); + + void start(); + + void init(); +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/app/DeviceChooserApplication.java b/src/main/java/com/github/xsavikx/androidscreencast/app/DeviceChooserApplication.java new file mode 100644 index 0000000..6382ba3 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/app/DeviceChooserApplication.java @@ -0,0 +1,96 @@ +package com.github.xsavikx.androidscreencast.app; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; +import com.github.xsavikx.androidscreencast.constant.Constants; +import com.github.xsavikx.androidscreencast.ui.JDialogDeviceList; +import org.apache.log4j.Logger; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +@Component +public class DeviceChooserApplication extends SwingApplication { + private static final Logger LOGGER = Logger.getLogger(DeviceChooserApplication.class); + + @Autowired + private Environment env; + @Autowired + private AndroidDebugBridge bridge; + + private IDevice device; + + @Override + public void close() { + // ignore + } + + @Override + public void start() { + LOGGER.debug("start() - start"); + initialize(); + + LOGGER.debug("start() - end"); + } + + @SuppressWarnings("boxing") + @Override + protected boolean isNativeLook() { + LOGGER.debug("isNativeLook() - start"); + + boolean returnboolean = env.getProperty(Constants.APP_NATIVE_LOOK_PROPERTY, Boolean.class, + Constants.DEFAULT_APP_NATIVE_LOOK); + LOGGER.debug("isNativeLook() - end"); + return returnboolean; + } + + private void waitDeviceList(AndroidDebugBridge bridge) { + LOGGER.debug("waitDeviceList(AndroidDebugBridge bridge=" + bridge + ") - start"); + + int count = 0; + while (!bridge.hasInitialDeviceList()) { + try { + Thread.sleep(100); + count++; + } catch (InterruptedException e) { + LOGGER.warn("waitDeviceList(AndroidDebugBridge) - exception ignored", e); + + } + // let's not wait > 10 sec. + if (count > 300) { + throw new RuntimeException("Timeout getting device list!"); + } + } + + LOGGER.debug("waitDeviceList(AndroidDebugBridge bridge=" + bridge + ") - end"); + } + + private void initialize() { + LOGGER.debug("initialize() - start"); + + waitDeviceList(bridge); + + IDevice devices[] = bridge.getDevices(); + // Let the user choose the device + if (devices.length == 1) { + device = devices[0]; + } else { + JDialogDeviceList jd = new JDialogDeviceList(devices); + jd.setVisible(true); + + device = jd.getDevice(); + } + if (device == null) { + System.exit(0); + + LOGGER.debug("initialize() - end"); + return; + } + + LOGGER.debug("initialize() - end"); + } + + public IDevice getDevice() { + return device; + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/app/GUIApplication.java b/src/main/java/com/github/xsavikx/androidscreencast/app/GUIApplication.java new file mode 100644 index 0000000..5f22a4e --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/app/GUIApplication.java @@ -0,0 +1,27 @@ +package com.github.xsavikx.androidscreencast.app; + +import java.lang.Thread.UncaughtExceptionHandler; + +public abstract class GUIApplication implements Application { + + public GUIApplication() { + Runtime.getRuntime().addShutdownHook(new Thread() { + @Override + public void run() { + close(); + } + }); + Thread.setDefaultUncaughtExceptionHandler(new UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread arg0, Throwable ex) { + try { + handleException(arg0, ex); + } catch (Exception ex2) { + // ignored + ex2.printStackTrace(); + } + } + }); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/app/SwingApplication.java b/src/main/java/com/github/xsavikx/androidscreencast/app/SwingApplication.java new file mode 100644 index 0000000..85dc733 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/app/SwingApplication.java @@ -0,0 +1,47 @@ +package com.github.xsavikx.androidscreencast.app; + +import com.github.xsavikx.androidscreencast.ui.JDialogError; + +import javax.swing.*; +import java.io.PrintWriter; +import java.io.StringWriter; + +public abstract class SwingApplication extends GUIApplication { + private JDialogError jd = null; + + protected abstract boolean isNativeLook(); + + @Override + public void init() { + try { + if (isNativeLook()) + UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + @Override + public void handleException(Thread thread, Throwable ex) { + try { + StringWriter sw = new StringWriter(); + ex.printStackTrace(new PrintWriter(sw)); + if (sw.toString().contains("SynthTreeUI")) + return; + ex.printStackTrace(System.err); + if (jd != null && jd.isVisible()) + return; + jd = new JDialogError(ex); + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + jd.setVisible(true); + } + }); + } catch (Exception ex2) { + // ignored + } + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/constant/Constants.java b/src/main/java/com/github/xsavikx/androidscreencast/constant/Constants.java new file mode 100644 index 0000000..372da9d --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/constant/Constants.java @@ -0,0 +1,14 @@ +package com.github.xsavikx.androidscreencast.constant; + +import javax.annotation.Resource; + +@Resource +public final class Constants { + public static final String APP_NATIVE_LOOK_PROPERTY = "app.nativeLook"; + public static final String ADB_PATH_PROPERTY = "adb.path"; + public static final String DEFAULT_WINDOW_WIDTH = "default.window.width"; + public static final String DEFAULT_WINDOW_HEIGHT = "default.window.height"; + + public static final boolean DEFAULT_APP_NATIVE_LOOK = true; + public static final int DEFAULT_ADB_PORT = 2345; +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/spring/config/ApplicationConfiguration.java b/src/main/java/com/github/xsavikx/androidscreencast/spring/config/ApplicationConfiguration.java new file mode 100644 index 0000000..06fd945 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/spring/config/ApplicationConfiguration.java @@ -0,0 +1,45 @@ +package com.github.xsavikx.androidscreencast.spring.config; + +import com.android.ddmlib.AndroidDebugBridge; +import com.android.ddmlib.IDevice; +import com.github.xsavikx.androidscreencast.app.DeviceChooserApplication; +import com.github.xsavikx.androidscreencast.constant.Constants; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.context.annotation.*; +import org.springframework.core.env.Environment; + +@Configuration +@ComponentScan(basePackages = "com.github.xsavikx.androidscreencast") +@PropertySources(value = { + @PropertySource(value = "file:${user.dir}/app.properties", ignoreResourceNotFound = true) +}) + +public class ApplicationConfiguration { + @Autowired + private Environment env; + + @Bean + public AndroidDebugBridge initAndroidDebugBridge() { + AndroidDebugBridge.initIfNeeded(false); + if (env.containsProperty(Constants.ADB_PATH_PROPERTY)) { + return AndroidDebugBridge.createBridge(env.getProperty(Constants.ADB_PATH_PROPERTY), false); + } + return AndroidDebugBridge.createBridge(); + } + + @Bean + public DefaultListableBeanFactory initBeanFactory() { + return new DefaultListableBeanFactory(); + } + + @Bean + @Autowired + public IDevice initDevice(DeviceChooserApplication application) { + application.init(); + application.start(); + application.close(); + return application.getDevice(); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/spring/config/ApplicationContextProvider.java b/src/main/java/com/github/xsavikx/androidscreencast/spring/config/ApplicationContextProvider.java new file mode 100644 index 0000000..a983374 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/spring/config/ApplicationContextProvider.java @@ -0,0 +1,26 @@ +package com.github.xsavikx.androidscreencast.spring.config; + +import org.springframework.beans.BeansException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; + +public class ApplicationContextProvider implements ApplicationContextAware { + private static ApplicationContext applicationContext; + + private ApplicationContextProvider() { + // + } + + public static ApplicationContext getApplicationContext() { + if (applicationContext == null) + applicationContext = new AnnotationConfigApplicationContext(ApplicationConfiguration.class); + return applicationContext; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + ApplicationContextProvider.applicationContext = applicationContext; + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogDeviceList.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogDeviceList.java new file mode 100644 index 0000000..e1f3e28 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogDeviceList.java @@ -0,0 +1,94 @@ +package com.github.xsavikx.androidscreencast.ui; + +import com.android.ddmlib.IDevice; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; + +public class JDialogDeviceList extends JDialog implements ActionListener { + + private static final long serialVersionUID = -3719844308147203239L; + private static final String DEFAULT_HOST = "127.0.0.1"; + private static final int DEFAULT_PORT = 1324; + + private JTextField jtfHost = new JTextField(DEFAULT_HOST); + private JFormattedTextField jftfPort = new JFormattedTextField(DEFAULT_PORT); + private JList jlDevices = new JList(); + private JPanel jpAgent = new JPanel(); + private JPanel jpButtons = new JPanel(); + private JButton jbOk = new JButton("OK"); + private JButton jbQuit = new JButton("Quit"); + + private boolean cancelled = false; + private IDevice[] devices; + + public JDialogDeviceList(IDevice[] devices) { + super(); + setModal(true); + this.devices = devices; + initialize(); + } + + @Override + public void actionPerformed(ActionEvent arg0) { + cancelled = arg0.getSource() == jbQuit; + + setVisible(false); + } + + public IDevice getDevice() { + if (cancelled) + return null; + return jlDevices.getSelectedValue(); + } + + private void initialize() { + setTitle("Please select a device"); + jlDevices.setListData(devices); + jlDevices.setPreferredSize(new Dimension(400, 300)); + if (devices.length != 0) + jlDevices.setSelectedIndex(0); + jbOk.setEnabled(!jlDevices.isSelectionEmpty()); + + jpAgent.setBorder(BorderFactory.createTitledBorder("Agent")); + jpAgent.setLayout(new BorderLayout(10, 10)); + jpAgent.add(jtfHost, BorderLayout.CENTER); + jpAgent.add(jftfPort, BorderLayout.EAST); + + jpButtons.setLayout(new FlowLayout(FlowLayout.RIGHT)); + jpButtons.add(jbOk, BorderLayout.CENTER); + jpButtons.add(jbQuit, BorderLayout.SOUTH); + + JPanel jpBottom = new JPanel(); + jpBottom.setLayout(new BorderLayout()); + jpBottom.add(jpAgent, BorderLayout.CENTER); + jpBottom.add(jpButtons, BorderLayout.SOUTH); + + setLayout(new BorderLayout()); + add(jlDevices, BorderLayout.CENTER); + add(jpBottom, BorderLayout.SOUTH); + + pack(); + setLocationRelativeTo(null); + + jbOk.addActionListener(this); + jbQuit.addActionListener(this); + jlDevices.addMouseListener(new MouseAdapter() { + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + int index = jlDevices.locationToIndex(e.getPoint()); + jlDevices.ensureIndexIsVisible(index); + cancelled = false; + setVisible(false); + } + } + + }); + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogError.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogError.java new file mode 100644 index 0000000..e73bdac --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogError.java @@ -0,0 +1,26 @@ +package com.github.xsavikx.androidscreencast.ui; + +import javax.swing.*; +import java.awt.*; +import java.io.PrintWriter; +import java.io.StringWriter; + +public class JDialogError extends JDialog { + + private static final long serialVersionUID = -2562084286663149628L; + + public JDialogError(Throwable ex) { + getRootPane().setLayout(new BorderLayout()); + JTextArea l = new JTextArea(); + StringWriter w = new StringWriter(); + if (ex.getClass() == RuntimeException.class && ex.getCause() != null) + ex = ex.getCause(); + ex.printStackTrace(new PrintWriter(w)); + l.setText(w.toString()); + getRootPane().add(l, BorderLayout.CENTER); + pack(); + setLocationRelativeTo(null); + setAlwaysOnTop(true); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogExecuteKeyEvent.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogExecuteKeyEvent.java new file mode 100644 index 0000000..3f3191b --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogExecuteKeyEvent.java @@ -0,0 +1,109 @@ +package com.github.xsavikx.androidscreencast.ui; + +import com.github.xsavikx.androidscreencast.api.command.executor.CommandExecutor; +import com.github.xsavikx.androidscreencast.api.command.factory.AdbInputCommandFactory; +import com.github.xsavikx.androidscreencast.api.injector.InputKeyEvent; +import com.github.xsavikx.androidscreencast.ui.model.InputKeyEventTable; +import com.github.xsavikx.androidscreencast.ui.model.InputKeyEventTableModel; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.swing.*; +import javax.swing.border.TitledBorder; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +@Component +public class JDialogExecuteKeyEvent extends JDialog { + private static final long serialVersionUID = -4152020879675916776L; + private static final int HEIGHT = 600; + private static final int WIDTH = 800; + private static final int BUTTON_HEIGHT = 20; + private static final int BUTTON_WIDTH = WIDTH >> 1 - 5; + + private static final int TITLE_COLUMN_INDEX = 1; + + private static final String EXECUTE_BUTTON_TEXT = "Execute"; + private static final String USE_LONG_PRESS_BUTTON_TEXT = "Long press"; + private static final String CANCEL_BUTTON_TEXT = "Cancel"; + private static final String COMMAND_LIST_TITLE_TEXT = "Commands to execute"; + private static final String NO_COMMAND_CHOSEN_WARNING_MESSAGE = "Please, select command from the list"; + private static final String NO_COMMAND_CHOSEN_WARNING_DIALOG_TITLE = "Warning"; + + @Autowired + private CommandExecutor commandExecutor; + + /** + * Create the dialog. + */ + public JDialogExecuteKeyEvent() { + setResizable(false); + setTitle("Execute key event"); + setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE); + final InputKeyEventTableModel commandList = new InputKeyEventTableModel(InputKeyEvent.values()); + final InputKeyEventTable commandListTable = new InputKeyEventTable(commandList); + final JCheckBoxMenuItem useLongPress = new JCheckBoxMenuItem(USE_LONG_PRESS_BUTTON_TEXT, false); + + JButton executeCommandButton = new JButton(EXECUTE_BUTTON_TEXT); + executeCommandButton.setSize(new Dimension(BUTTON_WIDTH, BUTTON_HEIGHT)); + executeCommandButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + int rowIndex = commandListTable.getSelectedRow(); + if (rowIndex > 0) { + final String title = (String) commandList.getValueAt(rowIndex, TITLE_COLUMN_INDEX); + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + commandExecutor.execute(AdbInputCommandFactory.getKeyCommand(InputKeyEvent.valueOf(title), + useLongPress.getState())); + } + }); + closeDialog(); + } else { + JOptionPane.showMessageDialog(null, NO_COMMAND_CHOSEN_WARNING_MESSAGE, NO_COMMAND_CHOSEN_WARNING_DIALOG_TITLE, + JOptionPane.WARNING_MESSAGE); + } + } + }); + JButton cancelButton = new JButton(CANCEL_BUTTON_TEXT); + cancelButton.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + closeDialog(); + } + }); + JScrollPane listScrollPane = new JScrollPane(commandListTable, ScrollPaneConstants.VERTICAL_SCROLLBAR_AS_NEEDED, + ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED); + listScrollPane.setPreferredSize(new Dimension(WIDTH, HEIGHT)); + listScrollPane.setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(), + COMMAND_LIST_TITLE_TEXT, TitledBorder.CENTER, TitledBorder.TOP)); + JPanel buttonPane = new JPanel(); + buttonPane.add(executeCommandButton); + buttonPane.add(useLongPress); + buttonPane.add(cancelButton); + buttonPane.setLayout(new GridLayout(1, 2)); + JSplitPane splitPane = new JSplitPane(JSplitPane.VERTICAL_SPLIT, listScrollPane, buttonPane); + splitPane.setEnabled(false); + getContentPane().add(splitPane); + pack(); + setLocationRelativeTo(null); + } + + /** + * Launch the application. + */ + public static void main(String[] args) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + new JDialogExecuteKeyEvent().setVisible(true); + } + }); + } + + private void closeDialog() { + setVisible(false); + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogUrl.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogUrl.java new file mode 100644 index 0000000..f677f21 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/JDialogUrl.java @@ -0,0 +1,55 @@ +package com.github.xsavikx.androidscreencast.ui; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class JDialogUrl extends JDialog { + + private static final long serialVersionUID = -331017582679776599L; + private JTextField jtfUrl = new JTextField(); + private JButton jbOk = new JButton("Ok"); + private boolean result = false; + + public JDialogUrl() { + setModal(true); + setTitle("Open url"); + + setLayout(new BorderLayout()); + add(jbOk, BorderLayout.SOUTH); + add(jtfUrl, BorderLayout.CENTER); + jtfUrl.setColumns(50); + + jbOk.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent arg0) { + setResult(true); + JDialogUrl.this.setVisible(false); + } + }); + + jbOk.setDefaultCapable(true); + getRootPane().setDefaultButton(jbOk); + pack(); + setLocationRelativeTo(null); + + } + + public JTextField getJtfUrl() { + return jtfUrl; + } + + public void setJtfUrl(JTextField jtfUrl) { + this.jtfUrl = jtfUrl; + } + + public boolean isResult() { + return result; + } + + public void setResult(boolean result) { + this.result = result; + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/JFrameMain.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/JFrameMain.java new file mode 100644 index 0000000..cf935ef --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/JFrameMain.java @@ -0,0 +1,205 @@ +package com.github.xsavikx.androidscreencast.ui; + +import com.github.xsavikx.androidscreencast.api.AndroidDevice; +import com.github.xsavikx.androidscreencast.api.injector.Injector; +import com.github.xsavikx.androidscreencast.api.injector.InputKeyEvent; +import com.github.xsavikx.androidscreencast.api.injector.ScreenCaptureThread.ScreenCaptureListener; +import com.github.xsavikx.androidscreencast.constant.Constants; +import com.github.xsavikx.androidscreencast.spring.config.ApplicationContextProvider; +import com.github.xsavikx.androidscreencast.ui.explorer.JFrameExplorer; +import com.github.xsavikx.androidscreencast.ui.interaction.KeyEventDispatcherFactory; +import com.github.xsavikx.androidscreencast.ui.interaction.KeyboardActionListenerFactory; +import com.github.xsavikx.androidscreencast.ui.interaction.MouseActionAdapterFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.env.Environment; +import org.springframework.stereotype.Component; + +import javax.swing.*; +import javax.swing.filechooser.FileNameExtensionFilter; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.awt.event.MouseAdapter; +import java.awt.image.BufferedImage; + +@Component +public class JFrameMain extends JFrame { + + private static final long serialVersionUID = -2085909236767692371L; + private JPanelScreen jp = new JPanelScreen(); + private JToolBar jtb = new JToolBar(); + private JToolBar jtbHardkeys = new JToolBar(); + // private JToggleButton jtbRecord = new JToggleButton("Record"); + + // private JButton jbOpenUrl = new JButton("Open Url"); + private JScrollPane jsp; + private JButton jbExplorer = new JButton("Explore"); + private JButton jbRestartClient = new JButton("Restart client"); + private JButton jbExecuteKeyEvent = new JButton("Execute keycode"); + + private JButton jbKbHome = new JButton("Home"); + private JButton jbKbMenu = new JButton("Menu"); + private JButton jbKbBack = new JButton("Back"); + private JButton jbKbSearch = new JButton("Search"); + + private JButton jbKbPhoneOn = new JButton("Call"); + + private JButton jbKbPhoneOff = new JButton("End call"); + private AndroidDevice androidDevice; + private Injector injector; + private Environment env; + private Dimension oldImageDimension; + + @Autowired + public JFrameMain(Environment env, Injector injector, AndroidDevice androidDevice) { + this.injector = injector; + this.env = env; + this.androidDevice = androidDevice; + initialize(); + KeyboardFocusManager.getCurrentKeyboardFocusManager() + .addKeyEventDispatcher(KeyEventDispatcherFactory.getKeyEventDispatcher(this)); + } + + private void setPrefferedWindowSize() { + if (env.containsProperty(Constants.DEFAULT_WINDOW_HEIGHT) && env.containsProperty(Constants.DEFAULT_WINDOW_WIDTH)) { + Integer height = env.getProperty(Constants.DEFAULT_WINDOW_HEIGHT, Integer.class); + Integer width = env.getProperty(Constants.DEFAULT_WINDOW_WIDTH, Integer.class); + if (height != null && width != null) + getContentPane().setPreferredSize(new Dimension(width, height)); + } + pack(); + } + + public void initialize() { + + jtb.setFocusable(false); + jbExplorer.setFocusable(false); + // jtbRecord.setFocusable(false); + // jbOpenUrl.setFocusable(false); + jbKbHome.setFocusable(false); + jbKbMenu.setFocusable(false); + jbKbBack.setFocusable(false); + jbKbSearch.setFocusable(false); + jbKbPhoneOn.setFocusable(false); + jbKbPhoneOff.setFocusable(false); + jbRestartClient.setFocusable(false); + jbExecuteKeyEvent.setFocusable(false); + + jbKbHome.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_HOME)); + jbKbMenu.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_MENU)); + jbKbBack.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_BACK)); + jbKbSearch.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_SEARCH)); + jbKbPhoneOn.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_CALL)); + jbKbPhoneOff.addActionListener(KeyboardActionListenerFactory.getInstance(InputKeyEvent.KEYCODE_ENDCALL)); + + jtbHardkeys.add(jbKbHome); + jtbHardkeys.add(jbKbMenu); + jtbHardkeys.add(jbKbBack); + jtbHardkeys.add(jbKbSearch); + jtbHardkeys.add(jbKbPhoneOn); + jtbHardkeys.add(jbKbPhoneOff); + + // setIconImage(Toolkit.getDefaultToolkit().getImage( + // getClass().getResource("icon.png"))); + setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE); + setLayout(new BorderLayout()); + add(jtb, BorderLayout.NORTH); + add(jtbHardkeys, BorderLayout.SOUTH); + jsp = new JScrollPane(jp); + add(jsp, BorderLayout.CENTER); + jsp.setPreferredSize(new Dimension(100, 100)); + pack(); + setLocationRelativeTo(null); + setPrefferedWindowSize(); + MouseAdapter ma = MouseActionAdapterFactory.getInstance(jp, injector); + + jp.addMouseMotionListener(ma); + jp.addMouseListener(ma); + jp.addMouseWheelListener(ma); + + // jtbRecord.addActionListener(new ActionListener() { + // + // @Override + // public void actionPerformed(ActionEvent arg0) { + // if (jtbRecord.isSelected()) { + // startRecording(); + // } else { + // stopRecording(); + // } + // } + // + // }); + // jtb.add(jtbRecord); + + jbExplorer.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent arg0) { + JFrameExplorer jf = ApplicationContextProvider.getApplicationContext().getBean(JFrameExplorer.class); + jf.setIconImage(getIconImage()); + jf.launch(); + jf.setVisible(true); + } + }); + jtb.add(jbExplorer); + + jtb.add(jbRestartClient); + + jbExecuteKeyEvent.addActionListener(new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + JDialogExecuteKeyEvent jdExecuteKeyEvent = ApplicationContextProvider.getApplicationContext() + .getBean(JDialogExecuteKeyEvent.class); + jdExecuteKeyEvent.setVisible(true); + } + }); + + jtb.add(jbExecuteKeyEvent); + + // jbOpenUrl.addActionListener(new ActionListener() { + // @Override + // public void actionPerformed(ActionEvent arg0) { + // JDialogUrl jdUrl = new JDialogUrl(); + // jdUrl.setVisible(true); + // if (!jdUrl.isResult()) + // return; + // String url = jdUrl.getJtfUrl().getText(); + // androidDevice.openUrl(url); + // } + // }); + // jtb.add(jbOpenUrl); + + } + + public void launchInjector() { + injector.screencapture.setListener(new ScreenCaptureListener() { + + @Override + public void handleNewImage(Dimension size, BufferedImage image, boolean landscape) { + if (oldImageDimension == null || !size.equals(oldImageDimension)) { + jsp.setPreferredSize(size); + JFrameMain.this.pack(); + oldImageDimension = size; + } + jp.handleNewImage(size, image); + } + }); + injector.start(); + } + + private void startRecording() { + JFileChooser jFileChooser = new JFileChooser(); + FileNameExtensionFilter filter = new FileNameExtensionFilter("Video file", "mov"); + jFileChooser.setFileFilter(filter); + int returnVal = jFileChooser.showSaveDialog(this); + if (returnVal == JFileChooser.APPROVE_OPTION) { + injector.screencapture.startRecording(jFileChooser.getSelectedFile()); + } + } + + private void stopRecording() { + injector.screencapture.stopRecording(); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/JPanelScreen.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/JPanelScreen.java new file mode 100644 index 0000000..a761196 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/JPanelScreen.java @@ -0,0 +1,49 @@ +package com.github.xsavikx.androidscreencast.ui; + +import javax.swing.*; +import java.awt.*; +import java.awt.image.BufferedImage; + +public class JPanelScreen extends JPanel { + + private static final long serialVersionUID = -2034873107028503004L; + private float coef = 1; + private double origX; + private double origY; + private Dimension size = null; + private BufferedImage image = null; + + public JPanelScreen() { + this.setFocusable(true); + } + + public Point getRawPoint(Point p1) { + Point p2 = new Point(); + p2.x = (int) ((p1.x - origX) / coef); + p2.y = (int) ((p1.y - origY) / coef); + return p2; + } + + public void handleNewImage(Dimension size, BufferedImage image) { + this.size = size; + this.image = image; + repaint(); + } + + @Override + protected void paintComponent(Graphics g) { + if (size == null) + return; + if (size.height == 0) + return; + Graphics2D g2 = (Graphics2D) g; + g2.clearRect(0, 0, getWidth(), getHeight()); + double width = Math.min(getWidth(), size.width * getHeight() / size.height); + coef = (float) width / size.width; + double height = width * size.height / size.width; + origX = (getWidth() - width) / 2; + origY = (getHeight() - height) / 2; + g2.drawImage(image, (int) origX, (int) origY, (int) width, (int) height, this); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/JSplashScreen.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/JSplashScreen.java new file mode 100644 index 0000000..29cb6f2 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/JSplashScreen.java @@ -0,0 +1,30 @@ +package com.github.xsavikx.androidscreencast.ui; + +import javax.swing.*; +import java.awt.*; + +public class JSplashScreen extends JWindow { + + private static final long serialVersionUID = -4537199368044671301L; + private JLabel label; + + public JSplashScreen(String text) { + label = new JLabel("Loading...", SwingConstants.CENTER); + initialize(); + setText(text); + } + + private void initialize() { + setLayout(new BorderLayout()); + label.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + // createLineBorder(Color.BLACK)); + add(label, BorderLayout.CENTER); + } + + public void setText(String text) { + label.setText(text); + pack(); + setLocationRelativeTo(null); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/MultiLineLabelUI.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/MultiLineLabelUI.java new file mode 100644 index 0000000..75f064f --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/MultiLineLabelUI.java @@ -0,0 +1,277 @@ +package com.github.xsavikx.androidscreencast.ui; + +import javax.swing.*; +import javax.swing.plaf.basic.BasicGraphicsUtils; +import javax.swing.plaf.basic.BasicLabelUI; +import java.awt.*; +import java.util.StringTokenizer; + +public class MultiLineLabelUI extends BasicLabelUI { + static final int LEADING = SwingConstants.LEADING; + static final int TRAILING = SwingConstants.TRAILING; + static final int LEFT = SwingConstants.LEFT; + static final int RIGHT = SwingConstants.RIGHT; + static final int TOP = SwingConstants.TOP; + static final int CENTER = SwingConstants.CENTER; + + static { + labelUI = new MultiLineLabelUI(); + } + + protected String str; + protected String[] strs; + + public static Dimension computeMultiLineDimension(FontMetrics fm, String[] strs) { + int i, c, width = 0; + for (i = 0, c = strs.length; i < c; i++) + width = Math.max(width, SwingUtilities.computeStringWidth(fm, strs[i])); + return new Dimension(width, fm.getHeight() * strs.length); + } + + /** + * 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 + * labels string. Locations are computed relative to the viewR rectangle. This layoutCompoundLabel() does not know how to handle LEADING/TRAILING + * values in horizontalTextPosition (they will default to RIGHT) and in horizontalAlignment (they will default to CENTER). Use the other version of + * layoutCompoundLabel() instead. + */ + public static String layoutCompoundLabel(FontMetrics fm, String[] text, Icon icon, int verticalAlignment, + int horizontalAlignment, int verticalTextPosition, int horizontalTextPosition, Rectangle viewR, Rectangle iconR, + Rectangle textR, int textIconGap) { + /* + * Initialize the icon bounds rectangle iconR. + */ + + if (icon != null) { + iconR.width = icon.getIconWidth(); + iconR.height = icon.getIconHeight(); + } else { + iconR.width = iconR.height = 0; + } + + /* + * 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. + */ + + // Fix for textIsEmpty sent by Paulo Santos + boolean textIsEmpty = (text == null) || (text.length == 0) + || (text.length == 1 && ((text[0] == null) || text[0].equals(""))); + + String rettext = ""; + if (textIsEmpty) { + textR.width = textR.height = 0; + } else { + Dimension dim = computeMultiLineDimension(fm, text); + textR.width = dim.width; + textR.height = dim.height; + } + + /* + * 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 + * textIconGap. + */ + + int gap = (textIsEmpty || (icon == null)) ? 0 : textIconGap; + + if (!textIsEmpty) { + + /* + * 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. + */ + + int availTextWidth; + + if (horizontalTextPosition == CENTER) { + availTextWidth = viewR.width; + } else { + availTextWidth = viewR.width - (iconR.width + gap); + } + + if (textR.width > availTextWidth && text.length == 1) { + String clipString = "..."; + int totalWidth = SwingUtilities.computeStringWidth(fm, clipString); + int nChars; + for (nChars = 0; nChars < text[0].length(); nChars++) { + totalWidth += fm.charWidth(text[0].charAt(nChars)); + if (totalWidth > availTextWidth) { + break; + } + } + rettext = text[0].substring(0, nChars) + clipString; + textR.width = SwingUtilities.computeStringWidth(fm, rettext); + } + } + + /* + * Compute textR.x,y given the verticalTextPosition and horizontalTextPosition properties + */ + + if (verticalTextPosition == TOP) { + if (horizontalTextPosition != CENTER) { + textR.y = 0; + } else { + textR.y = -(textR.height + gap); + } + } else if (verticalTextPosition == CENTER) { + textR.y = (iconR.height / 2) - (textR.height / 2); + } else { // (verticalTextPosition == BOTTOM) + if (horizontalTextPosition != CENTER) { + textR.y = iconR.height - textR.height; + } else { + textR.y = (iconR.height + gap); + } + } + + if (horizontalTextPosition == LEFT) { + textR.x = -(textR.width + gap); + } else if (horizontalTextPosition == CENTER) { + textR.x = (iconR.width / 2) - (textR.width / 2); + } else { // (horizontalTextPosition == RIGHT) + textR.x = (iconR.width + gap); + } + + /* + * labelR is the rectangle that contains iconR and textR. Move it to its proper position given the labelAlignment properties. + * + * To avoid actually allocating a Rectangle, Rectangle.union has been inlined below. + */ + int labelR_x = Math.min(iconR.x, textR.x); + int labelR_width = Math.max(iconR.x + iconR.width, textR.x + textR.width) - labelR_x; + int labelR_y = Math.min(iconR.y, textR.y); + int labelR_height = Math.max(iconR.y + iconR.height, textR.y + textR.height) - labelR_y; + + int dx, dy; + + if (verticalAlignment == TOP) { + dy = viewR.y - labelR_y; + } else if (verticalAlignment == CENTER) { + dy = (viewR.y + (viewR.height / 2)) - (labelR_y + (labelR_height / 2)); + } else { // (verticalAlignment == BOTTOM) + dy = (viewR.y + viewR.height) - (labelR_y + labelR_height); + } + + if (horizontalAlignment == LEFT) { + dx = viewR.x - labelR_x; + } else if (horizontalAlignment == RIGHT) { + dx = (viewR.x + viewR.width) - (labelR_x + labelR_width); + } else { // (horizontalAlignment == CENTER) + dx = (viewR.x + (viewR.width / 2)) - (labelR_x + (labelR_width / 2)); + } + + /* + * Translate textR and glypyR by dx,dy. + */ + + textR.x += dx; + textR.y += dy; + + iconR.x += dx; + iconR.y += dy; + + return rettext; + } + + /** + * 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 + * labels string. Locations are computed relative to the viewR rectangle. The JComponents orientation (LEADING/TRAILING) will also be taken into + * account and translated into LEFT/RIGHT values accordingly. + */ + public static String layoutCompoundLabel(JComponent c, FontMetrics fm, String[] text, Icon icon, + int verticalAlignment, int horizontalAlignment, int verticalTextPosition, int horizontalTextPosition, + Rectangle viewR, Rectangle iconR, Rectangle textR, int textIconGap) { + boolean orientationIsLeftToRight = true; + int hAlign = horizontalAlignment; + int hTextPos = horizontalTextPosition; + + if (c != null) { + if (!(c.getComponentOrientation().isLeftToRight())) { + orientationIsLeftToRight = false; + } + } + + // Translate LEADING/TRAILING values in horizontalAlignment + // to LEFT/RIGHT values depending on the components orientation + switch (horizontalAlignment) { + case LEADING: + hAlign = (orientationIsLeftToRight) ? LEFT : RIGHT; + break; + case TRAILING: + hAlign = (orientationIsLeftToRight) ? RIGHT : LEFT; + break; + } + + // Translate LEADING/TRAILING values in horizontalTextPosition + // to LEFT/RIGHT values depending on the components orientation + switch (horizontalTextPosition) { + case LEADING: + hTextPos = (orientationIsLeftToRight) ? LEFT : RIGHT; + break; + case TRAILING: + hTextPos = (orientationIsLeftToRight) ? RIGHT : LEFT; + break; + } + + return layoutCompoundLabel(fm, text, icon, verticalAlignment, hAlign, verticalTextPosition, hTextPos, viewR, iconR, + textR, textIconGap); + } + + protected void drawString(Graphics g, String s, int accChar, int textX, int textY) { + if (s.indexOf('\n') == -1) + BasicGraphicsUtils.drawString(g, s, accChar, textX, textY); + else { + String[] strs = splitStringByLines(s); + int height = g.getFontMetrics().getHeight(); + // Only the first line can have the accel char + BasicGraphicsUtils.drawString(g, strs[0], accChar, textX, textY); + for (int i = 1; i < strs.length; i++) + g.drawString(strs[i], textX, textY + (height * i)); + } + } + + @Override + protected String layoutCL(JLabel label, FontMetrics fontMetrics, String text, Icon icon, Rectangle viewR, + Rectangle iconR, Rectangle textR) { + String s = layoutCompoundLabel(label, fontMetrics, splitStringByLines(text), icon, label.getVerticalAlignment(), + label.getHorizontalAlignment(), label.getVerticalTextPosition(), label.getHorizontalTextPosition(), viewR, + iconR, textR, label.getIconTextGap()); + + if (s.equals("")) + return text; + return s; + } + + @Override + protected void paintDisabledText(JLabel l, Graphics g, String s, int textX, int textY) { + int accChar = l.getDisplayedMnemonic(); + g.setColor(l.getBackground()); + drawString(g, s, accChar, textX, textY); + } + + @Override + protected void paintEnabledText(JLabel l, Graphics g, String s, int textX, int textY) { + int accChar = l.getDisplayedMnemonic(); + g.setColor(l.getForeground()); + drawString(g, s, accChar, textX, textY); + } + + public String[] splitStringByLines(String str) { + if (str.equals(this.str)) + return strs; + + this.str = str; + + int lines = 1; + int i, c; + for (i = 0, c = str.length(); i < c; i++) { + if (str.charAt(i) == '\n') + lines++; + } + strs = new String[lines]; + StringTokenizer st = new StringTokenizer(str, "\n"); + + int line = 0; + while (st.hasMoreTokens()) + strs[line++] = st.nextToken(); + + return strs; + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/explorer/JFrameExplorer.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/explorer/JFrameExplorer.java new file mode 100644 index 0000000..399442f --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/explorer/JFrameExplorer.java @@ -0,0 +1,140 @@ +package com.github.xsavikx.androidscreencast.ui.explorer; + +import com.github.xsavikx.androidscreencast.api.AndroidDevice; +import com.github.xsavikx.androidscreencast.api.file.FileInfo; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import javax.swing.*; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.event.TreeSelectionListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.io.File; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Vector; + +@Component +public class JFrameExplorer extends JFrame { + + private static final long serialVersionUID = -5209265873286028854L; + private JTree jt; + private JSplitPane jSplitPane; + @Autowired + private AndroidDevice androidDevice; + private JList jListFichiers; + private Map> cache = new LinkedHashMap<>(); + + public JFrameExplorer() { + + setTitle("Explorer"); + setLayout(new BorderLayout()); + + jt = new JTree(new DefaultMutableTreeNode("Test")); + } + + public void launch() { + + jt.setModel(new DefaultTreeModel(new FolderTreeNode("Device", "/"))); + jt.setRootVisible(true); + jt.addTreeSelectionListener(new TreeSelectionListener() { + + @Override + public void valueChanged(TreeSelectionEvent e) { + TreePath tp = e.getPath(); + if (tp == null) + return; + if (!(tp.getLastPathComponent() instanceof FolderTreeNode)) + return; + FolderTreeNode node = (FolderTreeNode) tp.getLastPathComponent(); + displayFolder(node.path); + } + }); + + JScrollPane jsp = new JScrollPane(jt); + + jListFichiers = new JList<>(); + jListFichiers.setListData(new Object[]{}); + + jSplitPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, jsp, new JScrollPane(jListFichiers)); + + add(jSplitPane, BorderLayout.CENTER); + setSize(640, 480); + setLocationRelativeTo(null); + + jListFichiers.addMouseListener(new MouseAdapter() { + + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + int index = jListFichiers.locationToIndex(e.getPoint()); + ListModel dlm = jListFichiers.getModel(); + FileInfo item = (FileInfo) dlm.getElementAt(index); + launchFile(item); + } + } + + }); + } + + private void displayFolder(String path) { + List fileInfos = cache.get(path); + if (fileInfos == null) + fileInfos = androidDevice.list(path); + + List files = new Vector<>(); + for (FileInfo fi2 : fileInfos) { + if (fi2.directory) + continue; + files.add(fi2); + } + jListFichiers.setListData(files.toArray()); + + } + + private void launchFile(FileInfo node) { + try { + File tempFile = node.downloadTemporary(); + Desktop.getDesktop().open(tempFile); + } catch (Exception ex) { + throw new RuntimeException(ex); + } + } + + private class FolderTreeNode extends LazyMutableTreeNode { + private static final long serialVersionUID = 9131974430354670263L; + String name; + String path; + + public FolderTreeNode(String name, String path) { + this.name = name; + this.path = path; + } + + @Override + public void initChildren() { + List fileInfos = cache.get(path); + if (fileInfos == null) + fileInfos = androidDevice.list(path); + for (FileInfo fi : fileInfos) { + if (fi.directory) + add(new FolderTreeNode(fi.name, path + fi.name + "/")); + // else + // add(new FileTreeNode(fi)); + } + } + + @Override + public String toString() { + return name; + } + + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/explorer/LazyLoadingTreeNode.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/explorer/LazyLoadingTreeNode.java new file mode 100644 index 0000000..15f8754 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/explorer/LazyLoadingTreeNode.java @@ -0,0 +1,280 @@ +package com.github.xsavikx.androidscreencast.ui.explorer; + +import javax.swing.*; +import javax.swing.event.TreeExpansionEvent; +import javax.swing.event.TreeWillExpandListener; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.MutableTreeNode; +import javax.swing.tree.TreeModel; +import java.awt.event.ActionEvent; +import java.awt.event.KeyEvent; +import java.util.Vector; +import java.util.concurrent.ExecutionException; + +public abstract class LazyLoadingTreeNode extends DefaultMutableTreeNode implements TreeWillExpandListener { + + /** + * + */ + private static final long serialVersionUID = -4981073521761764327L; + private static final String ESCAPE_ACTION_NAME = "escape"; + private static final KeyStroke ESCAPE_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0); + /** + * The JTree containing this Node + */ + private JTree tree; + /** + * Can the worker be Canceled ? + */ + private boolean cancelable; + + /** + * Default Constructor + * + * @param userObject an Object provided by the user that constitutes the node's data + * @param tree the JTree containing this Node + * @param cancelable + */ + public LazyLoadingTreeNode(Object userObject, JTree tree, boolean cancelable) { + super(userObject); + tree.addTreeWillExpandListener(this); + this.tree = tree; + this.cancelable = cancelable; + setAllowsChildren(true); + } + + /** + * @return true if there are some childrens + */ + protected boolean areChildrenLoaded() { + return getChildCount() > 0 && getAllowsChildren(); + } + + /** + * @return a new Loading please wait node + */ + protected MutableTreeNode createLoadingNode() { + return new DefaultMutableTreeNode("Loading Please Wait ...", false); + } + + /** + * Create worker that will load the nodes + * + * @param tree the tree + * @return the newly created SwingWorker + */ + protected com.github.xsavikx.androidscreencast.ui.worker.SwingWorker createSwingWorker(final JTree tree) { + + com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker = new com.github.xsavikx.androidscreencast.ui.worker.SwingWorker() { + + @Override + protected MutableTreeNode[] doInBackground() { + return loadChildren(tree); + } + + @Override + protected void done() { + try { + if (!isCancelled()) { + MutableTreeNode[] nodes = get(); + setAllowsChildren(nodes.length > 0); + setChildren(nodes); + unRegisterSwingWorkerForCancel(tree, this); + } else { + reset(); + } + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + + } + + }; + registerSwingWorkerForCancel(tree, worker); + return worker; + } + + /** + * If the + * + * @return false, this node can't be a leaf + * @see #getAllowsChildren() + */ + @Override + public boolean isLeaf() { + return !getAllowsChildren(); + } + + /** + * This method will be executed in a background thread. If you have to do some GUI stuff use {@link SwingUtilities#invokeLater(Runnable)} + * + * @param tree the tree + * @return the Created nodes + */ + public abstract MutableTreeNode[] loadChildren(JTree tree); + + /** + * If the node is cancelable an escape Action is registered in the tree's InputMap and ActionMap that will cancel the execution + * + * @param tree the tree + * @param worker the worker to cancel + */ + protected void registerSwingWorkerForCancel(JTree tree, com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker) { + if (!cancelable) { + return; + } + tree.getInputMap().put(ESCAPE_KEY, ESCAPE_ACTION_NAME); + Action action = tree.getActionMap().get(ESCAPE_ACTION_NAME); + if (action == null) { + CancelWorkersAction cancelWorkerAction = new CancelWorkersAction(); + cancelWorkerAction.addSwingWorker(worker); + tree.getActionMap().put(ESCAPE_ACTION_NAME, cancelWorkerAction); + } else { + if (action instanceof CancelWorkersAction) { + CancelWorkersAction cancelAction = (CancelWorkersAction) action; + cancelAction.addSwingWorker(worker); + } + } + } + + /** + * Need some improvement ... This method should restore the Node initial state if the worker if canceled + */ + protected void reset() { + DefaultTreeModel defaultModel = (DefaultTreeModel) tree.getModel(); + int childCount = getChildCount(); + if (childCount > 0) { + for (int i = 0; i < childCount; i++) { + defaultModel.removeNodeFromParent((MutableTreeNode) getChildAt(0)); + } + } + setAllowsChildren(true); + } + + /** + * Define nodes children + * + * @param nodes new nodes + */ + protected void setChildren(MutableTreeNode... nodes) { + TreeModel model = tree.getModel(); + if (model instanceof DefaultTreeModel) { + DefaultTreeModel defaultModel = (DefaultTreeModel) model; + int childCount = getChildCount(); + if (childCount > 0) { + for (int i = 0; i < childCount; i++) { + defaultModel.removeNodeFromParent((MutableTreeNode) getChildAt(0)); + } + } + for (int i = 0; i < nodes.length; i++) { + defaultModel.insertNodeInto(nodes[i], this, i); + } + } + } + + /** + * set the loading state + */ + private void setLoading() { + setChildren(createLoadingNode()); + TreeModel model = tree.getModel(); + if (model instanceof DefaultTreeModel) { + DefaultTreeModel defaultModel = (DefaultTreeModel) model; + int[] indices = new int[getChildCount()]; + for (int i = 0; i < indices.length; i++) { + indices[i] = i; + } + defaultModel.nodesWereInserted(LazyLoadingTreeNode.this, indices); + } + } + + /** + * Default empty implementation, do nothing on collapse event. + */ + @Override + public void treeWillCollapse(TreeExpansionEvent event) { + // ignore + } + + /** + * Node will expand, it's time to retrieve nodes + */ + @Override + public void treeWillExpand(TreeExpansionEvent event) { + if (this.equals(event.getPath().getLastPathComponent())) { + if (areChildrenLoaded()) { + return; + } + setLoading(); + com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker = createSwingWorker(tree); + worker.execute(); + } + } + + /** + * Remove the swingWorker from the cancellable task of the tree + * + * @param tree + * @param worker + */ + protected void unRegisterSwingWorkerForCancel(JTree tree, com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker) { + if (!cancelable) { + return; + } + Action action = tree.getActionMap().get(ESCAPE_ACTION_NAME); + if (action != null && action instanceof CancelWorkersAction) { + CancelWorkersAction cancelWorkerAction = new CancelWorkersAction(); + cancelWorkerAction.removeSwingWorker(worker); + } + } + + /** + * 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. + * + * @author Thierry LEFORT 3 mars 08 + */ + protected static class CancelWorkersAction extends AbstractAction { + /** + * + */ + private static final long serialVersionUID = 3173288834368915117L; + /** + * the SwingWorkers + */ + private Vector> workers = new Vector<>(); + + /** + * Default constructor + */ + private CancelWorkersAction() { + super(ESCAPE_ACTION_NAME); + } + + /** + * Do the Cancel + */ + @Override + public void actionPerformed(ActionEvent e) { + for (com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker : workers) { + worker.cancel(true); + } + + } + + /** + * Add a Cancelable SwingWorker + */ + public void addSwingWorker(com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker) { + workers.add(worker); + } + + /** + * Remove a SwingWorker + */ + public void removeSwingWorker(com.github.xsavikx.androidscreencast.ui.worker.SwingWorker worker) { + workers.remove(worker); + } + + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/explorer/LazyMutableTreeNode.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/explorer/LazyMutableTreeNode.java new file mode 100644 index 0000000..a71d396 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/explorer/LazyMutableTreeNode.java @@ -0,0 +1,44 @@ +package com.github.xsavikx.androidscreencast.ui.explorer; + +import javax.swing.tree.DefaultMutableTreeNode; + +public abstract class LazyMutableTreeNode extends DefaultMutableTreeNode { + + private static final long serialVersionUID = -6383034137965603498L; + protected boolean _loaded = false; + + public LazyMutableTreeNode() { + super(); + } + + public LazyMutableTreeNode(Object userObject) { + super(userObject); + } + + public LazyMutableTreeNode(Object userObject, boolean allowsChildren) { + super(userObject, allowsChildren); + } + + public void clear() { + removeAllChildren(); + _loaded = false; + } + + @Override + public int getChildCount() { + synchronized (this) { + if (!_loaded) { + _loaded = true; + initChildren(); + } + } + return super.getChildCount(); + } + + protected abstract void initChildren(); + + public boolean isLoaded() { + return _loaded; + } + +} \ No newline at end of file diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyEventDispatcherFactory.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyEventDispatcherFactory.java new file mode 100644 index 0000000..bb9d2e9 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyEventDispatcherFactory.java @@ -0,0 +1,9 @@ +package com.github.xsavikx.androidscreencast.ui.interaction; + +import java.awt.*; + +public final class KeyEventDispatcherFactory { + public static KeyEventDispatcher getKeyEventDispatcher(Window frame) { + return new KeyEventDispatcherImpl(frame); + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyEventDispatcherImpl.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyEventDispatcherImpl.java new file mode 100644 index 0000000..8e3a6ea --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyEventDispatcherImpl.java @@ -0,0 +1,45 @@ +package com.github.xsavikx.androidscreencast.ui.interaction; + +import com.github.xsavikx.androidscreencast.api.command.executor.CommandExecutor; +import com.github.xsavikx.androidscreencast.api.command.factory.AdbInputCommandFactory; +import com.github.xsavikx.androidscreencast.api.injector.KeyCodeConverter; +import com.github.xsavikx.androidscreencast.spring.config.ApplicationContextProvider; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.KeyEvent; + +public class KeyEventDispatcherImpl implements KeyEventDispatcher { + private CommandExecutor commandExecutor; + private Window window; + + public KeyEventDispatcherImpl(Window frame) { + this.window = frame; + } + + @Override + public boolean dispatchKeyEvent(KeyEvent e) { + if (!window.isActive()) + return false; + if (e.getID() == KeyEvent.KEY_TYPED) { + final int code = KeyCodeConverter.getKeyCode(e); + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + getCommandExecutor().execute(AdbInputCommandFactory.getKeyCommand(code)); + + } + }); + + } + return false; + } + + private CommandExecutor getCommandExecutor() { + if (commandExecutor == null) { + commandExecutor = ApplicationContextProvider.getApplicationContext().getBean(CommandExecutor.class); + } + return commandExecutor; + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyboardActionListener.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyboardActionListener.java new file mode 100644 index 0000000..403b808 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyboardActionListener.java @@ -0,0 +1,37 @@ +package com.github.xsavikx.androidscreencast.ui.interaction; + +import com.github.xsavikx.androidscreencast.api.command.executor.CommandExecutor; +import com.github.xsavikx.androidscreencast.api.command.factory.AdbInputCommandFactory; +import com.github.xsavikx.androidscreencast.spring.config.ApplicationContextProvider; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class KeyboardActionListener implements ActionListener { + private CommandExecutor commandExecutor; + private int key; + + public KeyboardActionListener(int key) { + this.key = key; + } + + @Override + public void actionPerformed(ActionEvent e) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + getCommandExecutor().execute(AdbInputCommandFactory.getKeyCommand(key)); + } + }); + + } + + private CommandExecutor getCommandExecutor() { + if (commandExecutor == null) { + commandExecutor = ApplicationContextProvider.getApplicationContext().getBean(CommandExecutor.class); + } + return commandExecutor; + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyboardActionListenerFactory.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyboardActionListenerFactory.java new file mode 100644 index 0000000..b1ea861 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/KeyboardActionListenerFactory.java @@ -0,0 +1,9 @@ +package com.github.xsavikx.androidscreencast.ui.interaction; + +import com.github.xsavikx.androidscreencast.api.injector.InputKeyEvent; + +public final class KeyboardActionListenerFactory { + public static KeyboardActionListener getInstance(InputKeyEvent inputKeyEvent) { + return new KeyboardActionListener(inputKeyEvent.getCode()); + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/MouseActionAdapter.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/MouseActionAdapter.java new file mode 100644 index 0000000..96e17b6 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/MouseActionAdapter.java @@ -0,0 +1,97 @@ +package com.github.xsavikx.androidscreencast.ui.interaction; + +import com.github.xsavikx.androidscreencast.api.command.executor.CommandExecutor; +import com.github.xsavikx.androidscreencast.api.command.factory.AdbInputCommandFactory; +import com.github.xsavikx.androidscreencast.api.injector.Injector; +import com.github.xsavikx.androidscreencast.spring.config.ApplicationContextProvider; +import com.github.xsavikx.androidscreencast.ui.JPanelScreen; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseWheelEvent; + +public class MouseActionAdapter extends MouseAdapter { + private final static long ONE_SECOND = 1000L; + private final JPanelScreen jp; + private CommandExecutor commandExecutor; + private Injector injector; + private int dragFromX = -1; + private int dragFromY = -1; + private long timeFromPress = -1; + + public MouseActionAdapter(JPanelScreen jPanelScreen) { + this.jp = jPanelScreen; + } + + public MouseActionAdapter(JPanelScreen jPanelScreen, Injector injector) { + this(jPanelScreen); + this.injector = injector; + } + + @Override + public void mouseClicked(MouseEvent e) { + if (injector != null && e.getButton() == MouseEvent.BUTTON3) { + injector.screencapture.toogleOrientation(); + e.consume(); + return; + } + final Point p2 = jp.getRawPoint(e.getPoint()); + if (p2.x > 0 && p2.y > 0) { + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + getCommandExecutor().execute(AdbInputCommandFactory.getTapCommand(p2.x, p2.y)); + + } + }); + } + } + + @Override + public void mouseDragged(MouseEvent e) { + if (dragFromX == -1 && dragFromY == -1) { + Point p2 = jp.getRawPoint(e.getPoint()); + dragFromX = p2.x; + dragFromY = p2.y; + timeFromPress = System.currentTimeMillis(); + } + } + + @Override + public void mouseReleased(MouseEvent e) { + if (timeFromPress >= ONE_SECOND) { + final Point p2 = jp.getRawPoint(e.getPoint()); + final int xFrom = dragFromX; + final int yFrom = dragFromY; + final int xTo = p2.x; + final int yTo = p2.y; + SwingUtilities.invokeLater(new Runnable() { + + @Override + public void run() { + getCommandExecutor().execute(AdbInputCommandFactory.getSwipeCommand(xFrom, yFrom, xTo, yTo, timeFromPress)); + } + }); + dragFromX = -1; + dragFromY = -1; + timeFromPress = -1; + } + } + + @Override + public void mouseWheelMoved(MouseWheelEvent arg0) { + // if (JFrameMain.this.injector == null) + // return; + // JFrameMain.this.injector.injectTrackball(arg0.getWheelRotation() < 0 ? + // -1f : 1f); + } + + private CommandExecutor getCommandExecutor() { + if (commandExecutor == null) { + commandExecutor = ApplicationContextProvider.getApplicationContext().getBean(CommandExecutor.class); + } + return commandExecutor; + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/MouseActionAdapterFactory.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/MouseActionAdapterFactory.java new file mode 100644 index 0000000..97c4aad --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/interaction/MouseActionAdapterFactory.java @@ -0,0 +1,14 @@ +package com.github.xsavikx.androidscreencast.ui.interaction; + +import com.github.xsavikx.androidscreencast.api.injector.Injector; +import com.github.xsavikx.androidscreencast.ui.JPanelScreen; + +public final class MouseActionAdapterFactory { + public static MouseActionAdapter getInstance(JPanelScreen jPanelScreen) { + return new MouseActionAdapter(jPanelScreen); + } + + public static MouseActionAdapter getInstance(JPanelScreen jPanelScreen, Injector injector) { + return new MouseActionAdapter(jPanelScreen, injector); + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/model/InputKeyEventTable.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/model/InputKeyEventTable.java new file mode 100644 index 0000000..10cb824 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/model/InputKeyEventTable.java @@ -0,0 +1,43 @@ +package com.github.xsavikx.androidscreencast.ui.model; + +import javax.swing.*; +import javax.swing.table.TableCellRenderer; +import javax.swing.table.TableColumn; +import javax.swing.table.TableColumnModel; +import java.awt.*; + +public class InputKeyEventTable extends JTable { + private static final long serialVersionUID = 3978642864003531967L; + + private final static int MIN_COLUMN_WIDTH = 20; + + public InputKeyEventTable(InputKeyEventTableModel tableModel) { + super(tableModel); + setTableColumnsNames(tableModel.columnNames); + setAutoResizeMode(JTable.AUTO_RESIZE_OFF); + setTableColumnsPreferredSize(); + setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + } + + private void setTableColumnsNames(String[] columnNames) { + TableColumnModel columnModel = getColumnModel(); + for (int i = 0; i < columnNames.length; i++) { + TableColumn column = columnModel.getColumn(i); + column.setHeaderValue(columnNames[i]); + } + } + + private void setTableColumnsPreferredSize() { + final TableColumnModel columnModel = getColumnModel(); + for (int column = 0; column < getColumnCount(); column++) { + int width = MIN_COLUMN_WIDTH; // Min width + for (int row = 0; row < getRowCount(); row++) { + TableCellRenderer renderer = getCellRenderer(row, column); + Component comp = prepareRenderer(renderer, row, column); + width = Math.max(comp.getPreferredSize().width + 5, width); + } + columnModel.getColumn(column).setPreferredWidth(width); + } + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/model/InputKeyEventTableModel.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/model/InputKeyEventTableModel.java new file mode 100644 index 0000000..6d0098a --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/model/InputKeyEventTableModel.java @@ -0,0 +1,53 @@ +package com.github.xsavikx.androidscreencast.ui.model; + +import com.github.xsavikx.androidscreencast.api.injector.InputKeyEvent; + +import javax.swing.table.AbstractTableModel; +import java.util.ArrayList; +import java.util.List; + +public class InputKeyEventTableModel extends AbstractTableModel { + private static final long serialVersionUID = 1553313932570896541L; + private final static String INDEX_COLUMN_NAME = "#"; + private final static String TITLE_COLUMN_NAME = "title"; + private final static String DESCRIPTION_COLUMN_NAME = "description"; + public final String[] columnNames = { + INDEX_COLUMN_NAME, TITLE_COLUMN_NAME, DESCRIPTION_COLUMN_NAME + }; + private List> data = new ArrayList<>(); + private int rowCount = 0; + + public InputKeyEventTableModel(InputKeyEvent[] initialData) { + initData(initialData); + } + + private void initData(InputKeyEvent[] inputKeyEvents) { + for (InputKeyEvent e : inputKeyEvents) { + data.add(getDataRow(e)); + } + } + + private List getDataRow(InputKeyEvent e) { + List row = new ArrayList<>(); + row.add(rowCount++); + row.add(e.name()); + row.add(e.getDescription()); + return row; + } + + @Override + public int getRowCount() { + return data.size(); + } + + @Override + public int getColumnCount() { + return columnNames.length; + } + + @Override + public Object getValueAt(int rowIndex, int columnIndex) { + return data.get(rowIndex).get(columnIndex); + } + +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/worker/AccumulativeRunnable.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/worker/AccumulativeRunnable.java new file mode 100644 index 0000000..e4126db --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/worker/AccumulativeRunnable.java @@ -0,0 +1,148 @@ +package com.github.xsavikx.androidscreencast.ui.worker; + +/* + * $Id: AccumulativeRunnable.java,v 1.3 2008/07/25 19:32:29 idk Exp $ + * + * Copyright @ 2005 Sun Microsystems, Inc. All rights + * reserved. Use is subject to license terms. + */ + +import javax.swing.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * 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 + * might be appended after the {@code Runnable} is sent for the execution. Usually such {@code Runnables} are sent to the EDT. + *

+ *

+ * Usage example: + *

+ *

+ * 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 + * 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 + * updates overrides the previous one.) We might want to implement this {@code setText} in a way that only the last update is delivered. + *

+ * Here is how one can do this using {@code AccumulativeRunnable}: + *

+ *

+ * AccumulativeRunnable doSetTextImpl =
+ * new  AccumulativeRunnable() {
+ *     @Override
+ *     protected void run(List<String> args) {
+ *         //set to the last string being passed
+ *         setTextImpl(args.get(args.size() - 1);
+ *     }
+ * }
+ * void setText(String text) {
+ *     //add text and send for the execution if needed.
+ *     doSetTextImpl.add(text);
+ * }
+ * 
+ *

+ *

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

+ *

+ * Here is how it can be implemented using AccumulativeRunnable: + *

+ *

+ * AccumulativeRunnable<Rectangle> doHandleDirtyRegions = new AccumulativeRunnable<Rectangle>() {
+ *   @Override
+ *   protected void run(List<Rectangle> args) {
+ *     handleDirtyRegions(args);
+ *   }
+ * };
+ *
+ * void addDirtyRegion(Rectangle rect) {
+ *   doHandleDirtyRegions.add(rect);
+ * }
+ * 
+ * + * @param the type this {@code Runnable} accumulates + * @author Igor Kushnirskiy + * @version $Revision: 1.3 $ $Date: 2008/07/25 19:32:29 $ + */ +abstract class AccumulativeRunnable implements Runnable { + private List arguments = null; + + /** + * prepends or appends arguments and sends this {@code Runnable} for the execution if needed. + *

+ * This implementation uses {@see #submit} to send this {@code Runnable} for execution. + * + * @param isPrepend prepend or append + * @param args the arguments to add + */ + public final synchronized void add(boolean isPrepend, T... args) { + boolean isSubmitted = true; + if (arguments == null) { + isSubmitted = false; + arguments = new ArrayList<>(); + } + if (isPrepend) { + arguments.addAll(0, Arrays.asList(args)); + } else { + Collections.addAll(arguments, args); + } + if (!isSubmitted) { + submit(); + } + } + + /** + * appends arguments and sends this {@code Runnable} for the execution if needed. + *

+ * This implementation uses {@see #submit} to send this {@code Runnable} for execution. + * + * @param args the arguments to accumulate + */ + public final void add(T... args) { + add(false, args); + } + + /** + * Returns accumulated arguments and flashes the arguments storage. + * + * @return accumulated arguments + */ + private synchronized List flush() { + List list = arguments; + arguments = null; + return list; + } + + /** + * {@inheritDoc} + *

+ *

+ * This implementation calls {@code run(List args)} method with the list of accumulated arguments. + */ + @Override + public final void run() { + run(flush()); + } + + /** + * Equivalent to {@code Runnable.run} method with the accumulated arguments to process. + * + * @param args accumulated arguments to process. + */ + protected abstract void run(List args); + + /** + * Sends this {@code Runnable} for the execution + *

+ *

+ * This method is to be executed only from {@code add} method. + *

+ *

+ * This implementation uses {@code SwingWorker.invokeLater}. + */ + protected void submit() { + SwingUtilities.invokeLater(this); + } +} diff --git a/src/main/java/com/github/xsavikx/androidscreencast/ui/worker/SwingWorker.java b/src/main/java/com/github/xsavikx/androidscreencast/ui/worker/SwingWorker.java new file mode 100644 index 0000000..736bca8 --- /dev/null +++ b/src/main/java/com/github/xsavikx/androidscreencast/ui/worker/SwingWorker.java @@ -0,0 +1,789 @@ +/* + * $Id: SwingWorker.java,v 1.6 2008/07/25 19:32:29 idk Exp $ + * + * Copyright @ 2005 Sun Microsystems, Inc. All rights + * reserved. Use is subject to license terms. + */ + +package com.github.xsavikx.androidscreencast.ui.worker; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.List; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + +/** + * An abstract class to perform lengthy GUI-interacting tasks in a dedicated thread. + *

+ *

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

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

+ *

+ *

+ *

+ * 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 + * 2) the Event Dispatch Thread (EDT) for all GUI-related activities. This involves inter-thread communication which can be tricky to + * implement. + *

+ *

+ * {@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 + * either when done, or while processing. Subclasses of {@code SwingWorker} must implement the {@see #doInBackground} method to perform the background + * computation. + *

+ *

+ *

+ * Workflow + *

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

    + *
  • + *

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

  • + *

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

  • + *

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

+ *

+ *

+ * Often, the Current thread is the Event Dispatch Thread. + *

+ *

+ *

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

+ *

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

+ *

+ * Sample Usage + *

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

+ *

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

+ *

+ *   final JLabel label;
+ *   class MeaningOfLifeFinder extends SwingWorker<String, Object> {
+ *       {@code @Override}
+ *       public String doInBackground() {
+ *           return findTheMeaningOfLife();
+ *       }
+ *
+ *       {@code @Override}
+ *       protected void done() {
+ *           try {
+ *               label.setText(get());
+ *           } catch (Exception ignore) {
+ *           }
+ *       }
+ *   }
+ *
+ *   (new MeaningOfLifeFinder()).execute();
+ * 
+ *

+ *

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

+ *

+ * 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 + * progress in a {@code JProgressBar}. Finally, we also want to print the prime numbers to {@code System.out}. + *

+ *

+ * class PrimeNumbersTask extends
+ *         SwingWorker<List<Integer>, Integer> {
+ *     PrimeNumbersTask(JTextArea textArea, int numbersToFind) {
+ *         //initialize
+ *     }
+ *
+ *     {@code @Override}
+ *     public List<Integer> doInBackground() {
+ *         while (! enough && ! isCancelled()) {
+ *                 number = nextPrimeNumber();
+ *                 publish(number);
+ *                 setProgress(100 * numbers.size() / numbersToFind);
+ *             }
+ *         }
+ *         return numbers;
+ *     }
+ *
+ *     {@code @Override}
+ *     protected void process(List<Integer> chunks) {
+ *         for (int number : chunks) {
+ *             textArea.append(number + "\n");
+ *         }
+ *     }
+ * }
+ *
+ * JTextArea textArea = new JTextArea();
+ * final JProgressBar progressBar = new JProgressBar(0, 100);
+ * PrimeNumbersTask task = new PrimeNumbersTask(textArea, N);
+ * task.addPropertyChangeListener(
+ *     new PropertyChangeListener() {
+ *         public  void propertyChange(PropertyChangeEvent evt) {
+ *             if ("progress".equals(evt.getPropertyName())) {
+ *                 progressBar.setValue((Integer)evt.getNewValue());
+ *             }
+ *         }
+ *     });
+ *
+ * task.execute();
+ * System.out.println(task.get()); //prints all prime numbers we have got
+ * 
+ *

+ *

+ * Because {@code SwingWorker} implements {@code Runnable}, a {@code SwingWorker} can be submitted to an {@link java.util.concurrent.Executor} for + * execution. + * + * @param the result type returned by this {@code SwingWorker's} {@code doInBackground} and {@code get} methods + * @param the type used for carrying out intermediate results by this {@code SwingWorker's} {@code publish} and {@code process} methods + * @author Igor Kushnirskiy + * @version $Revision: 1.6 $ $Date: 2008/07/25 19:32:29 $ + */ +public abstract class SwingWorker implements Future, Runnable { + /** + * number of worker threads. + */ + private static final int MAX_WORKER_THREADS = 10; + private static final AccumulativeRunnable doSubmit = new DoSubmitAccumulativeRunnable(); + private static ExecutorService executorService = null; + /** + * everything is run inside this FutureTask. Also it is used as a delegatee for the Future API. + */ + private final FutureTask future; + /** + * all propertyChangeSupport goes through this. + */ + private final PropertyChangeSupport propertyChangeSupport; + /** + * current progress. + */ + private volatile int progress; + + /** + * current state. + */ + private volatile StateValue state; + /** + * handler for {@code process} method. + */ + private AccumulativeRunnable doProcess; + /** + * handler for progress property change notifications. + */ + private AccumulativeRunnable doNotifyProgressChange; + + /** + * Constructs this {@code SwingWorker}. + */ + public SwingWorker() { + Callable callable = new Callable() { + @Override + public T call() throws Exception { + setState(StateValue.STARTED); + return doInBackground(); + } + }; + + future = new FutureTask(callable) { + @Override + protected void done() { + doneEDT(); + setState(StateValue.DONE); + } + }; + + state = StateValue.PENDING; + propertyChangeSupport = new SwingWorkerPropertyChangeSupport(this); + doProcess = null; + doNotifyProgressChange = null; + } + + /** + * returns workersExecutorService. + *

+ * returns the service stored in the appContext or creates it if necessary. If the last one it triggers autoShutdown thread to get started. + * + * @return ExecutorService for the {@code SwingWorkers} + */ + private static synchronized ExecutorService getWorkersExecutorService() { + if (executorService == null) { + // this creates non-daemon threads. + ThreadFactory threadFactory = new ThreadFactory() { + final AtomicInteger threadNumber = new AtomicInteger(1); + + @Override + public Thread newThread(final Runnable r) { + StringBuilder name = new StringBuilder("SwingWorker-pool-"); + name.append(System.identityHashCode(this)); + name.append("-thread-"); + name.append(threadNumber.getAndIncrement()); + + Thread t = new Thread(r, name.toString()); + if (t.isDaemon()) + t.setDaemon(false); + if (t.getPriority() != Thread.NORM_PRIORITY) + t.setPriority(Thread.NORM_PRIORITY); + return t; + } + }; + + /* + * We want a to have no more than MAX_WORKER_THREADS running threads. + * + * We want a worker thread to wait no longer than 1 second for new tasks before terminating. + */ + executorService = new ThreadPoolExecutor(0, MAX_WORKER_THREADS, 5L, TimeUnit.SECONDS, + new LinkedBlockingQueue(), threadFactory) { + + private final ReentrantLock pauseLock = new ReentrantLock(); + private final Condition unpaused = pauseLock.newCondition(); + private final ReentrantLock executeLock = new ReentrantLock(); + private boolean isPaused = false; + + @Override + protected void afterExecute(Runnable r, Throwable t) { + super.afterExecute(r, t); + pauseLock.lock(); + try { + while (isPaused) { + unpaused.await(); + } + } catch (InterruptedException ignore) { + + } finally { + pauseLock.unlock(); + } + } + + @Override + public void execute(Runnable command) { + /* + * 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 + * it run task in maximumPool. + * + * 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 + * 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. + * + * 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. + * + * While we changing the corePoolSize we need to stop running worker threads from accepting new tasks. + */ + + // we need atomicity for the execute method. + executeLock.lock(); + try { + + pauseLock.lock(); + try { + isPaused = true; + } finally { + pauseLock.unlock(); + } + + setCorePoolSize(MAX_WORKER_THREADS); + super.execute(command); + setCorePoolSize(0); + + pauseLock.lock(); + try { + isPaused = false; + unpaused.signalAll(); + } finally { + pauseLock.unlock(); + } + } finally { + executeLock.unlock(); + } + } + }; + } + return executorService; + } + + /** + * Adds a {@code PropertyChangeListener} to the listener list. The listener is registered for all properties. The same listener object may be added + * 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 + * taken. + *

+ *

+ * Note: This is merely a convenience wrapper. All work is delegated to {@code PropertyChangeSupport} from {@link #getPropertyChangeSupport}. + * + * @param listener the {@code PropertyChangeListener} to be added + */ + public final void addPropertyChangeListener(PropertyChangeListener listener) { + getPropertyChangeSupport().addPropertyChangeListener(listener); + } + + /** + * {@inheritDoc} + */ + @Override + public final boolean cancel(boolean mayInterruptIfRunning) { + return future.cancel(mayInterruptIfRunning); + } + + /** + * Computes a result, or throws an exception if unable to do so. + *

+ *

+ * Note that this method is executed only once. + *

+ *

+ * Note: this method is executed in a background thread. + * + * @return the computed result + * @throws Exception if unable to compute a result + */ + protected abstract T doInBackground() throws Exception; + + // PropertyChangeSupports methods START + + /** + * Executed on the Event Dispatch Thread after the {@code doInBackground} method is finished. The default implementation does nothing. + * Subclasses may override this method to perform completion actions on the Event Dispatch Thread. Note that you can query status inside the + * implementation of this method to determine the result of this task or whether this task has been cancelled. + * + * @see #doInBackground + * @see #isCancelled() + * @see #get + */ + protected void done() { + } + + // Future methods START + + /** + * Invokes {@code done} on the EDT. + */ + private void doneEDT() { + Runnable doDone = new Runnable() { + @Override + public void run() { + done(); + } + }; + if (SwingUtilities.isEventDispatchThread()) { + doDone.run(); + } else { + doSubmit.add(doDone); + } + } + + /** + * Schedules this {@code SwingWorker} for execution on a worker thread. There are a number of worker threads available. In the event + * all worker threads are busy handling other {@code SwingWorkers} this {@code SwingWorker} is placed in a waiting queue. + *

+ *

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

+ *

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

+ *

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

+ * Note: This is merely a convenience wrapper. All work is delegated to {@code PropertyChangeSupport} from {@link #getPropertyChangeSupport}. + * + * @param propertyName the programmatic name of the property that was changed + * @param oldValue the old value of the property + * @param newValue the new value of the property + */ + public final void firePropertyChange(String propertyName, Object oldValue, Object newValue) { + getPropertyChangeSupport().firePropertyChange(propertyName, oldValue, newValue); + } + + /** + * {@inheritDoc} + *

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

+ *

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

+ *

+ * For example: + *

+ *

+     * class SwingWorkerCompletionWaiter implements PropertyChangeListener {
+     *   private JDialog dialog;
+     *
+     *   public SwingWorkerCompletionWaiter(JDialog dialog) {
+     *     this.dialog = dialog;
+     *   }
+     *
+     *   public void propertyChange(PropertyChangeEvent event) {
+     *     if ("state".equals(event.getPropertyName()) && SwingWorker.StateValue.DONE == event.getNewValue()) {
+     *       dialog.setVisible(false);
+     *       dialog.dispose();
+     *     }
+     *   }
+     * }
+     * JDialog dialog = new JDialog(owner, true);
+     * swingWorker.addPropertyChangeListener(new SwingWorkerCompletionWaiter(dialog));
+     * swingWorker.execute();
+     * // the dialog will be visible until the SwingWorker is done
+     * dialog.setVisible(true);
+     * 
+ */ + @Override + public final T get() throws InterruptedException, ExecutionException { + return future.get(); + } + + /** + * {@inheritDoc} + *

+ * Please refer to {@link #get} for more details. + */ + @Override + public final T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + return future.get(timeout, unit); + } + + /** + * Returns the {@code progress} bound property. + * + * @return the progress bound property. + */ + public final int getProgress() { + return progress; + } + + /** + * Sets the {@code progress} bound property. The value should be from 0 to 100. + *

+ *

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

+ *

+ * For example, the following invocations: + *

+ *

+     * setProgress(1);
+     * setProgress(2);
+     * setProgress(3);
+     * 
+ *

+ * might result in a single {@code PropertyChangeListener} notification with the value {@code 3}. + * + * @param progress the progress value to set + * @throws IllegalArgumentException is value not from 0 to 100 + */ + protected final void setProgress(int progress) { + if (progress < 0 || progress > 100) { + throw new IllegalArgumentException("the value should be from 0 to 100"); + } + if (this.progress == progress) { + return; + } + int oldProgress = this.progress; + this.progress = progress; + if (!getPropertyChangeSupport().hasListeners("progress")) { + return; + } + synchronized (this) { + if (doNotifyProgressChange == null) { + doNotifyProgressChange = new AccumulativeRunnable() { + @Override + public void run(List args) { + firePropertyChange("progress", args.get(0), args.get(args.size() - 1)); + } + + @Override + protected void submit() { + doSubmit.add(this); + } + }; + } + } + doNotifyProgressChange.add(oldProgress, progress); + } + + /** + * Returns the {@code PropertyChangeSupport} for this {@code SwingWorker}. This method is used when flexible access to bound properties support is + * needed. + *

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

+ *

+ * Note: The returned {@code PropertyChangeSupport} notifies any {@code PropertyChangeListener}s asynchronously on the Event Dispatch Thread + * in the event that {@code firePropertyChange} or {@code fireIndexedPropertyChange} are called off the Event Dispatch Thread. + * + * @return {@code PropertyChangeSupport} for this {@code SwingWorker} + */ + public final PropertyChangeSupport getPropertyChangeSupport() { + return propertyChangeSupport; + } + + /** + * Returns the {@code SwingWorker} state bound property. + * + * @return the current state + */ + public final StateValue getState() { + /* + * DONE is a special case to keep getState and isDone is sync + */ + if (isDone()) { + return StateValue.DONE; + } else { + return state; + } + } + + // Future methods END + + /** + * Sets this {@code SwingWorker} state bound property. + * + * @param the state state to set + */ + private void setState(StateValue state) { + StateValue old = this.state; + this.state = state; + firePropertyChange("state", old, state); + } + + /** + * {@inheritDoc} + */ + @Override + public final boolean isCancelled() { + return future.isCancelled(); + } + + /** + * {@inheritDoc} + */ + @Override + public final boolean isDone() { + return future.isDone(); + } + + /** + * Receives data chunks from the {@code publish} method asynchronously on the Event Dispatch Thread. + *

+ *

+ * Please refer to the {@link #publish} method for more details. + * + * @param chunks intermediate results to process + * @see #publish + */ + protected void process(List chunks) { + } + + // PropertyChangeSupports methods END + + /** + * Sends data chunks to the {@link #process} method. This method is to be used from inside the {@code doInBackground} method to deliver intermediate + * results for processing on the Event Dispatch Thread inside the {@code process} method. + *

+ *

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

+ *

+ * For example: + *

+ *

+     * publish("1");
+     * publish("2", "3");
+     * publish("4", "5", "6");
+     * 
+ *

+ * might result in: + *

+ *

+     * process("1", "2", "3", "4", "5", "6")
+     * 
+ *

+ *

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

+ *

+     * class TableSwingWorker extends
+     *         SwingWorker<DefaultTableModel, Object[]> {
+     *     private final DefaultTableModel tableModel;
+     *
+     *     public TableSwingWorker(DefaultTableModel tableModel) {
+     *         this.tableModel = tableModel;
+     *     }
+     *
+     *     {@code @Override}
+     *     protected DefaultTableModel doInBackground() throws Exception {
+     *         for (Object[] row = loadData();
+     *                  ! isCancelled() && row != null;
+     *                  row = loadData()) {
+     *             publish((Object[]) row);
+     *         }
+     *         return tableModel;
+     *     }
+     *
+     *     {@code @Override}
+     *     protected void process(List<Object[]> chunks) {
+     *         for (Object[] row : chunks) {
+     *             tableModel.addRow(row);
+     *         }
+     *     }
+     * }
+     * 
+ * + * @param chunks intermediate results to process + * @see #process + */ + protected final void publish(V... chunks) { + synchronized (this) { + if (doProcess == null) { + doProcess = new AccumulativeRunnable() { + @Override + public void run(List args) { + process(args); + } + + @Override + protected void submit() { + doSubmit.add(this); + } + }; + } + } + doProcess.add(chunks); + } + + /** + * Removes a {@code PropertyChangeListener} from the listener list. This removes a {@code PropertyChangeListener} that was registered for all + * 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 + * {@code listener} is {@code null}, or was never added, no exception is thrown and no action is taken. + *

+ *

+ * Note: This is merely a convenience wrapper. All work is delegated to {@code PropertyChangeSupport} from {@link #getPropertyChangeSupport}. + * + * @param listener the {@code PropertyChangeListener} to be removed + */ + public final void removePropertyChangeListener(PropertyChangeListener listener) { + getPropertyChangeSupport().removePropertyChangeListener(listener); + } + + /** + * Sets this {@code Future} to the result of computation unless it has been cancelled. + */ + @Override + public final void run() { + future.run(); + } + + /** + * Values for the {@code state} bound property. + */ + public enum StateValue { + /** + * Initial {@code SwingWorker} state. + */ + PENDING, /** + * {@code SwingWorker} is {@code STARTED} before invoking {@code doInBackground}. + */ + STARTED, + + /** + * {@code SwingWorker} is {@code DONE} after {@code doInBackground} method is finished. + */ + DONE + } + + private static class DoSubmitAccumulativeRunnable extends AccumulativeRunnable implements ActionListener { + private final static int DELAY = 1000 / 30; + + @Override + public void actionPerformed(ActionEvent event) { + run(); + } + + @Override + protected void run(List args) { + int i = 0; + try { + for (Runnable runnable : args) { + i++; + runnable.run(); + } + } finally { + if (i < args.size()) { + /* + * there was an exception schedule all the unhandled items for the next time + */ + Runnable argsTail[] = new Runnable[args.size() - i]; + for (int j = 0; j < argsTail.length; j++) { + argsTail[j] = args.get(i + j); + } + add(true, argsTail); + } + } + } + + @Override + protected void submit() { + Timer timer = new Timer(DELAY, this); + timer.setRepeats(false); + timer.start(); + } + } + + private class SwingWorkerPropertyChangeSupport extends PropertyChangeSupport { + private static final long serialVersionUID = 2409754725172747617L; + + SwingWorkerPropertyChangeSupport(Object source) { + super(source); + } + + @Override + public void firePropertyChange(final PropertyChangeEvent evt) { + if (SwingUtilities.isEventDispatchThread()) { + super.firePropertyChange(evt); + } else { + doSubmit.add(new Runnable() { + @Override + public void run() { + SwingWorkerPropertyChangeSupport.this.firePropertyChange(evt); + } + }); + } + } + } +} diff --git a/src/log4j.properties b/src/main/resources/log4j.properties similarity index 89% rename from src/log4j.properties rename to src/main/resources/log4j.properties index e06459c..faaeae6 100644 --- a/src/log4j.properties +++ b/src/main/resources/log4j.properties @@ -1,6 +1,5 @@ # Root logger option log4j.rootLogger=TRACE, stdout, file - # Direct log messages to a log file log4j.appender.file=org.apache.log4j.RollingFileAppender log4j.appender.file.File=AndroidScreencast.log @@ -8,13 +7,11 @@ log4j.appender.file.MaxFileSize=1MB log4j.appender.file.MaxBackupIndex=1 log4j.appender.file.layout=org.apache.log4j.PatternLayout log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss} [%-5p] [%C{1}:%L] - %m%n - # Direct log messages to stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.Target=System.out log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d{HH:mm:ss} [%-5p] [%C{1}] - %m%n - # Set levels for appenders -log4j.appender.file.Threshold = DEBUG -log4j.appender.stdout.Threshold = DEBUG +log4j.appender.file.Threshold=DEBUG +log4j.appender.stdout.Threshold=DEBUG