/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.ddmuilib;
import java.io.File;
import java.io.IOException;
import java.util.Calendar;
import lrscp.lib.swt.SwtUtils;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.dnd.Clipboard;
import org.eclipse.swt.dnd.ImageTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.events.ShellEvent;
import org.eclipse.swt.events.ShellListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Dialog;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
import com.android.chimpchat.core.IChimpDevice;
import com.android.chimpchat.core.TouchPressType;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.RawImage;
import com.android.ddmlib.TimeoutException;
/**
* Gather a screen shot from the device and save it to a file.
*/
public class ScreenShotDialog extends Dialog implements DisposeListener, Listener {
public static final float MAX_WIDTH = 1000;
public static final float MAX_HEIGHT = 700;
private Label mBusyLabel;
private Label mImageLabel;
private Button mSave;
private Button mCopy;
private Button mRotate;
private Button mRefresh;
private Button mRepeat;
private IDevice mDevice;
private IChimpDevice mChimDevice;
private RawImage mRawImage;
private Clipboard mClipboard;
public Display mDisplay = null;
public Shell mShell = null;
private boolean isMouseDown = false;
/** Number of 90 degree rotations applied to the current image */
private int mRotateCount = 0;
public float mImageScaleRatio = 1;
private Image mCurImage = null;
private int charIdx = 0;
ContinualCapture captureThread = null;
private OnShowListener mLsnr;
public static interface OnShowListener{
void onShow();
}
/**
* Create with default style.
*/
public ScreenShotDialog(Shell parent) {
this(parent, SWT.DIALOG_TRIM | SWT.APPLICATION_MODAL);
mClipboard = new Clipboard(parent.getDisplay());
}
/**
* Create with app-defined style.
*/
public ScreenShotDialog(Shell parent, int style) {
super(parent, style);
}
public void setOnShowListener(OnShowListener lsnr){
mLsnr = lsnr;
}
/**
* Prepare and display the dialog.
*
* @param device
* The {@link IDevice} from which to get the screenshot.
*/
public void open(IDevice device) {
mDevice = device;
mChimDevice = device.getChimpDevice();
Shell parent = getParent();
mShell = new Shell(parent, getStyle());
mShell.setText("Device Screen Capture");
mDisplay = parent.getDisplay();
createContents(mShell);
mShell.pack();
mShell.open();
SwtUtils.center(mShell, mShell.getBounds().height + 150);
notifyOnShow();
updateDeviceImage(mShell);
while (!mShell.isDisposed()) {
if (!mDisplay.readAndDispatch())
mDisplay.sleep();
}
}
private void notifyOnShow() {
if(mLsnr != null){
mLsnr.onShow();
}
}
/*
* Create the screen capture dialog contents.
*/
private void createContents(final Shell shell) {
GridData data;
final int colCount = 5;
shell.setLayout(new GridLayout(colCount, true));
// Add listeners
shell.addDisposeListener(this);
mDisplay.addFilter(SWT.KeyDown, this);
mDisplay.addFilter(SWT.KeyUp, this);
// "refresh" button
mRefresh = new Button(shell, SWT.PUSH);
mRefresh.setText("Refresh");
data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
data.widthHint = 80;
mRefresh.setLayoutData(data);
mRefresh.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
updateDeviceImage(shell);
// RawImage only allows us to rotate the image 90 degrees at the
// time,
// so to preserve the current rotation we must call getRotated()
// the same number of times the user has done it manually.
// TODO: improve the RawImage class.
for (int i = 0; i < mRotateCount; i++) {
mRawImage = mRawImage.getRotated();
}
updateImageDisplay(shell);
}
});
// "rotate" button
mRotate = new Button(shell, SWT.PUSH);
mRotate.setText("Rotate");
data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
data.widthHint = 80;
mRotate.setLayoutData(data);
mRotate.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
if (mRawImage != null) {
mRotateCount = (mRotateCount + 1) % 4;
mRawImage = mRawImage.getRotated();
updateImageDisplay(shell);
}
}
});
// "save" button
mSave = new Button(shell, SWT.PUSH);
mSave.setText("Save");
data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
data.widthHint = 80;
mSave.setLayoutData(data);
mSave.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
saveImage(shell);
}
});
mCopy = new Button(shell, SWT.PUSH);
mCopy.setText("Copy");
mCopy.setToolTipText("Copy the screenshot to the clipboard");
data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
data.widthHint = 80;
mCopy.setLayoutData(data);
mCopy.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetSelected(SelectionEvent e) {
copy();
}
});
// "Repeat start" button
mRepeat = new Button(shell, SWT.PUSH);
mRepeat.setText("Repeat start");
data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
data.widthHint = 80;
mRepeat.setLayoutData(data);
mRepeat.addSelectionListener(new SelectionAdapter() {
boolean enabled = false;
@Override
public void widgetSelected(SelectionEvent e) {
if (enabled) {
enabled = false;
mRefresh.setEnabled(true);
mCopy.setEnabled(true);
mSave.setEnabled(true);
mRotate.setEnabled(true);
if (captureThread != null) {
captureThread.exit();
}
} else {
enabled = true;
mRefresh.setEnabled(false);
mCopy.setEnabled(false);
mSave.setEnabled(false);
mRotate.setEnabled(false);
captureThread = new ContinualCapture(shell);
captureThread.start();
}
}
});
// title/"capturing" label
mBusyLabel = new Label(shell, SWT.NONE);
mBusyLabel.setText("Preparing...");
data = new GridData(GridData.HORIZONTAL_ALIGN_BEGINNING);
data.horizontalSpan = colCount;
mBusyLabel.setLayoutData(data);
// space for the image
mImageLabel = new Label(shell, SWT.BORDER);
data = new GridData(GridData.HORIZONTAL_ALIGN_CENTER);
data.horizontalSpan = colCount;
mImageLabel.setLayoutData(data);
Display display = shell.getDisplay();
mImageLabel.setImage(ImageLoader.createPlaceHolderArt(display, 50, 50,
display.getSystemColor(SWT.COLOR_BLUE)));
mImageLabel.addMouseListener(new MouseListener() {
@Override
public void mouseUp(MouseEvent e) {
isMouseDown = false;
mChimDevice.touch((int) (e.x * mImageScaleRatio), (int) (e.y * mImageScaleRatio),
TouchPressType.UP);
}
@Override
public void mouseDown(MouseEvent e) {
isMouseDown = true;
mChimDevice.touch((int) (e.x * mImageScaleRatio), (int) (e.y * mImageScaleRatio),
TouchPressType.DOWN);
}
@Override
public void mouseDoubleClick(MouseEvent e) {}
});
mImageLabel.addMouseMoveListener(new MouseMoveListener() {
@Override
public void mouseMove(MouseEvent e) {
if (isMouseDown) {
try {
mChimDevice.getManager().touchMove((int) (e.x * mImageScaleRatio),
(int) (e.y * mImageScaleRatio));
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
});
// shell.setDefaultButton(mRepeat);
}
/**
* Copies the content of {@link #mImageLabel} to the clipboard.
*/
private void copy() {
mClipboard.setContents(new Object[] { mImageLabel.getImage().getImageData() },
new Transfer[] { ImageTransfer.getInstance() });
}
final class ContinualCapture extends Thread implements ShellListener {
private boolean mExit = false;
private Shell mShell;
public ContinualCapture(Shell shell) {
mShell = shell;
shell.addShellListener(this);
}
public void exit() {
mExit = true;
try {
this.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
Log.d(null, "ContinualCapture exits");
}
@Override
public void run() {
while (!mExit) {
mRawImage = getDeviceImage();
// convert raw data to an Image.
if (mRawImage == null) {
continue;
}
PaletteData palette = new PaletteData(mRawImage.getRedMask(),
mRawImage.getGreenMask(), mRawImage.getBlueMask());
ImageData imageData = new ImageData(mRawImage.width, mRawImage.height,
mRawImage.bpp, palette, 1, mRawImage.data);
if (mCurImage != null) {
synchronized (mCurImage) {
try {
mCurImage.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
mImageScaleRatio = Math.max(1,
Math.max(mRawImage.width / MAX_WIDTH, mRawImage.height / MAX_HEIGHT));
mCurImage = new Image(mShell.getDisplay(), imageData.scaledTo(
(int) (mRawImage.width / mImageScaleRatio),
(int) (mRawImage.height / mImageScaleRatio)));
mShell.getDisplay().asyncExec(new Runnable() {
@Override
public void run() {
if (mShell.isDisposed()) {
return;
}
mBusyLabel.setText("refresh".substring(0, charIdx++ % 8));
mImageLabel.setImage(mCurImage);
mImageLabel.pack();
mShell.pack();
// there's no way to restore old cursor; assume it's
// ARROW
mShell.setCursor(mShell.getDisplay().getSystemCursor(SWT.CURSOR_ARROW));
synchronized (mCurImage) {
mCurImage.notifyAll();
mCurImage = null;
}
}
});
}
}
@Override
public void shellActivated(ShellEvent e) {}
@Override
public void shellClosed(ShellEvent e) {
exit();
}
@Override
public void shellDeactivated(ShellEvent e) {}
@Override
public void shellDeiconified(ShellEvent e) {}
@Override
public void shellIconified(ShellEvent e) {}
}
/**
* Captures a new image from the device, and display it.
*/
private void updateDeviceImage(Shell shell) {
mBusyLabel.setText("Capturing..."); // no effect
shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_WAIT));
mRawImage = getDeviceImage();
updateImageDisplay(shell);
}
/**
* Updates the display with {@link #mRawImage}.
*
* @param shell
*/
private void updateImageDisplay(Shell shell) {
Image image;
if (mRawImage == null) {
Display display = shell.getDisplay();
image = ImageLoader.createPlaceHolderArt(display, 320, 240,
display.getSystemColor(SWT.COLOR_BLUE));
mSave.setEnabled(false);
mBusyLabel.setText("Screen not available");
} else {
// convert raw data to an Image.
PaletteData palette = new PaletteData(mRawImage.getRedMask(), mRawImage.getGreenMask(),
mRawImage.getBlueMask());
mImageScaleRatio = Math.max(1,
Math.max(mRawImage.width / MAX_WIDTH, mRawImage.height / MAX_HEIGHT));
ImageData imageData = new ImageData(mRawImage.width, mRawImage.height, mRawImage.bpp,
palette, 1, mRawImage.data);
image = new Image(getParent().getDisplay(), imageData.scaledTo(
(int) (mRawImage.width / mImageScaleRatio),
(int) (mRawImage.height / mImageScaleRatio)));
mSave.setEnabled(true);
mBusyLabel.setText("Captured image:");
}
mImageLabel.setImage(image);
mImageLabel.pack();
shell.pack();
// there's no way to restore old cursor; assume it's ARROW
shell.setCursor(shell.getDisplay().getSystemCursor(SWT.CURSOR_ARROW));
}
/**
* Grabs an image from an ADB-connected device and returns it as a
* {@link RawImage}.
*/
private RawImage getDeviceImage() {
try {
return mDevice.getScreenshot();
} catch (IOException ioe) {
Log.w("ddms", "Unable to get frame buffer: " + ioe.getMessage());
return null;
} catch (TimeoutException e) {
Log.w("ddms", "Unable to get frame buffer: timeout ");
return null;
} catch (AdbCommandRejectedException e) {
Log.w("ddms", "Unable to get frame buffer: " + e.getMessage());
return null;
}
}
/*
* Prompt the user to save the image to disk.
*/
private void saveImage(Shell shell) {
FileDialog dlg = new FileDialog(shell, SWT.SAVE);
Calendar now = Calendar.getInstance();
String fileName = String.format("device-%tF-%tH%tM%tS.png", now, now, now, now);
dlg.setText("Save image...");
dlg.setFileName(fileName);
String lastDir = DdmUiPreferences.getStore().getString("lastImageSaveDir");
if (lastDir.length() == 0) {
lastDir = DdmUiPreferences.getStore().getString("imageSaveDir");
}
dlg.setFilterPath(lastDir);
dlg.setFilterNames(new String[] { "PNG Files (*.png)" });
dlg.setFilterExtensions(new String[] { "*.png" //$NON-NLS-1$
});
fileName = dlg.open();
if (fileName != null) {
// FileDialog.getFilterPath() does NOT always return the current
// directory of the FileDialog; on the Mac it sometimes just returns
// the value the dialog was initialized with. It does however return
// the full path as its return value, so just pick the path from
// there.
if (!fileName.endsWith(".png")) {
fileName = fileName + ".png";
}
String saveDir = new File(fileName).getParent();
if (saveDir != null) {
DdmUiPreferences.getStore().setValue("lastImageSaveDir", saveDir);
}
Log.d("ddms", "Saving image to " + fileName);
ImageData imageData = mImageLabel.getImage().getImageData();
try {
org.eclipse.swt.graphics.ImageLoader loader = new org.eclipse.swt.graphics.ImageLoader();
loader.data = new ImageData[] { imageData };
loader.save(fileName, SWT.IMAGE_PNG);
} catch (SWTException e) {
Log.w("ddms", "Unable to save " + fileName + ": " + e.getMessage());
}
}
}
@Override
public void widgetDisposed(DisposeEvent e) {
if (captureThread != null) {
captureThread.exit();
}
mDevice.releaseChimpDevice();
mDisplay.removeFilter(SWT.KeyDown, this);
mDisplay.removeFilter(SWT.KeyUp, this);
}
int lastKeyDown = -1;
@Override
public void handleEvent(Event event) {
// Log.d(null, "key:" + event.keyCode + " " + event.type);
if (event.type == SWT.KeyDown || event.type == SWT.KeyUp) {
if (mChimDevice != null) {
TouchPressType type = event.type == SWT.KeyDown ? TouchPressType.DOWN
: TouchPressType.UP;
//Don't send repeat key downs
if(event.type == SWT.KeyDown){
if (lastKeyDown == event.keyCode){
return;
} else {
lastKeyDown = event.keyCode;
}
} else{
lastKeyDown = -1;
}
mChimDevice.press(PcKeyMap.getDeviceKey(event.keyCode), type);
event.type = SWT.NONE;
}
}
}
}