/*
* Copyright (C) 2010 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.hierarchyviewerlib;
import com.android.ddmlib.AdbCommandRejectedException;
import com.android.ddmlib.AndroidDebugBridge;
import com.android.ddmlib.AndroidDebugBridge.IDeviceChangeListener;
import com.android.ddmlib.IDevice;
import com.android.ddmlib.Log;
import com.android.ddmlib.RawImage;
import com.android.ddmlib.TimeoutException;
import com.android.hierarchyviewerlib.device.DeviceBridge;
import com.android.hierarchyviewerlib.device.DeviceBridge.ViewServerInfo;
import com.android.hierarchyviewerlib.device.ViewNode;
import com.android.hierarchyviewerlib.device.Window;
import com.android.hierarchyviewerlib.device.WindowUpdater;
import com.android.hierarchyviewerlib.device.WindowUpdater.IWindowChangeListener;
import com.android.hierarchyviewerlib.models.DeviceSelectionModel;
import com.android.hierarchyviewerlib.models.PixelPerfectModel;
import com.android.hierarchyviewerlib.models.TreeViewModel;
import com.android.hierarchyviewerlib.ui.CaptureDisplay;
import com.android.hierarchyviewerlib.ui.TreeView;
import com.android.hierarchyviewerlib.ui.util.DrawableViewNode;
import com.android.hierarchyviewerlib.ui.util.PsdFile;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.ImageLoader;
import org.eclipse.swt.graphics.PaletteData;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.FileDialog;
import org.eclipse.swt.widgets.Shell;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Timer;
import java.util.TimerTask;
/**
* This is the class where most of the logic resides.
*/
public abstract class HierarchyViewerDirector implements IDeviceChangeListener,
IWindowChangeListener {
protected static HierarchyViewerDirector sDirector;
public static final String TAG = "hierarchyviewer";
private int mPixelPerfectRefreshesInProgress = 0;
private Timer mPixelPerfectRefreshTimer = new Timer();
private boolean mAutoRefresh = false;
public static final int DEFAULT_PIXEL_PERFECT_AUTOREFRESH_INTERVAL = 5;
private int mPixelPerfectAutoRefreshInterval = DEFAULT_PIXEL_PERFECT_AUTOREFRESH_INTERVAL;
private PixelPerfectAutoRefreshTask mCurrentAutoRefreshTask;
private String mFilterText = ""; //$NON-NLS-1$
public void terminate() {
WindowUpdater.terminate();
mPixelPerfectRefreshTimer.cancel();
}
public abstract String getAdbLocation();
public static HierarchyViewerDirector getDirector() {
return sDirector;
}
/**
* Init the DeviceBridge with an existing {@link AndroidDebugBridge}.
* @param bridge the bridge object to use
*/
public void acquireBridge(AndroidDebugBridge bridge) {
DeviceBridge.acquireBridge(bridge);
}
/**
* Creates an {@link AndroidDebugBridge} connected to adb at the given location.
*
* If a bridge is already running, this disconnects it and creates a new one.
*
* @param adbLocation the location to adb.
*/
public void initDebugBridge() {
DeviceBridge.initDebugBridge(getAdbLocation());
}
public void stopDebugBridge() {
DeviceBridge.terminate();
}
public void populateDeviceSelectionModel() {
IDevice[] devices = DeviceBridge.getDevices();
for (IDevice device : devices) {
deviceConnected(device);
}
}
public void startListenForDevices() {
DeviceBridge.startListenForDevices(this);
}
public void stopListenForDevices() {
DeviceBridge.stopListenForDevices(this);
}
public abstract void executeInBackground(String taskName, Runnable task);
@Override
public void deviceConnected(final IDevice device) {
executeInBackground("Connecting device", new Runnable() {
@Override
public void run() {
if (DeviceSelectionModel.getModel().containsDevice(device)) {
windowsChanged(device);
} else if (device.isOnline()) {
DeviceBridge.setupDeviceForward(device);
if (!DeviceBridge.isViewServerRunning(device)) {
if (!DeviceBridge.startViewServer(device)) {
// Let's do something interesting here... Try again
// in 2 seconds.
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
if (!DeviceBridge.startViewServer(device)) {
Log.e(TAG, "Unable to debug device " + device);
DeviceBridge.removeDeviceForward(device);
} else {
loadViewServerInfoAndWindows(device);
}
return;
}
}
loadViewServerInfoAndWindows(device);
}
}
});
}
private void loadViewServerInfoAndWindows(final IDevice device) {
ViewServerInfo viewServerInfo = DeviceBridge.loadViewServerInfo(device);
if (viewServerInfo == null) {
return;
}
Window[] windows = DeviceBridge.loadWindows(device);
DeviceSelectionModel.getModel().addDevice(device, windows, viewServerInfo);
if (viewServerInfo.protocolVersion >= 3) {
WindowUpdater.startListenForWindowChanges(HierarchyViewerDirector.this, device);
focusChanged(device);
}
}
@Override
public void deviceDisconnected(final IDevice device) {
executeInBackground("Disconnecting device", new Runnable() {
@Override
public void run() {
ViewServerInfo viewServerInfo = DeviceBridge.getViewServerInfo(device);
if (viewServerInfo != null && viewServerInfo.protocolVersion >= 3) {
WindowUpdater.stopListenForWindowChanges(HierarchyViewerDirector.this, device);
}
DeviceBridge.removeDeviceForward(device);
DeviceBridge.removeViewServerInfo(device);
DeviceSelectionModel.getModel().removeDevice(device);
if (PixelPerfectModel.getModel().getDevice() == device) {
PixelPerfectModel.getModel().setData(null, null, null);
}
Window treeViewWindow = TreeViewModel.getModel().getWindow();
if (treeViewWindow != null && treeViewWindow.getDevice() == device) {
TreeViewModel.getModel().setData(null, null);
mFilterText = ""; //$NON-NLS-1$
}
}
});
}
@Override
public void deviceChanged(IDevice device, int changeMask) {
if ((changeMask & IDevice.CHANGE_STATE) != 0 && device.isOnline()) {
deviceConnected(device);
}
}
@Override
public void windowsChanged(final IDevice device) {
executeInBackground("Refreshing windows", new Runnable() {
@Override
public void run() {
if (!DeviceBridge.isViewServerRunning(device)) {
if (!DeviceBridge.startViewServer(device)) {
Log.e(TAG, "Unable to debug device " + device);
return;
}
}
Window[] windows = DeviceBridge.loadWindows(device);
DeviceSelectionModel.getModel().updateDevice(device, windows);
}
});
}
@Override
public void focusChanged(final IDevice device) {
executeInBackground("Updating focus", new Runnable() {
@Override
public void run() {
int focusedWindow = DeviceBridge.getFocusedWindow(device);
DeviceSelectionModel.getModel().updateFocusedWindow(device, focusedWindow);
}
});
}
public void refreshPixelPerfect() {
final IDevice device = PixelPerfectModel.getModel().getDevice();
if (device != null) {
// Some interesting logic here. We don't want to refresh the pixel
// perfect view 1000 times in a row if the focus keeps changing. We
// just
// want it to refresh following the last focus change.
boolean proceed = false;
synchronized (this) {
if (mPixelPerfectRefreshesInProgress <= 1) {
proceed = true;
mPixelPerfectRefreshesInProgress++;
}
}
if (proceed) {
executeInBackground("Refreshing pixel perfect screenshot", new Runnable() {
@Override
public void run() {
Image screenshotImage = getScreenshotImage(device);
if (screenshotImage != null) {
PixelPerfectModel.getModel().setImage(screenshotImage);
}
synchronized (HierarchyViewerDirector.this) {
mPixelPerfectRefreshesInProgress--;
}
}
});
}
}
}
public void refreshPixelPerfectTree() {
final IDevice device = PixelPerfectModel.getModel().getDevice();
if (device != null) {
executeInBackground("Refreshing pixel perfect tree", new Runnable() {
@Override
public void run() {
ViewNode viewNode =
DeviceBridge.loadWindowData(Window.getFocusedWindow(device));
if (viewNode != null) {
PixelPerfectModel.getModel().setTree(viewNode);
}
}
});
}
}
public void loadPixelPerfectData(final IDevice device) {
executeInBackground("Loading pixel perfect data", new Runnable() {
@Override
public void run() {
Image screenshotImage = getScreenshotImage(device);
if (screenshotImage != null) {
ViewNode viewNode =
DeviceBridge.loadWindowData(Window.getFocusedWindow(device));
if (viewNode != null) {
PixelPerfectModel.getModel().setData(device, screenshotImage, viewNode);
}
}
}
});
}
private Image getScreenshotImage(IDevice device) {
try {
final RawImage screenshot = device.getScreenshot();
if (screenshot == null) {
return null;
}
class ImageContainer {
public Image image;
}
final ImageContainer imageContainer = new ImageContainer();
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
ImageData imageData =
new ImageData(screenshot.width, screenshot.height, screenshot.bpp,
new PaletteData(screenshot.getRedMask(), screenshot
.getGreenMask(), screenshot.getBlueMask()), 1,
screenshot.data);
imageContainer.image = new Image(Display.getDefault(), imageData);
}
});
return imageContainer.image;
} catch (IOException e) {
Log.e(TAG, "Unable to load screenshot from device " + device);
} catch (TimeoutException e) {
Log.e(TAG, "Timeout loading screenshot from device " + device);
} catch (AdbCommandRejectedException e) {
Log.e(TAG, "Adb rejected command to load screenshot from device " + device);
}
return null;
}
public void loadViewTreeData(final Window window) {
executeInBackground("Loading view hierarchy", new Runnable() {
@Override
public void run() {
mFilterText = ""; //$NON-NLS-1$
ViewNode viewNode = DeviceBridge.loadWindowData(window);
if (viewNode != null) {
DeviceBridge.loadProfileData(window, viewNode);
viewNode.setViewCount();
TreeViewModel.getModel().setData(window, viewNode);
}
}
});
}
public void loadOverlay(final Shell shell) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
FileDialog fileDialog = new FileDialog(shell, SWT.OPEN);
fileDialog.setFilterExtensions(new String[] {
"*.jpg;*.jpeg;*.png;*.gif;*.bmp" //$NON-NLS-1$
});
fileDialog.setFilterNames(new String[] {
"Image (*.jpg, *.jpeg, *.png, *.gif, *.bmp)"
});
fileDialog.setText("Choose an overlay image");
String fileName = fileDialog.open();
if (fileName != null) {
try {
Image image = new Image(Display.getDefault(), fileName);
PixelPerfectModel.getModel().setOverlayImage(image);
} catch (SWTException e) {
Log.e(TAG, "Unable to load image from " + fileName);
}
}
}
});
}
public void showCapture(final Shell shell, final ViewNode viewNode) {
executeInBackground("Capturing node", new Runnable() {
@Override
public void run() {
final Image image = loadCapture(viewNode);
if (image != null) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
CaptureDisplay.show(shell, viewNode, image);
}
});
}
}
});
}
public Image loadCapture(ViewNode viewNode) {
final Image image = DeviceBridge.loadCapture(viewNode.window, viewNode);
if (image != null) {
viewNode.image = image;
// Force the layout viewer to redraw.
TreeViewModel.getModel().notifySelectionChanged();
}
return image;
}
public void loadCaptureInBackground(final ViewNode viewNode) {
executeInBackground("Capturing node", new Runnable() {
@Override
public void run() {
loadCapture(viewNode);
}
});
}
public void showCapture(Shell shell) {
DrawableViewNode viewNode = TreeViewModel.getModel().getSelection();
if (viewNode != null) {
showCapture(shell, viewNode.viewNode);
}
}
public void refreshWindows() {
executeInBackground("Refreshing windows", new Runnable() {
@Override
public void run() {
IDevice[] devicesA = DeviceSelectionModel.getModel().getDevices();
IDevice[] devicesB = DeviceBridge.getDevices();
HashSet<IDevice> deviceSet = new HashSet<IDevice>();
for (int i = 0; i < devicesB.length; i++) {
deviceSet.add(devicesB[i]);
}
for (int i = 0; i < devicesA.length; i++) {
if (deviceSet.contains(devicesA[i])) {
windowsChanged(devicesA[i]);
deviceSet.remove(devicesA[i]);
} else {
deviceDisconnected(devicesA[i]);
}
}
for (IDevice device : deviceSet) {
deviceConnected(device);
}
}
});
}
public void loadViewHierarchy() {
Window window = DeviceSelectionModel.getModel().getSelectedWindow();
if (window != null) {
loadViewTreeData(window);
}
}
public void inspectScreenshot() {
IDevice device = DeviceSelectionModel.getModel().getSelectedDevice();
if (device != null) {
loadPixelPerfectData(device);
}
}
public void saveTreeView(final Shell shell) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
final DrawableViewNode viewNode = TreeViewModel.getModel().getTree();
if (viewNode != null) {
FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
fileDialog.setFilterExtensions(new String[] {
"*.png" //$NON-NLS-1$
});
fileDialog.setFilterNames(new String[] {
"Portable Network Graphics File (*.png)"
});
fileDialog.setText("Choose where to save the tree image");
final String fileName = fileDialog.open();
if (fileName != null) {
executeInBackground("Saving tree view", new Runnable() {
@Override
public void run() {
Image image = TreeView.paintToImage(viewNode);
ImageLoader imageLoader = new ImageLoader();
imageLoader.data = new ImageData[] {
image.getImageData()
};
String extensionedFileName = fileName;
if (!extensionedFileName.toLowerCase().endsWith(".png")) { //$NON-NLS-1$
extensionedFileName += ".png"; //$NON-NLS-1$
}
try {
imageLoader.save(extensionedFileName, SWT.IMAGE_PNG);
} catch (SWTException e) {
Log.e(TAG, "Unable to save tree view as a PNG image at "
+ fileName);
}
image.dispose();
}
});
}
}
}
});
}
public void savePixelPerfect(final Shell shell) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
Image untouchableImage = PixelPerfectModel.getModel().getImage();
if (untouchableImage != null) {
final ImageData imageData = untouchableImage.getImageData();
FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
fileDialog.setFilterExtensions(new String[] {
"*.png" //$NON-NLS-1$
});
fileDialog.setFilterNames(new String[] {
"Portable Network Graphics File (*.png)"
});
fileDialog.setText("Choose where to save the screenshot");
final String fileName = fileDialog.open();
if (fileName != null) {
executeInBackground("Saving pixel perfect", new Runnable() {
@Override
public void run() {
ImageLoader imageLoader = new ImageLoader();
imageLoader.data = new ImageData[] {
imageData
};
String extensionedFileName = fileName;
if (!extensionedFileName.toLowerCase().endsWith(".png")) { //$NON-NLS-1$
extensionedFileName += ".png"; //$NON-NLS-1$
}
try {
imageLoader.save(extensionedFileName, SWT.IMAGE_PNG);
} catch (SWTException e) {
Log.e(TAG, "Unable to save tree view as a PNG image at "
+ fileName);
}
}
});
}
}
}
});
}
public void capturePSD(final Shell shell) {
Display.getDefault().syncExec(new Runnable() {
@Override
public void run() {
final Window window = TreeViewModel.getModel().getWindow();
if (window != null) {
FileDialog fileDialog = new FileDialog(shell, SWT.SAVE);
fileDialog.setFilterExtensions(new String[] {
"*.psd" //$NON-NLS-1$
});
fileDialog.setFilterNames(new String[] {
"Photoshop Document (*.psd)"
});
fileDialog.setText("Choose where to save the window layers");
final String fileName = fileDialog.open();
if (fileName != null) {
executeInBackground("Saving window layers", new Runnable() {
@Override
public void run() {
PsdFile psdFile = DeviceBridge.captureLayers(window);
if (psdFile != null) {
String extensionedFileName = fileName;
if (!extensionedFileName.toLowerCase().endsWith(".psd")) { //$NON-NLS-1$
extensionedFileName += ".psd"; //$NON-NLS-1$
}
try {
psdFile.write(new FileOutputStream(extensionedFileName));
} catch (FileNotFoundException e) {
Log.e(TAG, "Unable to write to file " + fileName);
}
}
}
});
}
}
}
});
}
public void reloadViewHierarchy() {
Window window = TreeViewModel.getModel().getWindow();
if (window != null) {
loadViewTreeData(window);
}
}
public void invalidateCurrentNode() {
final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection();
if (selectedNode != null) {
executeInBackground("Invalidating view", new Runnable() {
@Override
public void run() {
DeviceBridge.invalidateView(selectedNode.viewNode);
}
});
}
}
public void relayoutCurrentNode() {
final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection();
if (selectedNode != null) {
executeInBackground("Request layout", new Runnable() {
@Override
public void run() {
DeviceBridge.requestLayout(selectedNode.viewNode);
}
});
}
}
public void dumpDisplayListForCurrentNode() {
final DrawableViewNode selectedNode = TreeViewModel.getModel().getSelection();
if (selectedNode != null) {
executeInBackground("Dump displaylist", new Runnable() {
@Override
public void run() {
DeviceBridge.outputDisplayList(selectedNode.viewNode);
}
});
}
}
public void loadAllViews() {
executeInBackground("Loading all views", new Runnable() {
@Override
public void run() {
DrawableViewNode tree = TreeViewModel.getModel().getTree();
if (tree != null) {
loadViewRecursive(tree.viewNode);
// Force the layout viewer to redraw.
TreeViewModel.getModel().notifySelectionChanged();
}
}
});
}
private void loadViewRecursive(ViewNode viewNode) {
Image image = DeviceBridge.loadCapture(viewNode.window, viewNode);
if (image == null) {
return;
}
viewNode.image = image;
final int N = viewNode.children.size();
for (int i = 0; i < N; i++) {
loadViewRecursive(viewNode.children.get(i));
}
}
public void filterNodes(String filterText) {
this.mFilterText = filterText;
DrawableViewNode tree = TreeViewModel.getModel().getTree();
if (tree != null) {
tree.viewNode.filter(filterText);
// Force redraw
TreeViewModel.getModel().notifySelectionChanged();
}
}
public String getFilterText() {
return mFilterText;
}
private static class PixelPerfectAutoRefreshTask extends TimerTask {
@Override
public void run() {
HierarchyViewerDirector.getDirector().refreshPixelPerfect();
}
};
public void setPixelPerfectAutoRefresh(boolean value) {
synchronized (mPixelPerfectRefreshTimer) {
if (value == mAutoRefresh) {
return;
}
mAutoRefresh = value;
if (mAutoRefresh) {
mCurrentAutoRefreshTask = new PixelPerfectAutoRefreshTask();
mPixelPerfectRefreshTimer.schedule(mCurrentAutoRefreshTask,
mPixelPerfectAutoRefreshInterval * 1000,
mPixelPerfectAutoRefreshInterval * 1000);
} else {
mCurrentAutoRefreshTask.cancel();
mCurrentAutoRefreshTask = null;
}
}
}
public void setPixelPerfectAutoRefreshInterval(int value) {
synchronized (mPixelPerfectRefreshTimer) {
if (mPixelPerfectAutoRefreshInterval == value) {
return;
}
mPixelPerfectAutoRefreshInterval = value;
if (mAutoRefresh) {
mCurrentAutoRefreshTask.cancel();
long timeLeft =
Math.max(0, mPixelPerfectAutoRefreshInterval
* 1000
- (System.currentTimeMillis() - mCurrentAutoRefreshTask
.scheduledExecutionTime()));
mCurrentAutoRefreshTask = new PixelPerfectAutoRefreshTask();
mPixelPerfectRefreshTimer.schedule(mCurrentAutoRefreshTask, timeLeft,
mPixelPerfectAutoRefreshInterval * 1000);
}
}
}
public int getPixelPerfectAutoRefreshInverval() {
return mPixelPerfectAutoRefreshInterval;
}
}