diff --git a/app/src/main/java/jp/co/cyberagent/stf/Agent.java b/app/src/main/java/jp/co/cyberagent/stf/Agent.java index 0f1830c..e20dd0c 100644 --- a/app/src/main/java/jp/co/cyberagent/stf/Agent.java +++ b/app/src/main/java/jp/co/cyberagent/stf/Agent.java @@ -8,11 +8,15 @@ import android.view.KeyEvent; import android.view.Surface; +import com.google.protobuf.ByteString; + import java.io.IOException; +import java.io.OutputStream; import java.net.UnknownHostException; import jp.co.cyberagent.stf.compat.InputManagerWrapper; import jp.co.cyberagent.stf.compat.PowerManagerWrapper; +import jp.co.cyberagent.stf.compat.ScreenshotManagerWrapper; import jp.co.cyberagent.stf.compat.WindowManagerWrapper; import jp.co.cyberagent.stf.proto.Wire; import jp.co.cyberagent.stf.util.InternalApi; @@ -25,6 +29,7 @@ public class Agent { private InputManagerWrapper inputManager; private PowerManagerWrapper powerManager; private WindowManagerWrapper windowManager; + private ScreenshotManagerWrapper screenshotManager; private LocalServerSocket serverSocket; private int deviceId = -1; // KeyCharacterMap.VIRTUAL_KEYBOARD private KeyCharacterMap keyCharacterMap; @@ -100,6 +105,7 @@ private void run() { powerManager = new PowerManagerWrapper(); inputManager = new InputManagerWrapper(); windowManager = new WindowManagerWrapper(); + screenshotManager = new ScreenshotManagerWrapper(); selectDevice(); loadKeyCharacterMap(); @@ -189,7 +195,7 @@ public void run() { System.err.println("InputClient started"); try { - while (!isInterrupted()) { + while (!isInterrupted() && !clientSocket.isClosed()) { Wire.Envelope envelope = Wire.Envelope.parseDelimitedFrom(clientSocket.getInputStream()); @@ -210,6 +216,9 @@ public void run() { case SET_ROTATION: handleSetRotationRequest(envelope); break; + case DO_SCREENSHOT: + handleScreenshotRequest(envelope); + break; default: System.err.printf("Unknown request type %d; maybe it's a Service call?\n", envelope.getType()); } @@ -309,6 +318,11 @@ private void handleSetRotationRequest(Wire.Envelope envelope) throws IOException } } + private void handleScreenshotRequest(Wire.Envelope envelope) throws IOException { + Wire.DoScreenshotRequest request = Wire.DoScreenshotRequest.parseFrom(envelope.getMessage()); + screenshot(); + } + private void keyDown(int keyCode, int metaState) { long time = SystemClock.uptimeMillis(); inputManager.injectKeyEvent(new KeyEvent( @@ -367,5 +381,25 @@ private void freezeRotation(int rotation) { private void thawRotation() { windowManager.thawRotation(); } + + private void screenshot() { + try { + byte[] bmp = screenshotManager.screenshot(); + if (bmp == null) { + return; + } + OutputStream out = clientSocket.getOutputStream(); + out.write(Wire.GetScreenshotResponse.newBuilder() + .setSuccess(true) + .setScreenshot(ByteString.copyFrom(bmp)) + .build().toByteArray()); + + out.flush(); + out.close(); + clientSocket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } } } diff --git a/app/src/main/java/jp/co/cyberagent/stf/Capture.java b/app/src/main/java/jp/co/cyberagent/stf/Capture.java new file mode 100644 index 0000000..e2b1293 --- /dev/null +++ b/app/src/main/java/jp/co/cyberagent/stf/Capture.java @@ -0,0 +1,144 @@ +package jp.co.cyberagent.stf; + +import android.net.LocalServerSocket; +import android.net.LocalSocket; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import jp.co.cyberagent.stf.compat.ScreenshotManagerWrapper; +import jp.co.cyberagent.stf.util.ProcUtil; + +public class Capture { + public static final String PROCESS_NAME = "stf.capture"; + public static final String SOCKET = "javacap"; + + private LocalServerSocket serverSocket; + + public Capture() { + } + + public static void main(String[] args) { + ProcUtil.setArgV0(PROCESS_NAME); + + for (String arg : args) { + if (arg.equals("--version")) { + System.out.println(Version.name); + return; + } else { + System.err.println("Error: unknown argument " + arg); + System.exit(1); + } + } + + new Capture().run(); + } + + private void run() { + startServer(); + waitForClients(); + } + + private void startServer() { + try { + serverSocket = new LocalServerSocket(SOCKET); + System.err.printf("Listening on @%s\n", SOCKET); + } catch (UnknownHostException e) { + e.printStackTrace(); + System.exit(1); + } catch (IOException e) { + stopServer(); + e.printStackTrace(); + System.exit(1); + } + } + + private void stopServer() { + if (serverSocket != null) { + try { + serverSocket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + + private void waitForClients() { + while (true) { + try { + LocalSocket clientSocket = serverSocket.accept(); + Capture.InputClient client = new Capture.InputClient(clientSocket); + client.start(); + } catch (IOException e) { + e.printStackTrace(); + break; + } + } + } + + private class InputClient extends Thread { + private LocalSocket clientSocket; + private ScreenshotManagerWrapper screenshotManager; + + public InputClient(LocalSocket clientSocket) { + this.clientSocket = clientSocket; + } + + @Override + public void interrupt() { + try { + clientSocket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + @Override + public void run() { + System.err.println("InputClient started"); + + screenshotManager = new ScreenshotManagerWrapper(); + + try { + + OutputStream out = clientSocket.getOutputStream(); + byte[] jpegData = screenshotManager.screenshot(); + ByteBuffer buf = ByteBuffer.allocate(24); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.put((byte) 0x01); // version + buf.put((byte) 0x03); // length 3*8 bit + buf.putInt(0); // pid + buf.putInt(screenshotManager.getWidth()); // real width + buf.putInt(screenshotManager.getHeight()); // real height + buf.putInt(0); // virt width + buf.putInt(0); // virt height + buf.put((byte) 0x00); // display orientation + buf.put((byte) 0x01); // quirk: QUIRK_DUMB + buf.flip(); + + out.write(buf.array()); + out.flush(); + + while (true) { + // jpeg data size + buf = ByteBuffer.allocate(4); + buf.order(ByteOrder.LITTLE_ENDIAN); + buf.putInt(jpegData.length); + buf.flip(); + out.write(buf.array()); + // jpeg data + out.write(jpegData); + out.flush(); + jpegData = screenshotManager.screenshot(); + } + } catch (IOException e) { + System.out.println("I/O exception: " + e); + } + System.err.println("InputClient closing"); + } + } +} diff --git a/app/src/main/java/jp/co/cyberagent/stf/compat/ScreenshotManagerWrapper.java b/app/src/main/java/jp/co/cyberagent/stf/compat/ScreenshotManagerWrapper.java new file mode 100644 index 0000000..0f9c54d --- /dev/null +++ b/app/src/main/java/jp/co/cyberagent/stf/compat/ScreenshotManagerWrapper.java @@ -0,0 +1,111 @@ +package jp.co.cyberagent.stf.compat; + +import android.graphics.Bitmap; +import android.graphics.Matrix; +import android.os.Build; +import android.os.IBinder; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.view.IWindowManager; +import java.io.ByteArrayOutputStream; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + + +/** + * Created by Stanley Huang on 2016/11/21. + */ +public class ScreenshotManagerWrapper { + + private static String SURFACE_CONTROL_CLASS = "android.view.SurfaceControl"; + private static String SURFACE_CLASS = "android.view.Surface"; + private Method injector; + private final IWindowManager wm; + private Bitmap lastBitmap; + + public ScreenshotManagerWrapper() { + IBinder wmbinder = ServiceManager.getService("window"); + wm = IWindowManager.Stub.asInterface(wmbinder); + if (Build.VERSION.SDK_INT <= 17) { + injector = new OldApiScreenshotInjector().injectorMethod(); + } else { + injector = new NewApiScreenshotInjector().injectorMethod(); + } + } + + private interface ScreenshotInjector { + public Method injectorMethod(); + } + + public int getWidth() { + return lastBitmap.getWidth(); + } + + public int getHeight() { + return lastBitmap.getHeight(); + } + + public byte[] screenshot() { + byte[] bmpArray = null; + try { + int rotation = wm.getRotation(); + Matrix m = new Matrix(); + if (rotation == 1) { + m.postRotate(-90.0f); + } else if (rotation == 2) { + m.postRotate(-180.0f); + } else if (rotation == 3) { + m.postRotate(-270.0f); + } + Bitmap bmp = (Bitmap) injector.invoke(null, new Object[]{Integer.valueOf(0), Integer.valueOf(0)}); + if (rotation != 0) { + bmp = Bitmap.createBitmap(bmp, 0, 0, bmp.getWidth(), bmp.getHeight(), m, false); + } + lastBitmap = bmp; + ByteArrayOutputStream bout = new ByteArrayOutputStream(); + bmp.compress(Bitmap.CompressFormat.JPEG, 75, bout); + bmpArray = bout.toByteArray(); + } catch (RemoteException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } + return bmpArray; + } + + + private class NewApiScreenshotInjector implements ScreenshotInjector { + private Method injector; + + public Method injectorMethod() { + try { + injector = Class.forName(SURFACE_CONTROL_CLASS).getDeclaredMethod("screenshot", + new Class[]{Integer.TYPE, Integer.TYPE}); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + return injector; + } + } + + private class OldApiScreenshotInjector implements ScreenshotInjector { + private Method injector; + + public Method injectorMethod() { + try { + injector = Class.forName(SURFACE_CLASS).getDeclaredMethod("screenshot", + new Class[]{Integer.TYPE, Integer.TYPE}); + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + return injector; + } + } + +} diff --git a/proto/src/main/proto/wire.proto b/proto/src/main/proto/wire.proto index e38b9ac..eb5add9 100644 --- a/proto/src/main/proto/wire.proto +++ b/proto/src/main/proto/wire.proto @@ -9,6 +9,7 @@ enum MessageType { DO_WAKE = 4; DO_ADD_ACCOUNT_MENU = 24; DO_REMOVE_ACCOUNT = 20; + DO_SCREENSHOT = 30; GET_ACCOUNTS = 26; GET_BROWSERS = 5; GET_CLIPBOARD = 6; @@ -293,3 +294,11 @@ message SetRotationRequest { message DoWakeRequest { } + +message DoScreenshotRequest { +} + +message GetScreenshotResponse { + required bool success = 1; + required bytes screenshot = 2; +}